Skip to content

Commit

Permalink
Forms (#36)
Browse files Browse the repository at this point in the history
* init forms

* docs about base form validation.

* wip form submissions

* kinda sorta more form data

* last bit of formdata

* implement forms feedback

* merge form feedback

* use currentTarget
  • Loading branch information
yoshuawuyts authored Jan 9, 2018
1 parent c3ea6d1 commit 2235b8d
Show file tree
Hide file tree
Showing 3 changed files with 352 additions and 0 deletions.
312 changes: 312 additions & 0 deletions content/reference/forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
# 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 often it's not needed 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.

```html
<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.

```html
<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:

```html
<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 often we'll want to control submissions using
JavaScript.

Perhaps we can pre-populate some input fields. Perhaps there's 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.

```js
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`](https://developer.mozilla.org/en-US/docs/Web/API/Event/target) and
[`e.currentTarget`](https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget).
`e.target` gives you the DOM node the event was triggered from. Where
`e.currentTarget` gives you the event 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.

```js
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.
```html
<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
```js
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>
`
```
40 changes: 40 additions & 0 deletions examples/form-data/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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()
var body = new window.FormData(e.currentTarget) // 4.
window.fetch('/dashboard', { method: 'POST', body }) // 5.
.then(res => console.log('request ok!'))
.catch(err => console.log('oh no!', err))
}
}
Empty file added examples/form-data/server.js
Empty file.

0 comments on commit 2235b8d

Please sign in to comment.