@ibyar/core
Version:
Ibyar core, Implements Aurora's core functionality, low-level services, and utilities
476 lines • 22.4 kB
JavaScript
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