14.1 Forms and Page Transitions
# Working with Forms
In your Web Design Course you have learned how to create forms with HTML and style them with CSS. We have talked about JSON and FormData objects in this course. FormData objects are a JavaScript representation of what you would be sending to the webserver if you submitted the HTML form, without JS.
A quick way to create a JavaScript FormData
object from a form is to pass a reference to the form to the FormData
constructor.
Remember
All values
that come from any web form are going to be strings. It doesn't matter if the value is numeric or a boolean. If you are using a value
property then you get a String. If you need a
number then you have to do the conversion with Number()
or parseInt()
or parseFloat()
.
Here is an overview of how to interact with HTML forms using JavaScript. It includes discussions on submitting and resetting forms, checkboxes and radio buttons, the input
and change
and select
events, and more.
A select
element will have a value
property as well as a selectedIndex
property. The value
is the value of the <option>
picked by the user. The selectedIndex
is the numeric index of the
chosen <option>
element inside the select
.
If you would rather or need to work with JSON
instead of a FormData
object, this video talks about building a JavaScript Object from the data in your form.
Once you have a JS object you can then call JSON.stringify()
on that object to convert it to a JSON
string that can be uploaded with a fetch
call.
See the notes in Module 11.2 for more details on uploading data with fetch
.
Here is a refresher on Keyboard
Events.
A quick video talking about how you can properly test for an empty input field.
These next two videos explain modern web form validation in depth and in detail.
And finally, how to enable and disable autocomplete features in web forms.
MDN Guide to Web Forms (opens new window)
# Dispatching Events
We have discussed and used many events. Did you know that some events also have matching methods that you can call?
//submit a form
document.querySelector('#myForm').submit();
//reset a form
document.querySelector('#myForm').reset();
//click a link
document.querySelector('a.someLink').click();
2
3
4
5
6
Not all events have a matching method and not all elements are able to run the methods.
There is also a way that you dispatch
and event. Basically, you can make an element think that an event has happened to it. And if that element has an event listener then it will be triggered. The
syntax to do this is actually very simple. It is a method that you can call on any DOM Element to dispatch the event. You just need to create an Event
object to pass to the method.
someElement.dispatchEvent(someEventObject);
The Event
object will have a type
property that indicates which type of event is happening to the referenced element.
//target the 3rd li in the sidebar
let someElement = document.querySelectorAll('.sideNav li')[2];
someElement.addEventListener('click', (ev) => {
//this will run when our event gets dispatched
console.log('I was just clicked... I think.');
});
//create a click event
let ev = new Event('click', {
bubbles: false,
cancelable: false,
composed: false,
});
//the options object is optional. This one shows the default values for all the properties
//now make the click happen to the <li>
someElement.dispatchEvent(ev);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Custom Events
What is a custom event? It is an event that does not already exist in the DOM or other HTML5 APIs. You might want an event that fires on your document
object telling you that the next set of items
have been fetched from an API. Maybe you aren't adding data to the screen as soon as it comes back from the server, but you want the document
object to know that data is ready to be loaded. Maybe
you want to create custom events that fire during css animations or transitions. Whatever the event, you can create it. You really just create an Event
and give it a name.
As long as your object is able to call addEventListener
then it can know that the event was triggered.
There are a few ways that you can create your own custom events too. Here are the first two.
//create a basic event with a name
let ev1 = new Event('beerOpened');
//create a custom event that can contain data that will be part of the event object
let ev2 = new CustomEvent('beerOpened', {
detail: { today: new Date(), brand: 'Corona' },
});
2
3
4
5
6
If you use the CustomEvent
constructor, the second parameter is an object that must have a detail
property. The detail property can contain whatever you want. This is the custom data that will be
available through the event when addEventListener
calls the listener function.
let p = document.querySelector('main p');
p.addEventListener('beerOpened', displayBeer);
//listen for the string name (type) of the event
p.dispatchEvent(ev2);
//use the event object to dispatch, not the string type name of the event
function displayBeer(ev) {
let span = document.createElement('span');
span.textContent = `A ${ev.detail.brand} was opened at ${ev.detail.today}`;
p.append(span);
}
2
3
4
5
6
7
8
9
10
11
All the properties inside the Event
must be put inside detail
.
# Custom Events with Class Syntax
Another way of creating a custom event is to use the class
syntax and extend
the base Error object.
class ScreamEvent extends Event {
constructor(prop) {
super('scream'); //create a basic Event object that sets the type as 'scream'
this.detail = prop; //create a custom property for our Event
}
}
2
3
4
5
6
And then we can add our ScreamEvent
to objects that have the addEventListener
method.
//create a ScreamEvent
let sev = new ScreamEvent({ msg: 'hi' });
//add a listener for the ScreamEvent on the document object
document.addEventListener('scream', (ev) => {
console.log('screamed', ev.detail.msg);
});
//dispatch the ScreamEvent
document.dispatchEvent(sev);
2
3
4
5
6
7
8
# View Transition API
The new View Transition API is currently supported in Chrome, Edge, and Opera. Hopefully, in the near future Firefox and Safari will also start to support it too. In the meantime, you can use these as a progressive enhancement.
The initial version of the API works only on View Transitions between states on the current page. In other words, this is intended to be used for Single Page Applications. As the user clicks on buttons and links and your script changes what is being displayed on the page, this API will be able to create transitions between the starting and ending state of the page.
Future Versions
Later versions of the API are slated to include the ability to create transitions between different pages.
# Checking for Feature Support
Whenever you use something as a progressive enhancement you need to test for browser support first. Call a function that will start the process of updating the interface of your SPA. Check if the
browser supports the method document.startViewTransition
. Whether it does or not you will end up calling your function to make the actual changes to the DOM.
If the browser does support the startViewTransition
method then you call that and pass it your function for updating the DOM.
function doSomeNavigation() {
// check for support for the startViewTransition method
if (!document.startViewTransition) {
//this code runs for browsers that do NOT support it yet
//call your method that will make changes to the page
updateTheDOMSomehow();
//exit the function if there was no support
return;
}
// With a transition:
document.startViewTransition(() => updateTheDOMSomehow());
}
2
3
4
5
6
7
8
9
10
11
12
13
What the startViewTransition
method does is take a snapshot of the page just before your DOM update function is called. It monitors all the changes that would be made by your method and then creates
the visual transition between the starting state and the ending state.
# CSS Pseudo Elements
To help with the visual changes, in addition to the snapshot, the startViewTransition
method will create the following pseudo-elements.
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
2
3
4
5
The ::view-transition
sits in an overlay, over everything else on the page. This is useful if you want to set a background color for the transition.
::view-transition-old(root)
is a screenshot of the old view, and ::view-transition-new(root)
is a live representation of the new view. Both render as CSS 'replaced content' (like an <img>
element).
The old view animates by default from opacity: 1
to opacity: 0
, while the new view animates from opacity: 0
to opacity: 1
, creating a cross-fade. All of the animation is performed using CSS
animations, so they can be customized with CSS.
You can use the pseudo classes in your own CSS file to control the transition from the old state to the new state. See below for an example.
@keyframes fade-in {
from {
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}
@keyframes slide-from-right {
from {
transform: translateX(30px);
}
}
@keyframes slide-to-left {
to {
transform: translateX(-30px);
}
}
::view-transition-old(root) {
animation: 90ms linear both fade-out, 300ms linear both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms linear 90ms both fade-in, 300ms linear both slide-from-right;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Transitioning Multiple Elements
If you want to animate different parts of the interface with different transitions then we can add a view-transition-name
property for each DOM area that will be animated. Each part needs to have a
unique name.
.masthead {
view-transition-name: heading;
}
.content {
view-transition-name: content;
}
2
3
4
5
6
This will create a pseudo element tree for the DOM like this.
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(heading)
│ └─ ::view-transition-image-pair(heading)
│ ├─ ::view-transition-old(heading)
│ └─ ::view-transition-new(heading)
└─::view-transition-group(content)
└─ ::view-transition-image-pair(content)
├─ ::view-transition-old(content)
└─ ::view-transition-new(content)
2
3
4
5
6
7
8
9
10
11
12
13
The heading
and content
view-transition-name
values will target specific parts of the page. And the ::view-transition-image-pair(root)
will cover everything else on the page outside of the two
named areas.
The names can also be added through JavaScript, if needed.
const header = document.querySelector('.heading');
header.style.viewTransitionName = 'heading';
2
# Asynchronous Transitions
There will sometimes be transitions where you have an element that exists on the screen in one state but not in the other. Sometimes the element will only exist on the new state and sometimes only on the old state.
To handle situations like this we can add the ::only-child
pseudo-class to the element. This will target the element when it only exists in one of the two states in the pair.
::view-transition-new(heading):only-child {
animation: 300ms linear both fade-in; /* entry transition */
}
::view-transition-old(heading):only-child {
animation: 300ms linear both fade-out; /* exit transition */
}
2
3
4
5
6
# JavaScript Control
So, moving beyond the CSS part, there are a few JavaScript Promise-based properties which resolve and a method that can be accessed on the ViewTransition
object created by calling the
startViewTransition
method.
function someMethod() {
if (!document.startViewTransition) {
updateTheDOM();
return;
}
const viewTransition = document.startViewTransition(updateTheDOM);
//viewTransition.ready.then()
//viewTransition.finished.then()
//viewTransition.updateCallbackDone.then()
//viewTransition.skipTransition()
}
2
3
4
5
6
7
8
9
10
11
The viewTransition.updateCallbackDone
Promise resolves when the function called by startViewTransition()
is complete and its Promise resolves and creates the viewTransition
object.
The viewTransition.ready
Promise resolves after startViewTransition
has taken the snapshot and finished building the pseudo-element tree for the transition.
The viewTransition.finished
Promise resolves when the transitions created by the call to updateTheDOM
has completed.
The viewTransition.skipTransition()
method, if called, will skip over any transition animations that are referenced by the pseudo-element styles - ::view-transition-new()
,
::view-transition-old()
, and ::view-transition-group()
. The updateTheDOM
method still gets called and changes are still made to the DOM. It's just those transitions that are skipped.
# What to do this week
TODO Things to do before next week.
- Read all the content from
Modules 14.1, 14.2
. - Work on Final Project