import { IContainer, InstanceProvider, Writable, emptyArray, isFunction, onResolve, resolve } from '@aurelia/kernel'; import { Scope } from '@aurelia/runtime'; import { IInstruction, type HydrateElementInstruction } from '@aurelia/template-compiler'; import { IRenderLocation } from '../../dom'; import { CustomElementDefinition, CustomElementStaticAuDefinition, elementTypeName } from '../custom-element'; import { IHydrationContext } from '../../templating/controller'; import { IRendering } from '../../templating/rendering'; import { registerResolver } from '../../utilities-di'; import { createMutationObserver, isElement } from '../../utilities-dom'; import type { ControllerVisitor, ICustomElementController, ICustomElementViewModel, IHydratedController, IHydratedParentController, ISyntheticView } from '../../templating/controller'; import type { IViewFactory } from '../../templating/view'; import { type IAuSlot, type IAuSlotSubscriber, IAuSlotWatcher, defaultSlotName, auslotAttr } from '../../templating/controller.projection'; let emptyTemplate: CustomElementDefinition; export class AuSlot implements ICustomElementViewModel, IAuSlot { public static readonly $au: CustomElementStaticAuDefinition = { type: elementTypeName, name: 'au-slot', template: null, containerless: true, processContent(el, p, data) { data.name = el.getAttribute('name') ?? defaultSlotName; let node: Node | null = el.firstChild; let next: Node | null = null; while (node !== null) { next = node.nextSibling; if (isElement(node) && node.hasAttribute(auslotAttr)) { if (__DEV__) { // eslint-disable-next-line no-console console.warn( `[DEV:aurelia] detected [au-slot] attribute on a child node`, `of an element: "<${node.nodeName} au-slot>".`, `This element will be ignored and removed` ); } el.removeChild(node); } node = next; } }, bindables: ['expose', 'slotchange'], }; public readonly view: ISyntheticView; /** @internal */ public readonly $controller!: ICustomElementController; // This is set by the controller after this instance is constructed /** @internal */ private readonly _location: IRenderLocation; /** @internal */ private _parentScope: Scope | null = null; /** @internal */ private _outerScope: Scope | null = null; /** @internal */ private readonly _hasProjection: boolean; /** @internal */ private readonly _hdrContext: IHydrationContext; /** @internal */ private readonly _slotwatchers: readonly IAuSlotWatcher[]; /** @internal */ private readonly _hasSlotWatcher: boolean; /** @internal */ private _attached: boolean = false; /** * The binding context that will be exposed to slotted content */ public expose: object | null = null; /** * A callback that will be called when the content of this slot changed */ public slotchange: ((name: string, nodes: readonly Node[]) => void) | null = null; public constructor() { const hdrContext = resolve(IHydrationContext); const location = resolve(IRenderLocation); const instruction = resolve(IInstruction) as HydrateElementInstruction<{ name: string }>; const rendering = resolve(IRendering); const slotName = this.name = instruction.data.name; // when is empty, there's not even projections // hence ?. operator is used // for fallback, there's only default slot used const fallback = instruction.projections?.[defaultSlotName]; const projection = hdrContext.instruction?.projections?.[slotName]; const contextContainer = hdrContext.controller.container; let factory: IViewFactory; let container: IContainer; if (projection == null) { container = contextContainer.createChild({ inheritParentResources: true }); factory = rendering.getViewFactory(fallback ?? (emptyTemplate ??= CustomElementDefinition.create({ name: 'au-slot-empty-template', template: '', needsCompile: false, })), container); this._hasProjection = false; } else { // projection could happen within a projection, example: // --my-app-- // // ---projection 1--- // // ---projection 2--- // // for the template above, if is injecting , // we won't find the information in the hydration context hierarchy / // as it's a flat wysiwyg structure based on the template html // // since we are constructing the projection (2) view based on the // container of , we need to pre-register all information stored // in projection (1) into the container created for the projection (2) view // ============================= // my-app template: // my-app --- hydration context // --- owning element (this has this that uses ---projection) // --- projection // container = contextContainer.createChild(); // registering resources from the parent hydration context is necessary // as that's where the projection is declared in the template // // if neccessary, we can do the same gymnastic of registering information related to // a custom element registration like in renderer.ts from line 1088 to 1098 // so we don't accidentally get information related to owning element (host, controller, instruction etc...) // although it may be more desirable to have owning element information available here container.useResources(hdrContext.parent!.controller.container); // doing this to shadow the owning element hydration context // since we created a container out of the owning element container // instead of the hydration context container registerResolver(container, IHydrationContext, new InstanceProvider(void 0, hdrContext.parent)); factory = rendering.getViewFactory(projection, container); this._hasProjection = true; this._slotwatchers = contextContainer.getAll(IAuSlotWatcher, false)?.filter(w => w.slotName === '*' || w.slotName === slotName) ?? emptyArray; } this._hasSlotWatcher = (this._slotwatchers ??= emptyArray).length > 0; this._hdrContext = hdrContext; this.view = factory.create().setLocation(this._location = location); } // all the following properties (name, nodes, _subs, subscribe & unsubscribe) are relevant to the slot watcher feature // so grouping them here for better readability public readonly name: string; public get nodes() { const nodes = []; const location = this._location; let curr = location.$start!.nextSibling; while (curr != null && curr !== location) { if (curr.nodeType !== /* comment */8) { nodes.push(curr); } curr = curr.nextSibling; } return nodes; } /** @internal */ private readonly _subs = new Set(); public subscribe(subscriber: IAuSlotSubscriber): void { this._subs.add(subscriber); } public unsubscribe(subscriber: IAuSlotSubscriber): void { this._subs.delete(subscriber); } public binding( _initiator: IHydratedController, parent: IHydratedParentController, ): void | Promise { this._parentScope = parent.scope; // The following block finds the real host scope for the content of this // // if this was created by another au slot, the controller hierarchy will be like this: // C(au-slot)#1 --> C(synthetic)#1 --> C(au-slot)#2 --> C(synthetic)#2 // // C(synthetic)#2 is what will provide the content for C(au-slot)#1 // but C(au-slot)#1 is what will provide the $host value for the content of C(au-slot)#2 // // example: //