We use cookies to ensure you get the best experience on our website. Read our privacy policy

Skip to main content

Directory

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.

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 type
const calendarInfered = document.querySelector('calendar-wrapper');
console.log(calendarInfered);
// This will not infer the type and needs the generic argument
const calendarTyped = document.querySelector<CalendarElement>('#my-calendar');
console.log(calendarTyped);

All custom component classes extend the BaseComponent class. This class implements the ready event and property, an init method, and componentName property.

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 ready
const 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 it
ready(document.querySelector<CalendarElement>('#my-calendar')).then(
(calendar) => {
if (calendar) {
calendar.availableDateRange = {
start: new Date('2024-01-01'),
end: new Date('2024-01-05'),
};
}
}
);

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 listener
if (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<T extends BaseInteractiveElementType>(
element: T | null | undefined,
callback?: (element: Ready<T>) => Promise<void> | void,
options?: { timeout?: number }
): Promise<void>

whenReady takes three arguments:

  1. the element to be initialized,
  2. an optional callback function with the ready element as an argument,
  3. 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<T extends BaseInteractiveElementType>(
element: T | null | undefined,
options: { timeout?: number } = {}
): Promise<Ready<T> | null>

ready takes two arguments:

  1. the custom element to be initialized
  2. 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 });

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 ready
const 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 it
ready(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 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);
});

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 maps
const 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);
}
);
North Star Themes Logo

Subscribe to our newsletter

Get the latest AstroJS tips and tricks right to your inbox

Email: [email protected]

© 2025 North Star Themes

Web Kit Provided By North Star Themes