Forms

Websites generally consist of 3 main elements: paragraph text, lists and forms. While paragraph text is generally straight forward to place on a page, lists & forms require some more work. This section explains everything you need to know to work with forms in Choo.

This guide assumes you're working from a project generated by create-choo-app. This allows us to write inline CSS, which makes examples a little simpler to read. However our goal is to provide you with knowledge that translates to any setup - even if you don't end up using Choo.

How do forms work?

Before we dive into how forms work in Choo, let's dive into how forms work on pages that don't use any JavaScript. A lot of the web was designed to work without JavaScript, and to submit forms, you don't need any JS at all. Knowing what the default behavior of forms is allows us to build on top of it, rather than trying to rewrite functionality that's already available to us.

The Form Element

Forms are declared using the <form> tag. By themselves they don't do much, but they have a few important attributes that are good to know about.

The first attribute is method="". This tells the form which HTTP method to use. By default it's set to POST, so it's often not necessary to define this.

The second attribute is action="". This attribute tells the form where to redirect the page to when the submission was successful.

Forms also always should have an id="" attribute on them. This makes them easier to debug, and shows up in the payload that's sent to the server.

<form id="login" action="/dashboard">
</form>

The Input Element

Forms need data. And <input> elements provide that data. As a rule, each <input> element has a type="" attribute, and an accompanying <label> element. They also need a name="" and an id="". That's quite a bit of data required. But together it allows you to create a wide range of input.

<form id="login" action="/dashboard">
  <label for="username">username</label>
  <input id="username" name="username" type="text">
  <label for="password">password</label>
  <input id="password" name="password" type="password">
  <input type="submit" value="Login">
</form>

Validating Input

Forms come with a wide range of validation built in. Probably the biggest benefit is that it works on all platforms, with little effort. It respects user settings such as font-size, and supports screen readers out of the box.

To validate the form's input fields, there's a few attributes we can use:

  • pattern - validate the form's input field using a Regular Expression. For example pattern="^.{1,15}$" makes sure strings have a length of at least 1, and not more than 15.
  • required - make sure that the field is filled in, and valid.
  • title - the message to display if the pattern attribute is invalid. This is useful for everyone that can't read RegExes in their error messages.

Together these allow you to express a wide range of validation, and make sure your forms are filled in correctly and are accessible. Let's see what that that looks like:

<form id="login" action="/dashboard">
  <label for="username">
    username
  </label>
  <input id="username" name="username"
    type="text"
    required
    pattern=".{1,36}"
    title="Username must be between 1 and 36 characters long."
  >
  <label for="password">
    password
  </label>
  <input id="password" name="password"
    type="password"
    required
  >
  <input type="submit" value="Login">
</form>

Handling Form Submissions As Multipart

So far we've seen how to create basic HTML forms with validation. This is a great starting point, but we'll often want to control submissions using JavaScript.

Perhaps we can pre-populate some input fields. Perhaps there are input fields that rely on the values of other input fields. Starting off with JS from the start allows us to change the behavior without needing to change the architecture.

Creating forms with Choo is almost identical to basic HTML. The main difference is that we create a 'submit' event handler, and we control sending the data using window.fetch().

Let's create a form that sends data down as multipart/form-data. We'll talk about how to submit it as JSON in the next section.

var html = require('choo/html')
var choo = require('choo')

var app = choo()
app.route('/', main)
app.mount('body')

function main () {                                                    // 1.
  return html`
    <body>
      <form id="login" onsubmit=${onsubmit}>
        <label for="username">
          username
        </label>
        <input id="username" name="username"
          type="text"
          required
          pattern=".{1,36}"
          title="Username must be between 1 and 36 characters long."
        >
        <label for="password">
          password
        </label>
        <input id="password" name="password"
          type="password"
          required
        >
        <input type="submit" value="Login">
      </form>
    </body>
  `

  function onsubmit (e) {                                              // 2.
    e.preventDefault()                                                 // 3.
    var form = e.currentTarget                                         // 4.
    var body = new FormData(form)                                      // 5.
    fetch('/dashboard', { method: 'POST', body })                      // 6.
      .then(res => {
        if (!res.ok) return console.log('oh no!')
        console.log('request ok \o/')
      })
      .catch(err => console.log('oh no!'))
  }
}
  1. We create a basic Choo app, and a single view that renders a <form> element. Inside it we listen for the 'submit' event by setting the onsubmit attribute.
  2. We create a handler for the 'submit' event. This will fire whenever a user clicks the type="submit" button (or an equivalent action).
  3. Before we can handle the form's 'submit' event, we need to disable the form's default behavior.
  4. When the onsubmit function fires, we select the form element.
  5. Now that we have the <form> element, we can extract all values using window.FormData(). It gives us back a special object containing all the form data that we can directly pass to the fetch() API. It even works in all browsers!
  6. Now that we have our data, we can make a request to the server. We send it as an HTTP POST method, and attach the body. Depending on the result, it will now either succeed or fail.

