From 151a17dac71f6c4d57d919110abd25b2c17e23f0 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Wed, 18 Dec 2024 12:01:38 +0100 Subject: [PATCH] WIP --- packages/demo/src/js/app.ts | 11 +-- .../js/components/ParentNativeEvent/Child.js | 23 +++++++ .../js/components/ParentNativeEvent/index.js | 21 ++++-- .../templates/pages/child-native-event.twig | 3 +- packages/js-toolkit/Base/Base.ts | 68 +++++++++++-------- .../Base/managers/ChildrenManager.ts | 44 ++++++++---- 6 files changed, 111 insertions(+), 59 deletions(-) diff --git a/packages/demo/src/js/app.ts b/packages/demo/src/js/app.ts index 60757c5e2..660763c19 100644 --- a/packages/demo/src/js/app.ts +++ b/packages/demo/src/js/app.ts @@ -142,11 +142,6 @@ class App extends Base { this.$log('Mounted 🎉'); } - updated() { - console.log(this.$children.TestManyInstance) - console.log(getInstances()); - } - onModalOpen(...args) { this.$log('onModalOpen', ...args); } @@ -159,13 +154,9 @@ class App extends Base { this.$log('resized', props); } - onDocumentClick(event) { - console.log('onDocumentClick', event); - } - onWindowResize(event) { console.log('onWindowResize', event); } } -export default createApp(App); +export default createApp(App, { root: document.querySelector('main') }); diff --git a/packages/demo/src/js/components/ParentNativeEvent/Child.js b/packages/demo/src/js/components/ParentNativeEvent/Child.js index 66cd833ea..eaffb5dea 100644 --- a/packages/demo/src/js/components/ParentNativeEvent/Child.js +++ b/packages/demo/src/js/components/ParentNativeEvent/Child.js @@ -9,5 +9,28 @@ export default class Child extends Base { */ static config = { name: 'Child', + log: true, + debug: false, }; + + msg(...msg) { + this.$el.textContent = `[${this.$id}] ${msg.join(' ')}`; + } + + mounted() { + this.msg('Mounted', performance.now()); + } + + updated() { + this.msg('Updated', performance.now()); + } + + destroyed() { + this.$log('destroyed'); + } + + terminated() { + this.$log('terminated'); + this.msg('Terminated', performance.now()); + } } diff --git a/packages/demo/src/js/components/ParentNativeEvent/index.js b/packages/demo/src/js/components/ParentNativeEvent/index.js index da02ef527..b12ec3442 100644 --- a/packages/demo/src/js/components/ParentNativeEvent/index.js +++ b/packages/demo/src/js/components/ParentNativeEvent/index.js @@ -1,4 +1,5 @@ import { Base } from '@studiometa/js-toolkit'; +import { createElement } from '@studiometa/js-toolkit/utils'; import Child from './Child.js'; /** @@ -11,17 +12,27 @@ export default class ParentNativeEvent extends Base { static config = { name: 'ParentNativeEvent', log: true, - debug: true, + debug: false, components: { Child, }, }; - onChildClick(...args) { - this.$log(this.$id, 'onChildClick', ...args); + updated() { + this.$log(this.$children.Child); } - onChildDede(...args) { - this.$log(this.$id, 'onChildDede', ...args); + onDocumentClick({ event }) { + if (event.metaKey) { + this.$el.firstElementChild.remove(); + } else if (event.altKey) { + this.$children.Child[0].$el.dataset.component = ''; + } else { + this.$el.append( + createElement('button', { + dataComponent: 'Child', + }), + ); + } } } diff --git a/packages/demo/src/templates/pages/child-native-event.twig b/packages/demo/src/templates/pages/child-native-event.twig index fd0192b98..464141ccf 100644 --- a/packages/demo/src/templates/pages/child-native-event.twig +++ b/packages/demo/src/templates/pages/child-native-event.twig @@ -1,7 +1,6 @@ {% extends '@layouts/base.twig' %} {% block main %} -
- +
{% endblock %} diff --git a/packages/js-toolkit/Base/Base.ts b/packages/js-toolkit/Base/Base.ts index 1a1a60392..3bd416c28 100644 --- a/packages/js-toolkit/Base/Base.ts +++ b/packages/js-toolkit/Base/Base.ts @@ -316,7 +316,7 @@ export class Base { this[`__${service.toLowerCase()}`] = new this.__managers[`${service}Manager`](this); } - const service = useMutation(document.documentElement, { + const service = useMutation(document.querySelector('main'), { childList: true, subtree: true, attributes: true, @@ -325,36 +325,49 @@ export class Base { const key = this.constructor.__mutationSymbol; if (!service.has(key)) { service.add(key, (props) => { - const updates = new Map any>(); - - for (const instance of getInstances()) { - for (const mutation of props.mutations) { - const shouldUpdateAfterAttributeChange = - mutation.type === 'attributes' && instance.$el.contains(mutation.target.parentNode); - const shouldUpdateAfterNodeRemoval = - mutation.type === 'childList' && - Array.from(mutation.removedNodes).some((node) => instance.$el !== node && instance.$el.contains(node)); - const shouldUpdateAfterNodeAddition = - mutation.type === 'childList' && - Array.from(mutation.addedNodes).some((node) => instance.$el.contains(node)); - - // @todo removedNode has no parent - console.log(instance.$id, { - shouldUpdateAfterAttributeChange, - shouldUpdateAfterNodeRemoval, - shouldUpdateAfterNodeAddition, - }); - if ( - shouldUpdateAfterAttributeChange || - shouldUpdateAfterNodeRemoval || - shouldUpdateAfterNodeAddition - ) { - updates.set(instance, () => instance.$update()); + const updates = new Map any>(); + const terminations = new Map any>(); + const instances = getInstances(); + + for (const mutation of props.mutations) { + // Update parent instance when an instance node has been removed from the DOM. + for (const node of mutation.removedNodes) { + if (node.nodeType !== Node.ELEMENT_NODE) continue; + + for (const instance of instances) { + if (updates.has(instance)) continue; + if (instance.$el.isConnected) continue; + + if (node.contains(instance.$el)) { + updates.set(instance.$parent, () => instance.$parent.$update()); + } + } + } + + // Update instances whose child DOM has changed + for (const node of mutation.addedNodes) { + if (node.nodeType !== Node.ELEMENT_NODE) continue; + if (updates.has(node)) continue; + + for (const instance of instances) { + if (instance.$el.contains(node)) { + updates.set(node, () => instance.$update()); + } + } + } + + // Update instances when a data-component attribute has changed + if (mutation.type === 'attributes') { + for (const instance of instances) { + if (updates.has(instance)) continue; + + if (instance.$el.contains(mutation.target)) { + updates.set(instance, () => instance.$update()); + } } } } - console.log(updates); for (const update of updates.values()) { update(); } @@ -417,7 +430,6 @@ export class Base { await Promise.all([ // Undo addToQueue(() => this.__refs.unregisterAll()), - addToQueue(() => this.__children.unregisterAll()), addToQueue(() => this.__services.disableAll()), // Redo addToQueue(() => this.__children.registerAll()), diff --git a/packages/js-toolkit/Base/managers/ChildrenManager.ts b/packages/js-toolkit/Base/managers/ChildrenManager.ts index 449b53299..6df9c6446 100644 --- a/packages/js-toolkit/Base/managers/ChildrenManager.ts +++ b/packages/js-toolkit/Base/managers/ChildrenManager.ts @@ -25,27 +25,36 @@ export class ChildrenManager< return Object.keys(this.props); } - async unregisterAll() { - for (const instances of Object.values(this.props)) { - for (let instance of instances) { - if (instance instanceof Promise) { - instance = await instance; - } - - if (!instance.$el.isConnected) { - instance.$terminate(); - } - } - } - } - /** * Register instances of all children components. */ async registerAll() { + const previousProps = { ...this.__children }; + for (const [name, component] of Object.entries(this.__config.components)) { this.__register(name, component); } + + let childrenToDestroy = new Set(); + for (const [previousName, previousChildren] of Object.entries(previousProps)) { + if (!this.props[previousName]) { + childrenToDestroy = childrenToDestroy.union(new Set(previousChildren)); + continue; + } + + const previousChildrenSet = new Set(previousChildren); + const childrenSet = new Set(this.props[previousName]); + const diff = previousChildrenSet.difference(childrenSet); + childrenToDestroy = childrenToDestroy.union(diff); + } + + for (const child of childrenToDestroy) { + if (child instanceof Promise) { + child.then(instance => instance.$destroy()) + } else { + child.$destroy(); + } + } } /** @@ -94,6 +103,8 @@ export class ChildrenManager< await instance[hook](); } + __children: T = {} as T; + /** * Register instance of a child component. * @@ -105,6 +116,11 @@ export class ChildrenManager< */ __register(name: string, component: BaseConstructor | BaseAsyncConstructor) { Object.defineProperty(this.props, name, { + enumerable: true, + configurable: true, + get: () => this.__getValue(name, component), + }); + Object.defineProperty(this.__children, name, { enumerable: true, configurable: true, value: this.__getValue(name, component),