# Form builder

Source: https://signy.dev/examples/form-builder/

Fields from a config, sections you add and remove, and the data as JSON.

The fields come from a plain config array. The data lives in a store, and each address is one entry of an array in it, so adding or removing one renders only that section. A field shows only when its condition holds, and the preview prints the store as JSON.

## Preview

[Open the page on its own](https://signy.dev/examples/form-builder/demo/index.html.md)

## Code

Save it as an `.html` file and open it in a browser. It loads Signy and Iris CSS from the CDN, so there is nothing to install or build.

**Show the full page**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="color-scheme" content="light dark">
    <title>Form builder - Signy example</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/iriscss@1/dist/iris.min.css">
    <script src="https://cdn.jsdelivr.net/npm/signy@1/dist/signy.global.min.js"></script>
  </head>
  <body>
    <main class="container" id="app"></main>

    <script>
      const { Component, application, html, store, produce, For, Show, Switch, Match } = Signy;

      // The form is data. A field with a when function shows only while it returns true.
      const FIELDS = [
        { name: "name", label: "Full name", type: "text" },
        { name: "contact", label: "How should we reach you?", type: "select", options: ["Email", "Phone"] },
        { name: "email", label: "Email", type: "email", when: (data) => data.contact === "Email" },
        { name: "phone", label: "Phone", type: "tel", when: (data) => data.contact === "Phone" },
        { name: "newsletter", label: "Send me the newsletter", type: "checkbox" },
      ];

      const ADDRESS_FIELDS = [
        { name: "street", label: "Street", type: "text" },
        { name: "city", label: "City", type: "text" },
        { name: "postcode", label: "Postcode", type: "text" },
      ];

      let nextId = 2;
      const emptyAddress = (id) => ({ id, street: "", city: "", postcode: "" });

      class FormBuilder extends Component {
        form = store({
          name: "",
          contact: "Email",
          email: "",
          phone: "",
          newsletter: false,
          addresses: [emptyAddress(1)],
        });

        // One input for a field config. write puts the new value in the store.
        input(field, id, initial, write) {
          return Switch({
            fallback: html`
              <label for=${id}>${field.label}</label>
              <input id=${id} type=${field.type} value=${initial} onInput=${(e) => write(e.currentTarget.value)} />
            `,
            children: [
              Match({
                when: field.type === "select",
                children: () => html`
                  <label for=${id}>${field.label}</label>
                  <select id=${id} onChange=${(e) => write(e.currentTarget.value)}>
                    ${(field.options ?? []).map((option) => html`<option selected=${option === initial}>${option}</option>`)}
                  </select>
                `,
              }),
              Match({
                when: field.type === "checkbox",
                children: () => html`
                  <label>
                    <input type="checkbox" checked=${initial} onChange=${(e) => write(e.currentTarget.checked)} />
                    ${field.label}
                  </label>
                `,
              }),
            ],
          });
        }

        // Looked up on each write, since removing an address moves the ones after it
        indexOf(id) {
          return this.form.state.addresses.findIndex((address) => address.id === id);
        }

        addAddress() {
          this.form.set("addresses", produce((addresses) => {
            addresses.push(emptyAddress(nextId++));
          }));
        }

        removeAddress(id) {
          this.form.set("addresses", (addresses) => addresses.filter((address) => address.id !== id));
        }

        render() {
          return html`
            <h1>Form builder</h1>
            <div class="switcher">
              <form onSubmit=${(e) => e.preventDefault()}>
                ${For({
                  each: FIELDS,
                  keyBy: "name",
                  children: (field) => Show({
                    when: () => !field.when || field.when(this.form.state),
                    children: html`
                      <div>${this.input(field, field.name, this.form.state[field.name], (value) => this.form.set(field.name, value))}</div>
                    `,
                  }),
                })}

                ${For({
                  each: () => this.form.state.addresses,
                  keyBy: "id",
                  children: (address, index) => html`
                    <fieldset>
                      <legend>Address ${() => index() + 1}</legend>
                      ${ADDRESS_FIELDS.map((field) => html`
                        <div>${this.input(field, `${field.name}-${address.id}`, address[field.name],
                          (value) => this.form.set("addresses", this.indexOf(address.id), field.name, value))}</div>
                      `)}
                      <button type="button" class="small outlined danger"
                        disabled=${() => this.form.state.addresses.length === 1}
                        onClick=${() => this.removeAddress(address.id)}>Remove this address</button>
                    </fieldset>
                  `,
                })}
                <button type="button" class="secondary outlined" onClick=${() => this.addAddress()}>Add an address</button>
              </form>

              <section>
                <h2>Data</h2>
                <pre><code>${() => JSON.stringify(this.form.state, null, 2)}</code></pre>
              </section>
            </div>
          `;
        }
      }

      application(FormBuilder).mount("#app");
    </script>
  </body>
</html>

```

## What it uses

- [Store](https://signy.dev/documentation/1/reactivity/store/index.html.md) with path writes and `produce()`
- [For](https://signy.dev/documentation/1/templates/for-and-index/index.html.md) with `keyBy` for the sections
- [Show](https://signy.dev/documentation/1/templates/show/index.html.md) for conditional fields
- [Switch and Match](https://signy.dev/documentation/1/templates/switch-and-match/index.html.md) to pick the input for each field type
