UNPKG

@ibyar/core

Version:

Ibyar core, Implements Aurora's core functionality, low-level services, and utilities

476 lines 22.4 kB
import { ReactiveScope, Stack, isComputed, isSignal, isReactive } from '@ibyar/expressions'; import { CommentNode, DomStructuralDirectiveNode, LocalTemplateVariables, DomElementNode, DomFragmentNode, isLiveTextContent, isTagNameNative, isValidCustomElementName, TextContent, readInputValue, getChangeEventNameFoTag, getInputChangeEventName } from '@ibyar/elements'; import { isHTMLComponent } from '../component/custom-element.js'; import { documentStack } from '../context/stack.js'; import { classRegistryProvider } from '../providers/provider.js'; import { isOnDestroy, isOnInit } from '../component/lifecycle.js'; import { hasAttr } from '../utils/elements-util.js'; import { AttributeDirective, ReactiveSignalScope, DIRECTIVE_HOST_TOKEN, NATIVE_HOST_TOKEN, SUCCESSORS_TOKEN, } from '../directive/directive.js'; import { TemplateRef, TemplateRefImpl } from '../linker/template-ref.js'; import { ViewContainerRef, ViewContainerRefImpl } from '../linker/view-container-ref.js'; import { createDestroySubscription } from '../context/subscription.js'; import { isViewChildSignal } from '../component/initializer.js'; import { addProvider, inject, removeProvider } from '../di/inject.js'; import { AbstractAuroraZone } from '../zone/zone.js'; import { clearSignalScope, pushNewSignalScope } from '../signals/signals.js'; import { ShadowRootService } from './shadow-root.js'; export class ComponentRender { view; subscriptions; componentRef; template; contextStack; templateNameScope; exportAsScope; viewScope = new ReactiveScope({}); viewChildSignal; modelStack; constructor(view, subscriptions) { this.view = view; this.subscriptions = subscriptions; this.componentRef = this.view.getComponentRef(); this.contextStack = documentStack.copyStack(); this.contextStack.pushScope(this.view._modelScope); const signalMaskScope = this.contextStack.pushReactiveScope(); this.maskRawSignalScope(signalMaskScope, this.view._model); this.modelStack = Stack.forScopes(this.view._modelScope, signalMaskScope); this.exportAsScope = this.contextStack.pushReactiveScope(); this.templateNameScope = this.contextStack.pushReactiveScope(); this.viewChildSignal = this.componentRef.viewChild.filter(child => child.selector === 'ɵSignal'); } initView() { if (!this.componentRef.template) { return; } if (typeof this.componentRef.template === 'function') { this.template = this.componentRef.template(this.view._model); } else { this.template = this.componentRef.template; } let rootRef; const shadowRootService = inject(ShadowRootService); if (this.componentRef.isShadowDom && shadowRootService.has(this.view)) { rootRef = shadowRootService.get(this.view); } else { rootRef = this.view; } this.initTemplateRefMap(this.template); let rootFragment; if (this.template instanceof DomFragmentNode) { rootFragment = this.createDocumentFragment(this.template, this.contextStack, rootRef, this.subscriptions, this.view); } else { rootFragment = document.createDocumentFragment(); this.appendChildToParent(rootFragment, this.template, this.contextStack, rootRef, this.subscriptions, this.view); } rootRef.append(rootFragment); } isTemplateRefName(template) { if (template instanceof DomElementNode) { if (template.tagName === 'template' && template.templateRefName) { return true; } } return false; } initTemplateRefMap(domNode) { if (!(domNode instanceof DomElementNode || domNode instanceof DomFragmentNode) || !domNode.children) { return; } for (let index = 0; index < domNode.children.length; index++) { const child = domNode.children[index]; if (this.isTemplateRefName(child)) { this.templateNameScope.set(child.templateRefName.name, undefined); } else { this.initTemplateRefMap(child); } } } initViewBinding() { if (this.componentRef.viewBindings) { this.initHtmlElement(this.view, this.componentRef.viewBindings, this.contextStack, this.subscriptions); } if (this.componentRef.windowBindings) { this.subscriptions.push(...this.initAttribute(window, this.componentRef.windowBindings, this.contextStack)); } } getElementByName(name) { return Reflect.get(this.view, name); } createStructuralDirective(directive, comment, directiveStack, subscriptions, parentNode, host) { const directiveRef = classRegistryProvider.getDirectiveRef(directive.name); if (!directiveRef) { // didn't find directive or it is not define yet. // class registry should have 'when defined' callback return; } const stack = directiveStack.copyStack(); const templateRef = new TemplateRefImpl(this, directive.node, stack, directive.templateExpressions ?? []); const successorTemplateRefs = Object.fromEntries((directive.successors ?? [])?.map(successor => [successor.name, new TemplateRefImpl(this, successor, stack, [])])); const directiveZone = this.view._zone.fork(directiveRef.zone); const viewContainerRef = new ViewContainerRefImpl(parentNode, comment); directiveZone.onEmpty.subscribe(() => { const length = viewContainerRef.length; for (let index = 0; index < length; index++) { viewContainerRef.get(index)?.detectChanges(); } }); const provider = this.view._provider.fork(); provider.setToken(DIRECTIVE_HOST_TOKEN, host); provider.setType(TemplateRef, templateRef); provider.setType(AbstractAuroraZone, directiveZone); provider.setType(ViewContainerRef, viewContainerRef); provider.setToken(SUCCESSORS_TOKEN, successorTemplateRefs); addProvider(provider); const signalScope = pushNewSignalScope(); const StructuralDirectiveClass = directiveRef.modelClass; const structural = directiveZone.run(() => new StructuralDirectiveClass()); clearSignalScope(signalScope); removeProvider(provider); templateRef.host = structural; const scope = ReactiveSignalScope.readOnlyScopeForThis(structural); scope.getInnerScope('this').getContextProxy = () => structural; stack.pushScope(scope); if (directiveRef.exportAs) { stack.pushBlockScopeFor({ [directiveRef.exportAs]: structural }); } subscriptions.push(...this.initDirective(structural, directive, stack)); if (isOnInit(structural)) { directiveZone.run(structural.onInit, structural); } if (directive.attributeDirectives?.length) { this.initAttributeDirectives(directive.attributeDirectives, structural, stack, subscriptions); } if (isOnDestroy(structural)) { subscriptions.push(createDestroySubscription(() => structural.onDestroy())); } } createComment(node) { return document.createComment(`${node.comment}`); } createText(node) { return new Text(node.value); } createLiveText(textNode, contextStack, subscriptions) { const liveText = new Text(''); contextStack = contextStack.copyStack(); contextStack.pushBlockScopeFor({ this: liveText }); const textSubscriptions = textNode.expression.subscribe(contextStack, textNode.pipelineNames); subscriptions.push(...textSubscriptions); textNode.expression.get(contextStack); return liveText; } createLocalTemplateVariables(localNode, contextStack, subscriptions) { const entries = localNode.variables.map(variable => [variable.expression.getLeft().getDeclarationName(), undefined]); const context = Object.fromEntries(entries); contextStack.pushReactiveScopeFor(context); localNode.variables.forEach(variable => { const localSubscriptions = variable.expression.subscribe(contextStack, variable.pipelineNames); subscriptions.push(...localSubscriptions); variable.expression.get(contextStack); }); } createDocumentFragment(node, contextStack, parentNode, subscriptions, host) { const fragment = document.createDocumentFragment(); node.children?.forEach(child => this.appendChildToParent(fragment, child, contextStack, parentNode, subscriptions, host)); return fragment; } appendChildToParent(fragmentParent, child, contextStack, parentNode, subscriptions, host) { if (child instanceof DomElementNode) { if (this.isTemplateRefName(child)) { const templateRefName = child.templateRefName; // const oldRef = this.templateNameScope.get(templateRefName.name); // if (oldRef) { // return; // } // TODO: extract template expression const templateRef = new TemplateRefImpl(this, new DomFragmentNode(child.children), contextStack.copyStack(), []); this.templateNameScope.set(templateRefName.name, templateRef); return; } fragmentParent.append(this.createElement(child, contextStack, subscriptions, host)); } else if (child instanceof DomStructuralDirectiveNode) { const comment = document.createComment(` @${child.name.substring(1)} ${child.value ? `(${child.value}) {` : '{'}`); fragmentParent.append(comment); fragmentParent.append(document.createComment('}')); this.createStructuralDirective(child, comment, contextStack, subscriptions, parentNode, host); } else if (isLiveTextContent(child)) { fragmentParent.append(this.createLiveText(child, contextStack, subscriptions)); } else if (child instanceof LocalTemplateVariables) { this.createLocalTemplateVariables(child, contextStack, subscriptions); } else if (child instanceof TextContent) { fragmentParent.append(this.createText(child)); } else if (child instanceof CommentNode) { fragmentParent.append(this.createComment(child)); } else if (child instanceof DomFragmentNode) { fragmentParent.append(this.createDocumentFragment(child, contextStack, parentNode, subscriptions, host)); } } createElementByTagName(node) { let element; if (isValidCustomElementName(node.tagName)) { const ViewClass = customElements.get(node.tagName); if (ViewClass) { element = new ViewClass(); } else { element = document.createElement(node.tagName); if (element.constructor.name === 'HTMLElement') { customElements.whenDefined(node.tagName).then(() => customElements.upgrade(element)); } } } else if (isTagNameNative(node.tagName)) { // native tags // and custom tags can be used her element = document.createElement(node.tagName, node.is ? { is: node.is } : undefined); } else { // html unknown element element = document.createElement(node.tagName); } return element; } createElement(node, contextStack, subscriptions, host) { const element = this.createElementByTagName(node); this.initHtmlElement(element, node, contextStack, subscriptions, host); return element; } initHtmlElement(element, node, contextStack, subscriptions, host) { const elementStack = contextStack.copyStack(); const elementScope = isHTMLComponent(element) ? element._viewScope : (node.templateRefName?.name ? ReactiveScope.readOnlyScopeForAliasThis(element, node.templateRefName.name) : ReactiveScope.readOnlyScopeForThis(element)); elementStack.pushScope(elementScope); const attributesSubscriptions = this.initAttribute(element, node, elementStack); subscriptions.push(...attributesSubscriptions); const changeEventName = getChangeEventNameFoTag(node.tagName); if (changeEventName) { const inputScope = elementScope.getInnerScope('this'); const listener = (event) => { const input = event.target; inputScope.emit(getInputChangeEventName(input.type), readInputValue(input)); }; element.addEventListener(changeEventName, listener); subscriptions.push(createDestroySubscription(() => element.removeEventListener(changeEventName, listener))); } const templateRefName = node.templateRefName; if (templateRefName) { Reflect.set(this.view, templateRefName.name, element); this.viewScope.set(templateRefName.name, element); const view = this.componentRef.viewChild.find(child => child.selector === templateRefName.name); let signal; if (view) { Reflect.set(this.view._model, view.modelName, element); } else if (signal = this.getViewChildSignal(templateRefName.name)) { signal.set(element); } } if (node.children) { for (const child of node.children) { this.appendChildToParent(element, child, elementStack, element, subscriptions, host); } } if (node.attributeDirectives?.length) { this.initAttributeDirectives(node.attributeDirectives, element, contextStack, subscriptions); } } initAttributeDirectives(attributeDirectives, element, contextStack, subscriptions) { attributeDirectives?.forEach(directiveNode => { const directiveRef = classRegistryProvider.getDirectiveRef(directiveNode.name); if (!directiveRef || !(directiveRef.modelClass.prototype instanceof AttributeDirective)) { return; } const directiveZone = this.view._zone.fork(directiveRef.zone); const provider = this.view._provider.fork(); provider.setToken(NATIVE_HOST_TOKEN, element); provider.setType(AbstractAuroraZone, directiveZone); addProvider(provider); const signalScope = pushNewSignalScope(); const directive = directiveZone.run(() => new directiveRef.modelClass()); clearSignalScope(signalScope); removeProvider(provider); if (directiveRef.exportAs) { this.exportAsScope.set(directiveRef.exportAs, directive); } const stack = contextStack.copyStack(); const scope = ReactiveSignalScope.readOnlyScopeForThis(directive); const directiveScope = scope.getInnerScope('this'); directiveScope.getContextProxy = () => directive; stack.pushScope(scope); const directiveSubscriptions = this.initDirective(directive, directiveNode, stack); subscriptions.push(...directiveSubscriptions); if (isOnInit(directive)) { directiveZone.run(directive.onInit, directive); } if (isOnDestroy(directive)) { subscriptions.push(createDestroySubscription(() => directive.onDestroy())); } if (!(element instanceof HTMLElement)) { return; } const attributeName = directiveRef.selector.replace(/[A-Z]/g, m => '-' + m.toLowerCase()); if (!element.hasAttribute(attributeName)) { element.setAttribute(attributeName, ''); } if (directiveRef.viewBindings) { const directiveStack = stack.copyStack(); directiveStack.pushScope(directiveScope); this.initHtmlElement(element, directiveRef.viewBindings, directiveStack, subscriptions); } }); } initAttribute(element, node, contextStack) { const subscriptions = []; if (node.attributes?.length) { node.attributes.forEach(attr => { /** * <input id="23" name="person-name" data-id="1234567890" data-user="carinaanand" data-date-of-birth /> */ if (hasAttr(element, attr.name)) { if (attr.value === false) { element.removeAttribute(attr.name); } else if (attr.value === true) { element.setAttribute(attr.name, ''); } else { element.setAttribute(attr.name, attr.value); } } else { attr.expression.get(contextStack); // Reflect.set(element, attr.name, attr.value); } }); } if (node.twoWayBinding?.length) { node.twoWayBinding.forEach(attr => { const sub = attr.expression.subscribe(contextStack); subscriptions.push(...sub); attr.expression.get(contextStack); }); } if (node.inputs?.length) { node.inputs.forEach(attr => { const sub = attr.expression.subscribe(contextStack, attr.pipelineNames); subscriptions.push(...sub); attr.expression.get(contextStack); }); } if (node.outputs?.length) { node.outputs.forEach(event => { let listener; /** * <a (click)="onLinkClick($event)"></a> * <a @click="onLinkClick($event)"></a> * <input [(value)]="person.name" /> * <input (value)="person.name" /> * <!-- <input (value)="person.name = $event" /> --> * * TODO: diff of event listener and back-way data binding */ if (typeof event.value === 'string') { listener = ($event) => { const stack = contextStack.copyStack(); stack.pushBlockScopeFor({ $event }); this.view._zone.run(event.expression.get, event.expression, [stack]); }; } else /* if (typeof event.sourceHandler === 'function')*/ { // let eventName: keyof HTMLElementEventMap = event.eventName; listener = event.value; } element.addEventListener(event.name, listener); subscriptions.push(createDestroySubscription(() => element.removeEventListener(event.name, listener))); }); } if (node.templateAttrs?.length) { node.templateAttrs.forEach(attr => { const sub = attr.expression.subscribe(contextStack); subscriptions.push(...sub); attr.expression.get(contextStack); }); } return subscriptions; } initDirective(context, node, contextStack) { const subscriptions = []; if (node.attributes?.length) { node.attributes.forEach(attr => attr.expression.get(contextStack)); } if (node.twoWayBinding?.length) { node.twoWayBinding.forEach(attr => { const sub = attr.expression.subscribe(contextStack); subscriptions.push(...sub); attr.expression.get(contextStack); }); } if (node.inputs?.length) { node.inputs.forEach(attr => { const sub = attr.expression.subscribe(contextStack, attr.pipelineNames); subscriptions.push(...sub); attr.expression.get(contextStack); }); } if (node.outputs?.length) { node.outputs.forEach(event => { const listener = ($event) => { const stack = contextStack.copyStack(); stack.pushBlockScopeFor({ $event }); this.view._zone.run(event.expression.get, event.expression, [stack]); }; const subscription = Reflect.get(context, event.name).subscribe(listener); subscriptions.push(createDestroySubscription(() => subscription.unsubscribe())); }); } if (node.templateAttrs?.length) { node.templateAttrs.forEach(attr => { const sub = attr.expression.subscribe(contextStack); subscriptions.push(...sub); attr.expression.get(contextStack); }); } // TODO: // check host binding return subscriptions; } getViewChildSignal(templateRefName) { for (const child of this.viewChildSignal) { const signal = this.view._model[child.modelName]; if (isViewChildSignal(signal) && signal.selector === templateRefName) { return signal; } } return; } maskRawSignalScope(maskScope, model) { Object.entries(model).forEach(([key, signal]) => { if (isSignal(signal)) { maskScope.set(key, signal.get()); signal.subscribe(value => maskScope.set(key, value)); maskScope.subscribe(key, value => signal.set(value)); } else if (isComputed(signal)) { maskScope.set(key, signal.get()); signal.subscribe(value => maskScope.set(key, value)); } else if (isReactive(signal)) { maskScope.set(key, undefined); signal.subscribe(value => maskScope.set(key, value)); } }); } } //# sourceMappingURL=render.js.map