Web Apps

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();
1
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);
1

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);
1
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' },
});
1
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);
}
1
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
  }
}
1
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);
1
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());
}
1
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)
1
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;
}
1
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;
}
1
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)
1
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';
1
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 */
}
1
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()
}
1
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
Last Updated: 6/11/2023, 2:31:24 PM