# Component Development Patterns
This document covers standard patterns and conventions for developing Lit
components in Common UI.
## Component Categories
Common UI components fall into distinct categories, each with specific patterns:
### 1. Layout Components
Components that arrange other components without providing content themselves.
**Examples:** `cf-vstack`, `cf-hstack`, `cf-screen`, `cf-autolayout`
**Characteristics:**
- Use flexbox or grid
- Accept child elements via slots
- Provide gap, alignment, and spacing controls
- No theme color consumption (mostly)
- Simple property-based configuration
**Pattern:**
```typescript
export class CFVStack extends BaseElement {
static override properties = {
gap: { type: String },
align: { type: String },
justify: { type: String },
};
declare gap: string;
declare align: string;
declare justify: string;
override render() {
const classes = {
stack: true,
[`gap-${this.gap}`]: true,
[`align-${this.align}`]: true,
[`justify-${this.justify}`]: true,
};
return html`
`;
}
}
```
### 2. Visual Components
Components that display content with styling.
**Examples:** `cf-label`, `cf-separator`, `cf-skeleton`
**Characteristics:**
- May consume theme
- Provide visual feedback or decoration
- Usually simple with few properties
**Pattern:**
```typescript
export class CFSeparator extends BaseElement {
static override properties = {
orientation: { type: String },
decorative: { type: Boolean },
};
declare orientation: "horizontal" | "vertical";
declare decorative: boolean;
override render() {
return html`
`;
}
}
```
### 3. Input Components
Components that capture user input.
**Examples:** `cf-button`, `cf-input`, `cf-checkbox`, `cf-textarea`
**Characteristics:**
- Consume theme for consistent styling
- Emit custom events (use `this.emit()`)
- May use `InputTimingController` for debouncing
- Handle disabled states
**Pattern:**
```typescript
export class CFButton extends BaseElement {
@consume({ context: cfThemeContext, subscribe: true })
@property({ attribute: false })
declare theme?: CFTheme;
static override properties = {
variant: { type: String },
disabled: { type: Boolean, reflect: true },
};
override firstUpdated(changed: Map) {
super.firstUpdated(changed);
this._updateThemeProperties();
}
override updated(changed: Map) {
super.updated(changed);
if (changed.has("theme")) {
this._updateThemeProperties();
}
}
private _updateThemeProperties() {
const currentTheme = this.theme || defaultTheme;
applyThemeToElement(this, currentTheme);
}
private _handleClick(e: Event) {
if (this.disabled) {
e.preventDefault();
e.stopPropagation();
return;
}
// Emit custom event
this.emit("cf-click", {/* detail */});
}
}
```
### 4. Complex/Integrated Components
Components that deeply integrate with the runtime and Cell abstractions.
**Examples:** `cf-render`, `cf-code-editor`, legacy tree editor implementations
**Characteristics:**
- Work with Cell properties
- Manage subscriptions
- Handle transactions for mutations
- Complex lifecycle management
- May use reactive controllers
**Pattern:** See `references/cell-integration.md` for detailed patterns.
## File Structure
Each component should follow this structure:
```
cf-component-name/
├── cf-component-name.ts # Component implementation
├── index.ts # Export and registration
└── styles.ts # Optional: extracted styles (for complex components)
```
### Component Implementation File
```typescript
// cf-button.ts
import { css, html } from "lit";
import { property } from "lit/decorators.js";
import { BaseElement } from "../../core/base-element.ts";
export type ButtonVariant = "primary" | "secondary" | "destructive";
export class CFButton extends BaseElement {
static override styles = [
BaseElement.baseStyles,
css`
/* component styles */
`,
];
static override properties = {
variant: { type: String },
};
declare variant: ButtonVariant;
constructor() {
super();
this.variant = "primary";
}
override render() {
return html`
`;
}
}
globalThis.customElements.define("cf-button", CFButton);
```
### Index File
```typescript
// index.ts
import { ButtonVariant, CFButton } from "./cf-button.ts";
if (!customElements.get("cf-button")) {
customElements.define("cf-button", CFButton);
}
export { CFButton };
export type { ButtonVariant };
```
Note: Both registrations are intentional. The component file registers
unconditionally on import (the dominant convention in
`packages/ui/src/v2/components/`); the index file's conditional check is then a
safe no-op, and prevents duplicate registration errors during hot module
replacement or alternate import paths. Keep both.
## Type Safety
### Export Types
Always export types separately:
```typescript
export type { ButtonSize, ButtonVariant };
```
### Property Type Declarations
Use `declare` for typed properties:
```typescript
static override properties = {
variant: { type: String },
size: { type: String },
disabled: { type: Boolean, reflect: true },
};
declare variant: ButtonVariant;
declare size: ButtonSize;
declare disabled: boolean;
```
### Type Imports
Import types with `type` keyword when possible:
```typescript
import type { Cell } from "@commonfabric/runner";
import type { CFTheme } from "../theme-context.ts";
```
## Styling Conventions
### Base Styles
Always extend `BaseElement.baseStyles` for CSS variables:
```typescript
static override styles = [
BaseElement.baseStyles,
css`
/* component styles */
`,
];
```
### Box Sizing
Always include box-sizing reset:
```css
:host {
display: block;
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
```
### CSS Parts
Expose major elements as parts for external styling:
```typescript
return html`
`;
```
### Class Organization
Use `classMap` from `lit/directives/class-map.js` for dynamic classes:
```typescript
import { classMap } from "lit/directives/class-map.js";
const classes = {
button: true,
[this.variant]: true,
[this.size]: true,
disabled: this.disabled,
};
return html`
`;
```
## Event Handling
### Emitting Events
Use the `emit()` helper from `BaseElement`:
```typescript
protected emit(
eventName: string,
detail?: T,
options?: EventInit,
): boolean
```
Events are automatically `bubbles: true` and `composed: true`.
**Pattern:**
```typescript
private handleChange(newValue: string) {
this.emit("cf-change", { value: newValue });
}
```
### Event Naming
- Prefix custom events with `cf-`
- Use present tense: `cf-change`, not `cf-changed`
- Be specific: `cf-add-item`, `cf-remove-item`
### Event Documentation
Document events in JSDoc:
```typescript
/**
* @fires cf-change - Fired when value changes with detail: { value }
* @fires cf-submit - Fired when form is submitted with detail: { formData }
*/
```
## Property Conventions
### Reflecting Properties
Use `reflect: true` for boolean states that should be visible in DOM:
```typescript
static override properties = {
disabled: { type: Boolean, reflect: true },
readonly: { type: Boolean, reflect: true },
};
```
### Default Values
Set defaults in constructor:
```typescript
constructor() {
super();
this.variant = "primary";
this.size = "default";
this.disabled = false;
}
```
### Non-Attribute Properties
Use `attribute: false` for objects, arrays, and Cells:
```typescript
static override properties = {
theme: { type: Object, attribute: false },
cell: { attribute: false },
items: { type: Array, attribute: false },
};
```
## Lifecycle Methods
### Common Lifecycle Pattern
```typescript
override firstUpdated(changed: Map) {
super.firstUpdated(changed);
// One-time setup
this._updateThemeProperties();
this._setupEventListeners();
}
override updated(changed: Map) {
super.updated(changed);
// React to property changes
if (changed.has("theme")) {
this._updateThemeProperties();
}
}
override disconnectedCallback() {
super.disconnectedCallback();
// Cleanup
this._cleanup();
}
```
## Documentation
### JSDoc Comments
Provide comprehensive JSDoc:
```typescript
/**
* CFButton - Interactive button element with multiple variants
*
* @element cf-button
*
* @attr {string} variant - Visual style: "primary" | "secondary" | "destructive"
* @attr {string} size - Button size: "default" | "sm" | "lg" | "icon"
* @attr {boolean} disabled - Whether the button is disabled
*
* @slot - Default slot for button content
*
* @fires cf-click - Fired when button is clicked
*
* @example
* Click Me
*/
```
## Testing
Tests should be colocated with components:
```
cf-component/
├── cf-component.ts
├── cf-component.test.ts
└── index.ts
```
### Basic Test Structure
```typescript
import { describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
import { CFButton } from "./cf-button.ts";
describe("CFButton", () => {
it("should be defined", () => {
expect(CFButton).toBeDefined();
});
it("should create element instance", () => {
const element = new CFButton();
expect(element).toBeInstanceOf(CFButton);
});
it("should have default properties", () => {
const element = new CFButton();
expect(element.variant).toBe("primary");
expect(element.disabled).toBe(false);
});
});
```
Run tests with: `deno task test` (NOT `deno test` - the task includes important
flags)