-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
c3ea6d1
commit 2235b8d
Showing
3 changed files
with
352 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
` | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.