From e616ee966cbee186a76e7a089d7a6a7bd60f7300 Mon Sep 17 00:00:00 2001 From: Dmitrii Boikov Date: Fri, 26 Jul 2024 14:00:16 +0300 Subject: [PATCH] fix #1275 (#1308) * fix #1275 * chore: stylish fixes * chore: fixed naming * fix: fixes after refactoring * chore: added new tests * Sync test for teleports (#1356) * tests(p-v4-components-demo): accept search param for goto * chore(i-static-page): remove console.log * fix(core/component/watch): fix flush post watchers * tests(i-block): fix sync className test for teleport --------- Co-authored-by: kobezzza Co-authored-by: Artem Shinkaruk <46344555+shining-mind@users.noreply.github.com> --- CHANGELOG.md | 10 +++ components-lock.json | 76 ++++++++++++++++- .../base/b-dynamic-page/CHANGELOG.md | 6 ++ src/components/base/b-dynamic-page/README.md | 2 +- .../base/b-dynamic-page/b-dynamic-page.ts | 4 +- src/components/friends/block/class.ts | 63 -------------- .../p-v4-components-demo.ss | 3 + .../p-v4-components-demo.ts | 11 ++- .../p-v4-components-demo/test/api/page.ts | 12 ++- src/components/super/i-block/base/README.md | 8 +- src/components/super/i-block/i-block.ts | 82 ++++++++++++++++++- .../i-block/modules/activation/README.md | 8 +- .../super/i-block/modules/activation/index.ts | 44 +++++----- .../b-super-i-block-deactivation-dummy.ss | 18 ++++ .../b-super-i-block-deactivation-dummy.styl | 15 ++++ .../b-super-i-block-deactivation-dummy.ts | 19 +++++ .../index.js | 14 ++++ .../b-super-i-block-teleport-dummy.ss | 15 ++++ .../b-super-i-block-teleport-dummy.styl | 15 ++++ .../b-super-i-block-teleport-dummy.ts | 18 ++++ .../b-super-i-block-teleport-dummy/index.js | 11 +++ .../super/i-block/test/unit/deactivation.ts | 57 +++++++++++++ .../super/i-block/test/unit/teleports.ts | 82 +++++++++++++++++++ .../super/i-static-page/i-static-page.ts | 2 +- src/core/component/init/CHANGELOG.md | 7 ++ src/core/component/init/states/created.ts | 4 +- src/core/component/watch/create.ts | 3 + 27 files changed, 505 insertions(+), 104 deletions(-) create mode 100644 src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ss create mode 100644 src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.styl create mode 100644 src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ts create mode 100644 src/components/super/i-block/test/b-super-i-block-deactivation-dummy/index.js create mode 100644 src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ss create mode 100644 src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.styl create mode 100644 src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ts create mode 100644 src/components/super/i-block/test/b-super-i-block-teleport-dummy/index.js create mode 100644 src/components/super/i-block/test/unit/deactivation.ts create mode 100644 src/components/super/i-block/test/unit/teleports.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d727dcfe4e..9bbaa2ee7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,16 @@ Changelog _Note: Gaps between patch versions are faulty, broken or test releases._ +## v4.0.0-beta.?? (2024-07-??) + +#### :bug: Bug Fix + +* `core/component/init`: + * Fixed a typo in the event name `hookChange` which is responsible for processing activation and deactivation in the component + * Amended the deactivation sequence within the component to ensure that children are deactivated first + +* Fixed an issue to prevent the `hookChange` event from bubbling up `bDynamicPage` + ## v4.0.0-beta.114 (2024-07-24) #### :house: Internal diff --git a/components-lock.json b/components-lock.json index 7d6f7b0245..b00962c838 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "23d17036073396142d81f3c91143966489f3337fbfb97bd1a3362b9cfb87c889", + "hash": "5ccda0c56681f6df98bcce9b5a48a424b09f104f6e403b75c7dd9ac02a30e93d", "data": { "%data": "%data:Map", "%data:Map": [ @@ -1387,6 +1387,44 @@ "etpl": null } ], + [ + "b-super-i-block-deactivation-dummy", + { + "index": "src/components/super/i-block/test/b-super-i-block-deactivation-dummy/index.js", + "declaration": { + "name": "b-super-i-block-deactivation-dummy", + "parent": "i-block", + "dependencies": [ + "b-button", + "b-bottom-slide" + ], + "libs": [] + }, + "name": "b-super-i-block-deactivation-dummy", + "parent": "i-block", + "dependencies": [ + "b-button", + "b-bottom-slide" + ], + "libs": [], + "resolvedLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "resolvedOwnLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "type": "block", + "mixin": false, + "logic": "src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ts", + "styles": [ + "src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.styl" + ], + "tpl": "src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ss", + "etpl": null + } + ], [ "b-super-i-block-decorators-dummy", { @@ -1515,6 +1553,42 @@ "etpl": null } ], + [ + "b-super-i-block-teleport-dummy", + { + "index": "src/components/super/i-block/test/b-super-i-block-teleport-dummy/index.js", + "declaration": { + "name": "b-super-i-block-teleport-dummy", + "parent": "i-block", + "dependencies": [ + "b-bottom-slide" + ], + "libs": [] + }, + "name": "b-super-i-block-teleport-dummy", + "parent": "i-block", + "dependencies": [ + "b-bottom-slide" + ], + "libs": [], + "resolvedLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "resolvedOwnLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "type": "block", + "mixin": false, + "logic": "src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ts", + "styles": [ + "src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.styl" + ], + "tpl": "src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ss", + "etpl": null + } + ], [ "b-super-i-block-watch-dummy", { diff --git a/src/components/base/b-dynamic-page/CHANGELOG.md b/src/components/base/b-dynamic-page/CHANGELOG.md index 9b08232c2d..0d0441f68c 100644 --- a/src/components/base/b-dynamic-page/CHANGELOG.md +++ b/src/components/base/b-dynamic-page/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-07-??) + +#### :bug: Bug Fix + +* Fixed an issue to prevent the `hookChange` event from bubbling up + ## v4.0.0-beta.112 (2024-07-22) #### :bug: Bug Fix diff --git a/src/components/base/b-dynamic-page/README.md b/src/components/base/b-dynamic-page/README.md index e06eb0a43d..bca237a294 100644 --- a/src/components/base/b-dynamic-page/README.md +++ b/src/components/base/b-dynamic-page/README.md @@ -182,7 +182,7 @@ to its inner page component. ## Catching Events of the Inner Page Component -By default, `bDynamicPage` dispatches all events from the inner page component. +By default, `bDynamicPage` dispatches all events from the inner page component, except `hookChange` and `hook:*` events. ``` /// `initLoad` is caught from the inner page component diff --git a/src/components/base/b-dynamic-page/b-dynamic-page.ts b/src/components/base/b-dynamic-page/b-dynamic-page.ts index 74ed32d940..22446fd38d 100644 --- a/src/components/base/b-dynamic-page/b-dynamic-page.ts +++ b/src/components/base/b-dynamic-page/b-dynamic-page.ts @@ -301,8 +301,8 @@ export default class bDynamicPage extends iDynamicPage { return component.reload(params); } - override canSelfDispatchEvent(_: string): boolean { - return true; + override canSelfDispatchEvent(event: string): boolean { + return !/^hook(?::\w+(-\w+)*|-change)$/.test(event.dasherize()); } /** diff --git a/src/components/friends/block/class.ts b/src/components/friends/block/class.ts index a3310d9de5..7e179de937 100644 --- a/src/components/friends/block/class.ts +++ b/src/components/friends/block/class.ts @@ -81,69 +81,6 @@ class Block extends Friend { Object.entries(component.mods).forEach(([name, val]) => { this.setMod(name, val, 'initSetMod'); }); - - const { - node, - ctx: { - $el: originalNode, - $async: $a - } - } = this; - - const - mountedAttrs = new Set(), - mountedAttrsGroup = {group: 'mountedAttrs'}; - - if (originalNode != null && node != null && originalNode !== node) { - Object.defineProperty(this.ctx, '$el', { - configurable: true, - get: () => node - }); - - node.component = component; - mountAttrs(this.ctx.$attrs); - - this.ctx.watch('$attrs', {deep: true}, (attrs) => { - $a.terminateWorker(mountedAttrsGroup); - mountAttrs(attrs); - }); - } - - function mountAttrs(attrs: Dictionary) { - if (node == null || originalNode == null) { - return; - } - - Object.entries(attrs).forEach(([name, attr]) => { - if (attr == null) { - return; - } - - if (name === 'class') { - attr.split(/\s+/).forEach((val) => { - node.classList.add(val); - mountedAttrs.add(`class.${val}`); - }); - - } else if (originalNode.hasAttribute(name)) { - node.setAttribute(name, attr); - mountedAttrs.add(name); - } - }); - - $a.worker(() => { - mountedAttrs.forEach((attr) => { - if (attr.startsWith('class.')) { - node.classList.remove(attr.split('.')[1]); - - } else { - node.removeAttribute(attr); - } - }); - - mountedAttrs.clear(); - }, mountedAttrsGroup); - } } } diff --git a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss index 2988d4c196..bbac322bfa 100644 --- a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss +++ b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ss @@ -11,3 +11,6 @@ - include 'components/super/i-static-page/i-static-page.component.ss'|b as placeholder - template index() extends ['i-static-page.component'].index + - block body + < template v-if = stage === 'teleports' + < b-bottom-slide diff --git a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts index 24d5acd134..1f7884f7b6 100644 --- a/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts +++ b/src/components/pages/p-v4-components-demo/p-v4-components-demo.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import iStaticPage, { component, prop, field, system } from 'components/super/i-static-page/i-static-page'; +import iStaticPage, { component, prop, field, system, hook } from 'components/super/i-static-page/i-static-page'; import VDOM, * as VDOMAPI from 'components/friends/vdom'; export * from 'components/super/i-static-page/i-static-page'; @@ -42,4 +42,13 @@ export default class pV4ComponentsDemo extends iStaticPage { */ @field() someField: unknown = 'foo'; + + @hook('beforeCreate') + setStageFromLocation(): void { + const matches = /stage=(.*)/.exec(globalThis.location.search); + + if (matches != null) { + this.stage = decodeURIComponent(matches[1]); + } + } } diff --git a/src/components/pages/p-v4-components-demo/test/api/page.ts b/src/components/pages/p-v4-components-demo/test/api/page.ts index 5195f4b72e..b13a358dc2 100644 --- a/src/components/pages/p-v4-components-demo/test/api/page.ts +++ b/src/components/pages/p-v4-components-demo/test/api/page.ts @@ -39,22 +39,30 @@ export default class DemoPage { return ''; } + /** + * Name of the HTML file + */ + protected pageFileName: string; + /** * @param page + * @param baseUrl */ constructor(page: Page, baseUrl: string) { this.page = page; this.baseUrl = baseUrl; + this.pageFileName = build.demoPage(); } /** * Opens a demo page + * @param [query] - query parameters for the URL, i.e. `a=1&b=1` */ - async goto(): Promise { + async goto(query: string = ''): Promise { const root = this.page.locator('#root-component'); - await this.page.goto(concatURLs(this.baseUrl, `${build.demoPage()}.html`), {waitUntil: 'networkidle'}); + await this.page.goto(concatURLs(this.baseUrl, `${this.pageFileName}.html`) + (query.length > 0 ? `?${query}` : ''), {waitUntil: 'networkidle'}); await root.waitFor({state: 'attached'}); this.component = await root.evaluateHandle((ctx) => ctx.component); diff --git a/src/components/super/i-block/base/README.md b/src/components/super/i-block/base/README.md index e6c0f914d1..5e1787c94f 100644 --- a/src/components/super/i-block/base/README.md +++ b/src/components/super/i-block/base/README.md @@ -264,16 +264,16 @@ export default class bExample extends iBlock { Activates the component. The deactivated component won't load data from its providers during initializing. -Basically, you don't need to think about the component activation, -because it's automatically synchronized with `keep-alive` or the component prop. +Essentially, you don't need to worry about component activation, +as it automatically synchronizes with the `keep-alive` mode or a specific component prop. #### deactivate Deactivates the component. The deactivated component won't load data from its providers during initializing. -Basically, you don't need to think about the component activation, -because it's automatically synchronized with `keep-alive` or the component prop. +Essentially, you don't need to worry about component activation, +as it automatically synchronizes with the `keep-alive` mode or a specific component prop. #### watch diff --git a/src/components/super/i-block/i-block.ts b/src/components/super/i-block/i-block.ts index c6cf42e7a2..bdaa604378 100644 --- a/src/components/super/i-block/i-block.ts +++ b/src/components/super/i-block/i-block.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import { component, UnsafeGetter } from 'core/component'; +import { component, hook, watch, UnsafeGetter } from 'core/component'; import type { Classes } from 'components/friends/provide'; import type { ModVal, ModsDecl, ModsProp, ModsDict } from 'components/super/i-block/modules/mods'; @@ -24,6 +24,8 @@ import('components/super/i-block/test/b-super-i-block-dummy'); import('components/super/i-block/test/b-super-i-block-watch-dummy'); import('components/super/i-block/test/b-super-i-block-lfc-dummy'); import('components/super/i-block/test/b-super-i-block-destructor-dummy'); +import('components/super/i-block/test/b-super-i-block-deactivation-dummy'); +import('components/super/i-block/test/b-super-i-block-teleport-dummy'); //#endif export * from 'core/component'; @@ -82,4 +84,82 @@ export default abstract class iBlock extends iBlockProviders { isComponent(obj: unknown, constructor?: {new(): T} | Function): obj is T { return Object.isTruly(obj) && (obj).instance instanceof (constructor ?? iBlock); } + + /** + * Handler: fixes the issue where the teleported component + * and its DOM nodes were rendered before the teleport container was ready + */ + @watch({ + path: 'r.shouldMountTeleports', + flush: 'post' + }) + + @hook('before:mounted') + protected onMountTeleports(): void { + const getNode = () => this.$refs[this.$resolveRef('$el')] ?? this.$el; + + const { + $el: originalNode, + $async: $a + } = this; + + const + node = getNode(), + mountedAttrs = new Set(), + mountedAttrsGroup = {group: 'mountedAttrs'}; + + if (originalNode != null && node != null && originalNode !== node) { + // Fix the DOM element link to the component + originalNode.component = this; + + // Fix the teleported DOM element link to the component + node.component = this; + + Object.defineProperty(this.unsafe, '$el', { + configurable: true, + get: () => node + }); + + mountAttrs(this.$attrs); + this.watch('$attrs', {deep: true}, mountAttrs); + } + + function mountAttrs(attrs: Dictionary) { + $a.terminateWorker(mountedAttrsGroup); + + if (node == null || originalNode == null) { + return; + } + + Object.entries(attrs).forEach(([name, attr]) => { + if (attr == null) { + return; + } + + if (name === 'class') { + attr.split(/\s+/).forEach((val) => { + node.classList.add(val); + mountedAttrs.add(`class.${val}`); + }); + + } else if (originalNode.hasAttribute(name)) { + node.setAttribute(name, attr); + mountedAttrs.add(name); + } + }); + + $a.worker(() => { + mountedAttrs.forEach((attr) => { + if (attr.startsWith('class.')) { + node.classList.remove(attr.split('.')[1]); + + } else { + node.removeAttribute(attr); + } + }); + + mountedAttrs.clear(); + }, mountedAttrsGroup); + } + } } diff --git a/src/components/super/i-block/modules/activation/README.md b/src/components/super/i-block/modules/activation/README.md index 29c26f6e0f..911e6eb41a 100644 --- a/src/components/super/i-block/modules/activation/README.md +++ b/src/components/super/i-block/modules/activation/README.md @@ -124,13 +124,13 @@ export default class bExample extends iBlock { Activates the component. A deactivated component won't load data from providers on initializing. -Basically, you don't need to think about component activation, -because it automatically synchronizes with the `keep-alive` mode or a special component prop. +Essentially, you don't need to worry about component activation, +as it automatically synchronizes with the `keep-alive` mode or a specific component prop. ### deactivate Deactivates the component. A deactivated component won't load data from providers on initializing. -Basically, you don't need to think about component activation, -because it automatically synchronizes with the `keep-alive` mode or a special component prop. +Essentially, you don't need to worry about component activation, +as it automatically synchronizes with the `keep-alive` mode or a specific component prop. diff --git a/src/components/super/i-block/modules/activation/index.ts b/src/components/super/i-block/modules/activation/index.ts index f8637dbc46..9ebdb74e95 100644 --- a/src/components/super/i-block/modules/activation/index.ts +++ b/src/components/super/i-block/modules/activation/index.ts @@ -40,8 +40,8 @@ const * Activates the component. * A deactivated component won't load data from providers on initializing. * - * Basically, you don't need to think about component activation, - * because it automatically synchronizes with the `keep-alive` mode or a special component prop. + * Essentially, you don't need to worry about component activation, + * as it automatically synchronizes with the `keep-alive` mode or a specific component prop. * * @param component * @param [force] - if true, then the component will be forced to be activated, even if it is already activated @@ -117,33 +117,36 @@ export function activate(component: iBlock, force?: boolean): void { * Deactivates the component. * A deactivated component won't load data from providers on initializing. * - * Basically, you don't need to think about component activation, - * because it automatically synchronizes with the `keep-alive` mode or a special component prop. + * Essentially, you don't need to worry about component activation, + * as it automatically synchronizes with the `keep-alive` mode or a specific component prop. * * @param component */ export function deactivate(component: iBlock): void { - const - {unsafe} = component; + const {unsafe} = component; if (unsafe.lfc.isBeforeCreate()) { return; } - if (unsafe.isActivated) { - // It's important to deactivate the component ASAP to prevent any unexpected re-renders - // because the state of the component might change during the deactivation process - onDeactivated(component); - runHook('deactivated', component).then(() => { - callMethodFromComponent(component, 'deactivated'); - }).catch(stderr); - } - unsafe.$children.forEach((component) => { if (!component.isFunctional) { component.unsafe.deactivate(); } }); + + if (unsafe.isActivated) { + runHook('deactivated', component).then(() => { + callMethodFromComponent(component, 'deactivated'); + }).catch(stderr); + + // It's important to deactivate the component ASAP to prevent any unexpected re-renders. + // The state of the component might change during the deactivation process, + // but it is crucial to call runHook before deactivation. + // This ensures that the onHookChange event listeners are not muted + // and that child dynamic components receive the deactivation signal. + onDeactivated(component); + } } /** @@ -152,9 +155,8 @@ export function deactivate(component: iBlock): void { * @param component * @param [force] - if true, then the component will be forced to be activated, even if it is already activated */ -export function onActivated(component: iBlock, force?: boolean): void { - const - {unsafe} = component; +export function onActivated(component: iBlock, force: boolean = false): void { + const {unsafe} = component; const cantActivate = unsafe.isActivated || @@ -210,8 +212,7 @@ export function onActivated(component: iBlock, force?: boolean): void { * @param component */ export function onDeactivated(component: iBlock): void { - const - {unsafe} = component; + const {unsafe} = component; const async = [ unsafe.$async, @@ -224,8 +225,7 @@ export function onDeactivated(component: iBlock): void { return; } - const - fn = $a[`mute-${asyncNames[key]}`.camelize(false)]; + const fn = $a[`mute-${asyncNames[key]}`.camelize(false)]; if (Object.isFunction(fn)) { fn.call($a); diff --git a/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ss b/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ss new file mode 100644 index 0000000000..708af4850c --- /dev/null +++ b/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ss @@ -0,0 +1,18 @@ +- namespace [%fileName%] + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +- include 'components/super/i-block'|b as placeholder + +- template index() extends ['i-block'].index + - block body + < b-button.target v-func = false | ref = button1 + + < b-bottom-slide + < b-button.target v-func = false | ref = button2 diff --git a/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.styl b/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.styl new file mode 100644 index 0000000000..b18cd628f6 --- /dev/null +++ b/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.styl @@ -0,0 +1,15 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +@import "components/super/i-block/i-block.styl" + +$p = { + +} + +b-super-i-block-deactivation-dummy extends i-block diff --git a/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ts b/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ts new file mode 100644 index 0000000000..6a171f5369 --- /dev/null +++ b/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy.ts @@ -0,0 +1,19 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bButton from 'components/form/b-button/b-button'; + +import iBlock, { component } from 'components/super/i-block/i-block'; + +@component() +export default class bSuperIBlockDeactivationDummy extends iBlock { + protected override $refs!: iBlock['$refs'] & { + button1: bButton; + button2: bButton; + }; +} diff --git a/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/index.js b/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/index.js new file mode 100644 index 0000000000..4a042054f3 --- /dev/null +++ b/src/components/super/i-block/test/b-super-i-block-deactivation-dummy/index.js @@ -0,0 +1,14 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +package('b-super-i-block-deactivation-dummy') + .extends('i-block') + .dependencies( + 'b-button', + 'b-bottom-slide' + ); diff --git a/src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ss b/src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ss new file mode 100644 index 0000000000..74c7762a5d --- /dev/null +++ b/src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ss @@ -0,0 +1,15 @@ +- namespace [%fileName%] + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +- include 'components/super/i-block'|b as placeholder + +- template index() extends ['i-block'].index + - block body + < b-bottom-slide ref = component diff --git a/src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.styl b/src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.styl new file mode 100644 index 0000000000..ef420a9bbf --- /dev/null +++ b/src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.styl @@ -0,0 +1,15 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +@import "components/super/i-block/i-block.styl" + +$p = { + +} + +b-super-i-block-teleport-dummy extends i-block diff --git a/src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ts b/src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ts new file mode 100644 index 0000000000..6f2de9f993 --- /dev/null +++ b/src/components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy.ts @@ -0,0 +1,18 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type bButtonSlide from 'components/base/b-bottom-slide/b-bottom-slide'; + +import iBlock, { component } from 'components/super/i-block/i-block'; + +@component() +export default class bSuperIBlockTeleportDummy extends iBlock { + protected override $refs!: iBlock['$refs'] & { + component: bButtonSlide; + }; +} diff --git a/src/components/super/i-block/test/b-super-i-block-teleport-dummy/index.js b/src/components/super/i-block/test/b-super-i-block-teleport-dummy/index.js new file mode 100644 index 0000000000..9fc30d1ce8 --- /dev/null +++ b/src/components/super/i-block/test/b-super-i-block-teleport-dummy/index.js @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +package('b-super-i-block-teleport-dummy') + .extends('i-block') + .dependencies('b-bottom-slide'); diff --git a/src/components/super/i-block/test/unit/deactivation.ts b/src/components/super/i-block/test/unit/deactivation.ts new file mode 100644 index 0000000000..7390dba245 --- /dev/null +++ b/src/components/super/i-block/test/unit/deactivation.ts @@ -0,0 +1,57 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { JSHandle } from 'playwright'; + +import test from 'tests/config/unit/test'; +import { Component } from 'tests/helpers'; + +import type { ComponentElement } from 'core/component'; +import type bSuperIBlockDeactivationDummy from 'components/super/i-block/test/b-super-i-block-deactivation-dummy/b-super-i-block-deactivation-dummy'; + +test.describe(' component deactivation', () => { + let target: JSHandle; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + target = await Component.createComponent(page, 'b-super-i-block-deactivation-dummy'); + }); + + test.describe('when the parent component is deactivated', () => { + test('children should also be deactivated', async () => { + const state = await target.evaluate((ctx) => { + ctx.deactivate(); + return ctx.unsafe.$refs.button1.hook; + }); + + test.expect(state).toBe('deactivated'); + }); + + test('children of a teleported component should be deactivated', async () => { + const state = await target.evaluate((ctx) => { + ctx.deactivate(); + return ctx.unsafe.$refs.button2.hook; + }); + + test.expect(state).toBe('deactivated'); + }); + + test('dynamic children of a component should be deactivated', async () => { + const result = await target.evaluate((ctx) => { + const vnode = ctx.vdom.create('b-radio-button'); + + const node: ComponentElement = Object.cast(ctx.vdom.render(vnode)); + ctx.deactivate(); + + return node.component?.hook; + }); + + test.expect(result).toBe('deactivated'); + }); + }); +}); diff --git a/src/components/super/i-block/test/unit/teleports.ts b/src/components/super/i-block/test/unit/teleports.ts new file mode 100644 index 0000000000..13c30880cf --- /dev/null +++ b/src/components/super/i-block/test/unit/teleports.ts @@ -0,0 +1,82 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { JSHandle } from 'playwright'; + +import test from 'tests/config/unit/test'; +import { Component } from 'tests/helpers'; +import { toQueryString } from 'core/url'; + +import type bBottomSlide from 'components/base/b-bottom-slide/b-bottom-slide'; +import type bSuperIBlockTeleportDummy from 'components/super/i-block/test/b-super-i-block-teleport-dummy/b-super-i-block-teleport-dummy'; + +test.describe(' using the root teleport', () => { + // NOTE: Component.createComponent uses async render + test.describe('using async render', () => { + let target: JSHandle; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(); + target = await Component.createComponent(page, 'b-super-i-block-teleport-dummy'); + }); + + test('the reference `$el` should be equal to the reference `$refs.$el`', async () => { + const res = await target.evaluate((ctx) => { + const {component: {unsafe: component}} = ctx.unsafe.$refs; + return component.$refs[component.$resolveRef('$el')] === component.$el; + }); + + test.expect(res).toBe(true); + }); + + test('the root node should have a reference to a component instance', async () => { + const componentName = await target.evaluate((ctx) => + ctx.unsafe.$refs.component.$el!.component!.componentName); + + test.expect(componentName).toBe('b-bottom-slide'); + }); + + test('the node must have the correct class name', async () => { + const attrs = await target.evaluate((ctx) => + ctx.unsafe.$refs.component.$el!.className); + + test.expect(attrs).toBe('i-block-helper u3b379f9a91182 b-bottom-slide b-bottom-slide_opened_false b-bottom-slide_stick_true b-bottom-slide_events_false b-bottom-slide_height-mode_full b-bottom-slide_visible_false b-bottom-slide_theme_light b-bottom-slide_hidden_true'); + }); + }); + + test.describe('using sync render', () => { + let target: JSHandle; + + test.beforeEach(async ({demoPage, page}) => { + await demoPage.goto(toQueryString({stage: 'teleports'})); + target = await Component.waitForComponentByQuery(page, '.b-bottom-slide'); + }); + + test('the reference `$el` should be equal to the reference `$refs.$el`', async () => { + const res = await target.evaluate( + (ctx) => ctx.unsafe.$refs[ctx.unsafe.$resolveRef('$el')] === ctx.unsafe.$el + ); + + test.expect(res).toBe(true); + }); + + test('the root node should have a reference to a component instance', async () => { + const componentName = await target.evaluate((ctx) => + ctx.unsafe.$el!.component!.componentName); + + test.expect(componentName).toBe('b-bottom-slide'); + }); + + test('the node must have the correct class name', async () => { + const attrs = await target.evaluate((ctx) => + ctx.unsafe.$el!.className); + + test.expect(attrs).toBe('i-block-helper ue3771dae8cf71 b-bottom-slide b-bottom-slide_opened_false b-bottom-slide_hidden_true b-bottom-slide_stick_true b-bottom-slide_events_false b-bottom-slide_height-mode_full b-bottom-slide_theme_light b-bottom-slide_visible_false'); + }); + }); +}); diff --git a/src/components/super/i-static-page/i-static-page.ts b/src/components/super/i-static-page/i-static-page.ts index 9631940387..59ad385269 100644 --- a/src/components/super/i-static-page/i-static-page.ts +++ b/src/components/super/i-static-page/i-static-page.ts @@ -342,7 +342,7 @@ export default abstract class iStaticPage extends iPage { id: 'teleports' })); - await this.async.nextTick(); + await this.nextTick(); this.shouldMountTeleports = true; } diff --git a/src/core/component/init/CHANGELOG.md b/src/core/component/init/CHANGELOG.md index 2c9227b042..feb8ad5151 100644 --- a/src/core/component/init/CHANGELOG.md +++ b/src/core/component/init/CHANGELOG.md @@ -9,6 +9,13 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-06-??) + +#### :bug: Bug Fix + +* Fixed a typo in the event name `hookChange` which is responsible for processing activation and deactivation in the component +* Amended the deactivation sequence within the component to ensure that children are deactivated first + ## v4.0.0-beta.91 (2024-04-19) #### :rocket: New Feature diff --git a/src/core/component/init/states/created.ts b/src/core/component/init/states/created.ts index b29ea30d89..3034506b75 100644 --- a/src/core/component/init/states/created.ts +++ b/src/core/component/init/states/created.ts @@ -94,8 +94,8 @@ export function createdState(component: ComponentInterface): void { onActivation(normalParent.hook); } - normalParent.$on('on-hook-change', onActivation); - $a.worker(() => normalParent.$off('on-hook-change', onActivation)); + normalParent.$on('onHookChange', onActivation); + $a.worker(() => normalParent.$off('onHookChange', onActivation)); } } diff --git a/src/core/component/watch/create.ts b/src/core/component/watch/create.ts index 0e5e2c0060..0911de470f 100644 --- a/src/core/component/watch/create.ts +++ b/src/core/component/watch/create.ts @@ -256,6 +256,9 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface return h(); }; + + } else if (flush === 'post') { + handler = (...args) => component.$nextTick().then(() => originalHandler.call(this, ...args)); } if (needImmediate) {