Advanced Components

#Dispatching Events

The Component class extends the standard EventTarget class so components can dispatch (aka "fire" or "raise") events.

Here is a custom button component dispatching a "click" event:

// A component that raises "click" events
class MyButton extends Component   Component extends EventTarget 
{
    onClick()
    {
        this.dispatchEvent(new Event("click")); Raise event 
    }

    static template = {
        type: "button",
        text: "MyButton",
        on_click: c => c.onClick(),
    }
}

In some of the samples, like the one above, we've hidden some of the supporting code to help emphasize the point being made.

These snipped sections of code are indicated with the scissor icon:

To view the unabridged code open the sample in the Lab by clicking the Edit button.

#Async Data Loads

Components will often need to load data from external sources. This is commonly referred to as "async data loading" and often a spinner will be shown while the data is being fetched.

Since this is a common pattern for many components, the Component class includes a couple of helpers: a load method, a loading and loadError property and some associated events.

The load method does the following:

  1. sets the loading property to true
  2. clears the loadError property
  3. calls the invalidate method to mark the component for update
  4. dispatches a loading event
  5. calls and awaits the supplied callback
  6. catches and stores any thrown errors to the loadError property
  7. calls the invalidate method again
  8. dispatches a loaded event

For example:

class Main extends Component
{
    loadData()
    {
        this.load(async () => {
            // Load data here
}); } static template = [ { if: c => c.loading, Show spinner while loading type: "div .spinner", }, { else: true, Otherwise show loaded data $: [ // Display data here
] } ] }

Using this approach provides a couple of benefits:

  • The load method is async, so you can await it to know when the load has finished.
  • It's exception safe - any thrown errors are captured and the loading flag is cleared on success or failure.
  • It provides an easy mechanism for a component to know if data, an error or a spinner should be shown.
  • It's integrated with a broader "environment" loading mechanism so other interested parties (eg: server side rendering) can be notified when the entire page is loaded - more on this later.

The load method also supports a second parameter silent. Silent mode allows refreshing the data without showing spinners or other feedback:

  1. calls and awaits the supplied callback
  2. calls the invalidate method

#Component Life-Cycle

Components have the following life-cycle states:

  • Constructed: Immediately after the JavaScript object is constructed but before the DOM elements have been created.

  • Created: After the DOM elements have been created. Usually this happens automatically just before the component is mounted. If you need to access the DOM elements beforehand, call the create() method.

  • Mounted: After the component's DOM is attached to the document DOM. The component's onMount() method is called when it's mounted.

  • Unmounted: After a component is removed from the document DOM.
    The component's onUnmount() method is called when it's unmounted.

  • Destroyed: After a parent component that constructed this component no longer needs the component it will call the component's destroy() method which releases the DOM elements associated with the component.

In general a component should connect to external resources in the onMount() and disconnect from them in onUnmount(). These are the most reliable mechanism to know if a component is still in use.

A component will never be destroyed while it's mounted.

A destroyed component is effectively reset to the "constructed" state and can re-created if necessary.

#Listening to External Events

When a component needs to listen to external events care must be taken to ensure the event handlers are removed otherwise dangling references to the component may prevent it from being garbage collected by the JavaScript runtime.

The correct way to handle this is for the component to add event listeners when the component is mounted and remove them when the component is unmounted.

This can be done manually by overriding onMount() and onUnmount() but a simpler method is to use the Component.listen method.

listen(target, event, handler)

where:

  • target - any object that supports add/removeEventListener()
  • event - the event to listen for
  • handler - a handler function for the event.

The listen function automatically adds the event listener when the component is mounted and removes the event listener when the component is unmounted.

See listen() for more.

#Custom Templates

Normally components use the template declared by the static template property but this can be changed by overriding the onProvideTemplate() function.

class MyComponent extends Component
{
    // Called by Component to get the template to be compiled
    static onProvideTemplate()
    {
        return {
            // ... template definition ...
        }
    }
}

Consider, for example, a dialog class where every dialog has the same frame but different content in the main body.

class Dialog extends Component
{
// Override to wrap template in dialog frame static onProvideTemplate() { return { type: "dialog", class: "dialog", id: this.template.id, From the derived class template $: { type: "form", method: "dialog", $: [ { type: "header", $: this.template.title, From the derived class template }, { type: "main", $: this.template.content, From the derived class template }, { type: "footer", $: { type: "button", $: "Close", } }, ] } }; } }
class MyDialog extends Dialog extending Dialog, not Component { // This template will be "re-templated" by the base Dialog class // to wrap it in <dialog>, <form> etc... static template = { title: "My Dialog's Title", id: "my-dialog", content: { type: "p", $: "Hello World! This is the dialog's content", } } }

In the above example, anything not directly related to template handling has been omitted. Click the "Edit" link above to see the full code.

Notice how the enclosing dialog, form, header, main and footer elements are provided automatically by the base Dialog class, but the content of the header and main elements is provided by the derived MyDialog class

<dialog class="dialog" id="my-dialog">
    <form method="dialog">
        <header>
            My Dialog
        </header>
        <main>
            <p>Hello World</p>
        </main>
        <footer>
            <button>Close</button>
        </footer>
    </form>
</dialog>

#The domTree Property

A component's domTree is the object responsible for managing the DOM elements associated with the component.

The domTree is usually the object created by a compiled template and has methods to update the tree, get the root node of the tree etc...

The domTree is created on demand when the component is first mounted, but can be manually created by calling the component's create() method.

When a component is destroyed, its domTree is released, and the component reverts from a "created" state to a "constructed" state. Remounting the component, or calling its create() method again will create a new domTree.

#The setMounted Method

The setMounted method is an internal method used to notify a component and its template that it has been mounted or unmounted.

When a component's setMounted method is called, it calls onMount() or onUmount() method to notify the component of the new state. It then calls setMounted() on the component's domTree so the notification is reflected recursively through all domTrees.

You can override the setMounted method however it's extremely important that you also call super.setMounted(mounted) so all other nested components receive the notification.

When overriding onMount() and onUnmount() calling super.onMount() and super.onUnmount() isn't required (unless you're extending another class that expects these notification).