A library for constructing Web Components
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Built-in template directives for common patterns like conditionals, loops, references, and DOM node observation, providing powerful declarative capabilities for dynamic UI construction.
Conditional rendering directive that shows or hides template content based on boolean expressions, with optional else templates.
/**
* A directive that enables basic conditional rendering in a template
* @param condition - The condition to test for rendering
* @param templateOrTemplateBinding - The template to render when condition is true
* @param elseTemplateOrTemplateBinding - Optional template to render when condition is false
* @returns A capture type for template interpolation
*/
function when<TSource = any, TReturn = any, TParent = any>(
condition: Expression<TSource, TReturn, TParent> | boolean,
templateOrTemplateBinding:
| SyntheticViewTemplate<TSource, TParent>
| Expression<TSource, SyntheticViewTemplate<TSource, TParent>, TParent>,
elseTemplateOrTemplateBinding?:
| SyntheticViewTemplate<TSource, TParent>
| Expression<TSource, SyntheticViewTemplate<TSource, TParent>, TParent>
): CaptureType<TSource, TParent>;Usage Examples:
import { FASTElement, customElement, html, when, attr, observable } from "@microsoft/fast-element";
const template = html<ConditionalExample>`
<div class="container">
<!-- Simple boolean condition -->
${when(x => x.isVisible, html`<p>This content is visible!</p>`)}
<!-- Condition with else template -->
${when(
x => x.user,
html`<p>Welcome, ${x => x.user.name}!</p>`,
html`<p>Please log in.</p>`
)}
<!-- Complex condition -->
${when(
x => x.items.length > 0,
html`
<div class="items-container">
<h3>Items (${x => x.items.length})</h3>
<ul>
${x => x.items.map(item => `<li>${item}</li>`).join('')}
</ul>
</div>
`,
html`<p class="empty">No items available.</p>`
)}
<!-- Nested conditions -->
${when(
x => x.isLoggedIn,
html`
<div class="user-area">
${when(
x => x.isAdmin,
html`<button class="admin-btn">Admin Panel</button>`
)}
${when(
x => x.notifications.length > 0,
html`
<div class="notifications">
${x => x.notifications.length} new notification(s)
</div>
`
)}
</div>
`
)}
<!-- Dynamic template selection -->
${when(
x => x.viewMode === 'list',
x => x.listTemplate,
x => x.gridTemplate
)}
<!-- Multiple conditions -->
${when(
x => x.status === 'loading',
html`<div class="spinner">Loading...</div>`
)}
${when(
x => x.status === 'error',
html`<div class="error">Error: ${x => x.errorMessage}</div>`
)}
${when(
x => x.status === 'success',
html`<div class="success">Operation completed!</div>`
)}
</div>
`;
@customElement({
name: "conditional-example",
template
})
export class ConditionalExample extends FASTElement {
@observable isVisible: boolean = true;
@observable isLoggedIn: boolean = false;
@observable isAdmin: boolean = false;
@observable user: { name: string } | null = null;
@observable items: string[] = [];
@observable notifications: string[] = [];
@observable viewMode: 'list' | 'grid' = 'list';
@observable status: 'loading' | 'error' | 'success' | 'idle' = 'idle';
@observable errorMessage: string = "";
// Dynamic templates
listTemplate = html`<div class="list-view">List View Content</div>`;
gridTemplate = html`<div class="grid-view">Grid View Content</div>`;
// Methods to change state
toggleVisibility() {
this.isVisible = !this.isVisible;
}
login(user: { name: string }) {
this.user = user;
this.isLoggedIn = true;
}
logout() {
this.user = null;
this.isLoggedIn = false;
this.isAdmin = false;
}
}
// Advanced conditional patterns
const advancedTemplate = html<ConditionalExample>`
<!-- Conditional classes -->
<div class="${x => when(x.isActive, 'active', 'inactive')}">
Content with conditional styling
</div>
<!-- Conditional attributes -->
<button ?disabled="${x => !x.canSubmit}">
${when(x => x.isSubmitting, 'Submitting...', 'Submit')}
</button>
<!-- Conditional content with interpolation -->
${when(
x => x.errorCount > 0,
html`
<div class="error-summary">
${x => x.errorCount === 1 ? '1 error' : `${x.errorCount} errors`} found:
<ul>
${x => x.errors.map(error => `<li>${error}</li>`).join('')}
</ul>
</div>
`
)}
`;Loop directive for rendering template content for each item in an array, with efficient DOM updates and optional view recycling.
/**
* A directive that renders a template for each item in an array
* @param expression - Expression that returns the array to iterate over
* @param template - The template to render for each item
* @param options - Configuration options for repeat behavior
* @returns A RepeatDirective instance
*/
function repeat<TSource = any, TParent = any>(
expression: Expression<any[], TSource, TParent>,
template: SyntheticViewTemplate<any, TSource>,
options?: RepeatOptions
): RepeatDirective<TSource, TParent>;
/**
* Options for configuring repeat behavior
*/
interface RepeatOptions {
/** Enables index, length, and dependent positioning updates in item templates */
positioning?: boolean;
/** Enables view recycling for performance */
recycle?: boolean;
}
/**
* Directive class for repeat functionality
*/
class RepeatDirective<TSource = any, TParent = any> implements HTMLDirective {
/**
* Creates HTML for the repeat directive
* @param add - Function to add view behavior factories
*/
createHTML(add: AddViewBehaviorFactory): string;
}
/**
* Behavior that manages the repeat rendering lifecycle
*/
class RepeatBehavior<TSource = any> implements ViewBehavior, Subscriber {
/**
* Binds the repeat behavior to a controller
* @param controller - The view controller
*/
bind(controller: ViewController): void;
/**
* Unbinds the repeat behavior
* @param controller - The view controller
*/
unbind(controller: ViewController): void;
/**
* Handles changes to the source array
* @param source - The source of the change
* @param args - Change arguments (splice, sort, etc.)
*/
handleChange(source: any, args: any): void;
}Usage Examples:
import { FASTElement, customElement, html, repeat, observable } from "@microsoft/fast-element";
// Item template
const itemTemplate = html<Item>`
<div class="item" data-id="${x => x.id}">
<h3>${x => x.title}</h3>
<p>${x => x.description}</p>
<span class="price">$${x => x.price}</span>
</div>
`;
// User template with context access
const userTemplate = html<User, UserListComponent>`
<tr class="user-row">
<td>${x => x.name}</td>
<td>${x => x.email}</td>
<td>${(x, c) => c.index + 1}</td>
<td>
<button @click="${(x, c) => c.parent.editUser(x)}">Edit</button>
<button @click="${(x, c) => c.parent.deleteUser(x.id)}">Delete</button>
</td>
</tr>
`;
// Advanced positioning template
const positionedTemplate = html<string, PositionedList>`
<li class="list-item ${(x, c) => c.isFirst ? 'first' : ''} ${(x, c) => c.isLast ? 'last' : ''}">
<span class="index">${(x, c) => c.index + 1}</span>
<span class="content">${x => x}</span>
<span class="total">of ${(x, c) => c.length}</span>
</li>
`;
const template = html<RepeatExample>`
<div class="repeat-examples">
<!-- Basic repeat -->
<section class="basic-repeat">
<h2>Basic Items</h2>
<div class="items-grid">
${repeat(x => x.items, itemTemplate)}
</div>
</section>
<!-- Repeat with positioning -->
<section class="positioned-repeat">
<h2>Positioned List</h2>
<ol class="positioned-list">
${repeat(x => x.names, positionedTemplate, { positioning: true })}
</ol>
</section>
<!-- Users table with context -->
<section class="users-table">
<h2>Users</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>#</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${repeat(x => x.users, userTemplate, { positioning: true, recycle: true })}
</tbody>
</table>
</section>
<!-- Nested repeats -->
<section class="nested-repeat">
<h2>Categories</h2>
${repeat(
x => x.categories,
html<Category, RepeatExample>`
<div class="category">
<h3>${x => x.name}</h3>
<div class="category-items">
${repeat(x => x.items, itemTemplate)}
</div>
</div>
`
)}
</section>
<!-- Conditional repeat -->
<section class="conditional-repeat">
<h2>Search Results</h2>
${when(
x => x.searchResults.length > 0,
html`
<div class="results">
${repeat(x => x.searchResults, itemTemplate)}
</div>
`,
html`<p class="no-results">No results found.</p>`
)}
</section>
</div>
<div class="controls">
<button @click="${x => x.addItem()}">Add Item</button>
<button @click="${x => x.removeItem()}">Remove Item</button>
<button @click="${x => x.shuffleItems()}">Shuffle</button>
<button @click="${x => x.clearItems()}">Clear All</button>
</div>
`;
@customElement({
name: "repeat-example",
template
})
export class RepeatExample extends FASTElement {
@observable items: Item[] = [
{ id: 1, title: "Item 1", description: "Description 1", price: 10.99 },
{ id: 2, title: "Item 2", description: "Description 2", price: 15.99 },
{ id: 3, title: "Item 3", description: "Description 3", price: 8.99 }
];
@observable names: string[] = ["Alice", "Bob", "Charlie", "Diana"];
@observable users: User[] = [
{ id: 1, name: "John Doe", email: "john@example.com" },
{ id: 2, name: "Jane Smith", email: "jane@example.com" }
];
@observable categories: Category[] = [
{
name: "Electronics",
items: [
{ id: 4, title: "Laptop", description: "Gaming laptop", price: 999.99 },
{ id: 5, title: "Mouse", description: "Wireless mouse", price: 29.99 }
]
}
];
@observable searchResults: Item[] = [];
// Array manipulation methods
addItem() {
const newItem: Item = {
id: Date.now(),
title: `Item ${this.items.length + 1}`,
description: `Description ${this.items.length + 1}`,
price: Math.random() * 100
};
this.items.push(newItem);
}
removeItem() {
if (this.items.length > 0) {
this.items.pop();
}
}
shuffleItems() {
this.items = [...this.items].sort(() => Math.random() - 0.5);
}
clearItems() {
this.items = [];
}
editUser(user: User) {
console.log("Edit user:", user);
}
deleteUser(userId: number) {
this.users = this.users.filter(user => user.id !== userId);
}
}
// Performance-optimized repeat with recycling
const recycledTemplate = html<LargeDataItem>`
<div class="data-item">
<span class="id">${x => x.id}</span>
<span class="value">${x => x.value}</span>
<span class="timestamp">${x => x.timestamp.toLocaleString()}</span>
</div>
`;
@customElement("performance-repeat")
export class PerformanceRepeatExample extends FASTElement {
@observable largeDataSet: LargeDataItem[] = [];
connectedCallback() {
super.connectedCallback();
this.generateLargeDataSet();
}
generateLargeDataSet() {
const items: LargeDataItem[] = [];
for (let i = 0; i < 10000; i++) {
items.push({
id: i,
value: Math.random() * 1000,
timestamp: new Date()
});
}
this.largeDataSet = items;
}
static template = html<PerformanceRepeatExample>`
<div class="performance-demo">
<p>Rendering ${x => x.largeDataSet.length} items with recycling enabled</p>
<div class="large-list" style="height: 400px; overflow-y: auto;">
${repeat(x => x.largeDataSet, recycledTemplate, {
positioning: false,
recycle: true
})}
</div>
</div>
`;
}
interface Item {
id: number;
title: string;
description: string;
price: number;
}
interface User {
id: number;
name: string;
email: string;
}
interface Category {
name: string;
items: Item[];
}
interface LargeDataItem {
id: number;
value: number;
timestamp: Date;
}Reference directive for obtaining direct access to DOM elements within templates, enabling imperative DOM operations when needed.
/**
* A directive that captures a reference to the DOM element
* @param propertyName - The property name on the source to assign the element to
* @returns A RefDirective instance
*/
function ref<TSource = any, TParent = any>(
propertyName: keyof TSource
): RefDirective;
/**
* Directive class for element references
*/
class RefDirective implements HTMLDirective {
/**
* Creates HTML for the ref directive
* @param add - Function to add view behavior factories
*/
createHTML(add: AddViewBehaviorFactory): string;
}Usage Examples:
import { FASTElement, customElement, html, ref, observable } from "@microsoft/fast-element";
const template = html<RefExample>`
<div class="ref-examples">
<!-- Basic element reference -->
<input ${ref("nameInput")}
type="text"
placeholder="Enter your name"
@input="${x => x.handleNameInput()}">
<!-- Canvas reference for drawing -->
<canvas ${ref("canvas")}
width="400"
height="200"
style="border: 1px solid #ccc;">
</canvas>
<!-- Video element reference -->
<video ${ref("videoPlayer")}
controls
width="400">
<source src="sample.mp4" type="video/mp4">
</video>
<!-- Form reference for validation -->
<form ${ref("form")} @submit="${x => x.handleSubmit}">
<input ${ref("emailInput")} type="email" required>
<button type="submit">Submit</button>
</form>
<!-- Multiple refs for focus management -->
<div class="focus-chain">
<input ${ref("input1")} placeholder="First">
<input ${ref("input2")} placeholder="Second">
<input ${ref("input3")} placeholder="Third">
</div>
<!-- Container for dynamic content -->
<div ${ref("dynamicContainer")} class="dynamic-content"></div>
</div>
<div class="controls">
<button @click="${x => x.focusName()}">Focus Name</button>
<button @click="${x => x.drawOnCanvas()}">Draw</button>
<button @click="${x => x.playVideo()}">Play Video</button>
<button @click="${x => x.validateForm()}">Validate</button>
<button @click="${x => x.cycleFocus()}">Cycle Focus</button>
<button @click="${x => x.addDynamicContent()}">Add Content</button>
</div>
`;
@customElement({
name: "ref-example",
template
})
export class RefExample extends FASTElement {
// Element references
nameInput!: HTMLInputElement;
canvas!: HTMLCanvasElement;
videoPlayer!: HTMLVideoElement;
form!: HTMLFormElement;
emailInput!: HTMLInputElement;
input1!: HTMLInputElement;
input2!: HTMLInputElement;
input3!: HTMLInputElement;
dynamicContainer!: HTMLDivElement;
@observable currentFocusIndex: number = 0;
// Methods using element references
focusName() {
if (this.nameInput) {
this.nameInput.focus();
this.nameInput.select();
}
}
handleNameInput() {
if (this.nameInput) {
console.log("Name input value:", this.nameInput.value);
}
}
drawOnCanvas() {
if (this.canvas) {
const ctx = this.canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.fillStyle = '#007ACC';
ctx.fillRect(50, 50, 100, 100);
ctx.fillStyle = 'white';
ctx.font = '16px Arial';
ctx.fillText('FAST Element', 60, 110);
}
}
}
playVideo() {
if (this.videoPlayer) {
this.videoPlayer.play().catch(console.error);
}
}
validateForm() {
if (this.form) {
const isValid = this.form.checkValidity();
console.log("Form is valid:", isValid);
if (!isValid) {
this.form.reportValidity();
}
}
}
handleSubmit(event: Event) {
event.preventDefault();
if (this.emailInput) {
console.log("Email submitted:", this.emailInput.value);
}
}
cycleFocus() {
const inputs = [this.input1, this.input2, this.input3];
const currentInput = inputs[this.currentFocusIndex];
if (currentInput) {
currentInput.focus();
this.currentFocusIndex = (this.currentFocusIndex + 1) % inputs.length;
}
}
addDynamicContent() {
if (this.dynamicContainer) {
const element = document.createElement('div');
element.textContent = `Dynamic content added at ${new Date().toLocaleTimeString()}`;
element.style.cssText = 'padding: 8px; margin: 4px; background: #f0f0f0; border-radius: 4px;';
this.dynamicContainer.appendChild(element);
}
}
}
// Advanced ref usage with custom elements
@customElement("advanced-ref-example")
export class AdvancedRefExample extends FASTElement {
chartContainer!: HTMLDivElement;
editorContainer!: HTMLDivElement;
private chart?: any; // External chart library instance
private editor?: any; // External editor library instance
connectedCallback() {
super.connectedCallback();
// Wait for next frame to ensure refs are available
requestAnimationFrame(() => {
this.initializeChart();
this.initializeEditor();
});
}
disconnectedCallback() {
super.disconnectedCallback();
// Clean up external library instances
if (this.chart) {
this.chart.destroy();
}
if (this.editor) {
this.editor.destroy();
}
}
private initializeChart() {
if (this.chartContainer) {
// Initialize external chart library
// this.chart = new ChartLibrary(this.chartContainer, options);
console.log("Chart initialized in:", this.chartContainer);
}
}
private initializeEditor() {
if (this.editorContainer) {
// Initialize external editor library
// this.editor = new EditorLibrary(this.editorContainer, options);
console.log("Editor initialized in:", this.editorContainer);
}
}
static template = html<AdvancedRefExample>`
<div class="advanced-refs">
<div ${ref("chartContainer")} class="chart-container"></div>
<div ${ref("editorContainer")} class="editor-container"></div>
</div>
`;
}Directive for observing child elements, providing reactive access to element children with filtering and observation capabilities.
/**
* A directive that observes the child nodes of an element
* @param propertyName - The property name to assign the child collection to
* @param options - Configuration options for child observation
* @returns A ChildrenDirective instance
*/
function children<TSource = any, TParent = any>(
propertyName: keyof TSource,
options?: ChildrenDirectiveOptions
): ChildrenDirective;
/**
* Options for configuring child observation
*/
interface ChildrenDirectiveOptions {
/** CSS selector to filter child elements */
filter?: ElementsFilter;
/** Whether to observe all descendants (subtree) */
subtree?: boolean;
/** Specific child list options */
childList?: boolean;
/** Attribute change observation */
attributes?: boolean;
/** Character data change observation */
characterData?: boolean;
}
/**
* Interface for filtering elements
*/
interface ElementsFilter {
/**
* Filters elements based on criteria
* @param node - The node to evaluate
* @param index - The index of the node
* @param nodes - All nodes being filtered
*/
(node: Node, index: number, nodes: Node[]): boolean;
}
/**
* Directive class for child observation
*/
class ChildrenDirective implements HTMLDirective {
/**
* Creates HTML for the children directive
* @param add - Function to add view behavior factories
*/
createHTML(add: AddViewBehaviorFactory): string;
}Usage Examples:
import { FASTElement, customElement, html, children, observable } from "@microsoft/fast-element";
// Element filter functions
const buttonFilter = (node: Node): node is HTMLElement =>
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'BUTTON';
const inputFilter = (node: Node): node is HTMLInputElement =>
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'INPUT';
const template = html<ChildrenExample>`
<div class="children-examples">
<!-- Observe all child elements -->
<div ${children("allChildren")} class="all-children-container">
<p>This paragraph will be observed</p>
<span>This span will be observed</span>
<button>This button will be observed</button>
</div>
<!-- Observe only button children -->
<div ${children("buttons", { filter: buttonFilter })} class="buttons-container">
<button>Button 1</button>
<span>Not a button</span>
<button>Button 2</button>
<p>Not a button either</p>
<button>Button 3</button>
</div>
<!-- Observe form inputs -->
<form ${children("formInputs", { filter: inputFilter })} class="form-container">
<input type="text" placeholder="Name">
<label>Not an input</label>
<input type="email" placeholder="Email">
<textarea placeholder="Comments"></textarea>
<input type="submit" value="Submit">
</form>
<!-- Observe with subtree option -->
<div ${children("allDescendants", { subtree: true })} class="subtree-container">
<div class="level-1">
<p>Level 1 paragraph</p>
<div class="level-2">
<span>Level 2 span</span>
<div class="level-3">
<strong>Level 3 strong</strong>
</div>
</div>
</div>
</div>
</div>
<div class="children-info">
<p>All children count: ${x => x.allChildren?.length ?? 0}</p>
<p>Button count: ${x => x.buttons?.length ?? 0}</p>
<p>Form inputs count: ${x => x.formInputs?.length ?? 0}</p>
<p>All descendants count: ${x => x.allDescendants?.length ?? 0}</p>
</div>
<div class="controls">
<button @click="${x => x.addChild()}">Add Child</button>
<button @click="${x => x.addButton()}">Add Button</button>
<button @click="${x => x.addInput()}">Add Input</button>
<button @click="${x => x.removeChildren()}">Remove Children</button>
</div>
`;
@customElement({
name: "children-example",
template
})
export class ChildrenExample extends FASTElement {
// Child collections populated by children directive
allChildren!: HTMLElement[];
buttons!: HTMLButtonElement[];
formInputs!: HTMLInputElement[];
allDescendants!: HTMLElement[];
private get allChildrenContainer(): HTMLElement {
return this.shadowRoot?.querySelector('.all-children-container') as HTMLElement;
}
private get buttonsContainer(): HTMLElement {
return this.shadowRoot?.querySelector('.buttons-container') as HTMLElement;
}
private get formContainer(): HTMLElement {
return this.shadowRoot?.querySelector('.form-container') as HTMLElement;
}
connectedCallback() {
super.connectedCallback();
// React to children changes
this.setupChildrenObservers();
}
private setupChildrenObservers() {
// Watch for changes in child collections
// These will be automatically updated by the children directive
// You can add custom logic here to react to children changes
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
console.log('Children changed:', mutation);
}
});
});
// Observe changes to containers
if (this.allChildrenContainer) {
observer.observe(this.allChildrenContainer, { childList: true });
}
}
addChild() {
if (this.allChildrenContainer) {
const child = document.createElement('div');
child.textContent = `Child ${this.allChildren?.length ?? 0 + 1}`;
child.style.cssText = 'padding: 4px; margin: 2px; background: #e0e0e0;';
this.allChildrenContainer.appendChild(child);
}
}
addButton() {
if (this.buttonsContainer) {
const button = document.createElement('button');
button.textContent = `Button ${this.buttons?.length ?? 0 + 1}`;
this.buttonsContainer.appendChild(button);
}
}
addInput() {
if (this.formContainer) {
const input = document.createElement('input');
input.type = 'text';
input.placeholder = `Input ${this.formInputs?.length ?? 0 + 1}`;
this.formContainer.appendChild(input);
}
}
removeChildren() {
// Remove last child from each container
if (this.allChildren?.length > 3) { // Keep original children
this.allChildrenContainer.removeChild(
this.allChildrenContainer.lastElementChild!
);
}
if (this.buttons?.length > 3) { // Keep original buttons
this.buttonsContainer.removeChild(
this.buttonsContainer.lastElementChild!
);
}
if (this.formInputs?.length > 3) { // Keep original inputs
this.formContainer.removeChild(
this.formContainer.lastElementChild!
);
}
}
}
// Advanced children observation with custom filtering
@customElement("advanced-children-example")
export class AdvancedChildrenExample extends FASTElement {
// Custom filter for data elements
customElements!: HTMLElement[];
static customElementFilter = (node: Node): node is HTMLElement => {
if (node.nodeType !== Node.ELEMENT_NODE) return false;
const element = node as HTMLElement;
return element.hasAttribute('data-item') ||
element.classList.contains('custom-item');
};
static template = html<AdvancedChildrenExample>`
<div ${children("customElements", {
filter: AdvancedChildrenExample.customElementFilter
})} class="custom-container">
<div data-item="1">Custom Item 1</div>
<div>Regular div</div>
<div class="custom-item">Custom Item 2</div>
<p>Regular paragraph</p>
<div data-item="2" class="custom-item">Custom Item 3</div>
</div>
<p>Custom elements found: ${x => x.customElements?.length ?? 0}</p>
`;
}Directive for observing slotted content in Shadow DOM, providing reactive access to distributed content with filtering capabilities.
/**
* A directive that observes slotted content
* @param propertyName - The property name to assign slotted elements to
* @param options - Configuration options for slotted observation
* @returns A SlottedDirective instance
*/
function slotted<TSource = any, TParent = any>(
propertyName: keyof TSource,
options?: SlottedDirectiveOptions
): SlottedDirective;
/**
* Options for configuring slotted content observation
*/
interface SlottedDirectiveOptions {
/** CSS selector to filter slotted elements */
filter?: ElementsFilter;
/** Whether to flatten assigned nodes */
flatten?: boolean;
}
/**
* Directive class for slotted content observation
*/
class SlottedDirective implements HTMLDirective {
/**
* Creates HTML for the slotted directive
* @param add - Function to add view behavior factories
*/
createHTML(add: AddViewBehaviorFactory): string;
}Usage Examples:
import { FASTElement, customElement, html, slotted, observable } from "@microsoft/fast-element";
// Slot filters
const headerFilter = (node: Node): node is HTMLElement =>
node.nodeType === Node.ELEMENT_NODE &&
['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes((node as Element).tagName);
const buttonFilter = (node: Node): node is HTMLButtonElement =>
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'BUTTON';
const template = html<SlottedExample>`
<div class="slotted-container">
<!-- Default slot observation -->
<div class="content-section">
<h2>Content</h2>
<slot ${slotted("defaultSlottedContent")}></slot>
</div>
<!-- Named slot with header filter -->
<header class="header-section">
<slot name="header" ${slotted("headerContent", { filter: headerFilter })}></slot>
</header>
<!-- Action slot with button filter -->
<footer class="actions-section">
<slot name="actions" ${slotted("actionButtons", { filter: buttonFilter })}></slot>
</footer>
<!-- Sidebar slot with flattening -->
<aside class="sidebar">
<slot name="sidebar" ${slotted("sidebarContent", { flatten: true })}></slot>
</aside>
</div>
<div class="slot-info">
<p>Default content items: ${x => x.defaultSlottedContent?.length ?? 0}</p>
<p>Header elements: ${x => x.headerContent?.length ?? 0}</p>
<p>Action buttons: ${x => x.actionButtons?.length ?? 0}</p>
<p>Sidebar items: ${x => x.sidebarContent?.length ?? 0}</p>
</div>
`;
@customElement({
name: "slotted-example",
template
})
export class SlottedExample extends FASTElement {
// Slotted content collections
defaultSlottedContent!: HTMLElement[];
headerContent!: HTMLElement[];
actionButtons!: HTMLButtonElement[];
sidebarContent!: HTMLElement[];
connectedCallback() {
super.connectedCallback();
this.setupSlotObservers();
}
private setupSlotObservers() {
// React to slotted content changes
this.addEventListener('slotchange', (e) => {
const slot = e.target as HTMLSlotElement;
console.log(`Slot '${slot.name || 'default'}' content changed`);
// Perform actions based on slotted content
this.updateSlottedContentStyles();
});
}
private updateSlottedContentStyles() {
// Apply styles based on slotted content
if (this.headerContent?.length > 0) {
this.headerContent.forEach((header, index) => {
header.style.color = index === 0 ? '#007ACC' : '#666';
});
}
if (this.actionButtons?.length > 0) {
this.actionButtons.forEach((button, index) => {
button.classList.toggle('primary', index === 0);
});
}
}
}
// Usage of slotted component
@customElement("slotted-consumer")
export class SlottedConsumer extends FASTElement {
static template = html`
<div>
<h1>Using Slotted Component</h1>
<slotted-example>
<!-- Default slot content -->
<p>This goes in the default slot</p>
<div>Another default slot item</div>
<!-- Named slot content -->
<h1 slot="header">Main Header</h1>
<h2 slot="header">Sub Header</h2>
<!-- Action slot content -->
<button slot="actions" @click="${() => console.log('Save')}">Save</button>
<button slot="actions" @click="${() => console.log('Cancel')}">Cancel</button>
<!-- Sidebar slot content -->
<nav slot="sidebar">
<ul>
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
</slotted-example>
</div>
`;
}
// Advanced slotted content management
@customElement("advanced-slotted")
export class AdvancedSlottedExample extends FASTElement {
allSlottedContent!: Node[];
imageContent!: HTMLImageElement[];
linkContent!: HTMLAnchorElement[];
@observable hasImages: boolean = false;
@observable hasLinks: boolean = false;
// Custom filters
static imageFilter = (node: Node): node is HTMLImageElement =>
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'IMG';
static linkFilter = (node: Node): node is HTMLAnchorElement =>
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'A';
slottedContentChanged() {
// Update observable properties based on slotted content
this.hasImages = this.imageContent?.length > 0;
this.hasLinks = this.linkContent?.length > 0;
// Apply lazy loading to images
if (this.imageContent) {
this.imageContent.forEach(img => {
if (!img.hasAttribute('loading')) {
img.setAttribute('loading', 'lazy');
}
});
}
// Add security attributes to external links
if (this.linkContent) {
this.linkContent.forEach(link => {
if (link.hostname !== window.location.hostname) {
link.setAttribute('rel', 'noopener noreferrer');
link.setAttribute('target', '_blank');
}
});
}
}
static template = html<AdvancedSlottedExample>`
<div class="advanced-slotted">
<slot ${slotted("allSlottedContent")}
@slotchange="${x => x.slottedContentChanged()}"></slot>
<!-- Hidden slots for filtered content -->
<slot ${slotted("imageContent", { filter: AdvancedSlottedExample.imageFilter })}
style="display: none;"></slot>
<slot ${slotted("linkContent", { filter: AdvancedSlottedExample.linkFilter })}
style="display: none;"></slot>
<div class="content-summary">
${when(x => x.hasImages, html`<p>📸 Contains images</p>`)}
${when(x => x.hasLinks, html`<p>🔗 Contains links</p>`)}
</div>
</div>
`;
}/**
* Base interface for HTML directives
*/
interface HTMLDirective {
/**
* Creates HTML to be used within a template
* @param add - Can be used to add behavior factories to a template
*/
createHTML(add: AddViewBehaviorFactory): string;
}
/**
* Function for adding view behavior factories during template compilation
*/
interface AddViewBehaviorFactory {
(factory: ViewBehaviorFactory): string;
}
/**
* Factory for creating view behaviors
*/
interface ViewBehaviorFactory {
/** Unique identifier for the factory */
id?: string;
/**
* Creates a view behavior
* @param targets - The targets for the behavior
*/
createBehavior(targets: ViewBehaviorTargets): ViewBehavior;
}
/**
* Targets for view behavior attachment
*/
interface ViewBehaviorTargets {
[key: string]: Node;
}
/**
* Base interface for view behaviors
*/
interface ViewBehavior {
/**
* Binds the behavior to a controller
* @param controller - The view controller
*/
bind(controller: ViewController): void;
/**
* Unbinds the behavior from a controller
* @param controller - The view controller
*/
unbind(controller: ViewController): void;
}
/**
* Controller for managing view lifecycle
*/
interface ViewController {
/** The source data */
source: any;
/** Execution context */
context: ExecutionContext;
/** Whether the controller is bound */
isBound: boolean;
}