diff --git a/packages/demo/meta.config.js b/packages/demo/meta.config.js
index f329eb864..03ee68e3e 100644
--- a/packages/demo/meta.config.js
+++ b/packages/demo/meta.config.js
@@ -4,6 +4,9 @@ import { prototyping } from '@studiometa/webpack-config-preset-prototyping';
export default defineConfig({
presets: [prototyping({ ts: true })],
+ server(config) {
+ config.snippet = false;
+ },
webpack(config) {
config.resolve.alias = {
...config.resolve.alias,
diff --git a/packages/demo/src/js/app.ts b/packages/demo/src/js/app.ts
index b89a31323..660763c19 100644
--- a/packages/demo/src/js/app.ts
+++ b/packages/demo/src/js/app.ts
@@ -11,6 +11,7 @@ import {
BaseConfig,
withDrag,
withName,
+ getInstances,
} from '@studiometa/js-toolkit';
import { matrix } from '@studiometa/js-toolkit/utils';
import ScrollToDemo from './components/ScrollToDemo.js';
@@ -24,52 +25,6 @@ import ScrolledInViewOffset from './components/ScrolledInViewOffset.js';
import MediaQueryDemo from './components/MediaQueryDemo.js';
import PointerProps from './components/PointerProps.js';
-let numberOfTick = 0;
-let time = performance.now();
-let interval = setInterval(() => {
- const newTime = performance.now();
- numberOfTick += 1;
- console.log('#%d blocking time: %d ms', numberOfTick, newTime - time);
- time = newTime;
-
- if (numberOfTick > total * 2) {
- clearInterval(interval)
- }
-}, 0)
-
-let count = 0;
-const total = 66;
-
-function getDeepNestedComponentName(index) {
- return `TestDeepNested${index}`
-}
-
-function makeDeepNestedComponent(index) {
- return class extends withExtraConfig(Base, { name: getDeepNestedComponentName(index), components: {} }) {
- mounted() {
- // console.log(this.$id);
- }
- };
-}
-
-const TestDeepNested = makeDeepNestedComponent(0);
-let CurrentClass = TestDeepNested;
-while (count < total) {
- count += 1;
- const NewClass = makeDeepNestedComponent(count);
- // @ts-ignore
- CurrentClass.config.components[NewClass.config.name] = NewClass;
-
- CurrentClass = NewClass;
-}
-
-
-const TestManyInstance = class extends withExtraConfig(Base, { name: 'TestManyInstance', debug: false }) {
- mounted() {
- // console.log(this.$id);
- }
-};
-
/**
* App class.
*/
@@ -98,8 +53,8 @@ class App extends Base {
AnimateTestMultiple,
ResponsiveOptions,
ScrolledInViewOffset,
- TestDeepNested,
- TestManyInstance,
+ // TestDeepNested,
+ // TestManyInstance,
Accordion: (app) =>
importWhenVisible(
async () => {
@@ -183,6 +138,7 @@ class App extends Base {
* @inheritdoc
*/
mounted() {
+ window.APP = this;
this.$log('Mounted 🎉');
}
@@ -198,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 71bcddf07..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,16 +12,27 @@ export default class ParentNativeEvent extends Base {
static config = {
name: 'ParentNativeEvent',
log: 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 e219359f4..61007699d 100644
--- a/packages/js-toolkit/Base/Base.ts
+++ b/packages/js-toolkit/Base/Base.ts
@@ -1,10 +1,10 @@
-/* eslint-disable no-use-before-define */
import {
getComponentElements,
getEventTarget,
addToQueue,
addInstance,
deleteInstance,
+ getInstances,
} from './utils.js';
import {
ChildrenManager,
@@ -13,7 +13,9 @@ import {
EventsManager,
OptionsManager,
} from './managers/index.js';
+import { useMutation } from '../services/MutationService.js';
import { noop, isDev, isFunction, isArray } from '../utils/index.js';
+import { features } from './features.js';
let id = 0;
@@ -74,6 +76,8 @@ export class Base
{
*/
static readonly $isBase = true as const;
+ static __mutationSymbol = Symbol('mutation');
+
/**
* The instance parent.
*/
@@ -312,6 +316,68 @@ export class Base {
this[`__${service.toLowerCase()}`] = new this.__managers[`${service}Manager`](this);
}
+ const service = useMutation(document.querySelector('main'), {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: [features.get('attributes').component],
+ });
+ const key = this.constructor.__mutationSymbol;
+ if (!service.has(key)) {
+ service.add(key, (props) => {
+ const actions = 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 (actions.has(instance.$parent ?? instance)) continue;
+ if (instance.$el.isConnected) continue;
+ if (!node.contains(instance.$el)) continue;
+
+ actions.set(instance.$parent ?? instance, () => {
+ if (instance.$parent) {
+ instance.$update();
+ } else {
+ instance.$destroy();
+ }
+ });
+ }
+ }
+
+ // Update instances whose child DOM has changed
+ for (const node of mutation.addedNodes) {
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
+ if (actions.has(node)) continue;
+
+ for (const instance of instances) {
+ if (instance.$el.contains(node)) {
+ actions.set(node, () => instance.$update());
+ }
+ }
+ }
+
+ // Update instances when a data-component attribute has changed
+ if (mutation.type === 'attributes') {
+ for (const instance of instances) {
+ if (actions.has(instance)) continue;
+
+ if (instance.$el.contains(mutation.target)) {
+ actions.set(instance, () => instance.$update());
+ }
+ }
+ }
+ }
+
+ for (const update of actions.values()) {
+ update();
+ }
+ });
+ }
+
this.$on('mounted', () => {
addInstance(this);
});
diff --git a/packages/js-toolkit/Base/managers/ChildrenManager.ts b/packages/js-toolkit/Base/managers/ChildrenManager.ts
index 8481de8bf..c9cca9110 100644
--- a/packages/js-toolkit/Base/managers/ChildrenManager.ts
+++ b/packages/js-toolkit/Base/managers/ChildrenManager.ts
@@ -5,7 +5,9 @@ import { getComponentElements, addToQueue } from '../utils.js';
/**
* Children manager.
*/
-export class ChildrenManager extends AbstractManager {
+export class ChildrenManager<
+ T extends Record[]>,
+> extends AbstractManager {
/**
* Store async component promises to avoid calling them multiple times and
* waiting for them when they are already resolved.
@@ -27,9 +29,32 @@ export class ChildrenManager extends AbstractManager {
* 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();
+ }
+ }
}
/**
@@ -78,6 +103,8 @@ export class ChildrenManager extends AbstractManager {
await instance[hook]();
}
+ __children: T = {} as T;
+
/**
* Register instance of a child component.
*
@@ -114,6 +141,12 @@ export class ChildrenManager extends AbstractManager {
}
}
+ Object.defineProperty(this.__children, name, {
+ enumerable: true,
+ configurable: true,
+ value: children,
+ });
+
return children;
}
diff --git a/packages/js-toolkit/helpers/createApp.ts b/packages/js-toolkit/helpers/createApp.ts
index dc855448e..588afbe7b 100644
--- a/packages/js-toolkit/helpers/createApp.ts
+++ b/packages/js-toolkit/helpers/createApp.ts
@@ -1,8 +1,6 @@
import type { Base, BaseConstructor, BaseProps } from '../Base/index.js';
import type { Features } from '../Base/features.js';
-import { getInstances } from '../Base/index.js';
import { features } from '../Base/features.js';
-import { useMutation } from '../services/index.js';
import { isBoolean, isObject, isString } from '../utils/index.js';
export type CreateAppOptions = Partial & {
@@ -42,25 +40,6 @@ export function createApp, T extends BaseProps =
features.set('attributes', attributes);
}
- // Terminate components whose root element has been removed from the DOM
- const service = useMutation(document.documentElement, { childList: true, subtree: true });
- const symbol = Symbol('createApp');
- service.add(symbol, (props) => {
- for (const mutation of props.mutations) {
- if (mutation.type === 'childList') {
- for (const node of mutation.removedNodes) {
- if (!node.isConnected) {
- for (const instance of getInstances()) {
- if (node === instance.$el || node.contains(instance.$el)) {
- instance.$terminate();
- }
- }
- }
- }
- }
- }
- });
-
async function init() {
app = new App(root) as S & Base;
await app.$mount();