Developing add-ons with Lit and TypeScript
Introduction
When you develop add-ons with a combination of Lit and TypeScript, you get the benefits of both worlds; a lightweight component library with reactive properties and templating capabilities, which help you build fast and efficient components, and the robust type system provided by TypeScript.
Lit Key Features
LitElement
Base Class
Lit provides the LitElement
base class for creating custom elements. It extends the standard HTMLElement
and adds reactive properties and templating capabilities. The LitElement
class is important to understand when working with Lit, as it provides the foundation for building custom elements.
Components must have dashes in their name to be valid custom elements. For example, my-component
is a valid custom element name, while MyComponent
is not.
Template Literals
A template literal is a string literal that allows embedded expressions. It is enclosed in backticks (`) and can contain placeholders (${expression})
for dynamic values. Template literals provide a more flexible and readable way to define strings compared to traditional string concatenation.
Decorators
A decorator is a certain type of declaration that can be attached to a class declaration. It is prefixed with an @
symbol and can be used to modify the behavior of a class or its members. Some popular decorators in Lit include:
@customElement
: defines a custom element with a given tag name.@property
: defines a reactive property that triggers a re-render when its value changes.@state
: defines a local state property that triggers a re-render when its value changes. The difference between@state
and@property
is that@state
properties are not exposed as custom element properties. It's useful for managing component-specific state that does not need to be shared with other components.@query
: allows you to query for elements in the component's shadow DOM, for instance, to access a button element with the idmyButton
, you can use@query('#myButton') myButton: HTMLButtonElement;
.@eventOptions
: allows you to specify event options likecapture
,once
, andpassive
for event listeners. For example,@eventOptions({ capture: true }) handleClick() { ... }
.
Directives
A Lit directive is a special kind of decorator that allows you to extend the template syntax with custom behavior. Some popular directives include:
until
: waits for a promise to resolve before rendering the content.repeat
: repeats a template for each item in an array.ifDefined
: conditionally renders content based on the value being defined.guard
: prevents re-rendering if the value has not changed.cache
: caches the result of an expression to improve performance.live
: updates the DOM when a reactive property changes. It's useful when you need to trigger a side effect or update the DOM based on a property change that's not directly related to rendering the component.asyncReplace
: asynchronously replaces the content of a template. This directive is useful when you need to fetch data asynchronously and update the template once the data is available.asyncAppend
: asynchronously appends content to a template. This directive is useful when you need to fetch data asynchronously and append it to the template once the data is available.css
: a directive that creates a CSS template from a template literal. This directive is used to define CSS styles for a component.
The difference between a directive and a decorator is that a directive is applied to a template, while a decorator is applied to a class or a class member.
render
Method
The render
method is defined as a template literal that returns the component's HTML structure. It uses the html
function from the Lit package to create the template. The render
method is called whenever the component needs to be re-rendered, for instance, when a reactive property changes. Some methods that are commonly used in the render
method include:
Reactive Properties
Lit uses reactive properties to automatically update the DOM when the state of your component changes. You define properties using decorators like @property
. When a property changes, Lit automatically triggers a re-render of the component. This reactive behavior simplifies the process of managing state and updating the UI.
TypeScript Key Features
Static Typing
TypeScript allows you to define types for variables, function parameters, and return values, which helps catch type-related errors at compile time.
Copied to your clipboardlet message: string = "Hello, TypeScript!";
Type Inference
TypeScript can automatically infer types based on the assigned values, reducing the need for explicit type annotations.
Copied to your clipboardlet count = 42; // inferred as number
Interfaces
Interfaces define the shape of an object, specifying the properties and their types. They help enforce consistent object structures.
Copied to your clipboardinterface User {name: string;age: number;}
Classes
TypeScript supports object-oriented programming with classes, including features like inheritance, access modifiers, and decorators.
Copied to your clipboardclass Person {constructor(public name: string, public age: number) {}greet() {console.log(`Hello, my name is ${this.name}`);}}
Modules
TypeScript uses ES6 module syntax to organize code into reusable modules, making it easier to manage large codebases.
Copied to your clipboard// math.tsexport function add(a: number, b: number): number {return a + b;}// main.tsimport { add } from "./math";console.log(add(2, 3));
Generics
Generics allow you to create reusable components that work with various types, providing flexibility and type safety.
Copied to your clipboardfunction identity<T>(arg: T): T {return arg;}
Add-on Project Anatomy
When you use the CLI to create an add-on based on Lit and TypeScript (ie: the swc-typescript
or swc-typescript-with-document-sandbox
templates), the CLI generates a project structure that includes the necessary files and configurations to get you started quickly. For instance:
File/Folder | Description |
---|---|
src/index.html | The main HTML template that loads your add-on. |
src/index.ts | The entry point for your add-on, where you define your Lit components. |
src/ui/components | The directory where you define your Lit components. |
src/ui/components/App.ts | The main application component that uses the Adobe Add-On UI SDK to interact with the document sandbox runtime. |
src/ui/components/App.css.ts | The CSS styles for the main application component. |
src/models | The directory where you define TypeScript interfaces for your add-on APIs. |
src/models/DocumentSandboxApi.ts | The TypeScript interface for the APIs exposed by the document sandbox runtime. |
src/sandbox/code.ts | The implementation of the document sandbox runtime. |
src/sandbox/tsconfig.json | The TypeScript configuration file that specifies the compiler options for your project. |
A more in-depth description of the files and folders in the project structure is provided below.
index.html
This is the main HTML file that serves as the entry point for the web application. It includes the custom element <add-on-root>
, which is defined in index.ts
.
Copied to your clipboard<body><add-on-root></add-on-root></body>
index.ts
This file defines the root custom element <add-on-root>
using Lit. It initializes the Adobe Add-On UI SDK and renders the <add-on-app>
component once the SDK is ready.
Copied to your clipboardimport { LitElement, html } from "lit";import { customElement, state } from "lit/decorators.js";import { until } from "lit/directives/until.js";import "./components/App";import addOnUISdk from "https://express.adobe.com/static/add-on-sdk/sdk.js";@customElement("add-on-root") // Lit customElement decorator defines a custom element <add-on-root>.export class Root extends LitElement {@state()private _isAddOnUISdkReady = addOnUISdk.ready;// The render method returns an HTML template that uses the until// directive to wait for the Add-On UI SDK to be ready. Once the// SDK is ready, it renders the <add-on-app> component.render() {// This block is a template literal that returns an HTML template// using the Lit html function. denoted by it being enclosed in// backticks (`). Dynamic values are inserted using placeholders// like (${expression}).return html`${until(// The until directive is used to wait for a promise// to resolve before rendering the content.this._isAddOnUISdkReady.then(async () => {console.log("addOnUISdk is ready for use.");return html`<add-on-app .addOnUISdk=${addOnUISdk}></add-on-app>`;}))}`;}}
App.ts
Defines the main application component <add-on-app>
using Lit. It uses the Adobe Add-On UI SDK to interact with the document sandbox runtime and provides a button to create a rectangle in the document.
Copied to your clipboardimport { LitElement, html } from "lit";import { customElement, property, state } from "lit/decorators.js";import { DocumentSandboxApi } from "../../models/DocumentSandboxApi";import { style } from "./App.css";import {AddOnSDKAPI,RuntimeType,} from "https://express.adobe.com/static/add-on-sdk/sdk.js";// The following line defines a custom element <add-on-app> using the Lit// customElement decorator.@customElement("add-on-app")export class App extends LitElement {@property({ type: Object })addOnUISdk!: AddOnSDKAPI;@state()private _sandboxProxy: DocumentSandboxApi;static get styles() {return style;}async firstUpdated(): Promise<void> {const { runtime } = this.addOnUISdk.instance;this._sandboxProxy = await runtime.apiProxy(RuntimeType.documentSandbox);}private _handleClick() {this._sandboxProxy.createRectangle();}// The render method returns an HTML template that uses the .container// class defined in the CSS.render() {// This block is a template literal that returns an HTML template// using the Lit html function. A template literal in Lit is// enclosed in backticks (`) and can contain placeholders (${expression})// for dynamic values.return html` <sp-themesystem="express"color="light"scale="medium"><div class="container"><sp-buttonsize="m"@click=${this._handleClick}>Create Rectangle</sp-button></div></sp-theme>`;}}
App.css.ts
Defines the CSS styles for the <add-on-app>
component using Lit's css
tagged template literal.
Copied to your clipboardimport { css } from "lit"; // Import the css function from the lit package// The following block defines the CSS styles for the .container class// using the css tagged template literal. The styles are defined within// backticks (`) and are passed to the css function to create a CSSResult// object. A CSSResult object is a representation of CSS that can be applied// to a LitElement component.export const style = css`.container {margin: 24px;display: flex;flex-direction: column;}`;
DocumentSandboxApi.ts
Defines the TypeScript interface for the APIs that the document sandbox runtime exposes to the UI runtime. Once you define an interface, any object that implements that interface must implement to the contract defined in the interface. The document sandbox runtime implements this interface in the code.ts
file.
Copied to your clipboardexport interface DocumentSandboxApi {//createRectangle(): void;}
code.ts
Contains the implementation of the document sandbox runtime. It defines the createRectangle
function and exposes it to the UI runtime (ie: the code running in the iframe in the ui
folder).
Copied to your clipboardimport addOnSandboxSdk from "add-on-sdk-document-sandbox";import { editor } from "express-document-sdk";// Import the DocumentSandboxApi interface from the models folderimport { DocumentSandboxApi } from "../models/DocumentSandboxApi";const { runtime } = addOnSandboxSdk.instance;function start(): void {// The following block defines a sandboxApi object that implements the// DocumentSandboxApi interface. Since it implements the interface, it// must provide an implementation for the createRectangle function.const sandboxApi: DocumentSandboxApi = {createRectangle: () => {const rectangle = editor.createRectangle();rectangle.width = 240;rectangle.height = 180;rectangle.translation = { x: 10, y: 10 };const color = { red: 0.32, green: 0.34, blue: 0.89, alpha: 1 };const rectangleFill = editor.makeColorFill(color);rectangle.fill = rectangleFill;const insertionParent = editor.context.insertionParent;insertionParent.children.append(rectangle);},};const sandboxApi: DocumentSandboxApi = {createRectangle: () => {const rectangle = editor.createRectangle();rectangle.width = 240;rectangle.height = 180;rectangle.translation = { x: 10, y: 10 };const color = { red: 0.32, green: 0.34, blue: 0.89, alpha: 1 };const rectangleFill = editor.makeColorFill(color);rectangle.fill = rectangleFill;const insertionParent = editor.context.insertionParent;insertionParent.children.append(rectangle);},};runtime.exposeApi(sandboxApi);}start();
tsconfig.json
Specifies the TypeScript compiler options for your project. It includes settings like the target ECMAScript version, module format, and output directory.
Copied to your clipboard{"compilerOptions": {"target": "ES2018","module": "ESNext","strict": true,"outDir": "./dist"},"include": ["src/**/*"]}
Create a New Lit Component
To create a new component using Lit and TypeScript, follow these steps:
Step 1: Create a new TypeScript file in the src/ui/components
directory.
Copied to your clipboardtouch src/ui/components/MyCustomButton.ts
Step 2: Define a new class that extends LitElement
and implements your component logic.
Copied to your clipboardimport { LitElement, html } from "lit";// Import the customElement and state decorators from the lit packageimport { customElement, state } from "lit/decorators.js";@customElement("my-custom-button") // Decorator defines my-custom-button// Define a custom LitElement component MyCustomButton that extends LitElement.// The code includes a state property message that holds the text to be// displayed and a render method that returns an HTML template. The template// includes a button element that triggers the handleClick method when clicked// and displays the message property value.export class MyCustomButton extends LitElement {@state()private message = "Hello, Lit!";render() {return html`<sp-button @click="${this.handleClick}">Send</sp-button><p>${this.message}</p>`;}handleClick() {this.message = "Custom Button Clicked!";}}
Step 2: Import the Component
To use the new component in your application, import it in the App.ts
file and include it in the render method.
Copied to your clipboardimport { LitElement, html } from "lit";import { customElement, property, state } from "lit/decorators.js";// Import the MyCustomButton componentimport { MyCustomButton } from "./MyCustomButton";@customElement("add-on-app")// Now you can use the MyCustomButton component in the render method of// the App component. For instance in the block below:export class App extends LitElement {...render() {return html` <sp-theme system="express" color="light" scale="medium"><div class="container"><sp-button size="m" @click=${this._handleClick}>Create Rectangle</sp-button><my-custom-button></my-custom-button></div></sp-theme>`;}...}
FAQ
Q: What are the main benefits of using Lit with TypeScript for Adobe Express add-ons?
A: The combination provides:
- Lightweight components: Lit creates fast, efficient web components with minimal overhead
- Type safety: TypeScript catches errors at compile time and improves code maintainability
- Modern standards: Built on web component standards for future-proof development
- Reactive properties: Automatic UI updates when component state changes
- Great developer experience: Excellent tooling, IntelliSense, and debugging support
Q: Do I need to know web components to use Lit?
A: Not necessarily! Lit abstracts away much of the complexity of web components. You work with familiar concepts like classes, properties, and templates. However, understanding the basics of web components (custom elements, shadow DOM) can be helpful for advanced use cases.
Q: What's the difference between @property
and @state
decorators?
A:
@property
: Creates a reactive property that's exposed as a custom element attribute. Use for data that comes from parent components or HTML attributes.@state
: Creates internal reactive state that's not exposed externally. Use for component-specific state that doesn't need to be shared with other components.
Q: How do I handle events in Lit components?
A: Use the @
syntax in templates to bind event listeners:
Copied to your clipboardrender() {return html`<button @click=${this.handleClick}>Click me</button>`;}handleClick(event: Event) {// Handle the click event}
Q: Can I use Lit components with other frameworks like React?
A: Yes! Lit components are standard web components, so they work with any framework or vanilla JavaScript. However, some frameworks (like React) may need special handling for events and properties.
Q: How do I style Lit components?
A: Use the css
tagged template literal and the styles
static property:
Copied to your clipboardimport { css } from 'lit';static styles = css`:host {display: block;padding: 16px;}button {background: blue;color: white;}`;
Q: What's the render()
method and when is it called?
A: The render()
method defines your component's HTML template using the html
tagged template literal. Lit automatically calls it when:
- The component is first created
- Any reactive property (marked with
@property
or@state
) changes - You manually call
this.requestUpdate()
Q: How do I query for elements in my component's shadow DOM?
A: Use the @query
decorator:
Copied to your clipboard@query('#myButton')myButton!: HTMLButtonElement;// Now you can access this.myButton in your methods
Q: Can I use async operations in Lit components?
A: Yes! Use the until
directive for promises and asyncReplace
/asyncAppend
for async iterables:
Copied to your clipboardrender() {return html`${until(this.fetchData(), html`Loading...`)}`;}
Q: How do I communicate between parent and child Lit components?
A:
- Parent to child: Pass data via properties
- Child to parent: Dispatch custom events using
this.dispatchEvent(new CustomEvent('my-event', { detail: data }))
Q: What TypeScript configuration works best with Lit?
A: The CLI-generated tsconfig.json
is a good starting point. Key settings include:
"target": "ES2018"
or higher for modern features"experimentalDecorators": true
for decorator support"strict": true
for better type checking
Next Steps
Next, you can explore more advanced features of Lit and TypeScript to enhance your components. Some areas to explore include:
- Event Handling: Learn how to handle events in Lit components and communicate between components.
- Component Composition: Explore how to compose multiple components together to create complex UIs.
- State Management: Implement state management solutions like Redux or MobX to manage the state of your components.
- Performance Optimization: Optimize your components for performance by using memoization, lazy loading, and other techniques.
- Testing: Write unit tests for your components using tools like Jest or Mocha to ensure their correctness and reliability.
Check out this handy cheat sheet on properties and state for further reference throughout your development.