Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/skills/typescript/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ await MyElement.define({

Use `compose()` when registration should be deferred β€” downstream libraries like Fluent
Web Components use this pattern with a design-system registry. `compose()` returns a
`Promise<FASTElementDefinition>` that always resolves immediately:
`FASTElementDefinition` synchronously, or a `Promise<FASTElementDefinition>` when the
definition's `template` is a resolver:

```ts
// my-element.definition.ts
export const definition = await MyElement.compose({
export const definition = MyElement.compose({
name: "my-element",
template,
styles,
Expand All @@ -61,6 +62,17 @@ export const definition = await MyElement.compose({
definition.define();
```

```ts
// my-element.definition-async.ts
export const declarativeDefinition = MyElement.compose({
name: "my-element",
template: declarativeTemplate(),
});

// define.ts (side-effect import)
void declarativeDefinition.then(definition => definition.define());
```

## Templates

Templates use the `html` tagged template literal typed to the element class:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Return compose() synchronously unless a template resolver is provided; only resolver-bearing definitions yield a Promise.",
"packageName": "@microsoft/fast-element",
"email": "863023+radium-v@users.noreply.github.com",
"dependentChangeType": "none"
}
2 changes: 1 addition & 1 deletion examples/csr/todo-mobx-app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ import "./todo-form.js";
// lifetime of the page.
connectStoreToStorage(todoStore, "fast-todo-mobx-app");

void app.then(definition => definition.define());
app.define();
2 changes: 1 addition & 1 deletion packages/fast-element/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ logging, telemetry, or a devtools panel.
- `onAttributeChangedCallback()` is the standard handler that processes attribute changes. During the prerendered bind, it is temporarily swapped to a no-op (see above) to avoid redundant processing of server-rendered attribute values.
- Exposes `addBehavior` / `removeBehavior` for dynamic `HostBehavior` management (used by `ElementStyles`).

`FASTElementDefinition` wraps all the metadata for a custom element class: its tag name, template, styles, and observed attribute list. It is created by `FASTElement.compose()` (which returns `Promise<FASTElementDefinition>`, always resolving immediately) and registered globally via `fastElementRegistry`. Consumers that need focused access to definition lookup can import `fastElementRegistry` from `@microsoft/fast-element/registry.js`. `PartialFASTElementDefinition.template` may be either a concrete `ElementViewTemplate<InstanceType<TType>>` or a `FASTElementTemplateResolver<TType>` function that receives the composed definition and returns the concrete template (sync or async). `FASTElementDefinition.template` always stores the concrete `ElementViewTemplate<InstanceType<TType>>` after composition or resolver settlement. The subclass static `define()` method returns `Promise<TType>` β€” resolving immediately for complete definitions or definitions without an initial template, and resolving async template resolver functions only after extensions have had a chance to update the definition. `FASTElementDefinition.register()` returns `Promise<Function>` β€” resolving when a definition with the given name has been registered.
`FASTElementDefinition` wraps all the metadata for a custom element class: its tag name, template, styles, and observed attribute list. It is created by `FASTElement.compose()` (which returns the definition synchronously, or a `Promise<FASTElementDefinition>` when the definition's `template` is a resolver function) and registered globally via `fastElementRegistry`. Consumers that need focused access to definition lookup can import `fastElementRegistry` from `@microsoft/fast-element/registry.js`. `PartialFASTElementDefinition.template` may be either a concrete `ElementViewTemplate<InstanceType<TType>>` or a `FASTElementTemplateResolver<TType>` function that receives the composed definition and returns the concrete template (sync or async). `FASTElementDefinition.template` always stores the concrete `ElementViewTemplate<InstanceType<TType>>` after composition or resolver settlement. The subclass static `define()` method returns `Promise<TType>` β€” resolving immediately for complete definitions or definitions without an initial template, and resolving async template resolver functions only after extensions have had a chance to update the definition. `FASTElementDefinition.register()` returns `Promise<Function>` β€” resolving when a definition with the given name has been registered.

#### Extensions

Expand Down
25 changes: 16 additions & 9 deletions packages/fast-element/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ This is a **breaking change** for SSR output format. Any system that produces or
| Removed | Replacement |
|---|---|
| `FASTElement.defineAsync()` | Subclass `define()` calls (now return `Promise<TType>`) |
| `FASTElementDefinition.composeAsync()` | `FASTElementDefinition.compose()` (now returns `Promise<FASTElementDefinition>`) |
| `FASTElementDefinition.composeAsync()` | `FASTElementDefinition.compose()` (returns `Promise<FASTElementDefinition>` when given a template resolver) |
| `FASTElementDefinition.registerAsync()` | `FASTElementDefinition.register()` (same `Promise<Function>` return type) |

### Changed behavior
Expand All @@ -323,8 +323,8 @@ This is a **breaking change** for SSR output format. Any system that produces or
template is provided at definition time, the Promise resolves immediately.
When `template: declarativeTemplate()` is used, the Promise resolves after
the matching `<f-template>` supplies the concrete template.
- **`FASTElement.compose()`** now returns `Promise<FASTElementDefinition>`. The Promise always resolves immediately.
- **`FASTElementDefinition.compose()`** now returns `Promise<FASTElementDefinition>`. The Promise always resolves immediately.
- **`FASTElement.compose()`** returns a `FASTElementDefinition` synchronously. It returns a `Promise<FASTElementDefinition>` when the definition's `template` is a resolver function.
- **`FASTElementDefinition.compose()`** returns a `FASTElementDefinition` synchronously. It returns a `Promise<FASTElementDefinition>` when the definition's `template` is a resolver function.
- **`@customElement` decorator** calls `define()` internally but does not return the Promise (fire-and-forget). For complete definitions with a template, the element is registered via a microtask.

### Migration steps
Expand All @@ -349,10 +349,16 @@ This is a **breaking change** for SSR output format. Any system that produces or

```typescript
// Before
const def = await FASTElementDefinition.composeAsync(MyElement, name);
const def = await FASTElementDefinition.composeAsync(MyElement, {
name,
template: declarativeTemplate(),
});

// After
const def = await FASTElementDefinition.compose(MyElement, name);
const def = await FASTElementDefinition.compose(MyElement, {
name,
template: declarativeTemplate(),
});
```

3. Replace `registerAsync()` calls with `register()`:
Expand All @@ -365,14 +371,15 @@ This is a **breaking change** for SSR output format. Any system that produces or
const el = await FASTElementDefinition.register(name);
```

4. Add `await` to `compose()` calls that chain `.define()`:
4. `compose()` calls that chain `.define()` stay synchronous unless `options`
passes a template resolver:

```typescript
// Before
// Concrete template or none β€” compose() is synchronous, no await needed
FASTElementDefinition.compose(MyElement, options).define();

// After
(await FASTElementDefinition.compose(MyElement, options)).define();
// Resolver-backed template β€” compose() returns a Promise
(await FASTElementDefinition.compose(MyElement, resolverOptions)).define();
```

## Dynamic stylesheet behaviors (v3)
Expand Down
10 changes: 5 additions & 5 deletions packages/fast-element/SIZES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Bundle sizes for `@microsoft/fast-element` exports.

| Export | Minified | Gzip | Brotli |
|--------|----------|------|--------|
| CDN Rollup Bundle | 79.42 KB | 23.81 KB | 21.04 KB |
| FASTElement (@microsoft/fast-element/fast-element.js) | 23.84 KB | 7.39 KB | 6.65 KB |
| CDN Rollup Bundle | 79.50 KB | 23.82 KB | 21.04 KB |
| FASTElement (@microsoft/fast-element/fast-element.js) | 23.89 KB | 7.40 KB | 6.67 KB |
| Updates (@microsoft/fast-element/updates.js) | 473 B | 335 B | 288 B |
| Observable (@microsoft/fast-element/observable.js) | 6.77 KB | 2.52 KB | 2.24 KB |
| observable (@microsoft/fast-element/observable.js) | 6.80 KB | 2.54 KB | 2.25 KB |
Expand All @@ -18,8 +18,8 @@ Bundle sizes for `@microsoft/fast-element` exports.
| html (@microsoft/fast-element/html.js) | 27.75 KB | 8.96 KB | 8.03 KB |
| repeat (@microsoft/fast-element/repeat.js) | 31.82 KB | 10.01 KB | 9.03 KB |
| css (@microsoft/fast-element/css.js) | 2.43 KB | 1.00 KB | 911 B |
| enableHydration (@microsoft/fast-element/hydration.js) | 45.56 KB | 13.82 KB | 12.42 KB |
| enableHydration (@microsoft/fast-element/hydration.js) | 45.59 KB | 13.83 KB | 12.43 KB |
| declarativeTemplate (@microsoft/fast-element/declarative.js) | 62.88 KB | 19.72 KB | 17.59 KB |
| ArrayObserver (@microsoft/fast-element/array-observer.js) | 12.57 KB | 4.47 KB | 4.03 KB |
| observerMap (@microsoft/fast-element/observer-map.js) | 20.61 KB | 7.33 KB | 6.59 KB |
| attributeMap (@microsoft/fast-element/attribute-map.js) | 15.84 KB | 5.60 KB | 5.05 KB |
| observerMap (@microsoft/fast-element/observer-map.js) | 20.64 KB | 7.34 KB | 6.61 KB |
| attributeMap (@microsoft/fast-element/attribute-map.js) | 15.88 KB | 5.61 KB | 5.06 KB |
15 changes: 12 additions & 3 deletions packages/fast-element/docs/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -538,8 +538,14 @@ export const FASTElement: FASTElementConstructor;
// @public
export interface FASTElementConstructor {
new (): FASTElement;
compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(this: TType, nameOrDef: string | PartialFASTElementDefinition<TType>): Promise<FASTElementDefinition<TType>>;
compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef?: string | PartialFASTElementDefinition<TType>): Promise<FASTElementDefinition<TType>>;
compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(this: TType, nameOrDef: PartialFASTElementDefinition<TType> & {
template: FASTElementTemplateResolver<TType>;
}): Promise<FASTElementDefinition<TType>>;
compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(this: TType, nameOrDef: string | PartialFASTElementDefinition<TType>): FASTElementDefinition<TType>;
compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef: PartialFASTElementDefinition<TType> & {
template: FASTElementTemplateResolver<TType>;
}): Promise<FASTElementDefinition<TType>>;
compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef?: string | PartialFASTElementDefinition<TType>): FASTElementDefinition<TType>;
define<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(this: TType, nameOrDef: string | PartialFASTElementDefinition<TType>, extensions?: FASTElementExtension[]): Promise<TType>;
define<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef?: string | PartialFASTElementDefinition<TType>, extensions?: FASTElementExtension[]): Promise<TType>;
from<TBase extends typeof HTMLElement>(BaseType: TBase): {
Expand All @@ -551,7 +557,10 @@ export interface FASTElementConstructor {
export class FASTElementDefinition<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>> {
readonly attributeLookup: Record<string, AttributeDefinition>;
readonly attributes: ReadonlyArray<AttributeDefinition>;
static compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef?: string | PartialFASTElementDefinition<TType>): Promise<FASTElementDefinition<TType>>;
static compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef: PartialFASTElementDefinition<TType> & {
template: FASTElementTemplateResolver<TType>;
}): Promise<FASTElementDefinition<TType>>;
static compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef?: string | PartialFASTElementDefinition<TType>): FASTElementDefinition<TType>;
define(registry?: CustomElementRegistry, extensions?: FASTElementExtension[]): this;
readonly elementOptions: ElementDefinitionOptions;
static readonly getByType: (key: Function) => FASTElementDefinition<Constructable<HTMLElement>> | undefined;
Expand Down
5 changes: 4 additions & 1 deletion packages/fast-element/docs/declarative/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,10 @@ export interface EventCachedPath extends CachedPathCommon {
export class FASTElementDefinition<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>> {
readonly attributeLookup: Record<string, AttributeDefinition>;
readonly attributes: ReadonlyArray<AttributeDefinition>;
static compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef?: string | PartialFASTElementDefinition<TType>): Promise<FASTElementDefinition<TType>>;
static compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef: PartialFASTElementDefinition<TType> & {
template: FASTElementTemplateResolver<TType>;
}): Promise<FASTElementDefinition<TType>>;
static compose<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(type: TType, nameOrDef?: string | PartialFASTElementDefinition<TType>): FASTElementDefinition<TType>;
define(registry?: CustomElementRegistry, extensions?: FASTElementExtension[]): this;
readonly elementOptions: ElementDefinitionOptions;
static readonly getByType: (key: Function) => FASTElementDefinition<Constructable<HTMLElement>> | undefined;
Expand Down
125 changes: 125 additions & 0 deletions packages/fast-element/src/components/fast-definitions.pw.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,131 @@ test.describe("FASTElementDefinition", () => {
expect(def1Extends).toBe(true);
expect(def2Extends).toBe(true);
});

test("returns the definition synchronously when no template resolver is provided", async ({
page,
}) => {
await page.goto("/");

const result = await page.evaluate(async () => {
const { FASTElementDefinition, uniqueElementName } =
// @ts-expect-error: Client module.
await import("/main.js");

class MyElement extends HTMLElement {}

const composed = FASTElementDefinition.compose(
MyElement,
uniqueElementName(),
);

return {
isPromise: typeof composed?.then === "function",
isDefinition: composed instanceof FASTElementDefinition,
};
});

expect(result.isPromise).toBe(false);
expect(result.isDefinition).toBe(true);
});

test("returns the definition synchronously when given a concrete template", async ({
page,
}) => {
await page.goto("/");

const result = await page.evaluate(async () => {
const { FASTElement, FASTElementDefinition, html, uniqueElementName } =
// @ts-expect-error: Client module.
await import("/main.js");

class TestElement extends FASTElement {}

const composed = FASTElementDefinition.compose(TestElement, {
name: uniqueElementName(),
template: html`<span>concrete</span>`,
});

return {
isPromise: typeof composed?.then === "function",
isDefinition: composed instanceof FASTElementDefinition,
hasTemplate: composed.template !== undefined,
};
});

expect(result.isPromise).toBe(false);
expect(result.isDefinition).toBe(true);
expect(result.hasTemplate).toBe(true);
});

test("returns a Promise when a template resolver is provided", async ({
page,
}) => {
await page.goto("/");

const result = await page.evaluate(async () => {
const { FASTElement, FASTElementDefinition, html, uniqueElementName } =
// @ts-expect-error: Client module.
await import("/main.js");

class TestElement extends FASTElement {}

const template = html`<span>resolved</span>`;
const composed = FASTElementDefinition.compose(TestElement, {
name: uniqueElementName(),
template: () => template,
});

const isPromise = typeof composed?.then === "function";
const definition = await composed;

return {
isPromise,
isDefinition: definition instanceof FASTElementDefinition,
// compose does not run the resolver; the template is resolved
// later, during define().
templateUnresolvedAfterCompose: definition.template === undefined,
};
});

expect(result.isPromise).toBe(true);
expect(result.isDefinition).toBe(true);
expect(result.templateUnresolvedAfterCompose).toBe(true);
});

test("composes synchronously for compose(type) even when the type carries a resolver definition", async ({
page,
}) => {
await page.goto("/");

const result = await page.evaluate(async () => {
const { FASTElement, FASTElementDefinition, html, uniqueElementName } =
// @ts-expect-error: Client module.
await import("/main.js");

const template = html`<span>resolved</span>`;

class ResolverElement extends FASTElement {
static definition = {
name: uniqueElementName(),
template: () => template,
};
}

// No explicit `nameOrDef`, so the resolver is inherited from
// `ResolverElement.definition`. This must stay synchronous to
// keep the `compose(type)` overload sound.
const composed = FASTElementDefinition.compose(ResolverElement);

return {
isPromise: typeof composed?.then === "function",
isDefinition: composed instanceof FASTElementDefinition,
};
});

expect(result.isPromise).toBe(false);
expect(result.isDefinition).toBe(true);
});
});

test.describe("register async", () => {
Expand Down
Loading
Loading