Package Exports
- element-vir
- element-vir/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (element-vir) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
element-vir
A wrapper for lit-element that adds type-safe custom element usage and I/O with declarative custom element definition.
Heroic. Reactive. Declarative. Type safe. Web components without compromise.
No need for an extra build step,
no need for side effect imports,
no need for unique file extensions,
no need for extra static analysis tools,
no need for a dedicated, unique syntax.
It's just TypeScript.
Uses the power of native JavaScript custom web elements, native JavaScript template literals, native JavaScript functions*, native HTML, and lit-element.
Works in every major web browser except Internet Explorer.
*okay I hope it's obvious that functions are native
Install
npm i element-vir
Make sure to install this as a normal dependency (not just a dev dependency) because it needs to exist at run time.
Usage
Most usage of this package is done through the defineElement
or defineElementNoInputs
functions. See the DeclarativeElementInit
type for that function's inputs. These inputs are also described below with examples.
All of lit
's syntax and functionality is also available for use if you wish.
Simple element definition
Use defineElementNoInputs
to define your element if it's not going to accept any inputs (or just for now as you're getting started). It must be given an object with at least tagName
and renderCallback
properties (the types enforce this). Here is a bare-minimum example custom element:
import {defineElementNoInputs, html} from 'element-vir';
export const MySimpleElement = defineElementNoInputs({
tagName: 'my-simple-element',
renderCallback: () => html`
<span>Hello there!</span>
`,
});
Make sure to export your element definition if you need to use it in other files.
Using in other elements
To use already defined elements (like my-simple-element
above), they must be interpolated into HTML templates like so:
import {defineElementNoInputs, html} from 'element-vir';
import {MySimpleElement} from './my-simple.element';
export const MyAppElement = defineElementNoInputs({
tagName: 'my-app-element',
renderCallback: () => html`
<h1>My App</h1>
<${MySimpleElement}></${MySimpleElement}>
`,
});
This requirement ensures that the element is properly imported and registered with the browser. (Compare to pure lit where you must remember to import each element file as a side effect, or without actually referencing any of its exports in your code.)
If you wish to bypass this interpolation, make sure to import the html
tagged template directly from lit
, import {html} from 'lit';
, instead of version contained in element-vir
.
Adding styles
Styles are added through the styles
property when defining a declarative element (similar to how they are defined in lit
):
import {css, defineElementNoInputs, html} from 'element-vir';
export const MySimpleWithStylesElement = defineElementNoInputs({
tagName: 'my-simple-with-styles-element',
styles: css`
:host {
display: flex;
flex-direction: column;
font-family: sans-serif;
}
span + span {
margin-top: 16px;
}
`,
renderCallback: () => html`
<span>Hello there!</span>
<span>How are you doing?</span>
`,
});
Element definition as style selector
Declarative element definitions can be used in the css
tagged template just like in the html
tagged template. This will be replaced by the element's tag name:
import {css, defineElementNoInputs, html} from 'element-vir';
import {MySimpleElement} from './my-simple.element';
export const MySimpleWithStylesAndInterpolatedSelectorElement = defineElementNoInputs({
tagName: 'my-simple-with-styles-and-interpolated-selector-element',
styles: css`
${MySimpleElement} {
background-color: blue;
}
`,
renderCallback: () => html`
<${MySimpleElement}></${MySimpleElement}>
`,
});
Defining and using Inputs
Define element inputs by using defineElement
to define a declarative element. Pass your input type as a generic to the defineElement
call. Then call that with the normal definition input (like when using defineElementNoInputs
).
To use an element's inputs for use in its template, grab inputs
from renderCallback
's parameters and interpolate it into your HTML template:
import {defineElement, html} from 'element-vir';
export const MySimpleWithInputsElement = defineElement<{
username: string;
email: string;
}>()({
tagName: 'my-simple-element-with-inputs',
renderCallback: ({inputs}) => html`
<span>Hello there ${inputs.username}!</span>
`,
});
Defining internal state
Define internal state with the stateInit
property when defining an element. Grab it with state
in renderCallback
to use state. Grab updateState
in renderCallback
to update state:
import {defineElementNoInputs, html, listen} from 'element-vir';
export const MySimpleWithUpdateStateElement = defineElementNoInputs({
tagName: 'my-simple-element-with-update-state',
stateInit: {
username: 'dev',
email: undefined as string | undefined,
},
renderCallback: ({state, updateState}) => html`
<span
${listen('click', () => {
updateState({username: 'new name!'});
})}
>
Hello there ${state.username}!
</span>
`,
});
Assigning to properties (inputs)
Use the assign
directive to assign properties to child custom elements:
import {assign, defineElementNoInputs, html} from 'element-vir';
import {MySimpleWithInputsElement} from './my-simple-with-inputs.element';
export const MyAppWithAssignmentElement = defineElementNoInputs({
tagName: 'my-app-with-assignment-element',
renderCallback: () => html`
<h1>My App</h1>
<${MySimpleWithInputsElement}
${assign(MySimpleWithInputsElement, {
email: 'user@example.com',
username: 'user',
})}
>
</${MySimpleWithInputsElement}>
`,
});
Other callbacks
There are two other callbacks you can define that are sort of similar to lifecycle callbacks. They are much simpler than lifecycle callbacks however.
initCallback
: called right before the first render, has all state and inputs setup.cleanupCallback
: called when an element is removed from the DOM. (This is the same as thedisconnectedCallback
in standard HTMLElement classes.)
import {defineElementNoInputs, html} from 'element-vir';
export const MyAppWithAssignmentCleanupCallbackElement = defineElementNoInputs({
tagName: 'my-app-with-cleanup-callback',
stateInit: {
intervalId: undefined as undefined | number,
},
initCallback: ({updateState}) => {
updateState({
intervalId: window.setInterval(() => console.log('hi'), 1000),
});
},
renderCallback: () => html`
<h1>My App</h1>
`,
cleanupCallback: ({state, updateState}) => {
window.clearInterval(state.intervalId);
updateState({
intervalId: undefined,
});
},
});
Element events (outputs)
Define events with events
when defining a declarative element. Each event must be initialized with defineElementEvent
and a type parameter. defineElementEvent
accepts no inputs as it doesn't make sense for events to have default values.
To dispatch an event, grab dispatch
from renderCallback
's parameters.
import {defineElementEvent, defineElementNoInputs, html, listen} from 'element-vir';
export const MySimpleWithEventsElement = defineElementNoInputs({
tagName: 'my-simple-element-with-events',
events: {
logoutClick: defineElementEvent<void>(),
randomNumber: defineElementEvent<number>(),
},
renderCallback: ({dispatch, events}) => html`
<button ${listen('click', () => dispatch(new events.logoutClick(undefined)))}>
log out
</button>
<button ${listen('click', () => dispatch(new events.randomNumber(Math.random())))}>
generate random number
</button>
`,
});
Listening to typed events (outputs)
Use the listen
directive to listen to typed events emitted by your custom elements:
import {defineElementNoInputs, html, listen} from 'element-vir';
import {MySimpleWithEventsElement} from './my-simple-with-events.element';
export const MyAppWithEventsElement = defineElementNoInputs({
tagName: 'my-app-with-events-element',
stateInit: {
myNumber: -1,
},
renderCallback: ({state, updateState}) => html`
<h1>My App</h1>
<${MySimpleWithEventsElement}
${listen(MySimpleWithEventsElement.events.logoutClick, () => {
console.info('logout triggered');
})}
${listen(MySimpleWithEventsElement.events.randomNumber, (event) => {
updateState({myNumber: event.detail});
})}
>
</${MySimpleWithEventsElement}>
<span>${state.myNumber}</span>
`,
});
listen
can also be used to listen to native DOM events (like click
) and the proper event type will be provided for the listener callback.
Typed events without an element
Create a custom event type with defineTypedEvent
. Make sure to include the type generic (like this: defineTypedEvent<number>
) and call it twice, the second time with the event type string, (like this: defineTypedEvent<number>()('my-event-type-name')
) to ensure type safety when using your event. Note that event type names should probably be unique, or they may clash with each other.
Creating a typed event
import {defineTypedEvent} from 'element-vir';
export const MyCustomEvent = defineTypedEvent<number>()('myCustomEventName');
Using a typed event
Both dispatching a custom event and listening to a custom event:
import {defineElementNoInputs, html, listen} from 'element-vir';
import {MyCustomEvent} from './custom-event-no-element';
export const MyElementWithCustomEvents = defineElementNoInputs({
tagName: 'my-app-with-custom-events',
renderCallback: ({genericDispatch}) => html`
<div
${listen(MyCustomEvent, (event) => {
console.info(`Got a number! ${event.detail}`);
})}
>
<div
${listen('click', () => {
genericDispatch(new MyCustomEvent(Math.random()));
})}
></div>
</div>
`,
});
Host classes
Host classes can be defined and used with type safety. Host classes are used to provide alternative styles for components. They are purely driven by CSS and are thus applied via the class
HTML attribute.
Host classes that are defined with a callback will automatically get applied if that callback returns true after a render is executed. These are executed after renderCallback
is executed. When a definition is set to false
, it's left to the element's consumer to apply the host class.
Apply host classes in the element's stylesheet by using a callback for the styles property.
import {css, defineElementNoInputs, html} from 'element-vir';
export const MyAppWithHostClasses = defineElementNoInputs({
tagName: 'my-app-with-host-classes',
stateInit: {
myProp: 'hello there',
},
hostClasses: {
/**
* Setting the value to false means this host class will not ever automatically be applied.
* It will simply be a static member on the element for manual application in consumers when desired.
*/
styleVariationA: false,
/**
* This host class will be automatically applied if the given callback evaluated to true
* after a call to renderCallback.
*/
automaticallyAppliedVariation: ({state}) => {
return state.myProp === 'foo';
},
},
/**
* Apply styles to the host classes by using a callback for "styles". The callback's argument
* contains the host classes defined above in the "hostClasses" property.
*/
styles: ({hostClass}) => css`
${hostClass.automaticallyAppliedVariation} {
color: blue;
}
${hostClass.styleVariationA} {
color: red;
}
`,
renderCallback: ({state}) => html`
${state.myProp}
`,
});
CSS Vars
Typed CSS vars are created in a similar way as host classes:
import {css, defineElementNoInputs, html} from 'element-vir';
export const MyAppWithCssVars = defineElementNoInputs({
tagName: 'my-app-with-css-vars',
cssVars: {
/**
* The value assigned here ('blue') becomes the fallback value for this CSS var when used
* via "cssVarValue".
*/
myCssVar: 'blue',
},
styles: ({cssVarName, cssVarValue}) => css`
:host {
/* Set CSS vars (or reference the name directly) via "cssVarName" */
${cssVarName.myCssVar}: yellow;
/* Use CSS vars with "cssVarValue". This includes a "var" wrapper and the assigned fallback value (which in this case is 'blue'). */
color: ${cssVarValue.myCssVar};
}
`,
renderCallback: () => html``,
});
Directives
The following custom lit
directives are contained within this package.
onDomCreated
This directive should be used instead of trying to use querySelector
directly on the custom element.
This triggers only once when the element it's attached has actually been created in the DOM. If it's attached element changes, the callback will be triggered again.
import {defineElementNoInputs, html, onDomCreated} from 'element-vir';
export const MySimpleWithOnDomCreatedElement = defineElementNoInputs({
tagName: 'my-simple-with-on-dom-created-element',
renderCallback: () => html`
<span
${onDomCreated((element) => {
// logs a span element
console.info(element);
})}
>
Hello there!
</span>
`,
});
onResize
This directive fulfills a common use case of triggering callbacks when something resizes. Instead of just tracking the globally resizing window though, this allows you to track resizes of an individual element. The callback here is passed an object with a portion of the ResizeObserverEntry
properties (since not all properties are supported well in browsers).
import {defineElementNoInputs, html, onResize} from 'element-vir';
export const MySimpleWithOnResizeElement = defineElementNoInputs({
tagName: 'my-simple-with-on-dom-created-element',
renderCallback: () => html`
<span
${onResize((entry) => {
// this will track resizing of this span
// the entry parameter contains target and contentRect properties
console.info(entry);
})}
>
Hello there!
</span>
`,
});
assign
Assign a value to one of a custom element's properties. This is explained in the Assigning to properties (inputs) section earlier in this README.
listen
Listen to a specific event emitted from a custom element. This is explained in the Listening to custom events (outputs) section earlier in this README.
assignWithCleanup
This directive is the same as the assign
directive but it accepts an additional cleanupCallback
input. Use this directive to assign values which need some kind of cleanup when they're overwritten. For example, a 3D rendering engine which uses the canvas that should free up memory when it's swapped out.
import {assignWithCleanup, defineElementNoInputs, html} from 'element-vir';
import {MySimpleWithInputsElement} from './my-simple-with-inputs.element';
export const MyAppWithAssignmentCleanupElement = defineElementNoInputs({
tagName: 'my-app-with-cleanup',
renderCallback: () => html`
<h1>My App</h1>
<${MySimpleWithInputsElement}
${assignWithCleanup(
MySimpleWithInputsElement,
{
email: 'user@example.com',
username: 'user',
},
(previousValue) => {
// here would be the cleanup code.
// In this specific example the value is just a string, so no cleanup is needed
// and the following line isn't actually doing anything.
previousValue.username.trim();
},
)}
>
</${MySimpleWithInputsElement}>
`,
});
Require all child custom elements to be declarative elements
To require all child elements to be declarative elements defined by this package, call requireAllCustomElementsToBeDeclarativeElements
anywhere in your app. This is a global setting so do not enable it unless you want it to be true everywhere in your current run-time. This should not be used if you're using custom elements from other libraries (unless they happen to also use this package to define their custom elements).
import {requireAllCustomElementsToBeDeclarativeElements} from 'element-vir';
requireAllCustomElementsToBeDeclarativeElements();
Dev
markdown out of date
If you see this: Code in Markdown file(s) is out of date. Run without --check to update. code-in-markdown failed.
, run npm run update-docs
to fix it.