Compare commits

..

16 Commits

Author SHA1 Message Date
Nicolas Bayet d0cdc489b1 up 2025-10-02 15:22:13 +02:00
Nicolas Bayet a309ec498b up 2025-10-01 13:57:58 +02:00
Nicolas Bayet 1ddf2ceff9 up 2025-10-01 13:55:40 +02:00
Nicolas Bayet 38574a1f3a no reactivity in setup 2025-09-29 18:50:26 +02:00
Nicolas Bayet 6f2600ba5f md 2025-09-29 18:50:16 +02:00
Nicolas Bayet ed9db6ea2a withoutReactivity 2025-09-29 13:34:58 +02:00
Nicolas Bayet 0c1f9f3ffa fix for dropdown 2025-09-28 14:57:49 +02:00
Nicolas Bayet 4385969e2e up 2025-09-26 18:58:15 +02:00
Nicolas Bayet e31d195bb7 up 2025-09-26 18:38:40 +02:00
Nicolas Bayet 836fdada7f up 2025-09-26 18:31:03 +02:00
Nicolas Bayet 22580661c4 up 2025-09-26 18:27:22 +02:00
Nicolas Bayet adb9d405bb up 2025-09-26 18:08:15 +02:00
Nicolas Bayet 8d12bf17dc comment 2025-09-26 16:06:50 +02:00
Nicolas Bayet c74c66ab3b up 2025-09-26 15:37:51 +02:00
Romeo Fragomeli 5187f01c44 [REL] v2.8.1
# v2.8.1

 - [FIX] parser, code_generator: wrapped children recognition
 - [DOC] `slots` prop validation
2025-09-23 09:17:54 +02:00
Mathieu Duckerts-Antoine 521111644c [FIX] parser, code_generator: wrapped children recognition
Several directives (t-key, t-log, t-translation, …) are represented by
wrapper ASTs that contain another AST or null.
Because these wrappers don't share the same type as their children,
various AST type checks were broken.

This commit addresses those issues by:

- Commuting Translation / TranslationContext and Multi ASTs so that
  Multi children are spread as expected (see parseChildren).

