@seahax/elemental
v0.8.7
Published
Functional, reactive, web component base library.
Readme
@seahax/elemental
Functional, reactive, web component base library.
Contains everything you need to build anything from a single component up to a full reactive application, with minimal overhead.
- Create fully portable web components
- Direct and safe access to the DOM (no virtual DOM)
- React-style code reuse using composable hooks
- Global & local state reactivity
- Client-side routing
- No Build Tooling
- No Dependencies
- Tiny Bundle Size
Define A Web Component
import {
defineComponent,
h,
useRef,
useStore,
useAttributes,
useParent,
useDocument,
useRoute,
useAsync,
useEffect,
useStyleEffect,
useChildEffect,
useDisconnectEffect,
useElementInternals,
useHost,
useShadow,
} from '@seahax/elemental';
export const MyComponent = defineComponent((shadow) => {
// This function is called every time the component is connected to the
// document.
//
// For the most part, a component's lifecycle should be handled as if it
// starts on connect and ends on disconnect, even though the component
// can be reconnected to the document. Restore internal state on
// connection from host attributes and properties. Effect hooks are
// designed to facilitate this pattern.
// Create HTML elements and save references to them.
const myInput = h('input');
// Render content to the shadow DOM.
h(shadow, [
h('p', { class: 'hello' }, ['Hello, World!']),
h('div', { class: 'inputs' }, [
myInput,
]),
]);
// Use a reference (reactive state) value.
const localStateRef = useRef('initial value', (newValue) => {
// Handle
});
// Use a reference (reactive state) bound to a (shared) store.
const globalStateRef = useStore(myStore, select, mutate);
// Use references (reactive state) bound to the component's attributes.
const [dataValueRef, ...] = useAttributes('data-value', ...);
// Use a reference (reactive state) bound to the component's parent node.
const parentNode = useParent();
// Use a reference (reactive state) bound to the component's owner document.
const ownerDocument = useDocument();
// Use references (reactive state) bound to route properties.
const [pathnameRef, searchRef, hashRef] = useRoute('pathname', 'search', 'hash');
// Use a reference (reactive state) bound to an async loader function.
const asyncRef = useAsync([
// dependency references
], async (signal, ...dependencyValues) => {
// Reactive async code runs when the component is connected to the
// document, and when any of the dependencies change. The signal is
// aborted if the dependencies change before the promise returned by
// this function is resolved.
});
// React to reference changes.
useEffect([
// dependency references
localStateRef,
globalStateRef,
dataValueRef,
pathnameRef,
searchRef,
hashRef,
asyncRef,
], (...dependencyValues) => {
// Reactive code runs when the component is connected to the document,
// and when any of the dependencies change.
return () => {
// Cleanup after dependency refs are changed (before the next effect
// callback) and after the component is disconnected from the
// document.
};
});
// React to reference changes and apply CSS styles to the shadow root.
useStyleEffect([
// dependency references
localStateRef
], (localState) => {
// Reactive code runs when the component is connected to the document,
// and when any of the dependencies change.
// Return a CSS string to be adopted by the shadow root.
return /* css */ `
.hello {
color: ${localState};
}
`;
});
// React to child list changes.
useChildEffect(() => {
// Reactive code runs when the component is connected to the document,
// and when the children of the component change. Access the current
// children using the `shadow.host.children` property.
return () => {
// Cleanup before the next effect callback and after the component
// is disconnected from the document.
};
});
// Register a document disconnection callback.
useDisconnectEffect(() => {
// Called when the component is disconnected from the document.
});
// Use the element's internals.
const elementInternals = useElementInternals();
// Use the component host element.
const host = useHost();
// Use the component shadow root.
const shadow = useShadow();
});Add Styles
const MyComponent = defineComponent(
(shadow) => {
...
},
{
// Styles to be adopted by the shadow root. String values are parsed
// as CSS to create new `CSSStyleSheet` instances.
styles: [cssText, cssStyleSheet],
}
)Customize The Shadow Root
const MyComponent = defineComponent(
(shadow) => {
...
},
{
// Use custom shadow root initialization options.
// (default: { mode: 'open' }).
shadow: {
mode: 'closed',
...
},
}
);Enable Form Association
import {
useElementInternals,
useForm,
useFormDisabled,
useFormResetCallback,
useFormRestoreCallback,
} from '@seahax/elemental';
const MyComponent = defineComponent(
(shadow) => {
// Use the element's internals.
const elementInternals = useElementInternals();
// Use a reference (reactive state) bound to the associated form.
const formRef = useForm();
// Use a reference (reactive state) bound to the form disabled state.
const formDisabledRef = useFormDisabled();
// Register a form reset callback.
useFormResetCallback(() => {
// Called when the associated form is reset. Only called on connect
// if the form was reset while the component was disconnected.
});
// Register a form restore callback.
useFormRestoreCallback((state, reason) => {
// Called when the associated form is restored. Only called on connect
// if the form was restored while the component was disconnected.
});
},
{
// Enable form association.
formAssociated: true,
}
);Add Web Component Properties
interface Props {
checked: boolean;
}
const MyComponent = defineComponent<Props>(
(shadow, propRefs) => {
// Get properties (the ref value is initially undefined).
const isChecked = propRefs.checked.value ?? shadow.host.hasAttribute('checked');
// Set properties.
propRefs.checked.value = true;
// Alternatively, access the property on the host element.
const isChecked = shadow.host.checked;
shadow.host.checked = true;
// React to property changes.
useEffect([propRefs.checked], (checked) => {
...
});
},
{
props: {
// Return a property descriptor that uses a pre-defined ref and the
// host element. The property descriptor must have a `get` function,
// `value` is not allowed, and all other properties are optional.
// The ref value is initially undefined.
checked: (ref, host) => {
return {
get: () => ref.value ?? host.hasAttribute('checked'),
set: (value) => (ref.value = value),
};
},
},
}),
);
const element = new MyComponent();
// Properties are defined publicly on component (`HTMLElement`) instances.
element.checked = true;Register The Custom Element On Definition
const MyComponent = defineComponent((shadow) => {
...
}, {
// Register the component as a custom element with this tag name.
tagName: 'my-component',
});Extend Component Definition
import { extendComponentDefinition } from '@seahax/elemental';
// Create a new component definition factory (ie. `defineComponent`
// function) with common styles and hooks (callbacks) that can be used to
// add shared behavior to all defined components.
export const defineComponent = extendComponentDefinition({
styles: [...],
preInit: (shadow) => {...},
postInit: (shadow) => {...},
preRender: (shadow) => {...},
postRender: (shadow) => {...},
});Render An Element
// By tag name.
const element = h('div', {
// Set attributes.
class: 'my-class',
// Set properties.
':id': 'my-id',
}, [
// Set children.
h('p', [text]),
]);
// By custom element constructor.
const element = h(MyComponent, {
// Set attributes.
class: 'my-class',
// Set properties.
':id': 'my-id',
}, [
// Set children.
h('p', [text]),
]);Update An Element
// Update an existing element.
h(element, {
// Set attributes.
class: 'my-class',
// Remove attributes.
class: null,
// Set properties.
':id': 'my-id',
// Attributes and properties that are not provided are left alone.
}, [
// Replace all children. Children are left alone if no child array is
// provided (undefined or omitted).
h('p', [text]),
]);Combine Conditional Classes
import { classes } from '@seahax/elemental';
// Using a dictionary of boolean values.
const classNames = classes({
'my-class-0': true,
'my-class-1': false,
'my-class-2': undefined,
'my-class-3': null,
'my-class-4': true,
}); // = 'my-class-0 my-class-4'
// Using a sparse array.
const classNames = classes([
'my-class-0',
false,
undefined,
null,
'my-class-4',
]); // = 'my-class-0 my-class-4'Define A Router Component
import { defineRouter } from '@seahax/elemental/router';
export const Router = defineRouter({
// (optional) Register the component as a custom element with this tag name.
tagName: 'ce-router',
// (optional) Define a component to render when a route parameters callback
// throws an error.
invalid: MyInvalidComponent,
// (optional) Define a fallback component to render when no route matches.
fallback: MyFallbackComponent,
})
// Simple exact pathname match.
.addRoute('/', MyHomeComponent)
// Exact pathname match with search (query) parameters.
.addRoute('/blog', MyBlogIndexComponent, (pathParams, searchParams) => {
// Get search (query) parameters.
const tag = searchParams.get('tag') ?? '';
// Validate parameters.
assert(!/[^a-z0-9-]/iu.test(tag), 'Invalid tag.');
// Return attributes to be set on the component.
return {
'data-blog-tag': tag,
};
})
// Dynamic pathname match with a path parameter. Path parameters match
// exactly one path segment (does not match forward slashes).
.addRoute('/blog/:id', MyBlogPostComponent, (pathParams) => {
// Validate parameters.
assert(!/[^a-z0-9-]/iu.test(pathParams.id), 'Invalid blog ID.');
// Return attributes to be set on the component.
return {
'data-blog-id': pathParams.id,
};
})
// Dynamic pathname match with a path splat parameter. Splats match one
// or more (not zero) path segments at the end of the pathname.
.addRoute('/resources/*path', MyResourcesComponent, (pathParams) => {
return {
'data-path': validatePath(pathParams.path),
}
})
.build();