From 71a6336b74ca5ea876f7b9f5856a7ed9c8820b66 Mon Sep 17 00:00:00 2001 From: chyizheng Date: Mon, 24 Jul 2023 20:32:45 +0800 Subject: [PATCH] feat: fix issues for #61 + Rename `ShadowRoot.calcContainingSlot` to `ShadowRoot.getContainingSlot` + Add comments for shadowRoot iterate methods + Rename `ShadowRoot.isAttached` to `ShadowRoot.isConnected` + Change optional `DoubleLinkedList.next` and `DoubleLinkedList.prev` to non-optional + Add `_$` and `@internal` for serval private methods + Remove all GeneralComponentInstance + Add check for linked slot list when testing + Add test cases for slot management --- glass-easel/src/component.ts | 7 +- glass-easel/src/component_params.ts | 6 -- glass-easel/src/data_proxy.ts | 6 +- glass-easel/src/element.ts | 103 ++++++++++++----------- glass-easel/src/index.ts | 7 +- glass-easel/src/shadow_root.ts | 64 +++++++++----- glass-easel/src/template_engine.ts | 4 +- glass-easel/src/text_node.ts | 4 +- glass-easel/src/tmpl/index.ts | 8 +- glass-easel/src/tmpl/native_rendering.ts | 8 +- glass-easel/tests/base/match.ts | 67 ++++++++++++--- glass-easel/tests/tmpl/structure.test.ts | 59 ++++++++++--- 12 files changed, 223 insertions(+), 120 deletions(-) diff --git a/glass-easel/src/component.ts b/glass-easel/src/component.ts index ec36c96..645c0e8 100644 --- a/glass-easel/src/component.ts +++ b/glass-easel/src/component.ts @@ -19,7 +19,6 @@ import { DataList, PropertyList, MethodList, - GeneralComponentInstance, DataWithPropertyValues, RelationParams, TraitRelationParams, @@ -569,7 +568,7 @@ export class Component< backendElement.setAttribute('exparser:info-component-id', componentInstanceId) } comp._$idPrefix = options.idPrefixGenerator - ? options.idPrefixGenerator.call(comp as unknown as GeneralComponentInstance) + ? options.idPrefixGenerator.call(comp as unknown as GeneralComponent) : '' // combine initial data @@ -597,7 +596,7 @@ export class Component< // init relations const relation = (comp._$relation = new Relation( - comp as unknown as GeneralComponentInstance, + comp as unknown as GeneralComponent, relationDefinitionGroup, )) @@ -738,7 +737,7 @@ export class Component< initDone = true // init data - const tmplInst = template.createInstance(comp as unknown as GeneralComponentInstance) + const tmplInst = template.createInstance(comp as unknown as GeneralComponent) const shadowRoot = tmplInst.shadowRoot comp.shadowRoot = shadowRoot const dataGroup = new DataGroup( diff --git a/glass-easel/src/component_params.ts b/glass-easel/src/component_params.ts index 05bb0d7..78e75ce 100644 --- a/glass-easel/src/component_params.ts +++ b/glass-easel/src/component_params.ts @@ -409,9 +409,3 @@ export type ComponentParams< | { [fields: string]: ComponentMethod | string } options?: ComponentOptions } - -export type GeneralComponentInstance = ComponentInstance< - Record, - Record, - Record -> diff --git a/glass-easel/src/data_proxy.ts b/glass-easel/src/data_proxy.ts index 92b3bb4..77c01a8 100644 --- a/glass-easel/src/data_proxy.ts +++ b/glass-easel/src/data_proxy.ts @@ -5,13 +5,13 @@ import { DataPath, MultiPaths } from './data_path' import { DeepCopyKind } from './global_options' import { MutationObserverTarget } from './mutation_observer' import { - GeneralComponentInstance, DataList, PropertyList, MethodList, ComponentInstance, DataWithPropertyValues, } from './component_params' +import { GeneralComponent } from './component' // eslint-disable-next-line @typescript-eslint/unbound-method const hasOwnProperty = Object.prototype.hasOwnProperty @@ -132,7 +132,7 @@ const callObserver = < f, comp.getMethodCaller() as any, args, - (comp as unknown as GeneralComponentInstance) || undefined, + (comp as unknown as GeneralComponent) || undefined, ) } @@ -641,7 +641,7 @@ export class DataGroup< prop.observer, comp.getMethodCaller() as any, [newValue, oldValue], - comp as unknown as GeneralComponentInstance, + comp as unknown as GeneralComponent, ) } if (comp._$mutationObserverTarget) { diff --git a/glass-easel/src/element.ts b/glass-easel/src/element.ts index f2f319a..67ef72e 100644 --- a/glass-easel/src/element.ts +++ b/glass-easel/src/element.ts @@ -21,12 +21,7 @@ import { Observer, IntersectionStatus, } from './backend/mode' -import { - DataList, - PropertyList, - MethodList, - ComponentInstance, -} from './component_params' +import { DataList, PropertyList, MethodList, ComponentInstance } from './component_params' import { Node, GeneralBackendContext, @@ -68,8 +63,8 @@ type composedElement = composedBackend.Element | domlikeBackend.Element export type DoubleLinkedList = { value: T - prev?: DoubleLinkedList | undefined - next?: DoubleLinkedList | undefined + prev: DoubleLinkedList | null + next: DoubleLinkedList | null } /** @@ -97,9 +92,9 @@ export class Element implements NodeCast { /** @internal */ _$slotValues: { [name: string]: unknown } | null /** @internal */ - _$subtreeSlotStart: DoubleLinkedList | undefined + _$subtreeSlotStart: DoubleLinkedList | null /** @internal */ - _$subtreeSlotEnd: DoubleLinkedList | undefined + _$subtreeSlotEnd: DoubleLinkedList | null /** @internal */ _$inheritSlots: boolean /** @internal */ @@ -153,8 +148,8 @@ export class Element implements NodeCast { this._$slotName = null this._$slotElement = null this._$slotValues = null - this._$subtreeSlotStart = undefined - this._$subtreeSlotEnd = undefined + this._$subtreeSlotStart = null + this._$subtreeSlotEnd = null this._$inheritSlots = false this._$placeholderHandler = undefined this._$virtual = virtual @@ -218,6 +213,7 @@ export class Element implements NodeCast { const newSlot = String(x) const oldSlot = this._$nodeSlot if (oldSlot === newSlot) return + /* istanbul ignore if */ if (this._$inheritSlots) { triggerWarning('slots-inherited nodes do not support "slot" attribute.') return @@ -227,6 +223,7 @@ export class Element implements NodeCast { if (slotParentShadowRoot) { const slotMode = slotParentShadowRoot.getSlotMode() + /* istanbul ignore if */ if (slotMode === SlotMode.Dynamic) { triggerWarning( 'nodes inside dynamic slots should change binding slots through Element#setSlotElement.', @@ -234,6 +231,7 @@ export class Element implements NodeCast { return } + /* istanbul ignore if */ if (slotMode === SlotMode.Direct) { triggerWarning('nodes inside direct slots should not change slot name.') return @@ -1077,8 +1075,8 @@ export class Element implements NodeCast { */ private static updateSubtreeSlotsInsertion( node: Node, - slotStart: DoubleLinkedList | undefined, - slotEnd: DoubleLinkedList | undefined, + slotStart: DoubleLinkedList | null, + slotEnd: DoubleLinkedList | null, posIndex: number, move: boolean, ): void { @@ -1086,8 +1084,8 @@ export class Element implements NodeCast { let parent = node // find a correct position to insert slot into double-linked slot list - let insertSlotPrev: DoubleLinkedList | undefined - let insertSlotNext: DoubleLinkedList | undefined + let insertSlotPrev = null as DoubleLinkedList | null + let insertSlotNext = null as DoubleLinkedList | null const findFirstSlot = (parent: Node, posIndex: number): void => { if (parent._$subtreeSlotStart) { @@ -1153,8 +1151,8 @@ export class Element implements NodeCast { } const ownerShadowRoot = parent.ownerShadowRoot - if (ownerShadowRoot?.isAttached(parent)) { - ownerShadowRoot.applySlotsInsertion(slotStart, slotEnd, move) + if (ownerShadowRoot?.isConnected(parent)) { + ownerShadowRoot._$applySlotsInsertion(slotStart, slotEnd, move) } } @@ -1163,8 +1161,8 @@ export class Element implements NodeCast { */ private static updateSubtreeSlotsRemoval( node: Node, - slotStart: DoubleLinkedList | undefined, - slotEnd: DoubleLinkedList | undefined, + slotStart: DoubleLinkedList | null, + slotEnd: DoubleLinkedList | null, move: boolean, ): void { if (!slotStart || !slotEnd) return @@ -1175,11 +1173,11 @@ export class Element implements NodeCast { // remove from double linked list if (removeSlotBefore) { removeSlotBefore.next = removeSlotAfter - slotStart.prev = undefined + slotStart.prev = null } if (removeSlotAfter) { removeSlotAfter.prev = removeSlotBefore - slotEnd.next = undefined + slotEnd.next = null } // update parent subtree start/end @@ -1187,7 +1185,7 @@ export class Element implements NodeCast { let changed = false if (parent._$subtreeSlotStart === slotStart && parent._$subtreeSlotEnd === slotEnd) { - parent._$subtreeSlotStart = parent._$subtreeSlotEnd = undefined + parent._$subtreeSlotStart = parent._$subtreeSlotEnd = null changed = true } if (parent._$subtreeSlotStart === slotStart) { @@ -1204,8 +1202,8 @@ export class Element implements NodeCast { } const ownerShadowRoot = parent.ownerShadowRoot - if (ownerShadowRoot?.isAttached(parent)) { - ownerShadowRoot.applySlotsRemoval(slotStart, slotEnd, move) + if (ownerShadowRoot?.isConnected(parent)) { + ownerShadowRoot._$applySlotsRemoval(slotStart, slotEnd, move) } } @@ -1214,10 +1212,10 @@ export class Element implements NodeCast { */ private static updateSubtreeSlotsReplacement( node: Node, - slotStart: DoubleLinkedList | undefined, - slotEnd: DoubleLinkedList | undefined, - oldSlotStart: DoubleLinkedList | undefined, - oldSlotEnd: DoubleLinkedList | undefined, + slotStart: DoubleLinkedList | null, + slotEnd: DoubleLinkedList | null, + oldSlotStart: DoubleLinkedList | null, + oldSlotEnd: DoubleLinkedList | null, posIndex: number, move: boolean, ): void { @@ -1238,12 +1236,12 @@ export class Element implements NodeCast { if (removeSlotBefore) { removeSlotBefore.next = slotStart slotStart.prev = removeSlotBefore - oldSlotStart.prev = undefined + oldSlotStart.prev = null } if (removeSlotAfter) { removeSlotAfter.prev = slotEnd slotEnd.next = removeSlotAfter - oldSlotEnd.next = undefined + oldSlotEnd.next = null } // update parent subtree start/end @@ -1265,9 +1263,9 @@ export class Element implements NodeCast { } const ownerShadowRoot = parent.ownerShadowRoot - if (ownerShadowRoot?.isAttached(parent)) { - ownerShadowRoot.applySlotsRemoval(oldSlotStart, oldSlotEnd, false) - ownerShadowRoot.applySlotsInsertion(slotStart, slotEnd, move) + if (ownerShadowRoot?.isConnected(parent)) { + ownerShadowRoot._$applySlotsRemoval(oldSlotStart, oldSlotEnd, false) + ownerShadowRoot._$applySlotsInsertion(slotStart, slotEnd, move) } } @@ -1285,6 +1283,7 @@ export class Element implements NodeCast { oriPosIndex: number, replace: boolean, ) { + /* istanbul ignore if */ if (newChild && parent.ownerShadowRoot !== newChild.ownerShadowRoot) { throw new Error('Cannot move the node from one shadow tree to another shadow tree.') } @@ -1448,10 +1447,10 @@ export class Element implements NodeCast { } // update subtree slots - const newChildSubtreeSlotStart = newChild?._$subtreeSlotStart - const newChildSubtreeSlotEnd = newChild?._$subtreeSlotEnd - const relChildSubtreeSlotStart = relChild?._$subtreeSlotStart - const relChildSubtreeSlotEnd = relChild?._$subtreeSlotEnd + const newChildSubtreeSlotStart = newChild ? newChild._$subtreeSlotStart : null + const newChildSubtreeSlotEnd = newChild ? newChild._$subtreeSlotEnd : null + const relChildSubtreeSlotStart = relChild ? relChild._$subtreeSlotStart : null + const relChildSubtreeSlotEnd = relChild ? relChild._$subtreeSlotEnd : null if (newChild) { if (oldParent) { Element.updateSubtreeSlotsRemoval( @@ -1571,8 +1570,8 @@ export class Element implements NodeCast { containingSlotUpdater?.updateContainingSlot() // update subtree slot - let subtreeSlotStart: DoubleLinkedList | undefined - let subtreeSlotEnd: DoubleLinkedList | undefined + let subtreeSlotStart: DoubleLinkedList | null = null + let subtreeSlotEnd: DoubleLinkedList | null = null for (let i = 0; i < count; i += 1) { const relChild = relChildren[i]! if (!subtreeSlotStart) subtreeSlotStart = relChild._$subtreeSlotStart @@ -1619,10 +1618,12 @@ export class Element implements NodeCast { } for (let i = 0; i < newChildList.length; i += 1) { const newChild = newChildList[i]! + /* istanbul ignore if */ if (parent.ownerShadowRoot !== newChild.ownerShadowRoot) { throw new Error('Cannot move the node from one shadow tree to another shadow tree.') } const oldParent = newChild.parentNode + /* istanbul ignore if */ if (oldParent) { throw new Error('Cannot batch-insert the node which already has a parent.') } @@ -1709,8 +1710,8 @@ export class Element implements NodeCast { } // update subtree slot - let subtreeSlotStart: DoubleLinkedList | undefined - let subtreeSlotEnd: DoubleLinkedList | undefined + let subtreeSlotStart: DoubleLinkedList | null = null + let subtreeSlotEnd: DoubleLinkedList | null = null for (let i = 0; i < newChildList.length; i += 1) { const newChild = newChildList[i]! const newChildSubtreeSlotStart = newChild._$subtreeSlotStart @@ -1752,13 +1753,16 @@ export class Element implements NodeCast { posIndex: number, replacer: Element, ) { + /* istanbul ignore if */ if (replacer && parent.ownerShadowRoot !== replacer.ownerShadowRoot) { throw new Error('Cannot move the node from one shadow tree to another shadow tree.') } + /* istanbul ignore if */ if (replacer.parentNode) { throw new Error('Cannot replace with the node which already has a parent.') } const placeholder = parent.childNodes[posIndex] + /* istanbul ignore if */ if (!(placeholder instanceof Element)) { throw new Error('Cannot replace on text nodes.') } @@ -2094,6 +2098,7 @@ export class Element implements NodeCast { targetParent: GeneralBackendElement, targetNode: GeneralBackendElement, ) { + /* istanbul ignore if */ if (element._$attached) { throw new Error('An attached element cannot be attached again') } @@ -2143,19 +2148,21 @@ export class Element implements NodeCast { * otherwise the slot content will always be dangled. */ static setSlotName(element: Element, name?: string) { + /* istanbul ignore if */ if (element._$inheritSlots) { throw new Error('Slot-inherit mode is not usable in slot element') } - if (element._$inheritSlots) { - throw new Error('Component cannot be used as slot element') - } const slotName = name ? String(name) : '' const oldSlotName = element._$slotName if (oldSlotName === slotName) return const needInsertSlot = oldSlotName === null element._$slotName = slotName if (needInsertSlot) { - element._$subtreeSlotStart = element._$subtreeSlotEnd = { value: element } + element._$subtreeSlotStart = element._$subtreeSlotEnd = { + value: element, + prev: null, + next: null, + } } if (BM.SHADOW || (BM.DYNAMIC && element.getBackendMode() === BackendMode.Shadow)) { ;(element._$backendElement as backend.Element | null)?.setSlotName(slotName) @@ -2177,7 +2184,7 @@ export class Element implements NodeCast { ) } } else { - if (owner.isAttached(element)) owner.applySlotRename(element, slotName, oldSlotName) + if (owner.isConnected(element)) owner._$applySlotRename(element, slotName, oldSlotName) } } } @@ -2197,9 +2204,11 @@ export class Element implements NodeCast { * the child nodes of the element will be treated as siblings and can have different target slot. */ static setInheritSlots(element: Element) { + /* istanbul ignore if */ if (!element._$virtual) { throw new Error('Cannot set slot-inherit on non-virtual node') } + /* istanbul ignore if */ if (element._$slotName !== null || element.childNodes.length !== 0) { throw new Error('Slot-inherit mode cannot be set when the element has any child node') } diff --git a/glass-easel/src/index.ts b/glass-easel/src/index.ts index 3139997..c3db9a3 100644 --- a/glass-easel/src/index.ts +++ b/glass-easel/src/index.ts @@ -90,11 +90,10 @@ import { PropertyList, MethodList, ComponentInstance, - GeneralComponentInstance, Empty, } from './component_params' import { Behavior } from './behavior' -import { Component, ComponentDefinition, GeneralComponentDefinition } from './component' +import { Component, ComponentDefinition, GeneralComponent, GeneralComponentDefinition } from './component' import { Event } from './event' import { getDefaultComponentSpace } from './global_options' @@ -123,7 +122,7 @@ export const registerElement = < export function createElement( tagName: string, compDef?: GeneralComponentDefinition, -): GeneralComponentInstance +): GeneralComponent export function createElement< TData extends DataList, TProperty extends PropertyList, @@ -135,7 +134,7 @@ export function createElement< export function createElement( tagName: string, compDef?: GeneralComponentDefinition, -): GeneralComponentInstance { +): GeneralComponent { return Component.create(tagName, compDef || null) } diff --git a/glass-easel/src/shadow_root.ts b/glass-easel/src/shadow_root.ts index 6fcffd4..ca5072a 100644 --- a/glass-easel/src/shadow_root.ts +++ b/glass-easel/src/shadow_root.ts @@ -73,6 +73,7 @@ export class ShadowRoot extends VirtualNode { constructor() { throw new Error('Element cannot be constructed directly') // eslint-disable-next-line no-unreachable + /* istanbul ignore next */ super() } @@ -228,6 +229,7 @@ export class ShadowRoot extends VirtualNode { // find in the space otherwise const comp = space.getGlobalUsingComponent(compName) ?? space.getDefaultComponent() + /* istanbul ignore if */ if (!comp) { throw new Error(`Cannot find component "${compName}"`) } @@ -327,6 +329,7 @@ export class ShadowRoot extends VirtualNode { /** Get the slot element with the specified name */ getSlotElementFromName(name: string): Element | Element[] | null { const slotMode = this._$slotMode + /* istanbul ignore if */ if (slotMode === SlotMode.Direct) throw new Error('cannot get slot element in directSlots') if (slotMode === SlotMode.Single) return this._$singleSlot! if (slotMode === SlotMode.Multiple) { @@ -350,7 +353,7 @@ export class ShadowRoot extends VirtualNode { * The provided node must be a valid child node of the host of this shadow root. * Otherwise the behavior is undefined. */ - calcContainingSlot(elem: Node | null): Element | null { + getContainingSlot(elem: Node | null): Element | null { const slotMode = this._$slotMode if (slotMode === SlotMode.Direct) return elem?.containingSlot || null if (slotMode === SlotMode.Single) return this._$singleSlot as Element | null @@ -399,6 +402,7 @@ export class ShadowRoot extends VirtualNode { */ forEachSlot(f: (slot: Element) => boolean | void) { const slotMode = this._$slotMode + /* istanbul ignore if */ if (slotMode === SlotMode.Direct) { throw new Error('Cannot iterate slots in directSlots') } @@ -417,9 +421,11 @@ export class ShadowRoot extends VirtualNode { } /** - * Iterate elements with their slots (slots-inherited nodes included) + * Iterate through elements ahd their corresponding slots (including slots-inherited nodes) + * @param f A function to execute for each element. Return false to break the iteration. + * @returns A boolean indicating whether the iteration is complete. */ - forEachNodeInSlot(f: (node: Node, slot: Element | null | undefined) => boolean | void) { + forEachNodeInSlot(f: (node: Node, slot: Element | null | undefined) => boolean | void): boolean { const childNodes = this._$host.childNodes for (let i = 0; i < childNodes.length; i += 1) { if (!Element.forEachNodeInSlot(childNodes[i]!, f)) return false @@ -428,7 +434,9 @@ export class ShadowRoot extends VirtualNode { } /** - * Iterate elements in specified slot (slots-inherited nodes included) + * Iterate through elements of a specified slot (including slots-inherited nodes) + * @param f A function to execute for each element. Return false to break the iteration. + * @returns A boolean indicating whether the iteration is complete. */ forEachNodeInSpecifiedSlot(slot: Element | null, f: (node: Node) => boolean | void): boolean { if (slot) { @@ -443,9 +451,13 @@ export class ShadowRoot extends VirtualNode { } /** - * Iterate elements with their slots (slots-inherited nodes not included) + * Iterate through elements ahd their corresponding slots (NOT including slots-inherited nodes) + * @param f A function to execute for each element. Return false to break the iteration. + * @returns A boolean indicating whether the iteration is complete. */ - forEachSlotContentInSlot(f: (node: Node, slot: Element | null | undefined) => boolean | void) { + forEachSlotContentInSlot( + f: (node: Node, slot: Element | null | undefined) => boolean | void, + ): boolean { const childNodes = this._$host.childNodes for (let i = 0; i < childNodes.length; i += 1) { if (!Element.forEachSlotContentInSlot(childNodes[i]!, f)) return false @@ -454,7 +466,9 @@ export class ShadowRoot extends VirtualNode { } /** - * Iterate elements in specified slot (slots-inherited nodes not included) + * Iterate through elements of a specified slot (NOT including slots-inherited nodes) + * @param f A function to execute for each element. Return false to break the iteration. + * @returns A boolean indicating whether the iteration is complete. */ forEachSlotContentInSpecifiedSlot( slot: Element | null, @@ -472,9 +486,9 @@ export class ShadowRoot extends VirtualNode { } /** - * Check whether a node is attached to this shadow root + * Check whether a node is connected to this shadow root */ - isAttached(node: Node): boolean { + isConnected(node: Node): boolean { if (node.ownerShadowRoot !== this) return false for (let p: Node | null = node; p; p = p.parentNode) { if (p === this) return true @@ -487,8 +501,8 @@ export class ShadowRoot extends VirtualNode { const slotsList = this._$slotsList! const slots = this._$slots! - // assume that slot name duplication is very rare in practice, - // so the complexity without name duplication is priority guaranteed. + // assuming that slot name duplication is very rare in practice, + // the complexity without name duplication is priority guaranteed. if (slotsList[slotName]) { const firstSlot = slotsList[slotName]! let insertPos = { next: firstSlot } as DoubleLinkedList // this is a pointer to pointer @@ -501,14 +515,14 @@ export class ShadowRoot extends VirtualNode { } } if (insertBeforeFirst) { - slotsList[slotName] = firstSlot.prev = { value: slot, next: firstSlot } + slotsList[slotName] = firstSlot.prev = { value: slot, prev: null, next: firstSlot } } else { const next = insertPos.next insertPos.next = { value: slot, prev: insertPos, next } if (next) next.prev = insertPos.next } } else { - slotsList[slotName] = { value: slot } + slotsList[slotName] = { value: slot, prev: null, next: null } } const oldSlot = slots[slotName] const newSlot = slotsList[slotName]!.value @@ -522,7 +536,7 @@ export class ShadowRoot extends VirtualNode { const slotsList = this._$slotsList! const slots = this._$slots! - let oldSlot = slotsList[slotName] + let oldSlot = slotsList[slotName] || null for (; oldSlot; oldSlot = oldSlot.next) { if (oldSlot.value === slot) break } @@ -554,7 +568,8 @@ export class ShadowRoot extends VirtualNode { } } - applySlotRename(slot: Element, newName: string, oldName: string): void { + /** @internal */ + _$applySlotRename(slot: Element, newName: string, oldName: string): void { const slotMode = this._$slotMode // single slot does not care slot name @@ -579,7 +594,8 @@ export class ShadowRoot extends VirtualNode { } } - applySlotsInsertion( + /** @internal */ + _$applySlotsInsertion( slotStart: DoubleLinkedList, slotEnd: DoubleLinkedList, move: boolean, @@ -601,7 +617,7 @@ export class ShadowRoot extends VirtualNode { if (slotMode === SlotMode.Multiple) { for ( - let it: DoubleLinkedList | undefined = slotStart; + let it: DoubleLinkedList | null = slotStart; it && it !== slotEnd.next; it = it.next ) { @@ -620,7 +636,7 @@ export class ShadowRoot extends VirtualNode { const insertDynamicSlot = this._$insertDynamicSlotHandler const slots: { slot: Element; name: string; slotValues: { [name: string]: unknown } }[] = [] for ( - let it: DoubleLinkedList | undefined = slotStart; + let it: DoubleLinkedList | null = slotStart; it && it !== slotEnd.next; it = it.next ) { @@ -633,7 +649,8 @@ export class ShadowRoot extends VirtualNode { } } - applySlotsRemoval( + /** @internal */ + _$applySlotsRemoval( slotStart: DoubleLinkedList, slotEnd: DoubleLinkedList, move: boolean, @@ -658,7 +675,7 @@ export class ShadowRoot extends VirtualNode { if (slotMode === SlotMode.Multiple) { for ( - let it: DoubleLinkedList | undefined = slotStart; + let it: DoubleLinkedList | null = slotStart; it && it !== slotEnd.next; it = it.next ) { @@ -677,7 +694,7 @@ export class ShadowRoot extends VirtualNode { const removeDynamicSlot = this._$removeDynamicSlotHandler const slots: Element[] = [] for ( - let it: DoubleLinkedList | undefined = slotStart; + let it: DoubleLinkedList | null = slotStart; it && it !== slotEnd.next; it = it.next ) { @@ -814,6 +831,7 @@ export class ShadowRoot extends VirtualNode { return this._$slotMode } + /** @internal */ static _$updateSubtreeSlotNodes( parentNode: Element, elements: Node[], @@ -844,7 +862,7 @@ export class ShadowRoot extends VirtualNode { let insertPos = -1 const slotNodesToUpdate: Node[] = [] const oldContainingSlot = elements[0]!.containingSlot - const containingSlot = shadowRoot?.calcContainingSlot(elements[0]!) + const containingSlot = shadowRoot?.getContainingSlot(elements[0]!) for (let i = 0; i < elements.length; i += 1) { const elem = elements[i]! @@ -896,7 +914,7 @@ export class ShadowRoot extends VirtualNode { const elem = elements[i]! Element.forEachNodeInSlot(elem, (node, oldContainingSlot) => { const containingSlot = - slotMode !== SlotMode.Direct ? shadowRoot?.calcContainingSlot(node) : undefined + slotMode !== SlotMode.Direct ? shadowRoot?.getContainingSlot(node) : undefined if (oldContainingSlot) { const slotNodesToRemove = slotNodesToRemoveMap.get(oldContainingSlot) diff --git a/glass-easel/src/template_engine.ts b/glass-easel/src/template_engine.ts index 91a5a2d..09afec6 100644 --- a/glass-easel/src/template_engine.ts +++ b/glass-easel/src/template_engine.ts @@ -1,16 +1,16 @@ import { GeneralBehavior } from './behavior' -import { GeneralComponentInstance } from './component_params' import { ShadowRoot } from './shadow_root' import { DataChange, DataValue } from './data_proxy' import { NormalizedComponentOptions } from './global_options' import { ExternalShadowRoot } from './external_shadow_tree' +import { GeneralComponent } from './component' export interface TemplateEngine { create(behavior: GeneralBehavior, componentOptions: NormalizedComponentOptions): Template } export interface Template { - createInstance(elem: GeneralComponentInstance): TemplateInstance + createInstance(elem: GeneralComponent): TemplateInstance } export interface TemplateInstance { diff --git a/glass-easel/src/text_node.ts b/glass-easel/src/text_node.ts index e8c390b..e007a90 100644 --- a/glass-easel/src/text_node.ts +++ b/glass-easel/src/text_node.ts @@ -21,9 +21,9 @@ export class TextNode implements NodeCast { /** @internal */ _$destroyOnDetach = false /** @internal */ - _$subtreeSlotStart: undefined + _$subtreeSlotStart = null /** @internal */ - _$subtreeSlotEnd: undefined + _$subtreeSlotEnd = null /** @internal */ _$inheritSlots: undefined /** @internal */ diff --git a/glass-easel/src/tmpl/index.ts b/glass-easel/src/tmpl/index.ts index e612903..4f1a328 100644 --- a/glass-easel/src/tmpl/index.ts +++ b/glass-easel/src/tmpl/index.ts @@ -8,8 +8,8 @@ import { GeneralBehavior, NormalizedComponentOptions, ShadowedEvent, + GeneralComponent, } from '..' -import type { GeneralComponentInstance } from '../component_params' import { DataChange } from '../data_proxy' import { GlassEaselTemplateDOM } from './native_rendering' import { @@ -83,20 +83,20 @@ class GlassEaselTemplate implements templateEngine.Template { this.eventObjectFilter = c.eventObjectFilter } - createInstance(comp: GeneralComponentInstance): templateEngine.TemplateInstance { + createInstance(comp: GeneralComponent): templateEngine.TemplateInstance { return new GlassEaselTemplateInstance(this, comp) } } class GlassEaselTemplateInstance implements templateEngine.TemplateInstance { template: GlassEaselTemplate - comp: GeneralComponentInstance + comp: GeneralComponent shadowRoot: ShadowRoot procGenWrapper: ProcGenWrapper bindingMapGen: { [field: string]: BindingMapGen[] } | undefined forceBindingMapUpdate: BindingMapUpdateEnabled - constructor(template: GlassEaselTemplate, comp: GeneralComponentInstance) { + constructor(template: GlassEaselTemplate, comp: GeneralComponent) { this.template = template const procGen = template.genObjectGroupEnv.group('') || DEFAULT_PROC_GEN_GROUP('') if (template.updateMode === 'bindingMap') { diff --git a/glass-easel/src/tmpl/native_rendering.ts b/glass-easel/src/tmpl/native_rendering.ts index e56a502..5261575 100644 --- a/glass-easel/src/tmpl/native_rendering.ts +++ b/glass-easel/src/tmpl/native_rendering.ts @@ -10,12 +10,12 @@ import { ShadowedEvent, GeneralBackendElement, BackendMode, + GeneralComponent, } from '..' import { DataChange } from '../data_proxy' import { ProcGenEnv, ProcGen, BindingMapGen } from './proc_gen_wrapper' import { ProcGenWrapperDom } from './proc_gen_wrapper_dom' import { ComponentTemplate, ProcGenGroupList } from './index' -import type { GeneralComponentInstance } from '../component_params' type ElementWithEvent = Element & { _$wxTmplEv: { [ev: string]: (event: ShadowedEvent) => unknown } @@ -55,7 +55,7 @@ export class GlassEaselTemplateDOM implements templateEngine.Template { this.methods = behavior._$methodMap } - createInstance(comp: GeneralComponentInstance): templateEngine.TemplateInstance { + createInstance(comp: GeneralComponent): templateEngine.TemplateInstance { return new GlassEaselTemplateDOMInstance(this, comp) } } @@ -64,7 +64,7 @@ export class GlassEaselTemplateDOMInstance implements templateEngine.TemplateInstance, ExternalShadowRoot { template: GlassEaselTemplateDOM - comp: GeneralComponentInstance + comp: GeneralComponent shadowRoot: ExternalShadowRoot shadowRootElement: Element root: GeneralBackendElement @@ -74,7 +74,7 @@ export class GlassEaselTemplateDOMInstance procGenWrapper: ProcGenWrapperDom bindingMapGen: { [field: string]: BindingMapGen[] } | undefined - constructor(template: GlassEaselTemplateDOM, comp: GeneralComponentInstance) { + constructor(template: GlassEaselTemplateDOM, comp: GeneralComponent) { if (comp.getBackendMode() !== BackendMode.Domlike) { throw new Error( `Component template of ${comp.is} cannot be initialized since external rendering is only supported in Domlike backend currently.`, diff --git a/glass-easel/tests/base/match.ts b/glass-easel/tests/base/match.ts index 85d9101..8db2fc6 100644 --- a/glass-easel/tests/base/match.ts +++ b/glass-easel/tests/base/match.ts @@ -6,10 +6,9 @@ import { Element, domlikeBackend, TextNode, - VirtualNode, - Node, BackendMode, } from '../../src' +import { DoubleLinkedList } from '../../src/element' // check internal structure of an external component (with its child nodes given) export const native = (structure: { @@ -68,18 +67,17 @@ export const virtual = (elem: Element, defDomElem?: HTMLElement, defIndex?: numb // for a component, check its shadow children and its shadow root if (elem instanceof Component) { const slotIndex = new Map() - const dfsInherited = (parent: Element, depth: number) => { + const dfsChildren = (parent: Element) => { parent.childNodes.forEach((child, i) => { const slot = child.containingSlot expect(child.parentNode).toBe(parent) expect(child.parentIndex).toBe(i) if (slot) { if (!slotIndex.has(slot)) slotIndex.set(slot, 0) - const slotContent = elem._$external - ? elem.getComposedChildren() - : (elem.shadowRoot as ShadowRoot).getSlotContentArray(slot)! + const slotContent = (elem.shadowRoot as ShadowRoot).getSlotContentArray(slot)! const index = slotIndex.get(slot)! expect(child).toBe(slotContent[index]) + expect(child).toBe(slot.slotNodes![index]) expect(child.slotIndex).toBe(index) slotIndex.set(slot, index + 1) } @@ -95,19 +93,68 @@ export const virtual = (elem: Element, defDomElem?: HTMLElement, defIndex?: numb throw new Error() } if (child instanceof Element && Element.getInheritSlots(child)) { - dfsInherited(child, depth + 1) + dfsChildren(child) } }) } - dfsInherited(elem, 1) + dfsChildren(elem) if (elem._$external) { const sr = (elem.shadowRoot as ExternalShadowRoot).root as domlikeBackend.Element expect(sr.__wxElement).toBe(elem) testBackend(elem) } else { + const shadowRoot = elem.shadowRoot as ShadowRoot + let subtreeSlotStart = null as DoubleLinkedList | null + let subtreeSlotEnd = null as DoubleLinkedList | null + + // check subtree slot linked list in shadow tree + const dfsShadow = (elem: Element) => { + const prevSubtreeSlotStart = subtreeSlotStart + const prevSubtreeSlotEnd = subtreeSlotEnd + + elem.childNodes.forEach((child) => { + if (child instanceof Element) { + dfsShadow(child) + } + }) + + if (prevSubtreeSlotStart) { + expect(prevSubtreeSlotStart).toBe(subtreeSlotStart) + } + if (subtreeSlotStart || subtreeSlotEnd) { + expect(subtreeSlotStart).not.toBe(null) + expect(subtreeSlotEnd).not.toBe(null) + } + + if (elem._$slotName !== null) { + const currentSlot = subtreeSlotEnd ? subtreeSlotEnd.next : shadowRoot._$subtreeSlotStart + expect(currentSlot).not.toBe(null) + expect(currentSlot!.value).toBe(elem) + expect(currentSlot!.prev).toBe(subtreeSlotEnd) + if (!subtreeSlotStart) subtreeSlotStart = currentSlot + subtreeSlotEnd = currentSlot + + expect(elem._$subtreeSlotStart).toBe(currentSlot) + expect(elem._$subtreeSlotEnd).toBe(currentSlot) + } else { + if (prevSubtreeSlotEnd !== subtreeSlotEnd) { + expect(elem._$subtreeSlotStart).toBe( + prevSubtreeSlotEnd ? prevSubtreeSlotEnd.next : subtreeSlotStart, + ) + expect(elem._$subtreeSlotEnd).toBe(subtreeSlotEnd) + } else { + expect(elem._$subtreeSlotStart).toBe(null) + expect(elem._$subtreeSlotEnd).toBe(null) + } + } + } + dfsShadow(shadowRoot) + if (subtreeSlotEnd) expect(subtreeSlotEnd.next).toBe(null) + expect(shadowRoot._$subtreeSlotStart).toBe(subtreeSlotStart) + expect(shadowRoot._$subtreeSlotEnd).toBe(subtreeSlotEnd) expect(elem.getComposedChildren()).toStrictEqual([elem.shadowRoot]) - expect((elem.shadowRoot as ShadowRoot).getHostNode()).toBe(elem) - expect((elem.shadowRoot as ShadowRoot).parentNode).toBe(null) + expect(shadowRoot.getHostNode()).toBe(elem) + expect(shadowRoot.parentNode).toBe(null) } } diff --git a/glass-easel/tests/tmpl/structure.test.ts b/glass-easel/tests/tmpl/structure.test.ts index ba99851..7a41f2f 100644 --- a/glass-easel/tests/tmpl/structure.test.ts +++ b/glass-easel/tests/tmpl/structure.test.ts @@ -1,4 +1,5 @@ import { tmpl, multiTmpl, domBackend, execWithWarn } from '../base/env' +import { virtual as matchElementWithDom } from '../base/match' import * as glassEasel from '../../src' const domHtml = (elem: glassEasel.Element): string => { @@ -149,16 +150,19 @@ describe('node tree structure', () => { const elem = glassEasel.Component.createWithContext('root', def, domBackend) const child = elem.getShadowRoot()!.getElementById('c')!.asInstanceOf(childComp)! expect(domHtml(elem)).toBe('
A
') + matchElementWithDom(elem) child.setData({ cond1: false, cond2: true, }) expect(domHtml(elem)).toBe('
C
') + matchElementWithDom(elem) child.setData({ cond1: false, cond2: false, }) expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) }) test('for blocks without key', () => { @@ -1096,10 +1100,12 @@ describe('node tree structure', () => { const elem = glassEasel.Component.createWithContext('root', def, domBackend) glassEasel.Element.pretendAttached(elem) expect(domHtml(elem)).toBe('
123
') + matchElementWithDom(elem) elem.setData({ d: '', }) expect(domHtml(elem)).toBe('
') + matchElementWithDom(elem) }) test('tag name cases', () => { @@ -1232,14 +1238,17 @@ describe('node tree structure', () => { const elem = glassEasel.Component.createWithContext('root', def, domBackend) glassEasel.Element.pretendAttached(elem) expect(domHtml(elem)).toBe('
') + matchElementWithDom(elem) elem.setData({ s: 'a', }) expect(domHtml(elem)).toBe('
') + matchElementWithDom(elem) elem.setData({ s: 'b', }) expect(domHtml(elem)).toBe('
') + matchElementWithDom(elem) }) test('setting slot name', () => { @@ -1248,12 +1257,14 @@ describe('node tree structure', () => { multipleSlots: true, }, template: tmpl(` -
- + + + `), data: { - a: 's', - b: undefined as string | undefined, + a: '', + b: '', + c: 's', }, }) const def = glassEasel @@ -1263,7 +1274,7 @@ describe('node tree structure', () => { }, template: tmpl(` - + `), }) @@ -1271,19 +1282,45 @@ describe('node tree structure', () => { const elem = glassEasel.Component.createWithContext('root', def, domBackend) const subElem = (elem.$.sub as glassEasel.GeneralComponent).asInstanceOf(subComp)! glassEasel.Element.pretendAttached(elem) - expect(domHtml(elem)).toBe('
') + expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) subElem.setData({ - a: '', + b: 's', }) - expect(domHtml(elem)).toBe('
') + expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) + subElem.setData({ + a: 's', + }) + expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) + subElem.setData({ + b: '', + c: '', + }) + expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) + subElem.setData({ + c: 's', + }) + expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) subElem.setData({ b: 's', }) - expect(domHtml(elem)).toBe('
') + expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) subElem.setData({ - a: 's', + a: 'a', + b: 'b', }) - expect(domHtml(elem)).toBe('
') + expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) + subElem.setData({ + c: '', + }) + expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) }) test('binding event listeners', () => {