Directory
- Welcome
Custom Component Architecture
The NST Component Library is built with a modular architecture that allows for easy customization and extension. Below are some of the essential concepts that you need to know to understand the component library.
Querying Components
Custom components are registered as custom elements with the browser. You can query them using querySelector. The returned value will only infer the elements type if the querySelector argument is the component name. Otherwise you will need to import the component type and pass it to the querySelector.
import type { CalendarElement } from '@/components/NST-components/UI/calendar/data';
// This will infer the typeconst calendarInfered = document.querySelector('calendar-wrapper');console.log(calendarInfered);
// This will not infer the type and needs the generic argumentconst calendarTyped = document.querySelector<CalendarElement>('#my-calendar');console.log(calendarTyped);
Base Component
All custom component classes extend the BaseComponent
class. This class implements the ready
event and property, an init
method, and componentName
property.
Ready State
To interact with any custom component you must wait until it’s class constructor has executed. Otherwise it’s methods and properties are undefined.
import { ready, type Ready,} from '@/components/NST-components/base';import type { CalendarElement } from '@/components/NST-components/UI/calendar/data';
//const calendar: CalendarElement | null = document.querySelector<CalendarElement>('#my-calendar');
if (calendar?.ready) { calendar.availableDateRange = { start: new Date('2024-01-01'), end: new Date('2024-01-05'), };}
// Typescript will throw an error if the element assigned to calendarBetter is not readyconst calendarBetter: Ready<CalendarElement> | null = await ready( document.querySelector<CalendarElement>('#my-calendar'));
if (calendarBetter) { calendarBetter.availableDateRange = { start: new Date('2024-01-01'), end: new Date('2024-01-05'), };}
// Another way to do itready(document.querySelector<CalendarElement>('#my-calendar')).then( (calendar) => { if (calendar) { calendar.availableDateRange = { start: new Date('2024-01-01'), end: new Date('2024-01-05'), }; } });
Ready Event
It is important to wait for a component to be initialized before interacting with it either through scripts or other components that depend on it.
The NST Components emit a ready
custom event and boolean property when they have been initialized. The components are typesafe so that you cannot access any of the custom element’s attributes or methods if you have not first checked that the component is ready.
import type { CalendarElement } from '@/components/NST-components/UI/calendar/data';
const calendar = document.querySelector<CalendarElement>('#my-calendar');
calendar?.addEventListener('ready', () => { if (calendar?.ready) { calendar.availableDateRange = { start: new Date('2024-01-01'), end: new Date('2024-01-05'), }; }});//The calendar might have been ready before we added the event listenerif (calendar?.ready) { calendar.availableDateRange = { start: new Date('2024-01-01'), end: new Date('2024-01-05'), };}
To make this simpler, there’s two helper functions that can be used to check if a component is ready.
whenReady
whenReady<T extends BaseInteractiveElementType>( element: T | null | undefined, callback?: (element: Ready<T>) => Promise<void> | void, options?: { timeout?: number }): Promise<void>
whenReady
takes three arguments:
- the element to be initialized,
- an optional callback function with the ready element as an argument,
- an optional timeout.
The callback function will be called when the element is ready. If the element is not ready after the timeout, it will throw an error. The timeout is only for the element being ready, not for the callback function to execute. The use case of having a callback optional is if you want to wait for the element to be ready but have no use for it after it’s ready.
import { whenReady } from '@/components/NST-components/base';import type { CalendarElement } from '@/components/NST-components/UI/calendar/data';
const calendar = document.querySelector<CalendarElement>('#my-calendar');
try { await whenReady( calendar, (calendar) => { calendar.availableDateRange = { start: new Date('2024-01-01'), end: new Date('2024-01-05'), }; }, { timeout: 1000, } );} catch (error) { console.error(error);}
ready
ready<T extends BaseInteractiveElementType>( element: T | null | undefined, options: { timeout?: number } = {}): Promise<Ready<T> | null>
ready
takes two arguments:
- the custom element to be initialized
- optional timeout.
It returns a promise that resolves with the element in its ready state. If the element is not ready after the timeout, or is not found, it will return null. ready
will not throw an error and does not need to be wrapped in a try catch block.
const calendar = await ready(document.querySelector<CalendarElement>('#my-calendar'), { timeout: 1000 });
Ready Type
The Ready
type is a type transformer to get the element type in its ready state as if it has been initialized.
import { ready, type Ready,} from '@/components/NST-components/base';import type { CalendarElement } from '@/components/NST-components/UI/calendar/data';
//const calendar: CalendarElement | null = document.querySelector<CalendarElement>('#my-calendar');
if (calendar?.ready) { calendar.availableDateRange = { start: new Date('2024-01-01'), end: new Date('2024-01-05'), };}
// Typescript will throw an error if the element assigned to calendarBetter is not readyconst calendarBetter: Ready<CalendarElement> | null = await ready( document.querySelector<CalendarElement>('#my-calendar'));
if (calendarBetter) { calendarBetter.availableDateRange = { start: new Date('2024-01-01'), end: new Date('2024-01-05'), };}
// Another way to do itready(document.querySelector<CalendarElement>('#my-calendar')).then( (calendar) => { if (calendar) { calendar.availableDateRange = { start: new Date('2024-01-01'), end: new Date('2024-01-05'), }; } });
In the example above you would not need to explicitly define the type of the element, but this is just to show how the type Ready
can be used.
Custom Events
Custom events names are a combination of the app name, component name, and the event name. This prevents naming collisions with other events.
data.ts
exports EventNames
that is used to create/remove event listeners for the component.
import { ready } from '@/components/NST-components/base';import { OTPEventNames } from '@/components/NST-components/UI/otp/data';
const otp = await ready(document.querySelector('otp-input'));
otp?.addEventListener(OTPEventNames.Change, (event) => { console.log(event);});
Compositing Events From Child Components
When a component has child components that emit events, you can access these events through the parent by extending its event map.
In this example, the drawer has a color picker child that emits a change event. The drawer extends its event map with the child so it can respond to the event.
This is particularly useful if the parent is listening for events from multiple children of the same type, so the events are handled in a single event listener on the parent rather than one on each child.
import { AudioPlayerEventNames, type AudioPlayerEventMap,} from '@/components/NST-components/UI/audio/data';import { ColorPickerEventNames, type ColorPickerEventMap,} from '@/components/NST-components/UI/color-picker/data';import type { DrawerElement } from '@/components/NST-components/UI/drawer/data';import type { CompositeEvents } from '@/components/NST-components/base';
const drawerWithColorPicker = document.querySelector<CompositeEvents<DrawerElement, ColorPickerEventMap>>( '#my-drawer' );
drawerWithColorPicker?.addEventListener(ColorPickerEventNames.Change, (e) => { console.log('Color changed', e);});
// You can compose multiple event mapsconst drawerWithColorPickerAndAudio = document.querySelector< CompositeEvents< DrawerElement, ColorPickerEventMap & AudioPlayerEventMap > >('#my-drawer');
drawerWithColorPickerAndAudio?.addEventListener( ColorPickerEventNames.Change, (e) => { console.log('Color changed', e); });
drawerWithColorPickerAndAudio?.addEventListener( AudioPlayerEventNames.VolumeChange, (e) => { console.log('Audio changed', e); });