@ibyar/core
Version:
Ibyar core, Implements Aurora's core functionality, low-level services, and utilities
291 lines • 13.3 kB
JavaScript
import { ReactiveScope, ReactiveControlScope, isReactive } from '@ibyar/expressions';
import { isAfterContentChecked, isAfterContentInit, isAfterViewChecked, isAfterViewInit, isDoCheck, isOnChanges, isOnDestroy, isOnInit } from '../component/lifecycle.js';
import { EventEmitter } from '../component/events.js';
import { ComponentRender } from './render.js';
import { getRootZone } from '../zone/bootstrap.js';
import { AbstractAuroraZone, ProxyAuroraZone } from '../zone/zone.js';
import { ChangeDetectorRef, createModelChangeDetectorRef } from '../linker/change-detector-ref.js';
import { createProxyZone } from '../zone/proxy.js';
import { clearSignalScope, pushNewSignalScope } from '../signals/signals.js';
import { forkProvider, addProvider, removeProvider } from '../di/inject.js';
import { isOutputSignal, VIEW_TOKEN } from '../component/initializer.js';
export function baseFactoryView(htmlElementType) {
return class CustomView extends htmlElementType {
_model;
_signalScope;
_render;
_shadowRoot;
_componentRef;
_modelScope;
_viewScope;
_zone;
_provider;
_detector;
subscriptions = [];
onDestroyCalls = [];
needRendering = true;
constructor(componentRef, modelClass) {
super();
this._componentRef = componentRef;
if (componentRef.isShadowDom && !componentRef.disabledFeatures?.includes('shadow')) {
this._shadowRoot = this.attachShadow(componentRef.shadowRootInit);
}
this._signalScope = pushNewSignalScope();
this._provider = forkProvider();
/* resolve dependency injection*/
this._detector = createModelChangeDetectorRef(() => this._modelScope);
this._zone = getRootZone().fork(componentRef.zone);
this._provider.setType(AbstractAuroraZone, this._zone);
this._provider.setType(ChangeDetectorRef, this._detector);
this._provider.setToken(VIEW_TOKEN, this);
addProvider(this._provider);
const model = new modelClass();
removeProvider(this._provider);
this._model = model;
clearSignalScope(this._signalScope);
const modelScope = ReactiveControlScope.for(model);
const modelProxyRef = this._zone instanceof ProxyAuroraZone
? createProxyZone(model, this._zone)
: model;
modelScope.getContextProxy = () => modelProxyRef;
this._modelScope = modelScope;
Object.keys(this._model).forEach(key => {
const node = this._model[key];
if (isReactive(node)) {
node.subscribe((value, old) => this._modelScope.emit(key, value, old));
}
});
this._viewScope = ReactiveScope.for({ 'this': this });
const elementScope = this._viewScope.getInnerScope('this');
componentRef.inputs.forEach(input => {
elementScope.subscribe(input.viewAttribute, (newValue, oldValue) => {
if (newValue === oldValue) {
return;
}
this._render.modelStack.set(input.modelProperty, newValue);
});
this._modelScope.subscribe(input.modelProperty, (newValue, oldValue) => {
if (newValue === oldValue) {
return;
}
elementScope.emit(input.viewAttribute, newValue, oldValue);
});
});
componentRef.outputs.forEach(output => {
const event = model[output.modelProperty];
if (event instanceof EventEmitter || isOutputSignal(event)) {
const options = isOutputSignal(event) ? event.options : output.options;
event.subscribe((value) => {
const event = new CustomEvent(output.viewAttribute, {
detail: value,
cancelable: false,
bubbles: options?.bubbles,
composed: options?.bubbles,
});
this.dispatchEvent(event);
});
}
});
// if property of the model has view decorator
if (this._componentRef.view) {
Reflect.set(this._model, this._componentRef.view, this);
}
this._render = new ComponentRender(this, this.subscriptions);
if (this.attributes.length > 0) {
let attrs = Array.prototype.slice.call(this.attributes);
attrs.forEach(attr => this.initOuterAttribute(attr));
}
if (this._componentRef.encapsulation === 'shadow-slot') {
// render view before inserting any slot element as child
this.initView();
}
}
doBlockCallback = () => {
if (isDoCheck(this._model)) {
this._zone.run(this._model.doCheck, this._modelScope.getContextProxy());
}
};
getComponentRef() {
return this._componentRef;
}
getInput(viewProp) {
return this._componentRef.inputs.find(input => input.viewAttribute === viewProp);
}
getInputValue(viewProp) {
const inputRef = this.getInput(viewProp);
if (inputRef) {
return this._render.modelStack.get(inputRef.modelProperty);
}
}
setInputValue(viewProp, value) {
const inputRef = this.getInput(viewProp);
if (inputRef) {
this._render.modelStack.set(inputRef.modelProperty, value);
}
}
setAttributeHelper(attrViewName, value) {
if (value === null || value === undefined) {
this.removeAttribute(attrViewName);
}
else if (typeof value === 'boolean') {
if (value) {
super.setAttribute(attrViewName, '');
}
else {
this.removeAttribute(attrViewName);
}
}
else {
super.setAttribute(attrViewName, value);
}
}
setAttribute(attrViewName, value) {
this.setInputValue(attrViewName, value);
this.setAttributeHelper(attrViewName, value);
}
getAttribute(attrViewName) {
return this.getInputValue(attrViewName) ?? super.getAttribute(attrViewName);
}
attributeChangedCallback(name, oldValue, newValue) {
if (newValue === oldValue) {
return;
}
if (isOnChanges(this._model)) {
this._zone.run(this._model.onChanges, this._modelScope.getContextProxy());
}
this.doBlockCallback();
}
initView() {
// setup ui view
this._render.initView();
// init view binding
this._render.initViewBinding();
this.needRendering = false;
}
connectedCallback() {
if (this.subscriptions.length) {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
this.subscriptions.splice(0, this.subscriptions.length);
const cds = this._zone.onEmpty.subscribe(() => this._modelScope.detectChanges());
this.onDestroy(() => cds.unsubscribe());
this._componentRef.inputs.forEach(input => {
const inputDefaultValue = this._model[input.modelProperty];
if (inputDefaultValue !== null && inputDefaultValue !== undefined) {
this.setAttributeHelper(input.viewAttribute, inputDefaultValue);
}
});
if (isOnChanges(this._model)) {
this._zone.run(this._model.onChanges, this._modelScope.getContextProxy());
}
if (isOnInit(this._model)) {
this._zone.run(this._model.onInit, this._modelScope.getContextProxy());
}
if (isDoCheck(this._model)) {
this._zone.run(this._model.doCheck, this._modelScope.getContextProxy());
}
if (isAfterContentInit(this._model)) {
this._zone.run(this._model.afterContentInit, this._modelScope.getContextProxy());
}
if (isAfterContentChecked(this._model)) {
this._zone.run(this._model.afterContentChecked, this._modelScope.getContextProxy());
}
// do once
if (this.needRendering) {
this.initView();
}
if (isAfterViewInit(this._model)) {
this._zone.run(this._model.afterViewInit, this._modelScope.getContextProxy());
}
if (isAfterViewChecked(this._model)) {
this._zone.run(this._model.afterViewChecked, this._modelScope.getContextProxy());
}
this.doBlockCallback = () => {
if (isDoCheck(this._model)) {
this._zone.run(this._model.doCheck, this._modelScope.getContextProxy());
}
if (isAfterContentChecked(this._model)) {
this._zone.run(this._model.afterContentChecked, this._modelScope.getContextProxy());
}
if (isAfterViewChecked(this._model)) {
this._zone.run(this._model.afterViewChecked, this._modelScope.getContextProxy());
}
};
const event = new CustomEvent('connected', { cancelable: true, bubbles: false, composed: false });
this.dispatchEvent(event);
}
initOuterAttribute(attr) {
// [window, this] scope
let elementAttr = attr.name;
let modelProperty = attr.value;
if (elementAttr.startsWith('[')) {
elementAttr = elementAttr.substring(1, elementAttr.length - 1);
if (Reflect.has(window, modelProperty)) {
this.setInputValue(elementAttr, Reflect.get(window, modelProperty));
}
}
else if ((elementAttr.startsWith('(') && elementAttr.endsWith(')')) || elementAttr.startsWith('@') || elementAttr.startsWith('on')) {
// (elementAttr)="modelProperty()"
// @elementAttr="modelProperty()"
// onElementAttr="modelProperty()"
// lower-case-event-name = "modelProperty()"
let start = 0, end = undefined;
if (elementAttr.startsWith('(') || elementAttr.startsWith('@')) {
start = 1;
}
else if (elementAttr.startsWith('on')) {
start = 2;
}
if (elementAttr.endsWith(')')) {
end = elementAttr.length - 1;
}
const eventName = elementAttr.substring(start, end)?.replaceAll('-', '').toLowerCase();
if (modelProperty.endsWith('()')) {
modelProperty = modelProperty.substring(0, modelProperty.length - 2);
}
const callback = Reflect.get(window, modelProperty);
const customEvent = this._componentRef.outputs.find(output => output.viewAttribute.toLowerCase() === eventName);
if (customEvent) {
this._modelScope.subscribe(customEvent.modelProperty, (data) => callback?.(data));
}
else {
this.addEventListener(elementAttr, event => callback?.(event));
}
}
else {
const inputRef = this.getInput(attr.name);
if (inputRef) {
this._render.modelStack.set(inputRef.modelProperty, attr.value);
}
}
}
onDestroy(callback) {
this.onDestroyCalls.push(callback);
}
adoptedCallback() {
// restart the process
this.innerHTML = '';
this.connectedCallback();
}
disconnectedCallback() {
// notify first, then call model.onDestroy func
if (isOnDestroy(this._model)) {
this._zone.run(this._model.onDestroy, this._modelScope.getContextProxy());
}
this.subscriptions.forEach(sub => sub.unsubscribe());
this.subscriptions.splice(0, this.subscriptions.length);
this.onDestroyCalls.forEach(callback => {
try {
callback();
}
catch (error) {
console.error(error);
}
});
this.onDestroyCalls.splice(0, this.onDestroyCalls.length);
const event = new CustomEvent('disconnected', { cancelable: true, bubbles: false, composed: false });
this.dispatchEvent(event);
}
};
}
//# sourceMappingURL=base-view.js.map