Technical JS Component Design Overview - NCIOCPL/ncids GitHub Wiki

Technical: Javascript: Component Design Overview

Background

The USWDS uses the receptor library to support their progressive enhancement javascript. Unfortunately the implementation does not really allow for extending their Javascript, or even simply adding support for new variants of components. Take the Footer component for example. The code is hard-coded to use the usa-footer--big variant and is not configurable. For our footer we have defined our own variant (usa-footer--nci-big). Our current process is to define the class for the element as <bem-block-name> <bem-modifier-name> (e.g. usa-footer usa-footer--nci-big). Adding the usa-footer--big class would cause conflicts with our styles. Additionally the use of the receptor library approach is to add the "behaviors" to document.body and not find all elements. This makes it hard to interface with those elements wrapped by the behavior code. For example expanding all accordions on the page. This all has been discussed in the USWDS issues and discussions and will be looked into in the future.

Approach for Developing Components

Given the current state of USWDS Javascript and our current sprints it is evident that we must implement our own components. (Still using the USWDS syles and structures of course.) Components will be developed following the Guiding Principals below.

Guiding Principals

  1. We should be able to initialize components at any point in the page life-cycle. This can occur on DOMContentLoaded, or after we have dynamically added a new component to the page.
  2. Initialization of components should be atomic. The initialization of a component should work against a single element and not ALL elements on the page.
    • Providing additional methods to initialize multiple components based on a selector or array of elements does not conflict with this rule.
  3. Component state should be maintained separate from the HTML Element In the old days of jQuery we used to dirty up the HTMLElement by just adding various functions or data points. This approach is dirty and leads to shortcuts in interacting with the "owning" code that added those elements. As such, component state should be maintained separate from the HTMLElement.
    • Component code CAN add additional child elements or manipulate other DOM properties (classes/styles/aria attributes) for the purpose of displaying controls of the component or accessibility.
  4. Each component should provide a static mechanism to access previously initialized instances of an element There may be times where we have access to an HTMLElement that had previously be initialized. For example, a hash change event listener may need to find accordions on the page in order to expand a specific section. The listener should be able to find the HTMLElement in the DOM following standard mechanisms for finding elements. Then it should be able to pass this to a component's initialization code to get an instance of the Component without resetting the state.
  5. Components will be defined as Classes This should allow for others to inherit, and extend our components.
  6. Components should be designed for extendability Components should be built to allow others to extend.
  7. Components should have proper and complete documentation Given that others will most likely use these components, we should be able to generate documentation from our code.

Component Class Rules

In the future these should hopefully be turned into linter rules, typescript decorators, or a combination of the two. For now, we just need a set of checks.

  • A component class file should be named <thing>.component.ts
    • this is so we can have future flexibility in defining linter rules and reducing boilerplating
  • A component class file should only export the component class itself (e.g. YourComponent). The type for the component classes options (from here on out called YourComponentOptions, but you need a better name such as NCIFooterOptions), should be exported in its own file. See Component options below.
  • All class member variables should protected or private, not public.
  • You should have a static variable to define the default options named optionDefaults of type.
  • Each class should have a static create method. (AKA a static factory)
    • This is how users can get previously initialized instances of your components without reinitializing new instances.
    • This should look like
      public static create(element: HTMLElement, options: Partial<YourComponentOptions>): YourComponent {
        return (this._components.get(element) || new this(element, options));
      }
  • You must have a protected constructor that accepts element: HTMLElement, options: YourComponentOptions
    • the constructor must ensure that it replaces any existing instances that have been registered on _components
      • while typescript code will check the protection level, any vanillajs will not care and see it as a regular constructor. (which means it acts just like public)
        • we cannot check that the caller to the constructor is our static create method
      • We want it public because:
        • we do not want duplicate event handlers.
        • if we do not undo our attachments and only replace it in components we will have messy stuff in the dom too
      • So you should do the following BEFORE initializing the new instance to clean up the mess. If we find a better way to enforce the protected status in vanillaJS, then this will not be needed.
        if (YourComponent._components.has(element)) {
          element.unregister();
          YourComponent._components.delete(element);
        }
    • the constructor must end with YourComponent._components.set(element, this). This registers the newly created and initialized class with the previously initialized components for the static create method
  • Each class should have an public unregister method
    • remove the instance from _components
    • remove event handlers listeners
    • remove any DOM modifications (e.g. added html elements, aria tags, classes, etc)
  • All events should be bubbled up. No caller should know what DOM manipulations should be happening.

Component options

Most components should expect options passed in the create method. These properties will be required by the component, but optionally set by the user.

Define options with a type alias. Do not set properties as optional.

export type NCIComponentOptions {
  a: string;
  b: UtilComponentOptions;
};

type UtilComponentOptions {
  c: boolean;
};

Use the Partial utility type to set all of NCIComponentOptions properties to optional in the create method, constructor, and/or any other times the type should be constructed as optional.

protected constructor(element: HTMLElement, options?: Partial<NCIComponentOptions>) {
 // ...		
}

public static create(element: HTMLElement, options?: Partial<NCIComponentOptions>): NCIComponent {
  // ...		
}
⚠️ **GitHub.com Fallback** ⚠️