# WebUI Lit Style Guide

[TOC]

## Overview

This style guide outlines coding standards and best practices for Chromium WebUI
development with Lit. These guidelines are intended to ensure code quality,
consistency, and performance across the codebase. They supplement the
[Chromium Web Development Style Guide](../../styleguide/web/web.md).

## CrLitElement Subclass Guidelines (.ts)

A Lit element class definition should have the following structure.

```ts
// my_button.ts example contents

import {getCss} from './my_button.css.js';
import {getHtml} from './my_button.html.js';

class MyButtonElement extends CrLitElement {
 static get is() {
    return 'my-button';
  }

  static override get styles() {
    return getCss();
  }

  override render() {
    return getHtml.bind(this)();
  }

  static override get properties() {
    return {
      ...
    };
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'cr-button': MyButtonElement;
  }
}

customElements.define(MyButtonElement.is, MyButtonElement);
```

Specifically the following pieces are required:

 1. The class name should end with the `Element` suffix, which matches the
    naming of native HTML elements (for example `HTMLButtonElement`), and
    clearly conveys that a class is a UI component.
 2. `static get is() {...}` holds the DOM name of the custom element.
 3. `interface HTMLElementTagNameMap {...}` informs the TypeScript compiler
     about the association between the DOM name and the class name, allowing it
     to infer the type in `document.createElement` or `querySelector`,
     `querySelectorAll` calls. Must be placed after the class definition in the
     same file.
 4. `customElements.define(...)` registers the custom element at runtime, so
     that the browser knows which class to instantiate when encountering the
     corresponding DOM name. Must be placed after the class definition in the
     same file.
 5. The HTML template is imported from the corresponding `.html.ts` template
    file using `getHtml()`, and is not defined inline. Exception: dummy test
    elements and low level cr_elements implementing customized rendering
    behavior may use html and render directly.

### Method definition order

Methods (if applicable) must be defined in the following order for consistency:
`is`, `styles`, `render`, `properties`, `constructor`, `connectedCallback`,
`disconnectedCallback`, `willUpdate`, `firstUpdated`, `updated`.

### Class fields definition order

Class fields should be defined after the `properties` getter and before the
`constructor` or, if no constructor exists, before any subsequent methods in the
method definition order.

Moreover, separate class fields that have a corresponding Lit reactive property
from class fields that don't correspond to any property with a blank
line. For example:

```ts
  static override get properties() {
    return {
      isActive: {type: Boolean},
      isDefault: {type: Boolean},
    };
  }

  accessor isActive: boolean = false;
  accessor isDefault: boolean = false;

  private browserProxy_: BrowserProxy = browserProxyFactory.getInstance();
```

### Leverage helper methods of the CrLitElement superclass.

#### Use `this.fire(...)` for firing events where possible.

**Do not:**
```ts
this.dispatchEvent(new CustomEvent(
  'event-one', {bubbles: true, composed: true}));

this.dispatchEvent(new CustomEvent(
  'event-two', {bubbles: true, composed: true, detail: someValue}));
```

**Do:**
```ts
this.fire('event-one');
this.fire('event-two', someValue);

// OK to use dispatchEvent() when firing non-bubbling or non-composed events.
this.dispatchEvent(new CustomEvent('event-three', {detail: someValue}));
```

*Note*: When a non-bubbling or non-composed event should be fired use
`dispatchEvent()` directly, as `fire()` creates bubbling and composed events.

## Template Guidelines (.html.ts)

### Logic-free Templates

Templates should remain declarative and logic-free. Avoid all of the following
in the `.html.ts` file:

* Local variable declarations like `let` or `const`.
* Function definitions, other than a single `getHtml()` function.
* if/for statements and other complex logic.

Instead:

* Delegate complex logic to helper methods defined in the component's logic file
  (`.ts`).
* If the same chunk of HTML template is needed in many places or is logically
  a mostly self-contained "module", make it a separate custom element.

The overall goal is to separate the HTML template from the element's business
logic as much as possible, and draw a clear boundary between the
responsibilities of each file. `.html.ts` files should mostly look like regular
HTML code with a bit of Lit extras (Lit expressions), and any non-trivial TS
logic should be delegated to helper methods in the .ts file. Similarly, the
`.ts` file should not be responsible for defining any portion of the template
directly, as this should be done in the `.html.ts` file.


**Do not:**
```ts
// my_element.html.ts
export function getHtml(this: MyElement) {
  const isVisible = this.someCondition && this.otherCondition;
  return html`
    <div ?hidden="${isVisible}">...</div>
  `;
}
```

