这是我根据百度前端技术学院的问题需求设计的表单生成器,写的还有很多不足的地方,望各位前辈指点。如果你有好的解决方案欢迎在评论区留言。

这是问题中给的以JavaScript对象形式表示的的配置(仅供参考)

{
    label: '名称',                    // 表单标签
    type: 'input',                   // 表单类型
    validator: function () {...},    // 表单验证规
    rules: '必填,长度为4-16个字符',    // 填写规则提示
    success: '格式正确',              // 验证通过提示
    fail: '名称不能为空'               // 验证失败提示
}

但是我觉的这个配置还有不足的:

  • input还有很多类型textpassword等,如何区分各种input呢?
  • 如果typeselecttextarea,它们不像input是一个空元素,它还有子元素,那么子元素该怎么表示呢?
  • 元素上应该还有一些其他的重要的属性如name, value, min, max,multiple,id等,这些属性也没有表示。
  • successfail字段可以去掉,只需要规则字段就可以了。这里的rules我们假设只有一个规则,使用单数形式rule

根据上面所发现的问题,我们重新设计了配置文件

{
    label: '名称',                    // 表单标签
    type: 'input',                   // 表单类型
    attributes: {},                    // 表单的各项属性
    children: [],                       // 表单的子元素
    validator: function () {...},    // 表单验证规
    rule: '必填,长度为4-16个字符',    // 填写规则提示
}

我们表单元素的形式可以有以下几种:
div > label + input/select/textarea + rule
其中select比较复杂的,因为它还可以包含子元素optionoptgroup,optgroup又可以包含option

下面是生成表单函数的实现,根据配置信息生成表单元素,主要分以下两步:

  • 创建并设置dom调用createElement或createTextNode生成dom

    • 设置dom的各项属性
    • 递归调用,生成子元素(如果需要的话)
    • 添加节点
  • 添加交互

    • 主要是验证输入是否符合规则
// generateFormElement.js
const genrateFormElement = (config) => {
  if (typeof config === "string") {
    const textNode = document.createTextNode(config);
    return textNode;
  }

  let wrapper = null,
    label = null,
    formElem = null,
    ruleElem = null;
  if (config.label) {
    wrapper = document.createElement("div");
    // 设置 label formElem 的排布方向 在一行排布 还是 在一列排布
    if (config.attributes.direction)
      wrapper.classList.add(config.attributes.direction);

    label = document.createElement("label");
    label.setAttribute("for", config.attributes.id);
    label.textContent = config.label;
  }

  formElem = document.createElement(config.type);

  for (const attr in config.attributes) {
    if (!Object.hasOwn(config.attributes, attr) || attr === "direction")
      continue;
    formElem.setAttribute(attr, config.attributes[attr]);
  }

  // 添加子元素,如果有的话。
  if (config.children) {
    for (const child of config.children) {
      formElem.append(genrateFormElement(child));
    }
  }

  if (config.rule) {
    ruleElem = document.createElement("span");
    ruleElem.textContent = config.rule;
  }
  // 将label, formElem, ruleElem包裹在wrapper中
  if (wrapper) {
    label && wrapper.append(label);
    formElem && wrapper.append(formElem);
    ruleElem && wrapper.append(ruleElem);
  }

  //   添加交互
  if (config.validator) {
    formElem.addEventListener("keyup", (event) => {
      if (config.validator(event.target.value)) {
        console.log("valid success");
        ruleElem?.classList.remove("valid-fail");
        ruleElem?.classList.add("valid-success");
      } else {
        console.log("valid fail");
        ruleElem?.classList.remove("valid-success");
        ruleElem?.classList.add("valid-fail");
      }
    });
  }

  return wrapper ? wrapper : formElem;
};

export default genrateFormElement;

手写配置(以后可以改成通过交互操作表单来设置配置信息),验证表单生成函数是否奏效

// index.js
import genrateFormElement from "./generateFormElement.js";

const form = document.querySelector("#generate-form");

/* 预期:
    <div class="row">
        <label id="name">姓名</label>
        <input name="name" id="name" required />
        <span>名字不能为空<span>
    </div>
*/
const config1 = {
  label: "姓名",
  type: "input",
  attributes: {
    name: "name",
    id: "name",
    direction: "row",
    required: true,
  },
  validator: function (value) {
    return value !== "";
  },
  rule: "名字不能为空",
};

const elem1 = genrateFormElement(config1);
form.append(elem1);

/*
    预期:
        <div class="column">
            <label id="preference">喜好</label>
            <select namme="preference"  id="preference" required>
                <option value="hiking">hiking</option>
                <option value="swimming">swimming</option>
                <option value="ping-pong">ping-pong</option>
            </select>
        </div>
*/
const config2 = {
  label: "喜好",
  type: "select",
  attributes: {
    name: "preference",
    id: "preference",
    direction: "column",
    required: true,
  },
  children: [
    {
      type: "option",
      attributes: {
        value: "hiking",
      },
      children: ["hiking"],
    },
    {
      type: "option",
      attributes: {
        value: "swimming",
      },
      children: ["swimming"],
    },
    {
      type: "option",
      attributes: {
        value: "ping-pong",
      },
      children: ["ping-pong"],
    },
  ],
};
const elem2 = genrateFormElement(config2);
form.append(elem2);

/*
    预期:
        <div class="column">
            <label for="all-preference">所有喜好</label>
            <select name="all-preference">
                <optgroup label="sports">
                    <option value="swimming">swimming</option>
                    <option value="running">running</option>
                </optgroup>
                <optgroup label="sports">
                    <option value="swimming">swimming</option>
                    <option value="running">running</option>
                </optgroup>
            </select>
        <div>
*/

const config3 = {
  label: "所有喜好",
  type: "select",
  attributes: {
    name: "all-preference",
    id: "all-preference",
    direction: "column",
    required: true,
  },
  children: [
    {
      type: "optgroup",
      attributes: {
        label: "sports",
      },
      children: [
        {
          type: "option",
          attributes: {
            value: "swimming",
          },
          children: ["swimming"],
        },
        {
          type: "option",
          attributes: {
            value: "running",
          },
          children: ["running"],
        },
      ],
    },
    {
      type: "optgroup",
      attributes: {
        label: "sports",
      },
      children: [
        {
          type: "option",
          attributes: {
            value: "swimming",
          },
          children: ["swimming"],
        },
        {
          type: "option",
          attributes: {
            value: "running",
          },
          children: ["running"],
        },
      ],
    },
  ],
};

/* 预期:
    <div class="column">
        <label id="comment">评论</label>
        <input name="comment" id="comment" required />
        <span>名字不能为空<span>
    </div>
*/
const elem3 = genrateFormElement(config3);
form.append(elem3);

const config4 = {
  label: "评论",
  type: "textarea",
  attributes: {
    name: "comment",
    id: "comment",
    direction: "column",
  },
};

const elem4 = genrateFormElement(config4);
form.append(elem4);样式的设置:* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

style设置:

html {
  block-size: 100%;
}

body {
  min-block-size: 100%;

  display: grid;
  place-content: center;
}
form {
  display: flex;
  flex-direction: column;
  gap: 1ch;
}

/* utils */
.row {
  display: flex;
  flex-direction: row;
  gap: 1ch;
}

.column {
  display: flex;
  flex-direction: column;
  gap: 1ch;
}

.valid-fail {
  color: red;
}

.valid-success {
  color: green;
}

结果截图:
image.png

codesandbox


peterhuan
681 声望590 粉丝