# Sign up form

Source: https://signy.dev/examples/sign-up-form/

Validation that waits for you to leave a field, and a submit that waits for the server.

Each field keeps its value and whether it was touched in signals. Computed values hold the errors, so a message shows only after you leave the field or press submit. The fake server takes a second and rejects one user name, to show an error that comes back from it.

## Preview

[Open the page on its own](https://signy.dev/examples/sign-up-form/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>Sign up form - 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, Signal, Computed, application, html, Show, createRef } = Signy;

      // A pretend server: answers after a second and knows two user names already
      const TAKEN = ["ada", "admin"];

      function register(data) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            if (TAKEN.includes(data.username.toLowerCase())) {
              reject({ field: "username", message: "This user name is taken." });
            } else {
              resolve();
            }
          }, 1000);
        });
      }

      // 0 to 4, one point each for length, mixed case, a digit and a symbol
      function strength(password) {
        return [
          password.length >= 8,
          /[a-z]/.test(password) && /[A-Z]/.test(password),
          /\d/.test(password),
          /[^A-Za-z0-9]/.test(password),
        ].filter(Boolean).length;
      }

      const STRENGTH = ["Too weak", "Weak", "Fair", "Good", "Strong"];
      const FIELDS = ["username", "email", "password", "confirm"];

      class SignUp extends Component {
        values = Object.fromEntries(FIELDS.map((name) => [name, new Signal("")]));
        refs = Object.fromEntries(FIELDS.map((name) => [name, createRef()]));
        // A field shows its error only once you leave it or press submit
        touched = new Signal(new Set());
        serverErrors = new Signal({});
        busy = new Signal(false);
        done = new Signal(false);

        strength = new Computed(() => strength(this.values.password.value));

        errors = new Computed(() => {
          const value = (name) => this.values[name].value;
          return {
            username: !value("username")
              ? "Pick a user name."
              : !/^\w{3,20}$/.test(value("username"))
                ? "Use 3 to 20 letters, digits or underscores."
                : this.serverErrors.value.username,
            email: !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value("email")) ? "Enter an email address." : undefined,
            password: value("password").length < 8 ? "Use at least 8 characters." : undefined,
            confirm: value("confirm") !== value("password") ? "The passwords do not match." : undefined,
          };
        });

        touch(name) {
          this.touched.set((names) => new Set(names).add(name));
        }

        async submit(event) {
          event.preventDefault();
          this.touched.value = new Set(FIELDS);
          const errors = this.errors.value;
          const invalid = FIELDS.find((name) => errors[name]);
          if (invalid) {
            this.refs[invalid].current.focus();
            return;
          }
          this.busy.value = true;
          try {
            await register({ username: this.values.username.peek(), email: this.values.email.peek() });
            this.done.value = true;
          } catch (error) {
            this.serverErrors.value = { [error.field]: error.message };
            this.refs[error.field].current.focus();
          } finally {
            this.busy.value = false;
          }
        }

        field(name, label, type, autocomplete, hint) {
          const error = () => (this.touched.value.has(name) ? this.errors.value[name] : undefined);
          return html`
            <div>
              <label for=${name}>${label}</label>
              <input
                ref=${this.refs[name]}
                id=${name}
                name=${name}
                type=${type}
                autocomplete=${autocomplete}
                aria-describedby=${`${name}-help`}
                aria-invalid=${() => (error() ? "true" : null)}
                onInput=${(e) => {
                  this.values[name].value = e.currentTarget.value;
                  if (name === "username") this.serverErrors.value = {};
                }}
                onBlur=${() => this.touch(name)}
              />
              <small id=${`${name}-help`}>${() => error() ?? hint}</small>
            </div>
          `;
        }

        form() {
          return html`
            <form novalidate onSubmit=${(e) => this.submit(e)}>
              ${this.field("username", "User name", "text", "username", "Try ada to get an error from the server.")}
              ${this.field("email", "Email", "email", "email", "We only use it to sign you in.")}
              ${this.field("password", "Password", "password", "new-password", "At least 8 characters.")}
              <p>
                <meter min="0" max="4" low="2" high="3" optimum="4" aria-label="Password strength"
                  value=${() => this.strength.value}></meter>
                <small>${() => STRENGTH[this.strength.value]}</small>
              </p>
              ${this.field("confirm", "Repeat the password", "password", "new-password", "Type it once more.")}
              <button type="submit" class="primary" aria-busy=${() => String(this.busy.value)}
                disabled=${() => this.busy.value}>Create account</button>
            </form>
          `;
        }

        render() {
          return html`
            <article class="card">
              <header><h1>Create an account</h1></header>
              ${Show({
                when: () => this.done.value,
                fallback: this.form(),
                children: html`
                  <p class="alert success" role="status">
                    Welcome, ${() => this.values.username.value}! Your account is ready.
                  </p>
                `,
              })}
            </article>
          `;
        }
      }

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

```

## What it uses

- [Signals](https://signy.dev/documentation/1/reactivity/signals/index.html.md) per field and [Computed](https://signy.dev/documentation/1/reactivity/computed/index.html.md) errors
- [Refs](https://signy.dev/documentation/1/components/refs-and-forms/index.html.md) to focus the first invalid field
- `aria-invalid` and `aria-busy` bound to signals
- [Show](https://signy.dev/documentation/1/templates/show/index.html.md) for the messages
