Camp Vienna

Lit it Up!

  1. No build, no framework, no external dependencies.
  2. Re-use WebComponents shipped with TYPO3.

What is Lit?

  • A small wrapper around the WebComponents standard
  • Lit enables you to write WebComponents in a convenient and productive way

... and what are WebComponents?

In short:

WebComponents allow you to define/invent your own custom HTML elements.

WebComponent example

The HTML Element:

<typo3-surfcamp year="2026"></typo3-surfcamp>

The corresponding JavaScript:

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 import map

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/',
    ],
];

Load the WebComponent

In the Controller

<?php

$this->pageRenderer->loadJavaScriptModule('@ochorocho/lit-demo/component/typo3-surfcamp.js');

In the fluid template

<f:asset.module identifier="@ochorocho/lit-demo/component/typo3-surfcamp.js"></f:asset.module>

Lifecycle methods

The most used methods:

  • constructor() - When an element is created. Set property defaults
  • connectedCallback() - When an element is added to the document's DOM
  • firstUpdated() - 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 TemplateResult
  • disconnectedCallback() - 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 changed

For more details see the Lit docs or MDN docs

⚠️ Watch out!

Some of the methods require super so the default lit behavior is maintained.

connectedCallback() {
  super.connectedCallback()
  // ...
}

Reactive properties

  • type — converts the HTML attribute string to a value (String, Number, Boolean, Array, Object)
  • reflect: true — writes property changes back to the attribute
  • attribute: false — property only, never read from HTML
  • state: 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.

Example

static properties = {
  label:  { type: String },
  count:  { type: Number, reflect: true },
  active: { type: Boolean },
  items:  { type: Array, attribute: false },
  _open:  { state: true },
};

Caching

  • No (!!!) relative imports, e.g. ./component/typo3-weather-data.js
  • Use the identifier defined in JavaScriptModules.php

Handle events

Use the @-shorthand to listen to regular events e.g. @click="${this.doSomething}"

Listen to a custom event fired by a child element:

Event dispatched in a child component

this.dispatchEvent(new CustomEvent(
    'my-custom-event', { bubbles: true, detail: { uid: 666 }}
));

Listen to the event

<the-parent-component @my-custom-event="${this.doSomething}"></the-parent-component>

The shadow DOM

  • The shadow DOM has its own HTML structure, styles and JavaScript that is isolated from the rest of the page.
  • The shadow DOM can be disabled if needed. For example if you want to use existing styles.
  • Drawback when the shadow DOM is disabled: No slots, no scoped styles.

Disable the shadow DOM

export class NoShadowDom extends LitElement {
    // ...
    
    createRenderRoot() {
        return this;
    }

    // ...
}

customElements.define('no-shadow-dom', NoShadowDom)

Slots

Insertion points for content placed inside your component:

  • Default slot: children with no slot attribute
  • Named slot: child's slot="x" matches <slot name="x">
  • Content between <slot> tags is shown as fallback
  • Style slotted children via ::slotted(selector)

⚠️ Slots require the shadow DOM to be enabled.

JavaScript

render() {
  return html`
    <header><slot name="title">Untitled</slot></header>
    <section><slot></slot></section>
  `;
}

HTML

<my-card>
  <h2 slot="title">Hello</h2>
  <p>Goes into the default slot.</p>
</my-card>

Scoped styles

Apply component specific styles

⚠️ Scoped style require the shadow DOM to be enabled.

Example

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);

Translate labels

Load labels from any XLF file. In the example from EXT:lit_demo/Resources/Private/Language/locallang_components.xlf

Example

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)

Fetching data

  • For one-shot fetches (no update) use firstUpdated() and fetch()
  • For update capabilities go with @lit/task

Example

// 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>
  `,
  })}
`;

The TYPO3.* Object

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')); 

Find existing WebComponents

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.

WebComponents in Numbers

  • ~129 WebComponents available
  • ~24 WebComponents (more or less) re-usable

Mind the Content Security Policy

  • When sending a request via JavaScript allow the target domain in your CSP config (ContentSecurityPolicies.php)
  • Proxy the API request through PHP, e.g. for authentication or caching

⚠️ Watch out! II

A few more traps for developers:

  • Mutating arrays or objects won't trigger a re-render. Replace the reference instead of mutating it:
    this.items.push(item); // no update
    this.items = [...this.items, item]; // updates
  • Don't do heavy work in render() — it runs on every update. Compute derived values in willUpdate() instead.
  • Await this.updateComplete to wait for the DOM to reflect the latest state. Useful in tests and after programmatic property changes.
  • Boolean indicates only if the attribute exists. It doesn't care about the value.
export class MyElement extends LitElement {
    static properties = {
        new: {type: Boolean, attribute: 'new'},
    };
    
    constructor() {
      super();
      this.new = true // ⚠️ Always true ⚠️ 
    }
    // ...
}

Thank you!