Skip to content

Commit

Permalink
feat: enable multiple instances of fast-element on a page at once (#5718
Browse files Browse the repository at this point in the history
)

* feat: enable multiple instances of fast-element on a page at once

* Change files

Co-authored-by: EisenbergEffect <[email protected]>
  • Loading branch information
2 people authored and nicholasrice committed May 3, 2022
1 parent c1f674d commit fc8da6f
Show file tree
Hide file tree
Showing 6 changed files with 467 additions and 403 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: enable multiple instances of fast-element on a page at once",
"packageName": "@microsoft/fast-element",
"email": "[email protected]",
"dependentChangeType": "patch"
}
60 changes: 44 additions & 16 deletions packages/web-components/fast-element/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class AttributeDefinition implements Accessor {
onAttributeChangedCallback(element: HTMLElement, value: any): void;
readonly Owner: Function;
setValue(source: HTMLElement, newValue: any): void;
}
}

// @public
export type AttributeMode = "reflect" | "boolean" | "fromView";
Expand Down Expand Up @@ -209,10 +209,10 @@ export const DOM: Readonly<{
supportsAdoptedStyleSheets: boolean;
setHTMLPolicy(policy: TrustedTypesPolicy): void;
createHTML(html: string): string;
setUpdateMode(isAsync: boolean): void;
queueUpdate(callable: Callable): void;
setUpdateMode: (isAsync: boolean) => void;
queueUpdate: (callable: Callable) => void;
nextUpdate(): Promise<void>;
processUpdates(): void;
processUpdates: () => void;
setAttribute(element: HTMLElement, attributeName: string, value: any): void;
setBooleanAttribute(element: HTMLElement, attributeName: string, value: boolean): void;
}>;
Expand Down Expand Up @@ -274,8 +274,15 @@ export class ExecutionContext<TParent = any, TGrandparent = any> {
length: number;
parent: TParent;
parentContext: ExecutionContext<TGrandparent>;
// @internal
static setEvent(event: Event | null): void;
}

// Warning: (ae-internal-missing-underscore) The name "FAST" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal
export const FAST: FASTGlobal;

