Source: ezform.js

/** @module ezform */

import React from "react";

//import { useState } from "react";
const h = React.createElement;
const gClass = "form-group";
const bClass = "btn btn-primary";
const cClass = "form-control"; // input, select, textarea
const checkClass = "form-check form-check-inline";
const checkiClass = "form-check-input";
const checklClass = "form-check-label";
//const iClass = "custom-control-input";
//const lClass = "custom-control-label";
//const checkClass = "custom-control custom-checkbox custom-control-inline";
//const radioClass = "custom-control custom-radio custom-control-inline";

/*
const omit = (fields) => (obj) =>
  Object.fromEntries(
    Object.entries(obj).filter(([key]) => !fields.includes(key))
  );
*/

const FormGroup = ({ children, ...props }) =>
  h("div", { className: gClass, ...props }, ...children);

const TickBox = ({ type, name, id, label, value = label, ...props }) => {
  //const className = `custom-control custom-${type} custom-control-inline`;
  return h(
    "div",
    { className: checkClass, key: id },
    h("input", { id, name, className: checkiClass, type, value, ...props }),
    h("label", { htmlFor: id, className: checklClass }, label)
  );
};

const TickBoxes = ({ type, name, id, key, label, options, ...p }) => {
  type = type || "checkbox";
  const xChildren = options.map((cProps, i) =>
    TickBox({ type, name, ...p, id: id + "_" + i, ...cProps })
  );

  return FormGroup({
    key,
    children: [label && h("label", null, label), h("div", null, ...xChildren)],
  });
};

const SelectBox = ({ name, key, label, options, ...props }) => {
  delete props.type;
  const xChildren = options.map(({ label, value = label, ...cProps }, i) => {
    const id = props.id + "_" + i;
    return h("option", { id, key: id, value, ...cProps }, label);
  });

  return FormGroup({
    key,
    children: [
      label && h("label", { htmlFor: props.id }, label),
      h("select", { name, ...props }, ...xChildren),
    ],
  });
};

const inferType = (name) => {
  name = name.toLowerCase();
  const types = ["number", "email", "textarea", "submit"];
  return types.find((v) => name.includes(v)) || "text";
};

const Text = ({ name, type = inferType(name), key, label, ...props }) => {
  //type = type || inferType(name);
  const children =
    type === "submit"
      ? [h("input", { type, name, ...props, value: label, className: bClass })]
      : [
          label && h("label", { htmlFor: props.id }, label),
          type === "textarea"
            ? //? h("textarea", { name, ...omit(["value"])(props) }, props.value)
              h("textarea", { name, ...props })
            : h("input", { type, name, ...props }),
        ];
  return FormGroup({ key, children });
};

/**
 * High level user input field inc optional label, multiple options etc
 * @param {Object} f field proerties
 * @param {string} [f.ezTitle] meaningful title for field
 * @param {string} [f.label = f.ezTitle] label element
 * @param {string} [f.placeholder = f.ezTitle] placeholder property
 * @param {string} [f.name] name property - defaults to ezTitle or placeholder snake-cased
 * @param {string} [f.prefix = ""] id prefix (eg. for use if >=2 forms on page)
 * @param {string} [f.id = f.prefix + f.name] id for input/textarea; number will be suffixed  for options' ids
 * @param {string} [f.type] html input type or "textarea"
 * @param {Object[]} [f.options] array of options' attributes for checkbox, radio and select
 * @param {Object} [f....props] other dom/vdom/react attributes
 * @return {Object} vnode with optional label
 *
 * @example Field({placeholder: "email"})
 *
 * @example Field({placeholder: "email", style:{color: red}})
 *
 * @example <Field placeholder="email"></Field>
 *
 */
const Field = ({
  ezTitle,
  label = ezTitle,
  placeholder = ezTitle,
  name = (ezTitle || placeholder).replace(/ /g, "_").toLowerCase(),
  prefix = "",
  id = prefix + name,
  key = id,
  options,
  ...props
}) => {
  const className = cClass;
  const commonProps = { ...props, label, key, id, name };
  if (!options) return Text({ ...commonProps, placeholder, className });
  return props.type === "select"
    ? SelectBox({ ...commonProps, options, className })
    : TickBoxes({ ...commonProps, options });
};

/**
 * Creates form element
 * @param {Object} f Form properties
 * @param {function} [f.onSubmit] callback will receive object containing values entered after submit suppressed
 * @param {function} [f.children] child elements (as in React for example)
 * @param {function} [f....props] form properties/attributes
 * @return {Object} vnode
 *
 * @example
 *
 *     <Form onSubmit={({email}) => console.log({email})}>
 *        <Field placeholder="email"> </Field>
 *     <Form>
 */

const Form = ({ onSubmit, children, ...props }) => {
  if (onSubmit)
    props.onSubmit = (e) => {
      e.preventDefault();
      onSubmit(parseFormInputs(e.target));
    };

  return h("form", props, ...children);
};

/** Expand field definition from shorthand: simple string, or array
 *
 * @param {string|array|Object} field shorthand field definition
 * @return {Object} field definition as an argument for Field()
 *
 */
const expandShorthand = (field) => {
  let f = field;
  if (typeof f === "string") return { ezTitle: f };
  if (Array.isArray(f)) f = { ezTitle: f[0], options: f[1] };
  if (Array.isArray(f.options))
    return {
      ...f,
      options: f.options.map((option) =>
        typeof option === "string" ? { label: option } : option
      ),
    };
  return f;
};

/** Parse form values, eg for onSubmit. Exposed for testing purposes.
 *
 * @param {Object} elements event.target
 * @return {Object} event target values in a convenient form
 *
 * @example
 *
 * example return value:
 *
 * {
 *  "inputs": {
 *   "text_input_with_value": "quick brown fox",
 *   "textarea": "quick brown fox jumped.\nYes he did.\n",
 *   "checkboxes": [
 *     "Apple"
 *   ],
 *   "radios": "Pear",
 *   "select_fruit": [
 *     "apple",
 *     "Pear"
 *   ]
 *  }
 * }
 */
const parseFormInputs = ({ elements }) => {
  const result = {};
  for (const { type, name, checked, selectedOptions, value } of elements) {
    if (type === "select-multiple") {
      result[name] = [];
      for (const { value } of selectedOptions) result[name].push(value);
    } else if (type === "checkbox") {
      if (!result[name]) result[name] = [];
      if (checked) result[name].push(value);
    } else if (type !== "submit" && (!(type === "radio") || checked)) {
      result[name] = value;
    }
  }
  return result;
};

export { Field, Form, expandShorthand, parseFormInputs };