Component Basics
To create views on the frontend, Hilla uses web components, a native technology that allows you to create custom HTML elements. It is almost like a framework built into the browser, and it is usually faster and smaller than other frameworks, while still sharing their advantages.
The Lit library is a convenient way of authoring web components, and it is recommended over the low-level native browser APIs.
Lit provides a base class named LitElement, and a set of tools to create reactive web components. Under the hood, it uses lit-html, a tiny library to render and re-render HTML templates in an efficient and declarative way.
Minimal Lit Component
To create a minimal working component, you must do two things: extend the LitElement class and implement a render() method.
Template
The render() method is responsible for rendering and re-rendering a component template.
It should return a TemplateResult instance produced by the html
tagged template literal.
The string tagged with the html
tag is displayed in the browser as the component’s content.
Learn more about tags and tagged template literals from MDN Web Docs.
Custom Element Definition
A web component class must be added to the browser’s custom element registry and assigned a unique element name.
The @customElement
decorator is a convenient way of doing this with LitElement.
It receives an HTML element name that should contain at least one dash -
(as required by the HTML specification).
For example, my-view
, some-fancy-button
or vaadin-checkbox
.
Data Binding
A web component produces a view based on the data it receives from outside or preserves within. That data is bound to the component template declaratively, and each time the data is changed, the template changes as well. This section describes approaches to receiving and storing the data, and to binding the data to the template.
Fields, Methods, and Accessors
Since the web component is a simple JavaScript class, you can define any number of fields, methods, and accessors you need and call them as you are used to doing. TypeScript, as a superset of JavaScript, also allows you to define private and protected methods.
There are no specific requirements for naming, but you may want to follow the convention that any non-public class members should have an underscore _
at the beginning of their names.
Properties
Components can have properties that receive data from the outside (from HTML or HTML templating system) or preserve the internal state. Changing these properties triggers an asynchronous re-rendering of the component to fill the template with the new data.
Lit provides two decorators that can transform a regular class field into a component data property.
@property
Makes a field a data property that is allowed to receive data from the outside. This means that you can send the data to the component via one of the following options:
Using the
html
tag from Lit:html`<my-view .myProperty=${this.myValue}></my-view>`
As an attribute of an instance of the component:
document.querySelector('my-view').setAttribute('myProperty', 'myValue')
.As a property of an instance of the component:
document.querySelector('my-view').myProperty = 'myValue'
.
@state
Makes a field a data property that contains an internal state of the component. These properties must not be used from outside. For example, you can use it if you need to trigger re-rendering on some events fired by children.
Read more about data properties in the Lit documentation.
Template Binding
The html
function is designed to assign JavaScript values to HTML elements in an obvious and easy way.
Lit provides four ways to do this:
- Assign to an attribute
Only values of simple types can be assigned to an attribute:
String
,Number
orBoolean
. The value is converted to a string (Boolean
values are corrected directly to their string representations) and also reflected in the HTML code. For this approach, you can use the regular HTML attribute syntax.- Assign to a property
Allows any value type. The value is not reflected in the HTML code. It is recommended to use this approach over assigning to an attribute, because it is simpler and more performant. For this approach, you must insert a dot
.
before the property name:.property
.- Assign a boolean attribute
Allows only boolean values. The only difference from assigning to an attribute is that, instead of being assigned directly, it is removed and added again according to the value. For this approach, you must add a question mark
?
before the property name.- Listen to an event by the name
Allows only functions. When the element fires an event, the function assigned to the name is called with the event as an argument. For this approach, you must add an
at
sign,@
, before the event name:@event
.
Note | Correct
You do not need to bind this reference in event listenersthis to the event listener method.
It refers to the component class in LitElement listeners automatically.
|
Learn about how to write templates from the Lit documentation.
Example
import { html, LitElement, TemplateResult } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
@customElement('data-binding-view')
class DataBindingView extends LitElement {
@property() message = '';
@property() name = '';
@state() active = false;
render(): TemplateResult {
return html`
<example-template-header
?active=${this.active}
name=${this.name}
.message=${this.message}>
<button @click=${this.onClick}>Toggle</button>
</example-template-header>
`;
}
private onClick() {
this.active = !this.active;
}
}
export default DataBindingView;
Lifecycle
Each component has lifecycle methods that you can override to execute code at a specific time. LitElement adds its own methods to handle the asynchronous render() calls to the standard web component lifecycle.
constructor
Specificity: JavaScript class
The constructor is invoked when a class is instantiated. For a component, it happens when document.createElement('my-view') is called.
In most cases, you do not need a
constructor
, because LitElement handles everything important. However, if you do create one, try to avoid performing any heavy work; the element might be discarded after invoking the constructor.NoteThe element needs to be connected to run the render() callback.When the element is created, no render happens.connectedCallback
Specificity: Web Components
This callback is invoked each time the element is connected to the DOM.
In most cases, the connectedCallback() is used to set up component-level event listeners, etc.
NoteUseSince render() is asynchronous, it is not over when the connectedCallback(()) finishes.firstUpdated
callback to execute the code after the first renderNoteRemember to call the super.connectedCallback().You must call super.connectedCallback() in your connectedCallback() method. Otherwise, you may lose the default LitElement lifecycle.disconnectedCallback
Specificity: Web Components
This callback is invoked each time the element is disconnected from the DOM.
The default role of the disconnectedCallback() is to remove or close everything that is set up during the connectedCallback().
NoteNo DOM accessThis callback is invoked after the element is disconnected from the DOM.NoteRemember to call the super.disconnectedCallback().You must call super.disconnectedCallback() in your disconnectedCallback() method. Otherwise, you may lose the default LitElement lifecycle.firstUpdated
Specificity: Lit
This callback is invoked right after the first render is over and before the first updated() callback. It receives an object that contains all changed properties.
updated
Specificity: Lit
This callback is invoked after each render; for the first render, it is invoked right after the firstUpdated() callback. Just like firstUpdated(), it receives an object that contains all properties changed after the previous render.
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('my-button')
class MyButton extends LitElement {
constructor() {
super();
this.onClick = this.onClick.bind(this);
}
connectedCallback() {
this.addEventListener('click', this.onClick);
}
disconnectedCallback() {
this.removeEventListener('click', this.onClick);
}
render() {
return html`<button>Some button</button>`;
}
private onClick() {
console.log('I am clicked');
}
}
export default MyButton;
Read more about the Lit-specific lifecycle methods in the Lit documentation.
Styling
For styling, LitElement provides a static property styles
.
You can use either a style sheet imported from a .css file or define an inline style sheet using the css
tagged function.
The styles
property also accepts an array of style sheets.
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './my-view.css';
@customElement('my-view')
class MyView extends LitElement {
static styles = [
styles,
css`
h1 {
color: red;
}
`,
];
render() {
return html`<h1>My View</h1>`;
}
}
export default MyView;
Shadow and Light DOM
You can use two main approaches for web components. You can find additional information in the Style Scopes article.
Shadow DOM
A web component can create a shadow tree within. It is a separate document fragment, almost inaccessible from the regular DOM, existing as a part of the component and displayed in the browser as a regular element tree. With this, you can create elements with styles independent of the global CSS and HTML content not added to children nodes.
Shadow DOM is enabled by default when using the LitElement class.
import { html, LitElement, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('my-view')
class MyView extends LitElement {
render(): TemplateResult {
return html`<h1>My View</h1>`;
}
}
export default MyView;
Light DOM
If you do not want to use the Shadow DOM, you can still use the Light DOM, a regular part of the DOM controlled by the component’s class. It lacks Shadow DOM advantages, such as scoped CSS or non-children HTML, but it is lighter and simpler for both development and performance.
To enable Light DOM for the LitElement component, add a createRenderRoot() { return this; } method to your component.
import { html, LitElement, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('my-view')
class MyView extends LitElement {
render(): TemplateResult {
return html`<h1>My View</h1>`;
}
protected createRenderRoot(): Element | ShadowRoot {
return this;
}
}
export default MyView;
Using in Routes
Read more about how to use components together with Hilla Router in the Hilla Router documentation.