// @public
export interface FASTElement extends HTMLElement {
$emit(type: string, detail?: any, options?: Omit<CustomEventInit, "detail">): boolean | void;
Expand All @@ -301,8 +308,8 @@ export class FASTElementDefinition<TType extends Function = Function> {
readonly attributes: ReadonlyArray<AttributeDefinition>;
define(registry?: CustomElementRegistry): this;
readonly elementOptions?: ElementDefinitionOptions;
static forType<TType extends Function>(type: TType): FASTElementDefinition | undefined;
readonly isDefined: boolean;
static readonly forType: <TType_1 extends Function>(key: TType_1) => FASTElementDefinition<Function> | undefined;
get isDefined(): boolean;
readonly name: string;
readonly propertyLookup: Record<string, AttributeDefinition>;
readonly shadowOptions?: ShadowRootInit;
Expand All @@ -311,9 +318,20 @@ export class FASTElementDefinition<TType extends Function = Function> {
readonly type: TType;
}

// Warning: (ae-internal-missing-underscore) The name "FASTGlobal" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal
export interface FASTGlobal {
getById<T>(id: string | number): T | null;
// (undocumented)
getById<T>(id: string | number, initialize: () => T): T;
readonly versions: string[];
}

// @public
export type Global = typeof globalThis & {
trustedTypes: TrustedTypes;
readonly FAST: FASTGlobal;
};

// @public
Expand Down Expand Up @@ -358,6 +376,20 @@ export abstract class InlinableHTMLDirective extends AspectedHTMLDirective {
abstract readonly rawAspect?: string;
}

// Warning: (ae-internal-missing-underscore) The name "KernelServiceId" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal
export const enum KernelServiceId {
// (undocumented)
contextEvent = 3,
// (undocumented)
elementRegistry = 4,
// (undocumented)
observable = 2,
// (undocumented)
updateQueue = 1
}

// @public
export const Markup: Readonly<{
marker: string;
Expand Down Expand Up @@ -394,12 +426,12 @@ export const nullableNumberConverter: ValueConverter;
// @public
export const Observable: Readonly<{
setArrayObserverFactory(factory: (collection: any[]) => Notifier): void;
getNotifier(source: any): Notifier;
getNotifier: (source: any) => Notifier;
track(source: unknown, propertyName: string): void;
trackVolatile(): void;
notify(source: unknown, args: any): void;
defineProperty(target: {}, nameOrAccessor: string | Accessor): void;
getAccessors(target: {}): Accessor[];
getAccessors: (target: {}) => Accessor[];
binding<TSource = any, TReturn = any, TParent = any>(binding: Binding<TSource, TReturn, TParent>, initialSubscriber?: Subscriber | undefined, isVolatileBinding?: boolean): BindingObserver<TSource, TReturn, TParent>;
isVolatileBinding<TSource_1 = any, TReturn_1 = any, TParent_1 = any>(binding: Binding<TSource_1, TReturn_1, TParent_1>): boolean;
}>;
Expand Down Expand Up @@ -467,26 +499,21 @@ export class RepeatBehavior<TSource = any> implements Behavior, Subscriber {
// @internal (undocumented)
handleChange(source: any, args: Splice[]): void;
unbind(): void;
}
}

// @public
export class RepeatDirective<TSource = any> extends HTMLDirective {
constructor(itemsBinding: Binding, templateBinding: Binding<TSource, SyntheticViewTemplate>, options: RepeatOptions);
createBehavior(targets: ViewBehaviorTargets): RepeatBehavior<TSource>;
createPlaceholder: (index: number) => string;
}
}

// @public
export interface RepeatOptions {
positioning?: boolean;
recycle?: boolean;
}

// Warning: (ae-internal-missing-underscore) The name "setCurrentEvent" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
export function setCurrentEvent(event: Event | null): void;

// @public
export function slotted<T = any>(propertyOrOptions: (keyof T & string) | SlottedDirectiveOptions<keyof T & string>): CaptureType<T>;

Expand Down Expand Up @@ -617,14 +644,15 @@ export class ViewTemplate<TSource = any, TParent = any, TGrandparent = any> impl
readonly directives: ReadonlyArray<HTMLDirective>;
readonly html: string | HTMLTemplateElement;
render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView<TSource, TParent, TGrandparent>;
}
}

// @public
export function volatile(target: {}, name: string | Accessor, descriptor: PropertyDescriptor): PropertyDescriptor;

// @public
export function when<TSource = any, TReturn = any>(binding: Binding<TSource, TReturn>, templateOrTemplateBinding: SyntheticViewTemplate | Binding<TSource, SyntheticViewTemplate>): CaptureType<TSource>;


// (No @packageDocumentation comment for this package)