**Do:**
```ts
// my_element.ts
protected accessor isVisible = false;

// my_element.html.ts
export function getHtml(this: MyElement) {
  return html`
    <div ?hidden="${!this.isVisible}">...</div>
  `;
}
```

#### Reusing chunks of HTML
In most cases, a chunk of HTML that is needed across multiple locations in
the DOM should be refactored into a custom element that can be used as needed.
Custom elements are the canonical way of creating reusable chunks of template,
just as shared styles are the way to share CSS between elements and mixins or
helper methods/classes can be used to share TypeScript logic/behavior.

However, there are some cases where a different solution may be preferred.

1. Trivial chunks of HTML can be duplicated. Refactoring to a custom element is
   overkill for very small bits of template.

**Example:**
```ts
// my_element.html.ts
export function getHtml(this: MyElement) {
  // Small amount of repeated HTML for the cr-button is okay.
  return html`
    ${this.submitButtonFirst ? html`
      <cr-button id="submit" @click="${this.onSubmitClick}">
        $i18n{submit}
      </cr-button>
    ` : ''}
    <cr-button id="cancel" @click="${this.onCancelClick}">
      $i18n{cancel}
    </cr-button>
    ${!this.submitButtonFirst ? html`
      <cr-button id="submit" @click="${this.onSubmitClick}">
        $i18n{submit}
      </cr-button>
    ` : ''}
  `;
}
```

2. Prefer conditional styling to conditional DOM changes for cases of
   modifying appearance. In some cases, it may look like a chunk of HTML
   needs to go in 2 different places in the DOM, but the only reason for
   this is to change its appearance (or the appearance of some DOM around
   it). In this case, conditional CSS styling is a better approach.

**Do not:**
```css
/* my_element.css */
.fancy-css {
  border: 4px solid blue;
}
```

```ts
// my_element.html.ts
export function getHtml(this: MyElement) {
  const button = html`
      <cr-button id="submit" @click="${this.onSubmitClick}">
        $i18n{submit}
      </cr-button>`;
  return html`
    ${this.fancyStyleEnabled ? html`
      <div class="fancy-css">${button}</div>
    ` : button}
  `;
}
```

**Do:**
```css
/* my_element.css */
:host([fancy-style-enabled]) .wrapper {
  border: 4px solid blue;
}
```

```ts
// my_element.html.ts
export function getHtml(this: MyElement) {
  return html`
    <div class="wrapper">
      <cr-button id="submit" @click="${this.onSubmitClick}">
        $i18n{submit}
      </cr-button>
    </div>
  `;
}
```

3. Long chunks of reused HTML that contain very few elements but a lot of
   bindings can be placed in their own helper .html.ts file, and imported.
   Cases like this would not benefit from refactoring into a custom element,
   because the repeated HTML being refactored is a long list of data bindings
   and event handlers that would simply be re-created for a new custom element.

**Example:**
```ts
// my_element_fancy_button.html.ts
export function getHtml(this: MyElement) {
  return html`
<fancy-button .prop1="${this.prop1}"
    .prop2="${this.prop2}"
    .prop3="${this.prop3}"
    .prop4="${this.prop4}"
    .prop5="${this.prop5}"
    .prop6="${this.prop6}"
    .prop7="${this.prop7}"
    .prop8="${this.prop8}"
    .prop9="${this.prop9}"
    @one="${this.onButtonOne}"
    @two="${this.onButtonTwo}"
    @three="${this.onButtonThree}"
    @four="${this.onButtonFour}">
</fancy-button>
`;
}
```

```ts
// my_element.html.ts
import {getHtml as getFancyButtonHtml} from './my_element_fancy_button.html.js';

export function getHtml(this: MyElement) {
  return html`
${this.compact ? html`
  <div class="compact">
    ${getFancyButtonHtml.bind(this)()}
    <cr-input id="input></cr-input>
  </div>
` : html`
  <div class="tall">
    <cr-textarea id="input></cr-textarea>
    <fancy-menu>
      ${getFancyButtonHtml.bind(this)()}
    </fancy-menu>
  </div>
`}
```

### Inline Lambdas

Do not use inline arrow functions (lambdas) in templates to pass data to event
handlers. Creating new functions on every render hurts performance and
readability.

A common reason to reach for lambdas is to bind additional data to an event
handler. Instead, bind a unique identifier, commonly an index, to a `data-*`
attribute on the element and retrieve it in the handler.


**Do not:**
```html
${this.items.map(item => html`
  <button @click="${() => this.onItemClick(item)}">${item}</button>
`)}
```

**Do:**
```html