- Parsing the t-key directive before t-esc / t-out (in line with https://github.com/odoo/owl/pull/1685).

- Ensuring wrappers around TSet ASTs are recognized as having no
  representation, so compileMulti properly discards children equivalent
  to TSet.
2025-09-22 16:59:47 +02:00
35 changed files with 8761 additions and 5401 deletions
+72 -26
View File
@@ -4539,7 +4539,7 @@ class CodeGenerator {
const isNewBlock = !block || forceNewBlock; const isNewBlock = !block || forceNewBlock;
let codeIdx = this.target.code.length; let codeIdx = this.target.code.length;
if (isNewBlock) { if (isNewBlock) {
const n = ast.content.filter((c) => c.type !== 6 /* TSet */).length; const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
let result = null; let result = null;
if (n <= 1) { if (n <= 1) {
for (let child of ast.content) { for (let child of ast.content) {
@@ -4553,15 +4553,15 @@ class CodeGenerator {
let index = 0; let index = 0;
for (let i = 0, l = ast.content.length; i < l; i++) { for (let i = 0, l = ast.content.length; i < l; i++) {
const child = ast.content[i]; const child = ast.content[i];
const isTSet = child.type === 6 /* TSet */; const forceNewBlock = !child.hasNoRepresentation;
const subCtx = createContext(ctx, { const subCtx = createContext(ctx, {
block, block,
index, index,
forceNewBlock: !isTSet, forceNewBlock,
isLast: ctx.isLast && i === l - 1, isLast: ctx.isLast && i === l - 1,
}); });
this.compileAST(child, subCtx); this.compileAST(child, subCtx);
if (!isTSet) { if (forceNewBlock) {
index++; index++;
} }
} }
@@ -4979,9 +4979,9 @@ function parseNode(node, ctx) {
parseTCallBlock(node) || parseTCallBlock(node) ||
parseTTranslation(node, ctx) || parseTTranslation(node, ctx) ||
parseTTranslationContext(node, ctx) || parseTTranslationContext(node, ctx) ||
parseTKey(node, ctx) ||
parseTEscNode(node, ctx) || parseTEscNode(node, ctx) ||
parseTOutNode(node, ctx) || parseTOutNode(node, ctx) ||
parseTKey(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
parseDOMNode(node, ctx) || parseDOMNode(node, ctx) ||
@@ -5049,19 +5049,29 @@ function parseTCustom(node, ctx) {
function parseTDebugLog(node, ctx) { function parseTDebugLog(node, ctx) {
if (node.hasAttribute("t-debug")) { if (node.hasAttribute("t-debug")) {
node.removeAttribute("t-debug"); node.removeAttribute("t-debug");
return { const content = parseNode(node, ctx);
const ast = {
type: 12 /* TDebug */, type: 12 /* TDebug */,
content: parseNode(node, ctx), content,
}; };
if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
if (node.hasAttribute("t-log")) { if (node.hasAttribute("t-log")) {
const expr = node.getAttribute("t-log"); const expr = node.getAttribute("t-log");
node.removeAttribute("t-log"); node.removeAttribute("t-log");
return { const content = parseNode(node, ctx);
const ast = {
type: 13 /* TLog */, type: 13 /* TLog */,
expr, expr,
content: parseNode(node, ctx), content,
}; };
if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
return null; return null;
} }
@@ -5291,11 +5301,19 @@ function parseTKey(node, ctx) {
} }
const key = node.getAttribute("t-key"); const key = node.getAttribute("t-key");
node.removeAttribute("t-key"); node.removeAttribute("t-key");
const body = parseNode(node, ctx); const content = parseNode(node, ctx);
if (!body) { if (!content) {
return null; return null;
} }
return { type: 10 /* TKey */, expr: key, content: body }; const ast = {
type: 10 /* TKey */,
expr: key,
content,
};
if (content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// t-call // t-call
@@ -5404,7 +5422,7 @@ function parseTSetNode(node, ctx) {
if (node.textContent !== node.innerHTML) { if (node.textContent !== node.innerHTML) {
body = parseChildren(node, ctx); body = parseChildren(node, ctx);
} }
return { type: 6 /* TSet */, name, value, defaultValue, body }; return { type: 6 /* TSet */, name, value, defaultValue, body, hasNoRepresentation: true };
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Components // Components
@@ -5583,30 +5601,51 @@ function parseTSlot(node, ctx) {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Translation // Translation
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function wrapInTTranslationAST(r) {
const ast = { type: 16 /* TTranslation */, content: r };
if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
function parseTTranslation(node, ctx) { function parseTTranslation(node, ctx) {
if (node.getAttribute("t-translation") !== "off") { if (node.getAttribute("t-translation") !== "off") {
return null; return null;
} }
node.removeAttribute("t-translation"); node.removeAttribute("t-translation");
return { const result = parseNode(node, ctx);
type: 16 /* TTranslation */, if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
content: parseNode(node, ctx), const children = result.content.map(wrapInTTranslationAST);
}; return makeASTMulti(children);
}
return wrapInTTranslationAST(result);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Translation Context // Translation Context
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function wrapInTTranslationContextAST(r, translationCtx) {
const ast = {
type: 17 /* TTranslationContext */,
content: r,
translationCtx,
};
if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
function parseTTranslationContext(node, ctx) { function parseTTranslationContext(node, ctx) {
const translationCtx = node.getAttribute("t-translation-context"); const translationCtx = node.getAttribute("t-translation-context");
if (!translationCtx) { if (!translationCtx) {
return null; return null;
} }
node.removeAttribute("t-translation-context"); node.removeAttribute("t-translation-context");
return { const result = parseNode(node, ctx);
type: 17 /* TTranslationContext */, if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
content: parseNode(node, ctx), const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
translationCtx, return makeASTMulti(children);
}; }
return wrapInTTranslationContextAST(result, translationCtx);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Portal // Portal
@@ -5651,6 +5690,13 @@ function parseChildren(node, ctx) {
} }
return children; return children;
} }
function makeASTMulti(children) {
const ast = { type: 3 /* Multi */, content: children };
if (children.every((c) => c.hasNoRepresentation)) {
ast.hasNoRepresentation = true;
}
return ast;
}
/** /**
* Parse all the child nodes of a given node and return an ast if possible. * Parse all the child nodes of a given node and return an ast if possible.
* In the case there are multiple children, they are wrapped in a astmulti. * In the case there are multiple children, they are wrapped in a astmulti.
@@ -5663,7 +5709,7 @@ function parseChildNodes(node, ctx) {
case 1: case 1:
return children[0]; return children[0];
default: default:
return { type: 3 /* Multi */, content: children }; return makeASTMulti(children);
} }
} }
/** /**
@@ -5767,7 +5813,7 @@ function compile(template, options = {
} }
// do not modify manually. This file is generated by the release script. // do not modify manually. This file is generated by the release script.
const version = "2.8.0"; const version = "2.8.1";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Scheduler // Scheduler
@@ -6238,6 +6284,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, htmlEscape, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml }; export { App, Component, EventBus, OwlError, __info__, batched, blockDom, htmlEscape, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
__info__.date = '2025-06-30T12:46:06.424Z'; __info__.date = '2025-09-23T07:17:45.055Z';
__info__.hash = 'b620502'; __info__.hash = '5211116';
__info__.url = 'https://github.com/odoo/owl'; __info__.url = 'https://github.com/odoo/owl';
+6739 -3859
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.8.0", "version": "2.8.1",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"module": "dist/owl.es.js", "module": "dist/owl.es.js",
@@ -22,7 +22,7 @@
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production", "build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production", "build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
"test": "jest", "test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000", "test:debug": "node node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py", "playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
"playground": "npm run build && npm run playground:serve", "playground": "npm run build && npm run playground:serve",
+18
View File
@@ -0,0 +1,18 @@
# encountered issues
## dropdown issue
- there was a problem that writing in a state while the effect was updated.
- the tracking of signal being written were dropped because we cleared it
after re-running the effect that made a write.
- solution: clear the tracked signal before re-executing the effects
- reading signal A while also writing signal A makes an infinite loop
- current solution: use toRaw in order to not track the read
- possible better solution to explore: do not track read if there is a write in a effect.
## website issue
- a rpc request was made on onWillStart, onWillStart was tracking reads. (see WebsiteBuilderClientAction)
- The read subsequently made a write, that re-triggered the onWillStart.
- A similar situation happened with onWillUpdateProps (see Transition)
- solution: prevent tracking reads in onWillStart and onWillUpdateProps
# future
- worker for computation?
- cap'n web
+24
View File
@@ -1,4 +1,28 @@
export type ExecutionContext = {
onReadAtom: (atom: Atom) => void;
unsubcribe?: (scheduledContexts: Set<ExecutionContext>) => void;
update?: Function;
atoms?: Set<Atom>;
meta?: any;
// getParent: () => ExecutionContext | undefined;
// getChildren: () => ExecutionContext[];
// schedule: () => void;
};
export type customDirectives = Record< export type customDirectives = Record<
string, string,
(node: Element, value: string, modifier: string[]) => void (node: Element, value: string, modifier: string[]) => void
>; >;
export type Atom = {
executionContexts: Set<ExecutionContext>;
dependents: Set<DerivedAtom>;
getValue: () => any;
};
export type OldValue = any;
export type DerivedAtom = Atom & {
dependencies: Map<Atom, OldValue>;
computed: boolean;
};
+4 -4
View File
@@ -995,7 +995,7 @@ export class CodeGenerator {
const isNewBlock = !block || forceNewBlock; const isNewBlock = !block || forceNewBlock;
let codeIdx = this.target.code.length; let codeIdx = this.target.code.length;
if (isNewBlock) { if (isNewBlock) {
const n = ast.content.filter((c) => c.type !== ASTType.TSet).length; const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
let result: string | null = null; let result: string | null = null;
if (n <= 1) { if (n <= 1) {
for (let child of ast.content) { for (let child of ast.content) {
@@ -1009,15 +1009,15 @@ export class CodeGenerator {
let index = 0; let index = 0;
for (let i = 0, l = ast.content.length; i < l; i++) { for (let i = 0, l = ast.content.length; i < l; i++) {
const child = ast.content[i]; const child = ast.content[i];
const isTSet = child.type === ASTType.TSet; const forceNewBlock = !child.hasNoRepresentation;
const subCtx = createContext(ctx, { const subCtx = createContext(ctx, {
block, block,
index, index,
forceNewBlock: !isTSet, forceNewBlock,
isLast: ctx.isLast && i === l - 1, isLast: ctx.isLast && i === l - 1,
}); });
this.compileAST(child, subCtx); this.compileAST(child, subCtx);
if (!isTSet) { if (forceNewBlock) {
index++; index++;
} }
} }
+93 -38
View File
@@ -31,12 +31,17 @@ export const enum ASTType {
TPortal, TPortal,
} }
export interface ASTText { export interface BaseAST {
type: ASTType;
hasNoRepresentation?: true;
}
export interface ASTText extends BaseAST {
type: ASTType.Text; type: ASTType.Text;
value: string; value: string;
} }
export interface ASTComment { export interface ASTComment extends BaseAST {
type: ASTType.Comment; type: ASTType.Comment;
value: string; value: string;
} }
@@ -52,7 +57,7 @@ interface TModelInfo {
specialInitTargetAttr: string | null; specialInitTargetAttr: string | null;
} }
export interface ASTDomNode { export interface ASTDomNode extends BaseAST {
type: ASTType.DomNode; type: ASTType.DomNode;
tag: string; tag: string;
content: AST[]; content: AST[];
@@ -65,24 +70,24 @@ export interface ASTDomNode {
ns: string | null; ns: string | null;
} }
export interface ASTMulti { export interface ASTMulti extends BaseAST {
type: ASTType.Multi; type: ASTType.Multi;
content: AST[]; content: AST[];
} }
export interface ASTTEsc { export interface ASTTEsc extends BaseAST {
type: ASTType.TEsc; type: ASTType.TEsc;
expr: string; expr: string;
defaultValue: string; defaultValue: string;
} }
export interface ASTTOut { export interface ASTTOut extends BaseAST {
type: ASTType.TOut; type: ASTType.TOut;
expr: string; expr: string;
body: AST[] | null; body: AST[] | null;
} }
export interface ASTTif { export interface ASTTif extends BaseAST {
type: ASTType.TIf; type: ASTType.TIf;
condition: string; condition: string;
content: AST; content: AST;
@@ -90,15 +95,16 @@ export interface ASTTif {
tElse: AST | null; tElse: AST | null;
} }
export interface ASTTSet { export interface ASTTSet extends BaseAST {
type: ASTType.TSet; type: ASTType.TSet;
name: string; name: string;
value: string | null; // value defined in attribute value: string | null; // value defined in attribute
defaultValue: string | null; // value defined in body, if text defaultValue: string | null; // value defined in body, if text
body: AST[] | null; // content of body if not text body: AST[] | null; // content of body if not text
hasNoRepresentation: true;
} }
export interface ASTTForEach { export interface ASTTForEach extends BaseAST {
type: ASTType.TForEach; type: ASTType.TForEach;
collection: string; collection: string;
elem: string; elem: string;
@@ -111,13 +117,13 @@ export interface ASTTForEach {
key: string | null; key: string | null;
} }
export interface ASTTKey { export interface ASTTKey extends BaseAST {
type: ASTType.TKey; type: ASTType.TKey;
expr: string; expr: string;
content: AST; content: AST;
} }
export interface ASTTCall { export interface ASTTCall extends BaseAST {
type: ASTType.TCall; type: ASTType.TCall;
name: string; name: string;
body: AST[] | null; body: AST[] | null;
@@ -132,7 +138,7 @@ interface SlotDefinition {
attrsTranslationCtx: Attrs | null; attrsTranslationCtx: Attrs | null;
} }
export interface ASTComponent { export interface ASTComponent extends BaseAST {
type: ASTType.TComponent; type: ASTType.TComponent;
name: string; name: string;
isDynamic: boolean; isDynamic: boolean;
@@ -143,7 +149,7 @@ export interface ASTComponent {
slots: { [name: string]: SlotDefinition } | null; slots: { [name: string]: SlotDefinition } | null;
} }
export interface ASTSlot { export interface ASTSlot extends BaseAST {
type: ASTType.TSlot; type: ASTType.TSlot;
name: string; name: string;
attrs: Attrs | null; attrs: Attrs | null;
@@ -152,34 +158,34 @@ export interface ASTSlot {
defaultContent: AST | null; defaultContent: AST | null;
} }
export interface ASTTCallBlock { export interface ASTTCallBlock extends BaseAST {
type: ASTType.TCallBlock; type: ASTType.TCallBlock;
name: string; name: string;
} }
export interface ASTDebug { export interface ASTDebug extends BaseAST {
type: ASTType.TDebug; type: ASTType.TDebug;
content: AST | null; content: AST | null;
} }
export interface ASTLog { export interface ASTLog extends BaseAST {
type: ASTType.TLog; type: ASTType.TLog;
expr: string; expr: string;
content: AST | null; content: AST | null;
} }
export interface ASTTranslation { export interface ASTTranslation extends BaseAST {
type: ASTType.TTranslation; type: ASTType.TTranslation;
content: AST | null; content: AST | null;
} }
export interface ASTTranslationContext { export interface ASTTranslationContext extends BaseAST {
type: ASTType.TTranslationContext; type: ASTType.TTranslationContext;
content: AST | null; content: AST | null;
translationCtx: string; translationCtx: string;
} }
export interface ASTTPortal { export interface ASTTPortal extends BaseAST {
type: ASTType.TPortal; type: ASTType.TPortal;
target: string; target: string;
content: AST; content: AST;
@@ -255,9 +261,9 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
parseTCallBlock(node, ctx) || parseTCallBlock(node, ctx) ||
parseTTranslation(node, ctx) || parseTTranslation(node, ctx) ||
parseTTranslationContext(node, ctx) || parseTTranslationContext(node, ctx) ||
parseTKey(node, ctx) ||
parseTEscNode(node, ctx) || parseTEscNode(node, ctx) ||
parseTOutNode(node, ctx) || parseTOutNode(node, ctx) ||
parseTKey(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
parseDOMNode(node, ctx) || parseDOMNode(node, ctx) ||
@@ -334,20 +340,30 @@ function parseTCustom(node: Element, ctx: ParsingContext): AST | null {
function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null { function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
if (node.hasAttribute("t-debug")) { if (node.hasAttribute("t-debug")) {
node.removeAttribute("t-debug"); node.removeAttribute("t-debug");
return { const content = parseNode(node, ctx);
const ast: ASTDebug = {
type: ASTType.TDebug, type: ASTType.TDebug,
content: parseNode(node, ctx), content,
}; };
if (content?.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
if (node.hasAttribute("t-log")) { if (node.hasAttribute("t-log")) {
const expr = node.getAttribute("t-log")!; const expr = node.getAttribute("t-log")!;
node.removeAttribute("t-log"); node.removeAttribute("t-log");
return { const content = parseNode(node, ctx);
const ast: ASTLog = {
type: ASTType.TLog, type: ASTType.TLog,
expr, expr,
content: parseNode(node, ctx), content,
}; };
if (content?.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
return null; return null;
} }
@@ -598,11 +614,19 @@ function parseTKey(node: Element, ctx: ParsingContext): AST | null {
} }
const key = node.getAttribute("t-key")!; const key = node.getAttribute("t-key")!;
node.removeAttribute("t-key"); node.removeAttribute("t-key");
const body = parseNode(node, ctx); const content = parseNode(node, ctx);
if (!body) { if (!content) {
return null; return null;
} }
return { type: ASTType.TKey, expr: key, content: body }; const ast: ASTTKey = {
type: ASTType.TKey,
expr: key,
content,
};
if (content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -724,7 +748,7 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
if (node.textContent !== node.innerHTML) { if (node.textContent !== node.innerHTML) {
body = parseChildren(node, ctx); body = parseChildren(node, ctx);
} }
return { type: ASTType.TSet, name, value, defaultValue, body }; return { type: ASTType.TSet, name, value, defaultValue, body, hasNoRepresentation: true };
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -916,32 +940,55 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
// Translation // Translation
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function wrapInTTranslationAST(r: AST | null) {
const ast: ASTTranslation = { type: ASTType.TTranslation, content: r };
if (r?.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null { function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
if (node.getAttribute("t-translation") !== "off") { if (node.getAttribute("t-translation") !== "off") {
return null; return null;
} }
node.removeAttribute("t-translation"); node.removeAttribute("t-translation");
return { const result = parseNode(node, ctx);
type: ASTType.TTranslation, if (result?.type === ASTType.Multi) {
content: parseNode(node, ctx), const children = result.content.map(wrapInTTranslationAST);
}; return makeASTMulti(children);
}
return wrapInTTranslationAST(result);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Translation Context // Translation Context
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function wrapInTTranslationContextAST(r: AST | null, translationCtx: string) {
const ast: ASTTranslationContext = {
type: ASTType.TTranslationContext,
content: r,
translationCtx,
};
if (r?.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null { function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null {
const translationCtx = node.getAttribute("t-translation-context"); const translationCtx = node.getAttribute("t-translation-context");
if (!translationCtx) { if (!translationCtx) {
return null; return null;
} }
node.removeAttribute("t-translation-context"); node.removeAttribute("t-translation-context");
return { const result = parseNode(node, ctx);
type: ASTType.TTranslationContext, if (result?.type === ASTType.Multi) {
content: parseNode(node, ctx), const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
translationCtx, return makeASTMulti(children);
}; }
return wrapInTTranslationContextAST(result, translationCtx);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -990,6 +1037,14 @@ function parseChildren(node: Element, ctx: ParsingContext): AST[] {
return children; return children;
} }
function makeASTMulti(children: AST[]) {
const ast: ASTMulti = { type: ASTType.Multi, content: children };
if (children.every((c) => c.hasNoRepresentation)) {
ast.hasNoRepresentation = true;
}
return ast;
}
/** /**
* Parse all the child nodes of a given node and return an ast if possible. * Parse all the child nodes of a given node and return an ast if possible.
* In the case there are multiple children, they are wrapped in a astmulti. * In the case there are multiple children, they are wrapped in a astmulti.
@@ -1002,7 +1057,7 @@ function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
case 1: case 1:
return children[0]; return children[0];
default: default:
return { type: ASTType.Multi, content: children }; return makeASTMulti(children);
} }
} }
+46
View File
@@ -0,0 +1,46 @@
export type TaskContext = { isCancelled: boolean; cancel: () => void; meta: Record<string, any> };
export const taskContextStack: TaskContext[] = [];
export function getTaskContext() {
return taskContextStack[taskContextStack.length - 1];
}
export function makeTaskContext(): TaskContext {
let isCancelled = false;
return {
get isCancelled() {
return isCancelled;
},
cancel() {
isCancelled = true;
},
meta: {},
};
}
export function useTaskContext(ctx?: TaskContext) {
ctx ??= makeTaskContext();
taskContextStack.push(ctx);
return {
ctx,
cleanup: () => {
taskContextStack.pop();
},
};
}
export function pushTaskContext(context: TaskContext) {
taskContextStack.push(context);
}
export function popTaskContext() {
taskContextStack.pop();
}
export function taskEffect(fn: Function) {
const { ctx, cleanup } = useTaskContext();
fn();
cleanup();
return ctx;
}
+46 -33
View File
@@ -1,12 +1,13 @@
import { OwlError } from "../common/owl_error";
import { Atom, ExecutionContext } from "../common/types";
import type { App, Env } from "./app"; import type { App, Env } from "./app";
import { BDom, VNode } from "./blockdom"; import { BDom, VNode } from "./blockdom";
import { makeTaskContext, TaskContext } from "./cancellableContext";
import { Component, ComponentConstructor, Props } from "./component"; import { Component, ComponentConstructor, Props } from "./component";
import { fibersInError } from "./error_handling"; import { fibersInError } from "./error_handling";
import { OwlError } from "../common/owl_error";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers"; import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity"; import { addAtomToContext, reactive, targets, withoutReactivity } from "./reactivity";
import { STATUS } from "./status"; import { STATUS } from "./status";
import { batched, Callback } from "./utils";
let currentNode: ComponentNode | null = null; let currentNode: ComponentNode | null = null;
@@ -42,7 +43,7 @@ function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>)
// Integration with reactivity system (useState) // Integration with reactivity system (useState)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>(); // const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/** /**
* Creates a reactive object that will be observed by the current component. * Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the * Reading data from the returned object (eg during rendering) will cause the
@@ -54,15 +55,7 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
* @see reactive * @see reactive
*/ */
export function useState<T extends object>(state: T): T { export function useState<T extends object>(state: T): T {
const node = getCurrent(); return reactive(state);
let render = batchedRenderFunctions.get(node)!;
if (!render) {
render = batched(node.render.bind(node, false));
batchedRenderFunctions.set(node, render);
// manual implementation of onWillDestroy to break cyclic dependency
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
}
return reactive(state, render);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -96,6 +89,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
willPatch: LifecycleHook[] = []; willPatch: LifecycleHook[] = [];
patched: LifecycleHook[] = []; patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = []; willDestroy: LifecycleHook[] = [];
taskContext: TaskContext;
executionContext: ExecutionContext;
constructor( constructor(
C: ComponentConstructor<P, E>, C: ComponentConstructor<P, E>,
@@ -109,6 +104,15 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
this.parent = parent; this.parent = parent;
this.props = props; this.props = props;
this.parentKey = parentKey; this.parentKey = parentKey;
this.taskContext = makeTaskContext();
this.executionContext = {
meta: this,
update: () => {
this.render(false);
},
onReadAtom: (atom: Atom) => addAtomToContext(atom, this.executionContext),
atoms: new Set<Atom>(),
};
const defaultProps = C.defaultProps; const defaultProps = C.defaultProps;
props = Object.assign({}, props); props = Object.assign({}, props);
if (defaultProps) { if (defaultProps) {
@@ -116,16 +120,18 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
const env = (parent && parent.childEnv) || app.env; const env = (parent && parent.childEnv) || app.env;
this.childEnv = env; this.childEnv = env;
for (const key in props) { // for (const key in props) {
const prop = props[key]; // const prop = props[key];
if (prop && typeof prop === "object" && targets.has(prop)) { // if (prop && typeof prop === "object" && targets.has(prop)) {
props[key] = useState(prop); // props[key] = useState(prop);
} // }
} // }
this.component = new C(props, env, this); this.component = new C(props, env, this);
const ctx = Object.assign(Object.create(this.component), { this: this.component }); const ctx = Object.assign(Object.create(this.component), { this: this.component });
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this); this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
this.component.setup(); withoutReactivity(() => {
this.component.setup();
});
currentNode = null; currentNode = null;
} }
@@ -142,7 +148,11 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
const component = this.component; const component = this.component;
try { try {
await Promise.all(this.willStart.map((f) => f.call(component))); let prom: Promise<any[]>;
withoutReactivity(() => {
prom = Promise.all(this.willStart.map((f) => f.call(component)));
});
await prom!;
} catch (e) { } catch (e) {
this.app.handleError({ node: this, error: e }); this.app.handleError({ node: this, error: e });
return; return;
@@ -258,15 +268,18 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
currentNode = this; currentNode = this;
for (const key in props) { // for (const key in props) {
const prop = props[key]; // const prop = props[key];
if (prop && typeof prop === "object" && targets.has(prop)) { // if (prop && typeof prop === "object" && targets.has(prop)) {
props[key] = useState(prop); // props[key] = useState(prop);
} // }
} // }
currentNode = null; currentNode = null;
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props))); let prom: Promise<any[]>;
await prom; withoutReactivity(() => {
prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
});
await prom!;
if (fiber !== this.fiber) { if (fiber !== this.fiber) {
return; return;
} }
@@ -384,8 +397,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
return this.component.constructor.name; return this.component.constructor.name;
} }
get subscriptions(): ReturnType<typeof getSubscriptions> { // get subscriptions(): ReturnType<typeof getSubscriptions> {
const render = batchedRenderFunctions.get(this); // const render = batchedRenderFunctions.get(this);
return render ? getSubscriptions(render) : []; // return render ? getSubscriptions(render) : [];
} // }
} }
+26
View File
@@ -0,0 +1,26 @@
import { ExecutionContext } from "../common/types";
export const executionContexts: ExecutionContext[] = [];
(window as any).executionContexts = executionContexts;
// export const scheduledContexts: Set<ExecutionContext> = new Set();
export function getExecutionContext() {
return executionContexts[executionContexts.length - 1];
}
export function pushExecutionContext(context: ExecutionContext) {
executionContexts.push(context);
}
export function popExecutionContext() {
executionContexts.pop();
}
// export function makeExecutionContext({ update, meta }: { update: () => void; meta?: any }) {
// const executionContext: ExecutionContext = {
// update,
// atoms: new Set(),
// meta: meta || {},
// };
// return executionContext;
// }
+6
View File
@@ -3,6 +3,8 @@ import type { ComponentNode } from "./component_node";
import { fibersInError } from "./error_handling"; import { fibersInError } from "./error_handling";
import { OwlError } from "../common/owl_error"; import { OwlError } from "../common/owl_error";
import { STATUS } from "./status"; import { STATUS } from "./status";
import { popTaskContext, pushTaskContext } from "./cancellableContext";
import { popExecutionContext, pushExecutionContext } from "./executionContext";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber { export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber; let current = node.fiber;
@@ -133,12 +135,16 @@ export class Fiber {
const node = this.node; const node = this.node;
const root = this.root; const root = this.root;
if (root) { if (root) {
pushTaskContext(node.taskContext);
pushExecutionContext(node.executionContext);
try { try {
(this.bdom as any) = true; (this.bdom as any) = true;
this.bdom = node.renderFn(); this.bdom = node.renderFn();
} catch (e) { } catch (e) {
node.app.handleError({ node, error: e }); node.app.handleError({ node, error: e });
} }
popExecutionContext();
popTaskContext();
root.setCounter(root.counter - 1); root.setCounter(root.counter - 1);
} }
} }
+27 -5
View File
@@ -1,5 +1,6 @@
import type { Env } from "./app"; import type { Env } from "./app";
import { getCurrent } from "./component_node"; import { getCurrent } from "./component_node";
import { popExecutionContext, pushExecutionContext } from "./executionContext";
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks"; import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
import { inOwnerDocument } from "./utils"; import { inOwnerDocument } from "./utils";
@@ -86,22 +87,43 @@ export function useEffect<T extends unknown[]>(
effect: Effect<T>, effect: Effect<T>,
computeDependencies: () => [...T] = () => [NaN] as never computeDependencies: () => [...T] = () => [NaN] as never
) { ) {
const context = getCurrent().component.__owl__.executionContext;
let cleanup: (() => void) | void; let cleanup: (() => void) | void;
let dependencies: T; let dependencies: T;
const runEffect = () => {
pushExecutionContext(context);
try {
cleanup = effect(...dependencies);
} finally {
popExecutionContext();
}
};
const computeDependenciesWithContext = () => {
pushExecutionContext(context);
let r: any;
try {
r = computeDependencies();
} finally {
popExecutionContext();
}
return r;
};
onMounted(() => { onMounted(() => {
dependencies = computeDependencies(); dependencies = computeDependenciesWithContext();
cleanup = effect(...dependencies); runEffect();
}); });
onPatched(() => { onPatched(() => {
const newDeps = computeDependencies(); const newDeps = computeDependenciesWithContext();
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]); const shouldReapply = newDeps.some((val: any, i: number) => val !== dependencies[i]);
if (shouldReapply) { if (shouldReapply) {
dependencies = newDeps; dependencies = newDeps;
if (cleanup) { if (cleanup) {
cleanup(); cleanup();
} }
cleanup = effect(...dependencies); runEffect();
} }
}); });
+1 -1
View File
@@ -39,7 +39,7 @@ export { Component } from "./component";
export type { ComponentConstructor } from "./component"; export type { ComponentConstructor } from "./component";
export { useComponent, useState } from "./component_node"; export { useComponent, useState } from "./component_node";
export { status } from "./status"; export { status } from "./status";
export { reactive, markRaw, toRaw } from "./reactivity"; export { reactive, markRaw, toRaw, effect, withoutReactivity } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks"; export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils"; export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
export { export {
+274 -145
View File
@@ -1,13 +1,9 @@
import type { Callback } from "./utils";
import { OwlError } from "../common/owl_error"; import { OwlError } from "../common/owl_error";
import { ExecutionContext, Atom, DerivedAtom, OldValue } from "../common/types";
import { getExecutionContext, popExecutionContext, pushExecutionContext } from "./executionContext";
// Special key to subscribe to, to be notified of key creation/deletion // Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes"); const KEYCHANGES = Symbol("Key changes");
// Used to specify the absence of a callback, can be used as WeakMap key but
// should only be used as a sentinel value and never called.
const NO_CALLBACK = () => {
throw new Error("Called NO_CALLBACK. Owl is broken, please report this to the maintainers.");
};
// The following types only exist to signify places where objects are expected // The following types only exist to signify places where objects are expected
// to be reactive or not, they provide no type checking benefit over "object" // to be reactive or not, they provide no type checking benefit over "object"
@@ -55,8 +51,8 @@ function canBeMadeReactive(value: any): boolean {
* @param value the value make reactive * @param value the value make reactive
* @returns a reactive for the given object when possible, the original otherwise * @returns a reactive for the given object when possible, the original otherwise
*/ */
function possiblyReactive(val: any, cb: Callback) { function possiblyReactive(val: any) {
return canBeMadeReactive(val) ? reactive(val, cb) : val; return canBeMadeReactive(val) ? reactive(val) : val;
} }
const skipped = new WeakSet<Target>(); const skipped = new WeakSet<Target>();
@@ -81,7 +77,37 @@ export function toRaw<T extends Target, U extends Reactive<T>>(value: U | T): T
return targets.has(value) ? (targets.get(value) as T) : value; return targets.has(value) ? (targets.get(value) as T) : value;
} }
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>(); const targetToKeysToAtomItem = new WeakMap<Target, Map<PropertyKey, Atom>>();
const scheduledAtoms = new Set<Atom>();
function makeAtom(getValue: () => any): Atom {
const atom: Atom = {
executionContexts: new Set<ExecutionContext>(),
dependents: new Set<Atom>(),
// getValue,
};
return atom;
}
function getTargetKeyAtom(target: Target, key: PropertyKey): Atom {
let keyToAtomItem: Map<PropertyKey, Atom> = targetToKeysToAtomItem.get(target)!;
if (!keyToAtomItem) {
keyToAtomItem = new Map();
targetToKeysToAtomItem.set(target, keyToAtomItem);
}
let atom = keyToAtomItem.get(key)!;
if (!atom) {
atom = makeAtom(() => Reflect.get(target, key));
keyToAtomItem.set(key, atom);
}
return atom;
}
export function addAtomToContext(atom: Atom, executionContext: ExecutionContext) {
executionContext.atoms.add(atom);
atom.executionContexts.add(executionContext);
}
/** /**
* Observes a given key on a target with an callback. The callback will be * Observes a given key on a target with an callback. The callback will be
* called when the given key changes on the target. * called when the given key changes on the target.
@@ -91,23 +117,73 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callbac
* or deletion) * or deletion)
* @param callback the function to call when the key changes * @param callback the function to call when the key changes
*/ */
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void { function onReadTargetKey(target: Target, key: PropertyKey, receiver: any): void {
if (callback === NO_CALLBACK) { const executionContext = getExecutionContext();
return; executionContext?.onReadAtom(getTargetKeyAtom(target, key));
}
if (!targetToKeysToCallbacks.get(target)) {
targetToKeysToCallbacks.set(target, new Map());
}
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
if (!keyToCallbacks.get(key)) {
keyToCallbacks.set(key, new Set());
}
keyToCallbacks.get(key)!.add(callback);
if (!callbacksToTargets.has(callback)) {
callbacksToTargets.set(callback, new Set());
}
callbacksToTargets.get(callback)!.add(target);
} }
let scheduled = false;
function scheduleAtom(atom: Atom) {
scheduledAtoms.add(atom);
// batched(processAtoms)();
if (scheduled) return;
scheduled = true;
Promise.resolve().then(() => {
scheduled = false;
processAtoms();
});
}
function processDerivedAtoms() {
const processedAtoms = new Set<Atom>();
for (const atom of scheduledAtoms) {
for (const dep of atom.dependents) {
if (processedAtoms.has(dep)) continue;
dep.computed = false;
processedAtoms.add(dep);
}
}
}
function processAtoms() {
processDerivedAtoms();
const scheduledContexts = new Set(
[...scheduledAtoms.values()].map((s) => [...s.executionContexts]).flat()
);
// schedule before context.update in case there is write operations during update
// todo: add a test in case there is write operations during update the test
// will break is scheduledAtoms.clear(); is called after context.update();
// that writes
scheduledAtoms.clear();
for (const ctx of [...scheduledContexts]) {
removeAtomsFromContext(ctx);
// custom unsubscribe depending on the context.
// scheduledContexts might be updated while we're iterating over it.
ctx.unsubcribe?.(scheduledContexts);
}
for (const context of scheduledContexts) {
pushExecutionContext(context);
try {
context.update?.();
} finally {
popExecutionContext();
}
}
}
/**
* Notify Reactives that are observing a given target that a key has changed on
}
});
};
for (const context of executionContexts) {
context.update();
}
}
/** /**
* Notify Reactives that are observing a given target that a key has changed on * Notify Reactives that are observing a given target that a key has changed on
* the target. * the target.
@@ -117,66 +193,21 @@ function observeTargetKey(target: Target, key: PropertyKey, callback: Callback):
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
* or deleted) * or deleted)
*/ */
function notifyReactives(target: Target, key: PropertyKey): void { function onWriteTargetKey(target: Target, key: PropertyKey): void {
const keyToCallbacks = targetToKeysToCallbacks.get(target); const keyToAtomItem = targetToKeysToAtomItem.get(target)!;
if (!keyToCallbacks) { if (!keyToAtomItem) {
return; return;
} }
const callbacks = keyToCallbacks.get(key); const atom = keyToAtomItem.get(key);
if (!callbacks) { if (!atom) {
return; return;
} }
// Loop on copy because clearReactivesForCallback will modify the set in place scheduleAtom(atom);
for (const callback of [...callbacks]) {
clearReactivesForCallback(callback);
callback();
}
} }
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
/**
* Clears all subscriptions of the Reactives associated with a given callback.
*
* @param callback the callback for which the reactives need to be cleared
*/
export function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) {
return;
}
for (const target of targetsToClear) {
const observedKeys = targetToKeysToCallbacks.get(target);
if (!observedKeys) {
continue;
}
for (const [key, callbacks] of observedKeys.entries()) {
callbacks.delete(callback);
if (!callbacks.size) {
observedKeys.delete(key);
}
}
}
targetsToClear.clear();
}
export function getSubscriptions(callback: Callback) {
const targets = callbacksToTargets.get(callback) || [];
return [...targets].map((target) => {
const keysToCallbacks = targetToKeysToCallbacks.get(target);
let keys = [];
if (keysToCallbacks) {
for (const [key, cbs] of keysToCallbacks) {
if (cbs.has(callback)) {
keys.push(key);
}
}
}
return { target, keys };
});
}
// Maps reactive objects to the underlying target // Maps reactive objects to the underlying target
export const targets = new WeakMap<Reactive<Target>, Target>(); export const targets = new WeakMap<Reactive<Target>, Target>();
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>(); const reactiveCache = new WeakMap<Target, Reactive<Target>>();
/** /**
* Creates a reactive proxy for an object. Reading data on the reactive object * Creates a reactive proxy for an object. Reading data on the reactive object
* subscribes to changes to the data. Writing data on the object will cause the * subscribes to changes to the data. Writing data on the object will cause the
@@ -204,7 +235,7 @@ const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>()
* reactive has changed * reactive has changed
* @returns a proxy that tracks changes to it * @returns a proxy that tracks changes to it
*/ */
export function reactive<T extends Target>(target: T, callback: Callback = NO_CALLBACK): T { export function reactive<T extends Target>(target: T): T {
if (!canBeMadeReactive(target)) { if (!canBeMadeReactive(target)) {
throw new OwlError(`Cannot make the given value reactive`); throw new OwlError(`Cannot make the given value reactive`);
} }
@@ -213,30 +244,130 @@ export function reactive<T extends Target>(target: T, callback: Callback = NO_CA
} }
if (targets.has(target)) { if (targets.has(target)) {
// target is reactive, create a reactive on the underlying object instead // target is reactive, create a reactive on the underlying object instead
return reactive(targets.get(target) as T, callback); // return reactive(targets.get(target) as T);
return target;
} }
if (!reactiveCache.has(target)) { const reactive = reactiveCache.get(target)!;
reactiveCache.set(target, new WeakMap()); if (reactive) return reactive as T;
}
const reactivesForTarget = reactiveCache.get(target)!; const targetRawType = rawType(target);
if (!reactivesForTarget.has(callback)) { const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
const targetRawType = rawType(target); ? collectionsProxyHandler(target as Collection, targetRawType as CollectionRawType)
const handler = COLLECTION_RAW_TYPES.includes(targetRawType) : basicProxyHandler<T>();
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType) const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
: basicProxyHandler<T>(callback);
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>; reactiveCache.set(target, proxy);
reactivesForTarget.set(callback, proxy); targets.set(proxy, target);
targets.set(proxy, target);
} return proxy;
return reactivesForTarget.get(callback) as Reactive<T>;
} }
function removeAtomsFromContext(executionContext: ExecutionContext) {
for (const sig of executionContext.atoms) {
sig.executionContexts.delete(executionContext);
}
executionContext.atoms.clear();
}
/**
* Unsubscribe an execution context and all its children from all atoms
* they are subscribed to.
*
* @param parentExecutionContext the context to unsubscribe
*/
function unsubscribeChildEffect(
parentExecutionContext: ExecutionContext,
scheduledContexts: Set<ExecutionContext>
) {
// executionContext.update = () => {};
for (const children of parentExecutionContext.meta.children) {
children.meta.parent = undefined;
removeAtomsFromContext(children);
scheduledContexts.delete(children);
unsubscribeChildEffect(children, scheduledContexts);
}
parentExecutionContext.meta.children.length = 0;
}
export function withoutReactivity<T extends (...args: any[]) => any>(fn: T): ReturnType<T> {
pushExecutionContext(undefined!);
let r: ReturnType<T>;
try {
r = fn();
} finally {
popExecutionContext();
}
return r;
}
export function effect(fn: Function) {
let parent = getExecutionContext();
// todo: is it useful?
if (parent && !parent?.meta.children) {
parent = undefined!;
}
const executionContext: ExecutionContext = {
unsubcribe: (scheduledContexts: Set<ExecutionContext>) => {
unsubscribeChildEffect(executionContext, scheduledContexts);
},
update: fn,
onReadAtom: (atom: Atom) => addAtomToContext(atom, executionContext),
atoms: new Set(),
meta: {
parent: parent,
children: [],
},
};
if (parent) {
// todo: is it useful?
parent.meta.children?.push?.(executionContext);
}
pushExecutionContext(executionContext);
try {
fn();
} finally {
popExecutionContext();
}
}
export function derived(fn: Function) {
let lastValue: any;
const derivedAtom: DerivedAtom = {
executionContexts: new Set<ExecutionContext>(),
dependents: new Set<Atom>(),
dependencies: new Map<Atom, OldValue>(),
getValue: () => lastValue,
computed: false,
};
return () => {
const executionContext = getExecutionContext();
executionContext?.onReadAtom(derivedAtom);
if (derivedAtom.computed) return lastValue;
const derivedExecutionContext: ExecutionContext = {
onReadAtom: (atom: Atom) => {
atom.dependents.add(derivedAtom);
// derivedAtom.executionContexts.add(executionContext);
},
};
pushExecutionContext(derivedExecutionContext);
try {
lastValue = fn();
} finally {
popExecutionContext();
}
derivedAtom.computed = true;
return lastValue;
};
}
/** /**
* Creates a basic proxy handler for regular objects and arrays. * Creates a basic proxy handler for regular objects and arrays.
* *
* @param callback @see reactive * @param callback @see reactive
* @returns a proxy handler object * @returns a proxy handler object
*/ */
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> { function basicProxyHandler<T extends Target>(): ProxyHandler<T> {
return { return {
get(target, key, receiver) { get(target, key, receiver) {
// non-writable non-configurable properties cannot be made reactive // non-writable non-configurable properties cannot be made reactive
@@ -244,15 +375,15 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
if (desc && !desc.writable && !desc.configurable) { if (desc && !desc.writable && !desc.configurable) {
return Reflect.get(target, key, receiver); return Reflect.get(target, key, receiver);
} }
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
return possiblyReactive(Reflect.get(target, key, receiver), callback); return possiblyReactive(Reflect.get(target, key, receiver));
}, },
set(target, key, value, receiver) { set(target, key, value, receiver) {
const hadKey = objectHasOwnProperty.call(target, key); const hadKey = objectHasOwnProperty.call(target, key);
const originalValue = Reflect.get(target, key, receiver); const originalValue = Reflect.get(target, key, receiver);
const ret = Reflect.set(target, key, toRaw(value), receiver); const ret = Reflect.set(target, key, toRaw(value), receiver);
if (!hadKey && objectHasOwnProperty.call(target, key)) { if (!hadKey && objectHasOwnProperty.call(target, key)) {
notifyReactives(target, KEYCHANGES); onWriteTargetKey(target, KEYCHANGES);
} }
// While Array length may trigger the set trap, it's not actually set by this // While Array length may trigger the set trap, it's not actually set by this
// method but is updated behind the scenes, and the trap is not called with the // method but is updated behind the scenes, and the trap is not called with the
@@ -261,26 +392,26 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
originalValue !== Reflect.get(target, key, receiver) || originalValue !== Reflect.get(target, key, receiver) ||
(key === "length" && Array.isArray(target)) (key === "length" && Array.isArray(target))
) { ) {
notifyReactives(target, key); onWriteTargetKey(target, key);
} }
return ret; return ret;
}, },
deleteProperty(target, key) { deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key); const ret = Reflect.deleteProperty(target, key);
// TODO: only notify when something was actually deleted // TODO: only notify when something was actually deleted
notifyReactives(target, KEYCHANGES); onWriteTargetKey(target, KEYCHANGES);
notifyReactives(target, key); onWriteTargetKey(target, key);
return ret; return ret;
}, },
ownKeys(target) { ownKeys(target) {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
return Reflect.ownKeys(target); return Reflect.ownKeys(target);
}, },
has(target, key) { has(target, key) {
// TODO: this observes all key changes instead of only the presence of the argument key // TODO: this observes all key changes instead of only the presence of the argument key
// observing the key itself would observe value changes instead of presence changes // observing the key itself would observe value changes instead of presence changes
// so we may need a finer grained system to distinguish observing value vs presence. // so we may need a finer grained system to distinguish observing value vs presence.
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
return Reflect.has(target, key); return Reflect.has(target, key);
}, },
} as ProxyHandler<T>; } as ProxyHandler<T>;
@@ -293,11 +424,11 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
* @param target @see reactive * @param target @see reactive
* @param callback @see reactive * @param callback @see reactive
*/ */
function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callback) { function makeKeyObserver(methodName: "has" | "get", target: any) {
return (key: any) => { return (key: any) => {
key = toRaw(key); key = toRaw(key);
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
return possiblyReactive(target[methodName](key), callback); return possiblyReactive(target[methodName](key));
}; };
} }
/** /**
@@ -310,16 +441,15 @@ function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callb
*/ */
function makeIteratorObserver( function makeIteratorObserver(
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator, methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
target: any, target: any
callback: Callback
) { ) {
return function* () { return function* () {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
const keys = target.keys(); const keys = target.keys();
for (const item of target[methodName]()) { for (const item of target[methodName]()) {
const key = keys.next().value; const key = keys.next().value;
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
yield possiblyReactive(item, callback); yield possiblyReactive(item);
} }
}; };
} }
@@ -331,16 +461,16 @@ function makeIteratorObserver(
* @param target @see reactive * @param target @see reactive
* @param callback @see reactive * @param callback @see reactive
*/ */
function makeForEachObserver(target: any, callback: Callback) { function makeForEachObserver(target: any) {
return function forEach(forEachCb: (val: any, key: any, target: any) => void, thisArg: any) { return function forEach(forEachCb: (val: any, key: any, target: any) => void, thisArg: any) {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
target.forEach(function (val: any, key: any, targetObj: any) { target.forEach(function (val: any, key: any, targetObj: any) {
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
forEachCb.call( forEachCb.call(
thisArg, thisArg,
possiblyReactive(val, callback), possiblyReactive(val),
possiblyReactive(key, callback), possiblyReactive(key),
possiblyReactive(targetObj, callback) possiblyReactive(targetObj)
); );
}, thisArg); }, thisArg);
}; };
@@ -367,10 +497,10 @@ function delegateAndNotify(
const ret = target[setterName](key, value); const ret = target[setterName](key, value);
const hasKey = target.has(key); const hasKey = target.has(key);
if (hadKey !== hasKey) { if (hadKey !== hasKey) {
notifyReactives(target, KEYCHANGES); onWriteTargetKey(target, KEYCHANGES);
} }
if (originalValue !== target[getterName](key)) { if (originalValue !== target[getterName](key)) {
notifyReactives(target, key); onWriteTargetKey(target, key);
} }
return ret; return ret;
}; };
@@ -385,9 +515,9 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
return () => { return () => {
const allKeys = [...target.keys()]; const allKeys = [...target.keys()];
target.clear(); target.clear();
notifyReactives(target, KEYCHANGES); onWriteTargetKey(target, KEYCHANGES);
for (const key of allKeys) { for (const key of allKeys) {
notifyReactives(target, key); onWriteTargetKey(target, key);
} }
}; };
} }
@@ -399,40 +529,40 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
* reactives that the key which is being added or deleted has been modified. * reactives that the key which is being added or deleted has been modified.
*/ */
const rawTypeToFuncHandlers = { const rawTypeToFuncHandlers = {
Set: (target: any, callback: Callback) => ({ Set: (target: any) => ({
has: makeKeyObserver("has", target, callback), has: makeKeyObserver("has", target),
add: delegateAndNotify("add", "has", target), add: delegateAndNotify("add", "has", target),
delete: delegateAndNotify("delete", "has", target), delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target, callback), keys: makeIteratorObserver("keys", target),
values: makeIteratorObserver("values", target, callback), values: makeIteratorObserver("values", target),
entries: makeIteratorObserver("entries", target, callback), entries: makeIteratorObserver("entries", target),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback), [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
forEach: makeForEachObserver(target, callback), forEach: makeForEachObserver(target),
clear: makeClearNotifier(target), clear: makeClearNotifier(target),
get size() { get size() {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
return target.size; return target.size;
}, },
}), }),
Map: (target: any, callback: Callback) => ({ Map: (target: any) => ({
has: makeKeyObserver("has", target, callback), has: makeKeyObserver("has", target),
get: makeKeyObserver("get", target, callback), get: makeKeyObserver("get", target),
set: delegateAndNotify("set", "get", target), set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target), delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target, callback), keys: makeIteratorObserver("keys", target),
values: makeIteratorObserver("values", target, callback), values: makeIteratorObserver("values", target),
entries: makeIteratorObserver("entries", target, callback), entries: makeIteratorObserver("entries", target),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback), [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
forEach: makeForEachObserver(target, callback), forEach: makeForEachObserver(target),
clear: makeClearNotifier(target), clear: makeClearNotifier(target),
get size() { get size() {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
return target.size; return target.size;
}, },
}), }),
WeakMap: (target: any, callback: Callback) => ({ WeakMap: (target: any) => ({
has: makeKeyObserver("has", target, callback), has: makeKeyObserver("has", target),
get: makeKeyObserver("get", target, callback), get: makeKeyObserver("get", target),
set: delegateAndNotify("set", "get", target), set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target), delete: delegateAndNotify("delete", "has", target),
}), }),
@@ -446,20 +576,19 @@ const rawTypeToFuncHandlers = {
*/ */
function collectionsProxyHandler<T extends Collection>( function collectionsProxyHandler<T extends Collection>(
target: T, target: T,
callback: Callback,
targetRawType: CollectionRawType targetRawType: CollectionRawType
): ProxyHandler<T> { ): ProxyHandler<T> {
// TODO: if performance is an issue we can create the special handlers lazily when each // TODO: if performance is an issue we can create the special handlers lazily when each
// property is read. // property is read.
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback); const specialHandlers = rawTypeToFuncHandlers[targetRawType](target);
return Object.assign(basicProxyHandler(callback), { return Object.assign(basicProxyHandler(), {
// FIXME: probably broken when part of prototype chain since we ignore the receiver // FIXME: probably broken when part of prototype chain since we ignore the receiver
get(target: any, key: PropertyKey) { get(target: any, key: PropertyKey) {
if (objectHasOwnProperty.call(specialHandlers, key)) { if (objectHasOwnProperty.call(specialHandlers, key)) {
return (specialHandlers as any)[key]; return (specialHandlers as any)[key];
} }
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
return possiblyReactive(target[key], callback); return possiblyReactive(target[key]);
}, },
}) as ProxyHandler<T>; }) as ProxyHandler<T>;
} }
+72
View File
@@ -0,0 +1,72 @@
import { getTaskContext, TaskContext, useTaskContext } from "./cancellableContext";
export class Task<T = any> {
_promise: Promise<T>;
_ctx?: TaskContext = getTaskContext();
constructor(
executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason: any) => void) => void,
public _onCancelled?: Function
) {
if (!this._ctx) {
this._promise = new Promise(executor);
return;
}
this._promise = new Promise((resolve, reject) => {
try {
executor(
(value: T | PromiseLike<T>) => {
if (!this._ctx?.isCancelled) resolve(value);
},
(error: any) => {
if (!this._ctx?.isCancelled) reject(error);
}
);
} catch (err) {
if (!this._ctx?.isCancelled) reject(err);
}
});
}
then(onFulfilled: (value: any) => any, onRejected: (error: any) => any) {
if (!this._ctx) return this._promise.then(onFulfilled, onRejected);
return this._promise.then((v) => {
if (this._ctx!.isCancelled) return;
let cleanup: Function;
Promise.resolve().then(() => {
const ctx = useTaskContext(this._ctx);
cleanup = ctx.cleanup;
});
const result = onFulfilled(v);
Promise.resolve().then(() => {
cleanup();
});
return result;
}, onRejected);
}
catch(onRejected: (error: any) => any) {
return this._promise.catch(onRejected);
}
finally(onFinally: () => any) {
return this._promise.finally(onFinally);
}
cancel() {
if (this._onCancelled) {
this._onCancelled();
}
}
get [Symbol.toStringTag]() {
return "Promise";
}
// static all(tasks) {
// return new Task((resolve, reject) => {
// Promise.all(tasks.map((t) => (t instanceof Task ? t._promise : t))).then(resolve, reject);
// });
// }
}
-43
View File
@@ -1,5 +1,4 @@
import { OwlError } from "../common/owl_error"; import { OwlError } from "../common/owl_error";
import { ComponentNode, getCurrent } from "./component_node";
export type Callback = () => void; export type Callback = () => void;
/** /**
@@ -82,52 +81,10 @@ export function validateTarget(target: HTMLElement | ShadowRoot) {
} }
export class EventBus extends EventTarget { export class EventBus extends EventTarget {
constructor(events?: string[]) {
if (events) {
let node: ComponentNode | null = null;
try {
node = getCurrent();
} catch {}
if (node?.app?.dev) {
return new DebugEventBus(events);
}
}
super();
}
trigger(name: string, payload?: any) { trigger(name: string, payload?: any) {
this.dispatchEvent(new CustomEvent(name, { detail: payload })); this.dispatchEvent(new CustomEvent(name, { detail: payload }));
} }
} }
class DebugEventBus extends EventBus {
private events: Set<string>;
constructor(events: string[]) {
super();
this.events = new Set(events);
}
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject | null,
options?: boolean | AddEventListenerOptions
): void {
if (!this.events.has(type)) {
throw new OwlError(`EventBus: subscribing to unknown event '${type}'`);
}
super.addEventListener(type, listener, options);
}
trigger(name: string, payload?: any) {
if (!this.events.has(name)) {
throw new OwlError(`EventBus: triggering unknown event '${name}'`);
}
super.trigger(name, payload);
}
dispatchEvent(event: Event): boolean {
if (!this.events.has(event.type)) {
throw new OwlError(`EventBus: dispatching unknown event '${event.type}'`);
}
return super.dispatchEvent(event);
}
}
export function whenReady(fn?: any): Promise<void> { export function whenReady(fn?: any): Promise<void> {
return new Promise(function (resolve) { return new Promise(function (resolve) {
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script. // do not modify manually. This file is generated by the release script.
export const version = "2.8.0"; export const version = "2.8.1";
-109
View File
@@ -1,51 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reactivity: useState concurrent renderings 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<span><block-text-0/><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['context'][ctx['props'].key].n;
let d2 = ctx['state'].x;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentC\`, {key: ctx['props'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentB\`, {key: ctx['context'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = ` exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -155,69 +109,6 @@ exports[`Reactivity: useState parent and children subscribed to same context 2`]
}" }"
`; `;
exports[`Reactivity: useState several nodes on different level use same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
let d2 = ctx['contextObj'].b;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].b;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
let b2 = component(\`L3A\`, {}, key + \`__1\`, node, ctx);
return block1([d1], [b2]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`L2A\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`L2B\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two components are updated in parallel 1`] = ` exports[`Reactivity: useState two components are updated in parallel 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -49,6 +49,27 @@ exports[`debugging t-debug on sub template 2`] = `
}" }"
`; `;
exports[`debugging t-debug: interaction with t-set 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
debugger;
setContextValue(ctx, \\"foo\\", 42);
debugger;
setContextValue(ctx, \\"bar\\", 49);
let txt1 = ctx['foo']+ctx['bar'];
return block1([txt1]);
}
}"
`;
exports[`debugging t-log 1`] = ` exports[`debugging t-log 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -66,3 +87,24 @@ exports[`debugging t-log 1`] = `
} }
}" }"
`; `;
exports[`debugging t-log: interaction with t-set 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
console.log(ctx['foo']);
setContextValue(ctx, \\"foo\\", 42);
console.log(ctx['bar']);
setContextValue(ctx, \\"bar\\", 49);
let txt1 = ctx['foo']+ctx['bar'];
return block1([txt1]);
}
}"
`;
@@ -103,3 +103,18 @@ exports[`t-key t-key on sub dom node pushes a child block in its parent 2`] = `
} }
}" }"
`; `;
exports[`t-key t-key: interaction with t-esc 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<p><block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['key'];
let txt1 = ctx['text'];
return toggler(tKey_1, block1([txt1]));
}
}"
`;
@@ -10,8 +10,7 @@ exports[`translation context body of t-sets are translated in context 1`] = `
ctx = Object.create(ctx); ctx = Object.create(ctx);
ctx[isBoundary] = 1 ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", \`traduit\`); setContextValue(ctx, \\"label\\", \`traduit\`);
const b2 = text(ctx['label']); return text(ctx['label']);
return multi([b2]);
} }
}" }"
`; `;
@@ -94,6 +93,23 @@ exports[`translation context slot attrs and text contents are translated in cont
}" }"
`; `;
exports[`translation context t-translation-context with several children 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><div/><div/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (true) {
b2 = text(\`\`);
}
return block1([], [b2]);
}
}"
`;
exports[`translation context translation of attributes in context 1`] = ` exports[`translation context translation of attributes in context 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -153,6 +169,21 @@ exports[`translation support body of t-sets inside translation=off are not trans
}" }"
`; `;
exports[`translation support body of t-sets inside translation=off are not translated 2 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", \`untranslated\`);
return text(ctx['label']);
}
}"
`;
exports[`translation support body of t-sets with html content are translated 1`] = ` exports[`translation support body of t-sets with html content are translated 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -264,6 +295,23 @@ exports[`translation support t-set and falsy t-value: t-body are translated 1`]
}" }"
`; `;
exports[`translation support t-translation with several children 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><div/><div/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (true) {
b2 = text(\`\`);
}
return block1([], [b2]);
}
}"
`;
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = ` exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
+13 -1
View File
@@ -692,6 +692,7 @@ describe("qweb parser", () => {
value: "value", value: "value",
defaultValue: null, defaultValue: null,
body: null, body: null,
hasNoRepresentation: true,
}); });
}); });
@@ -702,6 +703,7 @@ describe("qweb parser", () => {
defaultValue: "ok", defaultValue: "ok",
value: null, value: null,
body: null, body: null,
hasNoRepresentation: true,
}); });
expect(parse(`<t t-set="v"><div>ok</div></t>`)).toEqual({ expect(parse(`<t t-set="v"><div>ok</div></t>`)).toEqual({
@@ -723,6 +725,7 @@ describe("qweb parser", () => {
content: [{ type: ASTType.Text, value: "ok" }], content: [{ type: ASTType.Text, value: "ok" }],
}, },
], ],
hasNoRepresentation: true,
}); });
expect(parse(`<t t-set="v"><div>ok</div>abc</t>`)).toEqual({ expect(parse(`<t t-set="v"><div>ok</div>abc</t>`)).toEqual({
@@ -745,6 +748,7 @@ describe("qweb parser", () => {
}, },
{ type: ASTType.Text, value: "abc" }, { type: ASTType.Text, value: "abc" },
], ],
hasNoRepresentation: true,
}); });
}); });
@@ -758,6 +762,7 @@ describe("qweb parser", () => {
defaultValue: "ok", defaultValue: "ok",
value: null, value: null,
body: null, body: null,
hasNoRepresentation: true,
}, },
tElif: null, tElif: null,
tElse: null, tElse: null,
@@ -783,7 +788,14 @@ describe("qweb parser", () => {
condition: "flag", condition: "flag",
content: { type: ASTType.Text, value: "1" }, content: { type: ASTType.Text, value: "1" },
tElif: null, tElif: null,
tElse: { type: ASTType.TSet, name: "ourvar", value: "0", defaultValue: null, body: null }, tElse: {
type: ASTType.TSet,
name: "ourvar",
value: "0",
defaultValue: null,
body: null,
hasNoRepresentation: true,
},
}, },
], ],
}); });
+30
View File
@@ -38,4 +38,34 @@ describe("debugging", () => {
expect(console.log).toHaveBeenCalledWith(45); expect(console.log).toHaveBeenCalledWith(45);
console.log = consoleLog; console.log = consoleLog;
}); });
test("t-log: interaction with t-set", () => {
const consoleLog = console.log;
console.log = jest.fn();
const template = `
<t>
<t t-log="foo" t-set="foo" t-value="42"/>
<t t-log="bar" t-set="bar" t-value="49"/>
<span t-esc="foo + bar"/>
</t>
`;
snapshotTemplate(template);
renderToString(template);
expect(console.log).toHaveBeenCalledWith(undefined);
expect(console.log).toHaveBeenCalledWith(undefined);
console.log = consoleLog;
});
test("t-debug: interaction with t-set", () => {
const template = `
<t>
<t t-debug="" t-set="foo" t-value="42"/>
<t t-debug="" t-set="bar" t-value="49"/>
<span t-esc="foo + bar"/>
</t>
`;
snapshotTemplate(template);
renderToString(template);
});
}); });
+6
View File
@@ -63,4 +63,10 @@ describe("t-key", () => {
expect(renderToString(template2, { key: "1" })).toBe("<div><h1></h1></div>"); expect(renderToString(template2, { key: "1" })).toBe("<div><h1></h1></div>");
}); });
test("t-key: interaction with t-esc", async () => {
const template = `<p t-key="key" t-esc="text"/>`;
expect(renderToString(template, { key: "1", text: "abc" })).toBe("<p>abc</p>");
});
}); });
+47
View File
@@ -129,6 +129,21 @@ describe("translation support", () => {
expect(fixture.innerHTML).toBe("untranslated"); expect(fixture.innerHTML).toBe("untranslated");
}); });
test("body of t-sets inside translation=off are not translated 2", async () => {
class SomeComponent extends Component {
static template = xml`
<t>
<t t-translation="off" t-set="label">untranslated</t>
<t t-esc="label"/>
</t>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("untranslated");
});
test("body of t-sets with html content are translated", async () => { test("body of t-sets with html content are translated", async () => {
class SomeComponent extends Component { class SomeComponent extends Component {
static template = xml` static template = xml`
@@ -170,6 +185,22 @@ describe("translation support", () => {
await mount(SomeComponent, fixture, { translateFn }); await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("translated"); expect(fixture.innerHTML).toBe("translated");
}); });
test("t-translation with several children", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-translation="off">
<div/>
<div/>
</t>
<t t-if="true"/>
</div>
`;
}
await mount(SomeComponent, fixture);
expect(fixture.outerHTML).toBe("<div><div><div></div><div></div></div></div>");
});
}); });
describe("translation context", () => { describe("translation context", () => {
@@ -293,4 +324,20 @@ describe("translation context", () => {
expect(translateFn).toHaveBeenCalledWith("param", "fr"); expect(translateFn).toHaveBeenCalledWith("param", "fr");
expect(translateFn).toHaveBeenCalledWith("title", "pt"); expect(translateFn).toHaveBeenCalledWith("title", "pt");
}); });
test("t-translation-context with several children", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-translation-context="ctx">
<div/>
<div/>
</t>
<t t-if="true"/>
</div>
`;
}
await mount(SomeComponent, fixture);
expect(fixture.outerHTML).toBe("<div><div><div></div><div></div></div></div>");
});
}); });
@@ -52,6 +52,36 @@ exports[`reactivity in lifecycle Component is automatically subscribed to reacti
}" }"
`; `;
exports[`reactivity in lifecycle an external reactive object should be tracked 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`TestSubComponent\`, true, false, false, []);
let block1 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['obj1'].value;
const b2 = comp1({}, key + \`__1\`, node, this, null);
return block1([txt1], [b2]);
}
}"
`;
exports[`reactivity in lifecycle an external reactive object should be tracked 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['obj2'].value;
return block1([txt1]);
}
}"
`;
exports[`reactivity in lifecycle can use a state hook 1`] = ` exports[`reactivity in lifecycle can use a state hook 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -140,39 +170,3 @@ exports[`reactivity in lifecycle state changes in willUnmount do not trigger rer
} }
}" }"
`; `;
exports[`subscriptions subscriptions returns the keys and targets observed by the component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].a);
}
}"
`;
exports[`subscriptions subscriptions returns the keys observed by the component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"state\\"]);
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['state'].a);
const b3 = comp1({state: ctx['state']}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
}"
`;
exports[`subscriptions subscriptions returns the keys observed by the component 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].state.b);
}
}"
`;
+10 -9
View File
@@ -1,27 +1,28 @@
import { App, Component, mount, onWillDestroy } from "../../src"; import { App, Component, mount, onWillDestroy } from "../../src";
import { OwlError } from "../../src/common/owl_error";
import { import {
onError, onError,
onMounted, onMounted,
onPatched, onPatched,
onWillPatch,
onWillStart,
onWillRender,
onRendered, onRendered,
onWillPatch,
onWillRender,
onWillStart,
onWillUnmount, onWillUnmount,
useState, useState,
xml, xml,
} from "../../src/index"; } from "../../src/index";
import { getCurrent } from "../../src/runtime/component_node";
import { import {
logStep, logStep,
makeTestFixture, makeTestFixture,
nextTick,
nextMicroTick,
snapshotEverything,
useLogLifecycle,
nextAppError, nextAppError,
nextMicroTick,
nextTick,
snapshotEverything,
steps, steps,
useLogLifecycle,
} from "../helpers"; } from "../helpers";
import { OwlError } from "../../src/common/owl_error";
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -647,7 +648,7 @@ describe("can catch errors", () => {
setup() { setup() {
onWillStart(() => { onWillStart(() => {
this.state = useState({ value: 2 }); getCurrent();
}); });
} }
} }
-4
View File
@@ -450,14 +450,10 @@ test(".alike suffix in a list", async () => {
expect(fixture.innerHTML).toBe("<button>1V</button><button>2V</button>"); expect(fixture.innerHTML).toBe("<button>1V</button><button>2V</button>");
expect(steps.splice(0)).toMatchInlineSnapshot(` expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [ Array [
"Parent:willRender",
"Parent:rendered",
"Todo:willRender", "Todo:willRender",
"Todo:rendered", "Todo:rendered",
"Todo:willPatch", "Todo:willPatch",
"Todo:patched", "Todo:patched",
"Parent:willPatch",
"Parent:patched",
] ]
`); `);
}); });
+1 -1
View File
@@ -702,7 +702,7 @@ describe("props validation", () => {
const app = new App(Parent, { test: true }); const app = new App(Parent, { test: true });
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("12"); expect(fixture.innerHTML).toBe("12");
expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]); // expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
}); });
test("props are validated whenever component is updated", async () => { test("props are validated whenever component is updated", async () => {
+38 -43
View File
@@ -2,12 +2,11 @@ import {
Component, Component,
mount, mount,
onPatched, onPatched,
onWillRender,
onWillPatch, onWillPatch,
onWillRender,
onWillUnmount, onWillUnmount,
useState, reactive,
xml, xml,
toRaw,
} from "../../src"; } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers"; import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
@@ -20,10 +19,36 @@ beforeEach(() => {
}); });
describe("reactivity in lifecycle", () => { describe("reactivity in lifecycle", () => {
test("an external reactive object should be tracked", async () => {
const obj1 = reactive({ value: 1 });
const obj2 = reactive({ value: 100 });
class TestSubComponent extends Component {
obj2 = obj2;
static template = xml`<div>
<t t-esc="obj2.value"/>
</div>`;
}
class TestComponent extends Component {
obj1 = obj1;
static template = xml`<div>
<t t-esc="obj1.value"/>
<TestSubComponent/>
</div>`;
static components = { TestSubComponent };
}
await mount(TestComponent, fixture);
expect(fixture.innerHTML).toBe("<div>1<div>100</div></div>");
obj1.value = 2;
obj2.value = 200;
await nextTick();
expect(fixture.innerHTML).toBe("<div>2<div>200</div></div>");
});
test("can use a state hook", async () => { test("can use a state hook", async () => {
class Counter extends Component { class Counter extends Component {
static template = xml`<div><t t-esc="counter.value"/></div>`; static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = useState({ value: 42 }); counter = reactive({ value: 42 });
} }
const counter = await mount(Counter, fixture); const counter = await mount(Counter, fixture);
expect(fixture.innerHTML).toBe("<div>42</div>"); expect(fixture.innerHTML).toBe("<div>42</div>");
@@ -36,7 +61,7 @@ describe("reactivity in lifecycle", () => {
let n = 0; let n = 0;
class Comp extends Component { class Comp extends Component {
static template = xml`<div><t t-esc="state.a"/></div>`; static template = xml`<div><t t-esc="state.a"/></div>`;
state = useState({ a: 5, b: 7 }); state = reactive({ a: 5, b: 7 });
setup() { setup() {
onWillRender(() => n++); onWillRender(() => n++);
} }
@@ -57,7 +82,7 @@ describe("reactivity in lifecycle", () => {
test("can use a state hook on Map", async () => { test("can use a state hook on Map", async () => {
class Counter extends Component { class Counter extends Component {
static template = xml`<div><t t-esc="counter.get('value')"/></div>`; static template = xml`<div><t t-esc="counter.get('value')"/></div>`;
counter = useState(new Map([["value", 42]])); counter = reactive(new Map([["value", 42]]));
} }
const counter = await mount(Counter, fixture); const counter = await mount(Counter, fixture);
expect(fixture.innerHTML).toBe("<div>42</div>"); expect(fixture.innerHTML).toBe("<div>42</div>");
@@ -72,7 +97,7 @@ describe("reactivity in lifecycle", () => {
static template = xml` static template = xml`
<span><t t-esc="props.val"/><t t-esc="state.n"/></span> <span><t t-esc="props.val"/><t t-esc="state.n"/></span>
`; `;
state = useState({ n: 2 }); state = reactive({ n: 2 });
setup() { setup() {
onWillRender(() => { onWillRender(() => {
steps.push("render"); steps.push("render");
@@ -96,7 +121,7 @@ describe("reactivity in lifecycle", () => {
</div> </div>
`; `;
static components = { Child }; static components = { Child };
state = useState({ val: 1, flag: true }); state = reactive({ val: 1, flag: true });
} }
const parent = await mount(Parent, fixture); const parent = await mount(Parent, fixture);
expect(steps).toEqual(["render"]); expect(steps).toEqual(["render"]);
@@ -142,7 +167,7 @@ describe("reactivity in lifecycle", () => {
static template = xml` static template = xml`
<div><t t-esc="state.val"/></div> <div><t t-esc="state.val"/></div>
`; `;
state = useState({ val: 1 }); state = reactive({ val: 1 });
setup() { setup() {
STATE = this.state; STATE = this.state;
onWillRender(() => { onWillRender(() => {
@@ -167,7 +192,7 @@ describe("reactivity in lifecycle", () => {
class Parent extends Component { class Parent extends Component {
static template = xml`<Child t-if="state.renderChild" state="state"/>`; static template = xml`<Child t-if="state.renderChild" state="state"/>`;
static components = { Child }; static components = { Child };
state: any = useState({ renderChild: true, content: { a: 2 } }); state: any = reactive({ renderChild: true, content: { a: 2 } });
setup() { setup() {
useLogLifecycle(); useLogLifecycle();
} }
@@ -205,7 +230,8 @@ describe("reactivity in lifecycle", () => {
`); `);
}); });
test("Component is automatically subscribed to reactive object received as prop", async () => { // todo: unskip it
test.skip("Component is automatically subscribed to reactive object received as prop", async () => {
let childRenderCount = 0; let childRenderCount = 0;
let parentRenderCount = 0; let parentRenderCount = 0;
class Child extends Component { class Child extends Component {
@@ -218,7 +244,7 @@ describe("reactivity in lifecycle", () => {
static template = xml`<Child obj="obj" reactiveObj="reactiveObj"/>`; static template = xml`<Child obj="obj" reactiveObj="reactiveObj"/>`;
static components = { Child }; static components = { Child };
obj = { a: 1 }; obj = { a: 1 };
reactiveObj = useState({ b: 2 }); reactiveObj = reactive({ b: 2 });
setup() { setup() {
onWillRender(() => parentRenderCount++); onWillRender(() => parentRenderCount++);
} }
@@ -237,34 +263,3 @@ describe("reactivity in lifecycle", () => {
expect(fixture.innerHTML).toBe("34"); expect(fixture.innerHTML).toBe("34");
}); });
}); });
describe("subscriptions", () => {
test("subscriptions returns the keys and targets observed by the component", async () => {
class Comp extends Component {
static template = xml`<t t-esc="state.a"/>`;
state = useState({ a: 1, b: 2 });
}
const comp = await mount(Comp, fixture);
expect(fixture.innerHTML).toBe("1");
expect(comp.__owl__.subscriptions).toEqual([{ keys: ["a"], target: toRaw(comp.state) }]);
});
test("subscriptions returns the keys observed by the component", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.state.b"/>`;
setup() {
child = this;
}
}
let child: Child;
class Parent extends Component {
static template = xml`<t t-esc="state.a"/><Child state="state"/>`;
static components = { Child };
state = useState({ a: 1, b: 2 });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("12");
expect(parent.__owl__.subscriptions).toEqual([{ keys: ["a"], target: toRaw(parent.state) }]);
expect(child!.__owl__.subscriptions).toEqual([{ keys: ["b"], target: toRaw(parent.state) }]);
});
});
-4
View File
@@ -330,12 +330,8 @@ describe("rendering semantics", () => {
expect(fixture.innerHTML).toBe("444"); expect(fixture.innerHTML).toBe("444");
expect(steps.splice(0)).toMatchInlineSnapshot(` expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [ Array [
"Parent:willRender",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:willPatch",
"Parent:patched",
"Child:willPatch", "Child:willPatch",
"Child:patched", "Child:patched",
] ]
+109
View File
@@ -0,0 +1,109 @@
import { taskEffect } from "../../src/runtime/cancellableContext";
import { Task } from "../../src/runtime/task";
export type Deffered = Promise<any> & {
resolve: (value: any) => void;
reject: (reason: any) => void;
};
interface TaskWithResolvers<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}
let resolvers: Record<string, TaskWithResolvers<string>> = {};
function getTask(id: string) {
const resolver: {
task?: Task<string>;
resolve?: (value: string | PromiseLike<string>) => void;
reject?: (reason?: any) => void;
} = {};
const promise = new Task<string>((res, rej) => {
resolver.resolve = res;
resolver.reject = rej;
});
resolver.task = promise;
resolvers[id] = resolver as TaskWithResolvers<string>;
return promise;
}
function tick() {
return new Promise((r) => setTimeout(r, 0));
}
// const timeoutTask = (ms: number) => new Task((resolve) => setTimeout(() => resolve(ms), ms));
const steps: string[] = [];
function step(msg: string) {
steps.push(msg);
}
function verifySteps(expected: string[]) {
expect(steps).toEqual(expected);
steps.length = 0;
}
afterEach(() => {
resolvers = {};
});
describe("task", () => {
test("should run a task properly", async () => {
taskEffect(async () => {
let result;
step(`a:begin`);
result = await getTask("a");
step(`a:${result}`);
result = await getTask("b");
step(`b:${result}`);
});
verifySteps(["a:begin"]);
resolvers["a"].resolve("a");
await tick();
verifySteps(["a:a"]);
resolvers["b"].resolve("b");
await tick();
verifySteps(["b:b"]);
});
test.only("should cancel a task properly", async () => {
const ctx = taskEffect(async () => {
let result;
step(`a:begin`);
result = await getTask("a");
step(`a:${result}`);
result = await getTask("b");
step(`b:${result}`);
});
verifySteps(["a:begin"]);
resolvers["a"].resolve("a");
await tick();
verifySteps(["a:a"]);
ctx.cancel();
resolvers["b"].resolve("b");
await tick();
verifySteps([]);
});
test("should run a task with subtasks properly", async () => {
taskEffect(async () => {
let result;
step(`a:begin`);
result = await getTask("a");
step(`a:${result}`);
result = await getTask("b");
step(`b:${result}`);
});
verifySteps(["a:begin"]);
resolvers["a"].resolve("a");
await tick();
verifySteps(["a:a"]);
resolvers["b"].resolve("b");
await tick();
verifySteps(["b:b"]);
});
});
-4
View File
@@ -458,10 +458,8 @@ describe("Portal", () => {
"parent:willPatch", "parent:willPatch",
"child:mounted", "child:mounted",
"parent:patched", "parent:patched",
"parent:willPatch",
"child:willPatch", "child:willPatch",
"child:patched", "child:patched",
"parent:patched",
]); ]);
expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>'); expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>');
@@ -472,10 +470,8 @@ describe("Portal", () => {
"parent:willPatch", "parent:willPatch",
"child:mounted", "child:mounted",
"parent:patched", "parent:patched",
"parent:willPatch",
"child:willPatch", "child:willPatch",
"child:patched", "child:patched",
"parent:patched",
"parent:willPatch", "parent:willPatch",
"child:willUnmount", "child:willUnmount",
"parent:patched", "parent:patched",
+918 -968
View File
File diff suppressed because it is too large Load Diff
+1 -63
View File
@@ -1,7 +1,5 @@
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils"; import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
import { makeTestFixture, nextMicroTick } from "./helpers"; import { nextMicroTick } from "./helpers";
import { getCurrent } from "../src/runtime/component_node";
import { Component, mount, xml } from "../src";
describe("event bus behaviour", () => { describe("event bus behaviour", () => {
test("can subscribe and be notified", () => { test("can subscribe and be notified", () => {
@@ -35,66 +33,6 @@ describe("event bus behaviour", () => {
bus.addEventListener("event", (ev: any) => expect(ev.detail).toBe("hello world")); bus.addEventListener("event", (ev: any) => expect(ev.detail).toBe("hello world"));
bus.trigger("event", "hello world"); bus.trigger("event", "hello world");
}); });
test("events are not validated if the bus is created outside of dev mode", async () => {
let bus_empty: EventBus | null = null;
class Root extends Component {
static template = xml`<div/>`;
setup() {
getCurrent(); // checks that we're in a component context
bus_empty = new EventBus([]);
}
}
await mount(Root, makeTestFixture());
bus_empty!.addEventListener("a", () => {});
bus_empty!.trigger("a");
bus_empty!.dispatchEvent(new CustomEvent("a"));
});
test("events are validated if the bus is created in dev mode & events are provided", async () => {
let bus: EventBus | null = null;
let bus_empty: EventBus | null = null;
let bbus_no_validation: EventBus | null = null;
class Root extends Component {
static template = xml`<div/>`;
setup() {
getCurrent(); // checks that we're in a component context
bus = new EventBus(["a", "b"]);
bus_empty = new EventBus([]);
bbus_no_validation = new EventBus();
}
}
await mount(Root, makeTestFixture(), { test: true });
bbus_no_validation!.addEventListener("c", () => {});
bbus_no_validation!.trigger("c");
bbus_no_validation!.dispatchEvent(new CustomEvent("c"));
bus!.addEventListener("a", () => {});
bus!.trigger("a");
bus!.dispatchEvent(new CustomEvent("a"));
expect(() => bus!.addEventListener("c", () => {})).toThrow(
"EventBus: subscribing to unknown event 'c'"
);
expect(() => bus!.trigger("c")).toThrow("EventBus: triggering unknown event 'c'");
expect(() => bus!.dispatchEvent(new CustomEvent("c"))).toThrow(
"EventBus: dispatching unknown event 'c'"
);
expect(() => bus_empty!.addEventListener("a", () => {})).toThrow(
"EventBus: subscribing to unknown event 'a'"
);
expect(() => bus_empty!.trigger("a")).toThrow("EventBus: triggering unknown event 'a'");
expect(() => bus_empty!.dispatchEvent(new CustomEvent("a"))).toThrow(
"EventBus: dispatching unknown event 'a'"
);
});
}); });
describe("batched", () => { describe("batched", () => {