```
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isString, Mutable } from "../interfaces.js";
import { isString } from "../interfaces.js";
import { Observable } from "../observation/observable.js";
import { FAST, KernelServiceId } from "../platform.js";
import { ComposableStyles, ElementStyles } from "../styles/element-styles.js";
import type { ElementViewTemplate } from "../templating/template.js";
import { AttributeConfiguration, AttributeDefinition } from "./attributes.js";
Expand Down
143 changes: 78 additions & 65 deletions packages/web-components/fast-element/src/dom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Callable } from "./interfaces.js";
import { $global, TrustedTypesPolicy } from "./platform.js";
import { $global, KernelServiceId, TrustedTypesPolicy } from "./platform.js";

/* eslint-disable */
const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy(
Expand All @@ -10,31 +10,82 @@ const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy(
);
/* eslint-enable */

let htmlPolicy: TrustedTypesPolicy = fastHTMLPolicy;
const updateQueue: Callable[] = [];
const pendingErrors: any[] = [];
const rAF = $global.requestAnimationFrame;
let updateAsync = true;

function throwFirstError(): void {
if (pendingErrors.length) {
throw pendingErrors.shift();
const updateQueue = $global.FAST.getById(KernelServiceId.updateQueue, () => {
const tasks: Callable[] = [];
const pendingErrors: any[] = [];
const rAF = $global.requestAnimationFrame;
let updateAsync = true;

function throwFirstError(): void {
if (pendingErrors.length) {
throw pendingErrors.shift();
}
}
}

function tryRunTask(task: Callable): void {
try {
(task as any).call();
} catch (error) {
if (updateAsync) {
pendingErrors.push(error);
setTimeout(throwFirstError, 0);
} else {
updateQueue.length = 0;
throw error;

function tryRunTask(task: Callable): void {
try {
(task as any).call();
} catch (error) {
if (updateAsync) {
pendingErrors.push(error);
setTimeout(throwFirstError, 0);
} else {
tasks.length = 0;
throw error;
}
}
}

function process(): void {
const capacity = 1024;
let index = 0;

while (index < tasks.length) {
tryRunTask(tasks[index]);
index++;

// Prevent leaking memory for long chains of recursive calls to `DOM.queueUpdate`.
// If we call `DOM.queueUpdate` within a task scheduled by `DOM.queueUpdate`, the queue will
// grow, but to avoid an O(n) walk for every task we execute, we don't
// shift tasks off the queue after they have been executed.
// Instead, we periodically shift 1024 tasks off the queue.
if (index > capacity) {
// Manually shift all values starting at the index back to the
// beginning of the queue.
for (
let scan = 0, newLength = tasks.length - index;
scan < newLength;
scan++
) {
tasks[scan] = tasks[scan + index];
}

tasks.length -= index;
index = 0;
}
}

tasks.length = 0;
}

function enqueue(callable: Callable): void {
tasks.push(callable);

if (tasks.length < 2) {
updateAsync ? rAF(process) : process();
}
}
}

return Object.freeze({
enqueue,
process,
setUpdateMode(isAsync: boolean) {
updateAsync = isAsync;
},
});
});

let htmlPolicy: TrustedTypesPolicy = fastHTMLPolicy;

/**
* Common DOM APIs.
Expand Down Expand Up @@ -84,27 +135,19 @@ export const DOM = Object.freeze({
* ordering will still be preserved so that nested tasks do not run until
* after parent tasks complete.
*/
setUpdateMode(isAsync: boolean) {
updateAsync = isAsync;
},
setUpdateMode: updateQueue.setUpdateMode,

/**
* Schedules DOM update work in the next async batch.
* @param callable - The callable function or object to queue.
*/
queueUpdate(callable: Callable) {
updateQueue.push(callable);

if (updateQueue.length < 2) {
updateAsync ? rAF(DOM.processUpdates) : DOM.processUpdates();
}
},
queueUpdate: updateQueue.enqueue,

/**
* Resolves with the next DOM update.
*/
nextUpdate(): Promise<void> {
return new Promise(DOM.queueUpdate);
return new Promise(updateQueue.enqueue);
},

/**
Expand All @@ -114,37 +157,7 @@ export const DOM = Object.freeze({
* This also forces nextUpdate promises
* to resolve.
*/
processUpdates(): void {
const capacity = 1024;
let index = 0;

while (index < updateQueue.length) {
tryRunTask(updateQueue[index]);
index++;

// Prevent leaking memory for long chains of recursive calls to `DOM.queueUpdate`.
// If we call `DOM.queueUpdate` within a task scheduled by `DOM.queueUpdate`, the queue will
// grow, but to avoid an O(n) walk for every task we execute, we don't
// shift tasks off the queue after they have been executed.
// Instead, we periodically shift 1024 tasks off the queue.
if (index > capacity) {
// Manually shift all values starting at the index back to the
// beginning of the queue.
for (
let scan = 0, newLength = updateQueue.length - index;
scan < newLength;
scan++
) {
updateQueue[scan] = updateQueue[scan + index];
}

updateQueue.length -= index;
index = 0;
}
}

updateQueue.length = 0;
},
processUpdates: updateQueue.process,

/**
* Sets an attribute value on an element.
Expand Down
Loading

0 comments on commit fc8da6f

Please sign in to comment.