note: There's a difference between e.target and e.currentTarget. e.target gives you the DOM node the event was triggered from. Where e.currentTarget gives you the node the event listener was attached to. Because we need a reference to the <form> element, using e.currentTarget is the right choice here.

Handling Form Submissions as JSON

While traditional APIs might work with multipart/form-data, using JSON is much more convenient. Parsing JSON is built into almost every language, and there's a wide range of tools available to validate it on the server.

So unless you're uploading files inside forms, it can pay off to use JSON instead.

var html = require('choo/html')
var choo = require('choo')

var app = choo()
app.route('/', main)
app.mount('body')

function main () {
  return html`
    <body>
      <form id="login" onsubmit=${onsubmit}>
        <label for="username">
          username
        </label>
        <input id="username" name="username"
          type="text"
          required
          pattern=".{1,36}"
          title="Username must be between 1 and 36 characters long."
        >
        <label for="password">
          password
        </label>
        <input id="password" name="password"
          type="password"
          required
        >
        <input type="submit" value="Login">
      </form>
    </body>
  `

  function onsubmit (e) {                                               // 1.
    e.preventDefault()
    var form = e.currentTarget
    var data = new FormData(form)                                       // 2.
    var headers = new Headers({ 'Content-Type': 'application/json' })   // 3.
    var body = {}
    for (var pair of data.entries()) body[pair[0]] = pair[1]            // 4.
    body = JSON.stringify(body)                                         // 5.
    fetch('/dashboard', { method: 'POST', body, headers })              // 6.
      .then(res => {
        if (!res.ok) return console.log('oh no!')
        console.log('request ok \o/')
      })
      .catch(err => console.log('oh no!'))
  }
}
  1. We create a handler for the 'submit' event. This will fire whenever a user clicks the type="submit" button (or an equivalent action).
  2. We select the <form> element, and extract all of its data into a FormData instance.
  3. We need to send data as application/json, so we create a new Headers object that we can later attach to our fetch() call.
  4. We need to convert the FormData instance to an Object. This means iterating over it, and copying each key-value pair.
  5. Now that we have a regular Object, we can convert it into JSON using JSON.stringify.
  6. With our body and headers ready, we can send a POST request down to a server.

Note: perhaps you're thinking to yourself this might be a lot of typing, and you wouldn't be wrong! We wanted to show you what it's like to make requests using only DOM APIs. If you're planning to use this to write applications, it's probably best to use small abstractions to POST data, and convert <form> elements into JSON.

Uploading files

This is a hard thing to write about. Uploading files is similar to multipart/form-data, but usually requires some extra features - such as overriding the native form controls, showing upload progress, and validation strategies. There's quite a bit to cover here.

Instead of covering everything, we're going to share a few useful snippets. Because of time constraints, we can't quite write a full section about this yet. But we hope this is enough to help you on your way. Contributions would be very welcome!

Only allow certain filetypes

This restricts selection to only certain filetypes too.

<input type="file" name="pic" id="pic" accept="image/gif, image/jpeg" />
  • https://stackoverflow.com/questions/181214/file-input-accept-attribute-is-it-useful
  • https://stackoverflow.com/questions/7575482/restrict-file-upload-selection-to-specific-types

Create a hidden file button

var html = require('choo/html')
var css = require('sheetify')

css`
  .button {
    border: 2px solid gray;
    color: gray;
    background-color: white;
    padding: 8px 20px;
    border-radius: 8px;
    font-size: 20px;
    font-weight: bold;
  }

  .button-wrapper {
    position: relative;
    overflow: hidden;
    display: inline-block;
  }

  .button-wrapper input[type=file] {
    font-size: 100px;
    position: absolute;
    left: 0;
    top: 0;
    opacity: 0;
  }
`

var element = html`
  <div class="button-wrapper">
    <button class="button">Upload a file</button>
    <input type="file" name="some-file">
  </div>
`
Forms

Websites generally consist of 3 main elements: paragraph text, lists and forms. While paragraph text is generally straightforward to place on a page, lists and forms require some more work. This section explains everything you need to know to work with forms in Choo.

Networking

Connecting to the network is essential for applications. This section is all about the browser's network APIs, and how to use them in Choo.

Routing

Choo is built up out of two parts: stores and views. In order to render a view, it must be added to the application through app.route(). This is the router.

Server Rendering

Server rendering is an excellent way to speed up the load time of your pages. This section shows how to effectively render Choo apps in Node.

State Machines

State machines are a great way to manage different states in your application. In this section we'll learn how to use and implement state machines.

Stores

Stores are Choo's data abstraction. They're meant to both hold application data, and listen for events to change it. In traditional systems this is sometimes also known as "models".

Views

Views are Choo's rendering abstraction. It's the part that takes the internal state and renders elements to the DOM.

Made with 🚂 in Saigon, Tokyo, Berlin
By Yosh & friends