Technical JS Component Design Overview - NCIOCPL/ncids GitHub Wiki
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.
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
- 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.
-
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.
-
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.
- 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.
- Components will be defined as Classes This should allow for others to inherit, and extend our components.
- Components should be designed for extendability Components should be built to allow others to extend.
- 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.
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); }
- 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)
- 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 staticcreate
method
- the constructor must ensure that it replaces any existing instances that have been registered on
- 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.
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 {
// ...
}