diff --git a/src/app/template_helpers.ts b/src/app/template_helpers.ts index 845cd001..82773a39 100644 --- a/src/app/template_helpers.ts +++ b/src/app/template_helpers.ts @@ -2,6 +2,7 @@ import { BDom, multi, text, toggler } from "../blockdom"; import { validateProps } from "../component/props_validation"; import { Markup } from "../utils"; import { html } from "../blockdom/index"; +import { VPortal } from "../portal"; /** * This file contains utility functions that will be injected in each template, @@ -12,6 +13,16 @@ function withDefault(value: any, defaultValue: any): any { return value === undefined || value === null || value === false ? defaultValue : value; } +function callPortal( + ctx: any, + parent: any, + key: string, + target: string, + content: (ctx: any, node: any, key: string) => BDom +): BDom { + return new VPortal(target, content(ctx, parent, key)) as any; +} + function callSlot( ctx: any, parent: any, @@ -186,6 +197,7 @@ export const UTILS = { zero: Symbol("zero"), isBoundary, callSlot, + callPortal, capture, withKey, prepareList, diff --git a/src/blockdom/block_compiler.ts b/src/blockdom/block_compiler.ts index 35cfe2b6..8345c19b 100644 --- a/src/blockdom/block_compiler.ts +++ b/src/blockdom/block_compiler.ts @@ -47,7 +47,7 @@ const cache: { [key: string]: BlockType } = {}; * @param str * @returns a new block type, that can build concrete blocks */ -export function createBlock(str: string): BlockType { +export function createBlock(str: string, deepRemove: boolean = false): BlockType { if (str in cache) { return cache[str]; } @@ -67,7 +67,7 @@ export function createBlock(str: string): BlockType { // step 3: build the final block class const template = tree.el as HTMLElement; - const Block = buildBlock(template, context); + const Block = buildBlock(template, context, deepRemove); cache[str] = Block; return Block; } @@ -422,7 +422,7 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) { // building the concrete block class // ----------------------------------------------------------------------------- -function buildBlock(template: HTMLElement, ctx: BlockCtx): BlockType { +function buildBlock(template: HTMLElement, ctx: BlockCtx, deepRemove: boolean): BlockType { let B = createBlockClass(template, ctx); if (ctx.cbRefs.length) { @@ -447,6 +447,14 @@ function buildBlock(template: HTMLElement, ctx: BlockCtx): BlockType { } }; B.prototype.beforeRemove = VMulti.prototype.beforeRemove; + if (deepRemove) { + const blockRemove = B.prototype.remove; + const vMultiRemove = VMulti.prototype.remove; + B.prototype.remove = function () { + blockRemove.call(this); + vMultiRemove.call(this); + }; + } return (data?: any[], children: (VNode | undefined)[] = []) => new B(data, children); } diff --git a/src/blockdom/list.ts b/src/blockdom/list.ts index 7386f5ed..58417b9f 100644 --- a/src/blockdom/list.ts +++ b/src/blockdom/list.ts @@ -17,9 +17,11 @@ class VList { anchor: Node | undefined; parentEl?: HTMLElement | undefined; isOnlyChild?: boolean | undefined; + deepRemove: boolean; - constructor(children: VNode[]) { + constructor(children: VNode[], deepRemove: boolean) { this.children = children; + this.deepRemove = deepRemove; } mount(parent: HTMLElement, afterNode: Node | null) { @@ -75,7 +77,7 @@ class VList { const parent = this.parentEl!; // fast path: no new child => only remove - if (ch2.length === 0 && isOnlyChild) { + if (ch2.length === 0 && isOnlyChild && !this.deepRemove) { if (withBeforeRemove) { for (let i = 0, l = ch1.length; i < l; i++) { beforeRemove.call(ch1[i]); @@ -98,7 +100,6 @@ class VList { let endVn2 = ch2[endIdx2]; let mapping: any = undefined; - // let noFullRemove = this.hasNoComponent; while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) { // ------------------------------------------------------------------- @@ -202,7 +203,7 @@ class VList { remove() { const { parentEl, anchor } = this; - if (this.isOnlyChild) { + if (this.isOnlyChild && !this.deepRemove) { nodeSetTextContent.call(parentEl, ""); } else { const children = this.children; @@ -227,8 +228,8 @@ class VList { } } -export function list(children: VNode[]): VNode { - return new VList(children); +export function list(children: VNode[], deepRemove = false): VNode { + return new VList(children, deepRemove); } function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } { diff --git a/src/blockdom/multi.ts b/src/blockdom/multi.ts index 727b0190..a9a394c5 100644 --- a/src/blockdom/multi.ts +++ b/src/blockdom/multi.ts @@ -15,9 +15,11 @@ export class VMulti { anchors?: Node[] | undefined; parentEl?: HTMLElement | undefined; isOnlyChild?: boolean | undefined; + deepRemove: boolean; - constructor(children: (VNode | undefined)[]) { + constructor(children: (VNode | undefined)[], deepRemove: boolean) { this.children = children; + this.deepRemove = deepRemove; } mount(parent: HTMLElement, afterNode: Node | null) { @@ -103,7 +105,7 @@ export class VMulti { remove() { const parentEl = this.parentEl; - if (this.isOnlyChild) { + if (this.isOnlyChild && !this.deepRemove) { nodeSetTextContent.call(parentEl, ""); } else { const children = this.children; @@ -129,6 +131,6 @@ export class VMulti { } } -export function multi(children: (VNode | undefined)[]): VNode { - return new VMulti(children); +export function multi(children: (VNode | undefined)[], deepRemove = false): VNode { + return new VMulti(children, deepRemove); } diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 4acb752c..a724ea88 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -19,6 +19,7 @@ import { ASTTSet, ASTTranslation, ASTType, + ASTTPortal, } from "./parser"; type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment"; @@ -64,13 +65,15 @@ class BlockDescription { type: BlockType; parentVar: string = ""; id: number; + deepRemove: boolean; - constructor(target: CodeTarget, type: BlockType) { + constructor(target: CodeTarget, type: BlockType, deepRemove: boolean = false) { this.id = BlockDescription.nextBlockId++; this.varName = "b" + this.id; this.blockName = "block" + this.id; this.target = target; this.type = type; + this.deepRemove = deepRemove; } insertData(str: string, prefix: string = "d"): number { @@ -99,7 +102,7 @@ class BlockDescription { } return `${this.blockName}(${params})`; } else if (this.type === "list") { - return `list(c_block${this.id})`; + return `list(c_block${this.id}${this.deepRemove ? ", true" : ""})`; } return expr; } @@ -250,6 +253,8 @@ export class CodeGenerator { mainCode.push(`const ${id} = getTemplate(${template});`); } + const deepRemove = "deepRemove" in this.ast ? this.ast.deepRemove : false; + // define all blocks if (this.blocks.length) { mainCode.push(``); @@ -259,9 +264,15 @@ export class CodeGenerator { if (block.dynamicTagName) { xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`); xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`); - mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`); + mainCode.push( + `let ${block.blockName} = tag => createBlock(\`${xmlString}\`${ + deepRemove ? ", true" : "" + });` + ); } else { - mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`); + mainCode.push( + `let ${block.blockName} = createBlock(\`${xmlString}\`${deepRemove ? ", true" : ""});` + ); } } } @@ -317,10 +328,11 @@ export class CodeGenerator { createBlock( parentBlock: BlockDescription | null, type: BlockType, - ctx: Context + ctx: Context, + deepRemove: boolean = false ): BlockDescription { const hasRoot = this.target.hasRoot; - const block = new BlockDescription(this.target, type); + const block = new BlockDescription(this.target, type, deepRemove); if (!hasRoot && !ctx.preventRoot) { this.target.hasRoot = true; block.isRoot = true; @@ -445,6 +457,8 @@ export class CodeGenerator { case ASTType.TTranslation: this.compileTTranslation(ast, ctx); break; + case ASTType.TPortal: + this.compileTPortal(ast, ctx); } } @@ -704,7 +718,7 @@ export class CodeGenerator { if (ast.body) { const nextId = BlockDescription.nextBlockId; const subCtx: Context = createContext(ctx); - this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx); + this.compileAST({ type: ASTType.Multi, content: ast.body, deepRemove: false }, subCtx); this.helpers.add("withDefault"); expr = `withDefault(${expr}, b${nextId})`; } @@ -765,7 +779,7 @@ export class CodeGenerator { // note: this part is duplicated from end of compilemulti: const args = block!.children.map((c) => c.varName).join(", "); - this.insertBlock(`multi([${args}])`, block!, ctx)!; + this.insertBlock(`multi([${args}]${ast.deepRemove ? ", true" : ""})`, block!, ctx)!; } } @@ -774,7 +788,7 @@ export class CodeGenerator { if (block) { this.insertAnchor(block); } - block = this.createBlock(block, "list", ctx); + block = this.createBlock(block, "list", ctx, ast.deepRemove); this.target.loopLevel++; const loopVar = `i${this.target.loopLevel}`; this.addLine(`ctx = Object.create(ctx);`); @@ -909,7 +923,7 @@ export class CodeGenerator { } const args = block!.children.map((c) => c.varName).join(", "); - this.insertBlock(`multi([${args}])`, block!, ctx)!; + this.insertBlock(`multi([${args}]${ast.deepRemove ? ", true" : ""})`, block!, ctx)!; } } @@ -921,7 +935,7 @@ export class CodeGenerator { this.helpers.add("isBoundary"); const nextId = BlockDescription.nextBlockId; const subCtx: Context = createContext(ctx, { preventRoot: true }); - this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx); + this.compileAST({ type: ASTType.Multi, content: ast.body, deepRemove: false }, subCtx); if (nextId !== BlockDescription.nextBlockId) { this.helpers.add("zero"); this.addLine(`ctx[zero] = b${nextId};`); @@ -976,7 +990,7 @@ export class CodeGenerator { const expr = ast.value ? compileExpr(ast.value || "") : "null"; if (ast.body) { this.helpers.add("LazyValue"); - const bodyAst: AST = { type: ASTType.Multi, content: ast.body }; + const bodyAst: AST = { type: ASTType.Multi, content: ast.body, deepRemove: false }; const name = this.compileInNewTarget("value", bodyAst, ctx); let value = `new LazyValue(${name}, ctx, node)`; value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value; @@ -1162,4 +1176,15 @@ export class CodeGenerator { this.compileAST(ast.content, Object.assign({}, ctx, { translate: false })); } } + compileTPortal(ast: ASTTPortal, ctx: Context) { + this.helpers.add("callPortal"); + let { block } = ctx; + const name = this.compileInNewTarget("portalContent", ast.content, ctx); + const blockString = `callPortal(ctx, node, key, ${ast.target}, ${name})`; + if (block) { + this.insertAnchor(block); + } + block = this.createBlock(block, "multi", ctx); + this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false }); + } } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 72068c59..221d26d2 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -20,6 +20,7 @@ export const enum ASTType { TSlot, TCallBlock, TTranslation, + TPortal, } export interface ASTText { @@ -55,6 +56,7 @@ export interface ASTDomNode { export interface ASTMulti { type: ASTType.Multi; content: AST[]; + deepRemove: boolean; } export interface ASTTEsc { @@ -75,6 +77,7 @@ export interface ASTTif { content: AST; tElif: { condition: string; content: AST }[] | null; tElse: AST | null; + deepRemove: boolean; } export interface ASTTSet { @@ -92,8 +95,7 @@ export interface ASTTForEach { key: string | null; body: AST; memo: string; - isOnlyChild: boolean; - hasNoComponent: boolean; + deepRemove: boolean; hasNoFirst: boolean; hasNoLast: boolean; hasNoIndex: boolean; @@ -149,6 +151,12 @@ export interface ASTTranslation { content: AST | null; } +export interface ASTTPortal { + type: ASTType.TPortal; + target: string; + content: AST; +} + export type AST = | ASTText | ASTComment @@ -166,7 +174,8 @@ export type AST = | ASTTCallBlock | ASTLog | ASTDebug - | ASTTranslation; + | ASTTranslation + | ASTTPortal; // ----------------------------------------------------------------------------- // Parser @@ -195,6 +204,7 @@ function parseNode(node: ChildNode, ctx: ParsingContext): AST | null { parseTDebugLog(node, ctx) || parseTForEach(node, ctx) || parseTIf(node, ctx) || + parseTPortal(node, ctx) || parseTCall(node, ctx) || parseTCallBlock(node, ctx) || parseTEscNode(node, ctx) || @@ -351,9 +361,6 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { attrs[attr] = value; } } - if (children.length === 1 && children[0].type === ASTType.TForEach) { - children[0].isOnlyChild = true; - } return { type: ASTType.DomNode, tag: tagName, @@ -477,9 +484,8 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null { elem, body, memo, + deepRemove: needDeepRemove(body), key, - isOnlyChild: false, - hasNoComponent: hasNoComponent(body), hasNoFirst, hasNoLast, hasNoIndex, @@ -488,54 +494,40 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null { } /** - * @returns true if we are sure the ast does not contain any component + * @returns true if we are sure that a deep remove (without optimisation) is needed, for exemple + * if there is a portal. */ -function hasNoComponent(ast: AST): boolean { +function needDeepRemove(ast: AST): boolean { switch (ast.type) { + case ASTType.Multi: + case ASTType.TForEach: + case ASTType.TIf: + return ast.deepRemove; + + case ASTType.TPortal: + return true; + case ASTType.TComponent: case ASTType.TOut: case ASTType.TCall: case ASTType.TCallBlock: case ASTType.TSlot: - return false; - case ASTType.TSet: case ASTType.Text: case ASTType.Comment: case ASTType.TEsc: - return true; + return false; + case ASTType.TKey: - return hasNoComponent(ast.content); + return needDeepRemove(ast.content); case ASTType.TDebug: case ASTType.TLog: case ASTType.TTranslation: - return ast.content ? hasNoComponent(ast.content) : true; - case ASTType.TForEach: - return ast.hasNoComponent; - case ASTType.Multi: - case ASTType.DomNode: { - for (let elem of ast.content) { - if (!hasNoComponent(elem)) { - return false; - } - } - return true; - } - case ASTType.TIf: { - if (!hasNoComponent(ast.content)) { - return false; - } - if (ast.tElif) { - for (let elem of ast.tElif) { - if (!hasNoComponent(elem.content)) { - return false; - } - } - } - if (ast.tElse && !hasNoComponent(ast.tElse)) { - return false; - } - return true; - } + return ast.content ? needDeepRemove(ast.content) : false; + case ASTType.TSet: + return ast.body ? ast.body.some((ast) => needDeepRemove(ast)) : false; + + case ASTType.DomNode: + return ast.content.some((ast) => needDeepRemove(ast)); } } @@ -636,12 +628,21 @@ function parseTIf(node: Element, ctx: ParsingContext): AST | null { nextElement.remove(); } + let deepRemove = needDeepRemove(content); + if (tElifs) { + deepRemove = deepRemove || tElifs.some((ast) => needDeepRemove(ast)); + } + if (tElse) { + deepRemove = deepRemove || needDeepRemove(tElse); + } + return { type: ASTType.TIf, condition, content, tElif: tElifs.length ? tElifs : null, tElse, + deepRemove, }; } @@ -805,6 +806,35 @@ function parseTTranslation(node: Element, ctx: ParsingContext): AST | null { }; } +// ----------------------------------------------------------------------------- +// Portal +// ----------------------------------------------------------------------------- + +function parseTPortal(node: Element, ctx: ParsingContext): AST | null { + if (!node.hasAttribute("t-portal")) { + return null; + } + if (node.tagName !== "t") { + throw new Error( + `Directive 't-portal' can only be used on nodes (used on a <${node.tagName}>)` + ); + } + const target = node.getAttribute("t-portal")!; + node.removeAttribute("t-portal"); + const content = parseNode(node, ctx); + if (!content) { + return { + type: ASTType.Text, + value: "", + }; + } + return { + type: ASTType.TPortal, + target, + content, + }; +} + // ----------------------------------------------------------------------------- // helpers // ----------------------------------------------------------------------------- @@ -839,7 +869,11 @@ function parseChildNodes(node: Node, ctx: ParsingContext): AST | null { case 1: return children[0]; default: - return { type: ASTType.Multi, content: children }; + return { + type: ASTType.Multi, + content: children, + deepRemove: children.some((ast) => needDeepRemove(ast)), + }; } } diff --git a/src/index.ts b/src/index.ts index d61c7371..741a4639 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,6 @@ export { App, mount } from "./app/app"; export { Component } from "./component/component"; export { useComponent } from "./component/component_node"; export { status } from "./component/status"; -export { Portal } from "./portal"; export { Memo } from "./memo"; export { xml } from "./app/template_set"; export { useState, reactive } from "./reactivity"; diff --git a/src/portal.ts b/src/portal.ts index 7085b482..23fa759b 100644 --- a/src/portal.ts +++ b/src/portal.ts @@ -1,10 +1,8 @@ -import { xml } from "./app/template_set"; import { BDom, text, VNode } from "./blockdom"; -import { Component } from "./component/component"; const VText: any = text("").constructor; -class VPortal extends VText implements Partial> { +export class VPortal extends VText implements Partial> { // selector: string; realBDom: BDom | null; target: HTMLElement | null = null; @@ -49,19 +47,3 @@ class VPortal extends VText implements Partial> { } } } - -export class Portal extends Component { - static template = xml``; - static props = { - target: { - type: String, - }, - slots: true, - }; - - setup() { - const node = this.__owl__; - const renderFn = node.renderFn; - node.renderFn = () => new VPortal(this.props.target, renderFn()); - } -} diff --git a/tests/compiler/parser.test.ts b/tests/compiler/parser.test.ts index 3eb9437e..c6d64474 100644 --- a/tests/compiler/parser.test.ts +++ b/tests/compiler/parser.test.ts @@ -146,6 +146,7 @@ describe("qweb parser", () => { content: [], }, ], + deepRemove: false, }); }); @@ -253,6 +254,7 @@ describe("qweb parser", () => { content: [], }, ], + deepRemove: false, }); }); @@ -503,6 +505,7 @@ describe("qweb parser", () => { }, tElif: null, tElse: null, + deepRemove: false, }, ], }); @@ -518,6 +521,7 @@ describe("qweb parser", () => { }, tElif: null, tElse: null, + deepRemove: false, }); }); @@ -538,6 +542,7 @@ describe("qweb parser", () => { }, tElif: null, tElse: null, + deepRemove: false, }); }); @@ -554,6 +559,7 @@ describe("qweb parser", () => { type: ASTType.Text, value: "else", }, + deepRemove: false, }); }); @@ -572,6 +578,7 @@ describe("qweb parser", () => { }, ], tElse: null, + deepRemove: false, }); }); @@ -593,6 +600,7 @@ describe("qweb parser", () => { type: ASTType.Text, value: "else", }, + deepRemove: false, }); }); @@ -650,6 +658,7 @@ describe("qweb parser", () => { }, ], }, + deepRemove: false, }); }); @@ -731,6 +740,7 @@ describe("qweb parser", () => { }, tElif: null, tElse: null, + deepRemove: false, }); }); @@ -753,6 +763,7 @@ describe("qweb parser", () => { content: { type: ASTType.Text, value: "1" }, tElif: null, tElse: { type: ASTType.TSet, name: "ourvar", value: "0", defaultValue: null, body: null }, + deepRemove: false, }, ], }); @@ -776,14 +787,13 @@ describe("qweb parser", () => { collection: "list", elem: "item", key: "item_index", - hasNoComponent: true, - isOnlyChild: false, body: { type: ASTType.TEsc, expr: "item", defaultValue: "" }, memo: "", hasNoFirst: true, hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -793,14 +803,13 @@ describe("qweb parser", () => { collection: "list", elem: "item", key: "item_index", - hasNoComponent: true, - isOnlyChild: false, body: { type: ASTType.TEsc, expr: "item", defaultValue: "" }, memo: "", hasNoFirst: true, hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -809,8 +818,6 @@ describe("qweb parser", () => { type: ASTType.TForEach, collection: "list", elem: "item", - hasNoComponent: true, - isOnlyChild: false, key: "item_index", body: { type: ASTType.DomNode, @@ -828,6 +835,7 @@ describe("qweb parser", () => { hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -836,8 +844,6 @@ describe("qweb parser", () => { type: ASTType.TForEach, collection: "list", elem: "item", - hasNoComponent: true, - isOnlyChild: false, key: "item.id", body: { type: ASTType.TEsc, expr: "item", defaultValue: "" }, memo: "", @@ -845,6 +851,7 @@ describe("qweb parser", () => { hasNoIndex: true, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -856,8 +863,6 @@ describe("qweb parser", () => { collection: "list", elem: "item", key: "item_index", - hasNoComponent: true, - isOnlyChild: false, body: { type: ASTType.DomNode, tag: "span", @@ -874,6 +879,7 @@ describe("qweb parser", () => { hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -887,13 +893,12 @@ describe("qweb parser", () => { collection: "list", elem: "item", key: "item_index", - hasNoComponent: true, - isOnlyChild: false, body: { type: ASTType.TIf, condition: "condition", tElif: null, tElse: null, + deepRemove: false, content: { type: ASTType.DomNode, tag: "span", @@ -911,6 +916,7 @@ describe("qweb parser", () => { hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -924,8 +930,6 @@ describe("qweb parser", () => { collection: "categories", elem: "category", key: "category_index", - hasNoComponent: true, - isOnlyChild: false, body: { type: ASTType.DomNode, tag: "option", @@ -945,6 +949,7 @@ describe("qweb parser", () => { hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -966,14 +971,13 @@ describe("qweb parser", () => { collection: "list", elem: "item", key: "item_index", - hasNoComponent: true, - isOnlyChild: true, body: { type: ASTType.TEsc, expr: "item", defaultValue: "" }, memo: "", hasNoFirst: true, hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }, ], }); @@ -995,8 +999,6 @@ describe("qweb parser", () => { collection: "list", elem: "item", key: "item_index", - hasNoComponent: true, - isOnlyChild: false, body: { type: ASTType.DomNode, tag: "span", @@ -1013,6 +1015,7 @@ describe("qweb parser", () => { hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -1022,8 +1025,6 @@ describe("qweb parser", () => { collection: "list", elem: "item", key: "item_index", - hasNoComponent: false, - isOnlyChild: false, body: { type: ASTType.TComponent, isDynamic: false, @@ -1037,6 +1038,7 @@ describe("qweb parser", () => { hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -1048,8 +1050,6 @@ describe("qweb parser", () => { collection: "list", elem: "item", key: "item_index", - hasNoComponent: false, - isOnlyChild: false, body: { type: ASTType.TCall, name: "blap", @@ -1060,6 +1060,7 @@ describe("qweb parser", () => { hasNoIndex: false, hasNoLast: false, hasNoValue: false, + deepRemove: false, }); }); @@ -1073,14 +1074,13 @@ describe("qweb parser", () => { collection: "list", elem: "item", key: "item_index", - hasNoComponent: true, - isOnlyChild: false, body: { type: ASTType.TEsc, expr: "item", defaultValue: "" }, memo: "[row.x]", hasNoFirst: true, hasNoIndex: false, hasNoLast: true, hasNoValue: true, + deepRemove: false, }); }); @@ -1130,6 +1130,7 @@ describe("qweb parser", () => { condition: "condition", tElif: null, tElse: null, + deepRemove: false, content: { type: ASTType.TCall, name: "blabla", @@ -1284,6 +1285,7 @@ describe("qweb parser", () => { ns: null, }, ], + deepRemove: false, }, }, }, @@ -1604,7 +1606,7 @@ describe("qweb parser", () => { attrs: {}, content: [ { - type: 0, + type: ASTType.Text, value: "word", }, ], @@ -1616,19 +1618,18 @@ describe("qweb parser", () => { type: ASTType.DomNode, ns: null, }, - type: 16, + type: ASTType.TTranslation, }, collection: "list", elem: "item", - hasNoComponent: true, hasNoFirst: true, hasNoIndex: false, hasNoLast: true, hasNoValue: true, - isOnlyChild: false, key: "item_index", memo: "", - type: 9, + deepRemove: false, + type: ASTType.TForEach, }); }); @@ -1787,4 +1788,34 @@ describe("qweb parser", () => { ns: null, }); }); + + // --------------------------------------------------------------------------- + // t-portal + // --------------------------------------------------------------------------- + test("t-portal", async () => { + expect(parse(`Content`)).toEqual({ + type: ASTType.TPortal, + target: "target", + content: { type: ASTType.Text, value: "Content" }, + }); + }); + + test("t-portal must be in a node", async () => { + expect(() => parse(`
Content
`)).toThrowError(); + }); + + test("t-portal with t-if", async () => { + expect(parse(`Content`)).toEqual({ + condition: "condition", + content: { + content: { type: ASTType.Text, value: "Content" }, + target: "target", + type: ASTType.TPortal, + }, + tElif: null, + tElse: null, + type: ASTType.TIf, + deepRemove: true, + }); + }); }); diff --git a/tests/misc/__snapshots__/portal.test.ts.snap b/tests/misc/__snapshots__/portal.test.ts.snap index 36119058..8704aa52 100644 --- a/tests/misc/__snapshots__/portal.test.ts.snap +++ b/tests/misc/__snapshots__/portal.test.ts.snap @@ -1,5 +1,59 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Portal Add and remove portals 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { prepareList, callPortal, withKey } = helpers; + + function portalContent1(ctx, node, key = \\"\\") { + let b3 = text(\` Portal\`); + let b4 = text(ctx['portalId']); + return multi([b3, b4]); + } + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['portalIds']); + for (let i1 = 0; i1 < l_block1; i1++) { + ctx[\`portalId\`] = v_block1[i1]; + let key1 = ctx['portalId']; + c_block1[i1] = withKey(callPortal(ctx, node, key, '#outside', portalContent1), key1); + } + return list(c_block1, true); + } +}" +`; + +exports[`Portal Add and remove portals with t-foreach 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { prepareList, callPortal, withKey } = helpers; + + let block2 = createBlock(\`
\`, true); + + function portalContent1(ctx, node, key = \\"\\") { + let b4 = text(\` Portal\`); + let b5 = text(ctx['portalId']); + return multi([b4, b5]); + } + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['portalIds']); + for (let i1 = 0; i1 < l_block1; i1++) { + ctx[\`portalId\`] = v_block1[i1]; + let key1 = ctx['portalId']; + let txt1 = ctx['portalId']; + let b6 = callPortal(ctx, node, key, '#outside', portalContent1); + c_block1[i1] = withKey(block2([txt1], [b6]), key1); + } + return list(c_block1, true); + } +}" +`; + exports[`Portal Portal composed with t-slot 1`] = ` "function anonymous(bdom, helpers ) { @@ -22,14 +76,14 @@ exports[`Portal Portal composed with t-slot 2`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { callSlot } = helpers; + let { callPortal, callSlot } = helpers; - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return callSlot(ctx, node, key, 'default', false, {}); } return function template(ctx, node, key = \\"\\") { - return component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + return callPortal(ctx, node, key, '#outside', portalContent1); } }" `; @@ -52,16 +106,17 @@ exports[`Portal basic use of portal 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
1
\`); let block2 = createBlock(\`

2

\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return block2(); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b3]); } }" @@ -71,18 +126,17 @@ exports[`Portal basic use of portal in dev mode 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
1
\`); let block2 = createBlock(\`

2

\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return block2(); } return function template(ctx, node, key = \\"\\") { - const props1 = {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}} - helpers.validateProps(\`Portal\`, props1, ctx) - let b3 = component(\`Portal\`, props1, key + \`__1\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b3]); } }" @@ -92,10 +146,11 @@ exports[`Portal conditional use of Portal (with sub Component) 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; - let block2 = createBlock(\`1\`); + let block2 = createBlock(\`1\`, true); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return component(\`Child\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx); } @@ -103,9 +158,9 @@ exports[`Portal conditional use of Portal (with sub Component) 1`] = ` let b2,b4; b2 = block2(); if (ctx['state'].hasPortal) { - b4 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); + b4 = callPortal(ctx, node, key, '#outside', portalContent1); } - return multi([b2, b4]); + return multi([b2, b4], true); } }" `; @@ -128,11 +183,12 @@ exports[`Portal conditional use of Portal 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; - let block2 = createBlock(\`1\`); - let block3 = createBlock(\`

2

\`); + let block2 = createBlock(\`1\`, true); + let block3 = createBlock(\`

2

\`, true); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return block3(); } @@ -140,9 +196,33 @@ exports[`Portal conditional use of Portal 1`] = ` let b2,b4; b2 = block2(); if (ctx['state'].hasPortal) { - b4 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + b4 = callPortal(ctx, node, key, '#outside', portalContent1); } - return multi([b2, b4]); + return multi([b2, b4], true); + } +}" +`; + +exports[`Portal conditional use of Portal with div 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; + + let block2 = createBlock(\`
hasPortal
\`, true); + let block3 = createBlock(\`

thePortal

\`, true); + + function portalContent1(ctx, node, key = \\"\\") { + return block3(); + } + + return function template(ctx, node, key = \\"\\") { + let b2; + if (ctx['state'].hasPortal) { + let b4 = callPortal(ctx, node, key, '#outside', portalContent1); + b2 = block2([], [b4]); + } + return multi([b2], true); } }" `; @@ -151,17 +231,18 @@ exports[`Portal lifecycle hooks of portal sub component are properly called 1`] "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; - let block1 = createBlock(\`
\`); + let block1 = createBlock(\`
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return component(\`Child\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx); } return function template(ctx, node, key = \\"\\") { let b3; if (ctx['state'].hasChild) { - b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); + b3 = callPortal(ctx, node, key, '#outside', portalContent1); } return block1([], [b3]); } @@ -186,11 +267,12 @@ exports[`Portal portal could have dynamically no content 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); let block3 = createBlock(\`\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { let b3; if (ctx['state'].val) { let txt1 = ctx['state'].val; @@ -200,7 +282,7 @@ exports[`Portal portal could have dynamically no content 1`] = ` } return function template(ctx, node, key = \\"\\") { - let b4 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b4 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b4]); } }" @@ -210,15 +292,16 @@ exports[`Portal portal destroys on crash 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return component(\`Child\`, {error: ctx['state'].error}, key + \`__1\`, node, ctx); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b3]); } }" @@ -242,15 +325,16 @@ exports[`Portal portal with child and props 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return component(\`Child\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b3]); } }" @@ -274,12 +358,13 @@ exports[`Portal portal with dynamic body 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); let block3 = createBlock(\`\`); let block4 = createBlock(\`
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { let b3,b4; if (ctx['state'].val) { let txt1 = ctx['state'].val; @@ -291,7 +376,7 @@ exports[`Portal portal with dynamic body 1`] = ` } return function template(ctx, node, key = \\"\\") { - let b5 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b5 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b5]); } }" @@ -301,19 +386,20 @@ exports[`Portal portal with many children 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); let block3 = createBlock(\`
1
\`); let block4 = createBlock(\`

2

\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { let b3 = block3(); let b4 = block4(); return multi([b3, b4]); } return function template(ctx, node, key = \\"\\") { - let b5 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b5 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b5]); } }" @@ -323,10 +409,11 @@ exports[`Portal portal with no content 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { let b3; if (false) { b3 = text('ABC'); @@ -335,7 +422,7 @@ exports[`Portal portal with no content 1`] = ` } return function template(ctx, node, key = \\"\\") { - let b4 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b4 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b4]); } }" @@ -345,15 +432,16 @@ exports[`Portal portal with only text as content 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return text('only text'); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b3]); } }" @@ -363,16 +451,17 @@ exports[`Portal portal with target not in dom 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`
2
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return block2(); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#does-not-exist',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#does-not-exist', portalContent1); return block1([], [b3]); } }" @@ -382,15 +471,16 @@ exports[`Portal portal's parent's env is not polluted 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return component(\`Child\`, {}, key + \`__1\`, node, ctx); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b3]); } }" @@ -432,17 +522,18 @@ exports[`Portal simple catchError with portal 2`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
1
\`); let block2 = createBlock(\`

\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { let txt1 = ctx['a'].b.c; return block2([txt1]); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b3]); } }" @@ -452,16 +543,17 @@ exports[`Portal with target in template (after portal) 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
1
\`); let block2 = createBlock(\`

2

\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return block2(); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#local-target',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#local-target', portalContent1); return block1([], [b3]); } }" @@ -471,58 +563,57 @@ exports[`Portal with target in template (before portal) 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
1
\`); let block2 = createBlock(\`

2

\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return block2(); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#local-target',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#local-target', portalContent1); return block1([], [b3]); } }" `; -exports[`Portal: Props validation target is mandatory 1`] = ` +exports[`Portal: Props validation target must be a valid selector 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`
2
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return block2(); } return function template(ctx, node, key = \\"\\") { - const props1 = {slots: {'default': {__render: slot1, __ctx: ctx}}} - helpers.validateProps(\`Portal\`, props1, ctx) - let b3 = component(\`Portal\`, props1, key + \`__1\`, node, ctx); + let b3 = callPortal(ctx, node, key, ' ', portalContent1); return block1([], [b3]); } }" `; -exports[`Portal: Props validation target is not list 1`] = ` +exports[`Portal: Props validation target must be a valid selector 2 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`
2
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return block2(); } return function template(ctx, node, key = \\"\\") { - const props1 = {target: ['body'],slots: {'default': {__render: slot1, __ctx: ctx}}} - helpers.validateProps(\`Portal\`, props1, ctx) - let b3 = component(\`Portal\`, props1, key + \`__1\`, node, ctx); + let b3 = callPortal(ctx, node, key, 'aa', portalContent1); return block1([], [b3]); } }" @@ -532,15 +623,16 @@ exports[`Portal: UI/UX focus is kept across re-renders 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callPortal } = helpers; let block1 = createBlock(\`
\`); - function slot1(ctx, node, key = \\"\\") { + function portalContent1(ctx, node, key = \\"\\") { return component(\`Child\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx); } return function template(ctx, node, key = \\"\\") { - let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); + let b3 = callPortal(ctx, node, key, '#outside', portalContent1); return block1([], [b3]); } }" diff --git a/tests/misc/portal.test.ts b/tests/misc/portal.test.ts index 9baf12f3..0abcb3a1 100644 --- a/tests/misc/portal.test.ts +++ b/tests/misc/portal.test.ts @@ -8,9 +8,9 @@ import { onWillUnmount, useState, } from "../../src"; -import { Portal, xml } from "../../src/"; -import { elem, makeTestFixture, nextTick, snapshotEverything } from "../helpers"; +import { xml } from "../../src/"; import { DEV_MSG } from "../../src/app/app"; +import { elem, makeTestFixture, nextTick, snapshotEverything } from "../helpers"; let fixture: HTMLElement; let originalconsoleWarn = console.warn; @@ -52,13 +52,12 @@ afterEach(() => { describe("Portal", () => { test("basic use of portal", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
1 - +

2

-
+
`; } @@ -70,13 +69,12 @@ describe("Portal", () => { test("simple catchError with portal", async () => { class Boom extends Component { - static components = { Portal }; static template = xml`
1 - +

-
+
`; } @@ -107,13 +105,12 @@ describe("Portal", () => { test("basic use of portal in dev mode", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
1 - +

2

-
+
`; } @@ -125,12 +122,11 @@ describe("Portal", () => { test("conditional use of Portal", async () => { class Parent extends Component { - static components = { Portal }; static template = xml` 1 - +

2

-
`; + `; state = useState({ hasPortal: false }); } @@ -157,12 +153,12 @@ describe("Portal", () => { static template = xml`

`; } class Parent extends Component { - static components = { Portal, Child }; + static components = { Child }; static template = xml` 1 - + - `; + `; state = useState({ hasPortal: false, val: 1 }); } @@ -190,14 +186,13 @@ describe("Portal", () => { test("with target in template (before portal)", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
1 - +

2

-
+
`; } @@ -209,13 +204,12 @@ describe("Portal", () => { test("with target in template (after portal)", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
1 - +

2

-
+
`; } @@ -228,12 +222,11 @@ describe("Portal", () => { test("portal with target not in dom", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
- +
2
-
+
`; } @@ -269,12 +262,12 @@ describe("Portal", () => { } } class Parent extends Component { - static components = { Portal, Child }; + static components = { Child }; static template = xml`
- + - +
`; state = useState({ val: 1 }); } @@ -292,12 +285,11 @@ describe("Portal", () => { test("portal with only text as content", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
- + - +
`; } @@ -308,12 +300,11 @@ describe("Portal", () => { test("portal with no content", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
- + - +
`; } @@ -324,13 +315,12 @@ describe("Portal", () => { test("portal with many children", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
- +
1

2

-
+
`; } addOutsideDiv(fixture); @@ -340,13 +330,12 @@ describe("Portal", () => { test("portal with dynamic body", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
- +
- +
`; state = useState({ val: "ab" }); } @@ -363,12 +352,11 @@ describe("Portal", () => { test("portal could have dynamically no content", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
- + - +
`; state = useState({ val: "ab" }); } @@ -396,12 +384,12 @@ describe("Portal", () => { } class Parent extends Component { - static components = { Portal, Child }; + static components = { Child }; static template = xml`
- + - +
`; state = useState({ hasChild: false, val: 1 }); setup() { @@ -461,12 +449,12 @@ describe("Portal", () => { state = {}; } class Parent extends Component { - static components = { Portal, Child }; + static components = { Child }; static template = xml`
- + - +
`; state = { error: false }; setup() { @@ -492,12 +480,12 @@ describe("Portal", () => { `; } class Parent extends Component { - static components = { Portal, Child }; + static components = { Child }; static template = xml`
- + - +
`; } const env = {}; @@ -519,11 +507,11 @@ describe("Portal", () => { } } class Child extends Component { - static components = { Portal, Child2 }; + static components = { Child2 }; static template = xml` - + - `; +
`; } class Parent extends Component { static components = { Child, Child2 }; @@ -545,6 +533,111 @@ describe("Portal", () => { elem(childInst!).dispatchEvent(new CustomEvent("custom")); expect(steps).toEqual(["custom"]); }); + + test("Add and remove portals", async () => { + class Parent extends Component { + static template = xml` + + Portal + `; + portalIds = useState([] as any); + } + + addOutsideDiv(fixture); + const parent = await mount(Parent, fixture); + + expect(fixture.innerHTML).toBe('
'); + + parent.portalIds.push(1); + await nextTick(); + expect(fixture.innerHTML).toBe('
Portal1
'); + + parent.portalIds.push(2); + await nextTick(); + expect(fixture.innerHTML).toBe('
Portal1 Portal2
'); + + parent.portalIds.pop(); + await nextTick(); + expect(fixture.innerHTML).toBe('
Portal1
'); + + parent.portalIds.pop(); + await nextTick(); + expect(fixture.innerHTML).toBe('
'); + }); + + test("Add and remove portals with t-foreach", async () => { + class Parent extends Component { + static template = xml` + +
+ + + Portal + +
+
`; + portalIds = useState([] as any); + } + + addOutsideDiv(fixture); + const parent = await mount(Parent, fixture); + + expect(fixture.innerHTML).toBe('
'); + + parent.portalIds.push(1); + await nextTick(); + expect(fixture.innerHTML).toBe('
Portal1
1
'); + + parent.portalIds.push(2); + await nextTick(); + expect(fixture.innerHTML).toBe( + '
Portal1 Portal2
1
2
' + ); + + parent.portalIds.pop(); + await nextTick(); + expect(fixture.innerHTML).toBe('
Portal1
1
'); + + parent.portalIds.pop(); + await nextTick(); + expect(fixture.innerHTML).toBe('
'); + }); + + test("conditional use of Portal with div", async () => { + class Parent extends Component { + static template = xml` + +
+ hasPortal + +

thePortal

+
+
+
`; + + state = useState({ hasPortal: false }); + } + + addOutsideDiv(fixture); + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe('
'); + + parent.state.hasPortal = true; + await nextTick(); + expect(fixture.innerHTML).toBe( + '

thePortal

hasPortal
' + ); + + parent.state.hasPortal = false; + await nextTick(); + expect(fixture.innerHTML).toBe('
'); + + parent.state.hasPortal = true; + await nextTick(); + expect(fixture.innerHTML).toBe( + '

thePortal

hasPortal
' + ); + }); }); describe("Portal: UI/UX", () => { @@ -554,12 +647,12 @@ describe("Portal: UI/UX", () => { `; } class Parent extends Component { - static components = { Portal, Child }; + static components = { Child }; static template = xml`
- + - +
`; state = useState({ val: "ab" }); } @@ -582,17 +675,13 @@ describe("Portal: UI/UX", () => { }); describe("Portal: Props validation", () => { - test("target is mandatory", async () => { - const consoleInfo = console.info; - console.info = jest.fn(); - + test("target is mandatory 1", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
- +
2
-
+
`; } let error: Error; @@ -602,21 +691,16 @@ describe("Portal: Props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe(`Missing props 'target' (component 'Portal')`); - console.info = consoleInfo; - expect(mockConsoleWarn).toBeCalledTimes(1); + expect(error!.message).toContain(`attribute without value.`); }); - test("target is not list", async () => { - const consoleInfo = console.info; - console.info = jest.fn(); + test("target is mandatory 2", async () => { class Parent extends Component { - static components = { Portal }; static template = xml`
- +
2
-
+
`; } let error: Error; @@ -626,8 +710,44 @@ describe("Portal: Props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe(`Invalid Prop 'target' in component 'Portal'`); - console.info = consoleInfo; - expect(mockConsoleWarn).toBeCalledTimes(1); + expect(error!.message).toBe(`Unexpected token ','`); + }); + + test("target must be a valid selector", async () => { + class Parent extends Component { + static template = xml` +
+ +
2
+
+
`; + } + let error: Error; + try { + await mount(Parent, fixture, { dev: true }); + } catch (e) { + error = e as Error; + } + expect(error!).toBeDefined(); + expect(error!.message).toBe(`' ' is not a valid selector`); + }); + + test("target must be a valid selector 2", async () => { + class Parent extends Component { + static template = xml` +
+ +
2
+
+
`; + } + let error: Error; + try { + await mount(Parent, fixture, { dev: true }); + } catch (e) { + error = e as Error; + } + expect(error!).toBeDefined(); + expect(error!.message).toBe(`invalid portal target`); }); });