${this.items.map((item, index) => html`
  <button data-index="${index}" @click="${this.onItemClick}">${item}</button>
`)}
```

```ts
protected onItemClick(e: Event) {
  const currentTarget = e.currentTarget as HTMLElement;
  const item = items[Number(currentTarget.dataset['index'])];
}
```

### Loop Variables

When using `map` to render lists, prefer using `"item"` as the loop variable
name for consistency. Nested loops may use specific names to avoid confusion.

```ts
${this.items.map(item => html`...`)}
```

### Formatting

Wrap the return statement of your `getHtml` function or `render` method in `//
clang-format off` and `// clang-format on` comments. This prevents
`clang-format` from mangling the HTML template string, ensuring it remains
readable.

```ts
export function getHtml(this: MyCrLitElement) {
  // clang-format off
  return html`
    <!--_html_template_start_-->
    <div>...</div>
    <!--_html_template_end_-->`;
  // clang-format on
}
```

### Templatized UI elements

When binding properties (such as `.template` or `.items`) on generic/templatized
elements (e.g., `<cr-lazy-list>` or `<cr-infinite-list>`) in a Lit template file
(`.html.ts`), the `@webui-eslint/lit-element-expressions` ESLint rule checks
property type compatibility.

By default, the ESLint rule resolves element tags using `HTMLElementTagNameMap`,
which declares generic components with `unknown` type parameters (e.g.,
`CrLazyListElement<unknown>`). If a binding provides a strongly-typed callback (e.g.,
`(item: MyItem, index: number) => TemplateResult`), ESLint will flag a type
mismatch error because callback parameters of type `MyItem` are not assignable
to parameters expecting `unknown`.

To provide the more accurate type information to ESLint:

1. Ensure the element in the HTML template has an `id` attribute (e.g.,
   `id="list"`).
2. Export a `TemplatizedDomNodes` interface in the **`.html.ts`** template file
   (not in the `.ts` class file) mapping the element's `id` to its concrete
   generic type.

**Example:**

```ts
// my_element.html.ts
import type {CrLazyListElement} from '//resources/cr_elements/cr_lazy_list/cr_lazy_list.js';
import {html} from '//resources/lit/v3_0/lit.rollup.js';

import type {MyItem} from './my_item.js';
import type {MyElement} from './my_element.js';

export interface TemplatizedDomNodes {
  list: CrLazyListElement<MyItem>;
}

export function getHtml(this: MyElement) {
  return html`<!--_html_template_start_-->
<cr-lazy-list id="list" .items="${this.items}"
    .template="${(item: MyItem, index: number) => html`
      <div>${item.name}</div>
    `}">
</cr-lazy-list>
<!--_html_template_end_-->`;
}
```

## Naming Conventions

### CrLitElement DOM, file, class names.

* The DOM name **should not end** with the `-element` suffix.
* The file name **should not end** with the `_element` suffix.
* The class name **should end** with the `Element` suffix.
* The class and file name **should be derived** from the DOM name.

Where the pattern to derive the class and file names from the DOM name is

* DOM name: `foo-bar-baz`
* Class name candidates: `<OptionalPrefix>FooBarBazElement`, or `BarBazElement`,
  or `BazElement`
* File name candidates: `foo_bar_baz.{css,html,ts}`, `bar_baz.{css,html,ts}` or
  `baz.{css,html,ts}`,

### Events

Event names should be in lower kebab case.

Event names should state what happened, not what will happen by whoever handles
the event.

*   **Good:** `close-click`, `selection-change-click`
*   **Bad:** `close-dialog`, `update-selection`

### Event Handlers

Event handlers should be named using the pattern `on<OptionalContext>[Event]`.

*   **Good:** `onRetryClick`, `onSelectionChanged`, `onPointerdown`
*   **Bad:** `onRetryClicked`, `handleRetry`, `selectionHandler`

If the DOM node that the event handler is bound to has an ID, the
`<OptionalContext>` portion of the handler name should be that ID.

**Do not:**
```html
<button id="foo" @click="${this.onBarClick}"></button>
```

**Do:**
```html
<button id="foo" @click="${this.onClick}"></button>
<button id="foo" @click="${this.onFooClick}"></button>
```

## DOM Access

### Static Elements

For elements that always exist in the Shadow DOM (not part of any conditional
rendering), prefer `this.$.<id>` to access them. This provides a strictly typed
and consistent way to access elements compared to
`this.shadowRoot.querySelector`.

**Do not:**
```ts
const button = this.shadowRoot.querySelector('#submitButton');
```

**Do:**
```ts
// In your class definition
export interface MyElement {
  $: {
    submitButton: CrButtonElement,
  }
}

// In your code
const button = this.$.submitButton;
```
