In short:
WebComponents allow you to define/invent your own custom HTML elements.
<typo3-surfcamp year="2026"></typo3-surfcamp>
import {LitElement, html} from 'lit';
export class Typo3Surfcamp extends LitElement {
static properties = {
year: {type: Number, attribute: 'year'},
};
constructor() {
super();
this.year = 0;
}
render() {
return html`<div>TYPO3 SurfCamp ${this.year}</div>`;
}
}
customElements.define('typo3-surfcamp', Typo3Surfcamp);
Register the ES6 modules of your extension in Configuration/JavaScriptModules.php
<?php
return [
// required import configurations of other extensions,
// in case a module imports from another package
'dependencies' => [
'backend',
'core',
],
// recursive definiton, all *.js files in this folder are import-mapped
// trailing slash is required per importmap-specification
'imports' => [
'@ochorocho/lit-demo/' => 'EXT:lit_demo/Resources/Public/JavaScript/',
],
];
<?php
$this->pageRenderer->loadJavaScriptModule('@ochorocho/lit-demo/component/typo3-surfcamp.js');
<f:asset.module identifier="@ochorocho/lit-demo/component/typo3-surfcamp.js"></f:asset.module>
The most used methods:
constructor() - When an element is created. Set property defaultsconnectedCallback() - When an element is added to the document's DOMfirstUpdated() - Called when the elements DOM was updated for the first time. A good place to do API requests.render() - Renders the HTML template. Must return a TemplateResultdisconnectedCallback() - When an element is removed from the document's DOM. Handy to remove event listeners defined outside the element.updated(changedProperties) - Run tasks/actions on the updated DOM. Use changedProperties.has('<property>') to see if a property has actually changedSome of the methods require super so the default lit behavior is maintained.
connectedCallback() {
super.connectedCallback()
// ...
}
type — converts the HTML attribute string to a value (String, Number, Boolean, Array, Object)reflect: true — writes property changes back to the attributeattribute: false — property only, never read from HTMLstate: true — internal state, not part of the public API⚠️ Initialize defaults in the constructor, not as class fields.
Changing a reactive property triggers a re-render.
static properties = {
label: { type: String },
count: { type: Number, reflect: true },
active: { type: Boolean },
items: { type: Array, attribute: false },
_open: { state: true },
};
./component/typo3-weather-data.jsJavaScriptModules.phpUse the @-shorthand to listen to regular events e.g. @click="${this.doSomething}"
this.dispatchEvent(new CustomEvent(
'my-custom-event', { bubbles: true, detail: { uid: 666 }}
));
<the-parent-component @my-custom-event="${this.doSomething}"></the-parent-component>
export class NoShadowDom extends LitElement {
// ...
createRenderRoot() {
return this;
}
// ...
}
customElements.define('no-shadow-dom', NoShadowDom)
Insertion points for content placed inside your component:
slot attributeslot="x" matches <slot name="x"><slot> tags is shown as fallback::slotted(selector)⚠️ Slots require the shadow DOM to be enabled.
render() {
return html`
<header><slot name="title">Untitled</slot></header>
<section><slot></slot></section>
`;
}
<my-card> <h2 slot="title">Hello</h2> <p>Goes into the default slot.</p> </my-card>
Apply component specific styles
⚠️ Scoped style require the shadow DOM to be enabled.
import {LitElement, html, css} from 'lit';
export class MyElement extends LitElement {
// Define component specific CSS
static styles = css`
:host {
color: red;
}
`
render() {
return html`<p>I am red!</p>`;
}
}
customElements.define('my-element', MyElement);
Load labels from any XLF file. In the example from EXT:lit_demo/Resources/Private/Language/locallang_components.xlf
import {LitElement, html} from 'lit';
import componentLabels from '~labels/lit_demo.components'
export class TranslateExample extends LitElement {
render() {
return html`<h1>${componentLabels.get('label-key')}</h1>`;
}
}
customElements.define('translate-example', TranslateExample)
firstUpdated() and fetch()@lit/task// Constructor
this._nonce = 0;
this._jokeTask = new Task(this, {
task: async ([nonce], { signal }) => {
const url = `${API}/random`;
const res = await fetch(url, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
args: () => [this._nonce],
});
// Render
return html`
${this._jokeTask.render({
pending: () => html`<p>Loading joke…</p>`,
error: (e) => html`<p class="error">Failed: ${e.message}</p>`,
complete: (joke) => html`
<div>${joke.value}</div>
`,
})}
`;
Methods you might want to use in your web components to trigger certain actions.
// Open a modal
top.TYPO3.Modal.confirm('Title', 'Confirm something...')
// Show a notification
top.TYPO3.Notification.error('Error', 'Error occurred while processing this request.');
// Access ajax routes
top.TYPO3.settings.ajaxUrls
// Open a module by identifier
const params = new URLSearchParams({ task: 666});
top.TYPO3.ModuleMenu.App.showModule('recycler', params.toString());
// Reload the page tree
top.document.dispatchEvent(new CustomEvent('typo3:pagetree:refresh'));
Search for @customElement(' in the Build/Sources/TypeScript folder of the TYPO3 repository.
TYPO3 uses TypeScript. In TypeScript the decorator @customElement() is equivalent to customElements.define() in JavaScript
Note: Not all available components are designed to be re-used.
A few more traps for developers:
this.items.push(item); // no updatethis.items = [...this.items, item]; // updatesrender() — it runs on every update. Compute derived values in willUpdate() instead.this.updateComplete to wait for the DOM to reflect the latest state. Useful in tests and after programmatic property changes.export class MyElement extends LitElement {
static properties = {
new: {type: Boolean, attribute: 'new'},
};
constructor() {
super();
this.new = true // ⚠️ Always true ⚠️
}
// ...
}
friendsoftypo3/visual-editor ochorocho/revealjs-editor