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.
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.
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>
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>
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>
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!'))
}
}
<form>
element. Inside it we listen for the 'submit'
event by setting the
onsubmit
attribute.'submit'
event. This will fire whenever a user
clicks the type="submit"
button (or an equivalent action).'submit'
event, we need to disable the
form's default behavior.onsubmit
function fires, we select the form element.<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!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.
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!'))
}
}
'submit'
event. This will fire whenever a user
clicks the type="submit"
button (or an equivalent action).<form>
element, and extract all of its data into a
FormData
instance.application/json
, so we create a new Headers
object that we can later attach to our fetch()
call.FormData
instance to an Object
. This means
iterating over it, and copying each key-value pair.Object
, we can convert it into JSON using
JSON.stringify
.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
.
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!
This restricts selection to only certain filetypes too.
<input type="file" name="pic" id="pic" accept="image/gif, image/jpeg" />
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>
`
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.
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.
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 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 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 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 are Choo's rendering abstraction. It's the part that takes the internal state and renders elements to the DOM.