Compare commits

..

1 Commits

Author SHA1 Message Date
Abdallah (ameb) 2b943e7278 Update templates.md
an => a
2025-05-13 09:43:58 +04:00
35 changed files with 5177 additions and 8771 deletions
-22
View File
@@ -320,28 +320,6 @@ class ComponentB extends owl.Component {
Note: the props validation code is done by using the [validate utility function](utils.md#validate). Note: the props validation code is done by using the [validate utility function](utils.md#validate).
### `slots` prop
If a component that uses [slots](slots.md) also lists or validates its props, then
you will have to explicitely allow the `slots` prop (with an `Object` type), or
allow extra props using the `*` notation mentioned above. This is because slots
are provided to a component [as props](slots.md#slots-and-props).
For example:
```js
class MyComponent extends Component {
static props = [someProp, slots?];
}
class MyComponentWithValidation extends Component {
static props = {
someProp: {type: Number, optional: true},
slots : {type: Object, optional: true},
}
}
```
## Good Practices ## Good Practices
A `props` object is a collection of values that come from the parent. As such, A `props` object is a collection of values that come from the parent. As such,
+1 -1
View File
@@ -680,7 +680,7 @@ class MyComponent extends Component {
mount(MyComponent, document.body); mount(MyComponent, document.body);
``` ```
This function simply generates an unique string id, and register the template This function simply generates a unique string id, and register the template
under that id in the internals of Owl, then return the id. under that id in the internals of Owl, then return the id.
## Rendering svg ## Rendering svg
+29 -84
View File
@@ -3850,16 +3850,7 @@ class CodeTarget {
return key; return key;
} }
} }
const TRANSLATABLE_ATTRS = [ const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
"alt",
"aria-label",
"aria-placeholder",
"aria-roledescription",
"aria-valuetext",
"label",
"placeholder",
"title",
];
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/; const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
class CodeGenerator { class CodeGenerator {
constructor(ast, options) { constructor(ast, options) {
@@ -4539,7 +4530,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.hasNoRepresentation).length; const n = ast.content.filter((c) => c.type !== 6 /* TSet */).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 +4544,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 forceNewBlock = !child.hasNoRepresentation; const isTSet = child.type === 6 /* TSet */;
const subCtx = createContext(ctx, { const subCtx = createContext(ctx, {
block, block,
index, index,
forceNewBlock, forceNewBlock: !isTSet,
isLast: ctx.isLast && i === l - 1, isLast: ctx.isLast && i === l - 1,
}); });
this.compileAST(child, subCtx); this.compileAST(child, subCtx);
if (forceNewBlock) { if (!isTSet) {
index++; index++;
} }
} }
@@ -4977,11 +4968,11 @@ function parseNode(node, ctx) {
parseTPortal(node, ctx) || parseTPortal(node, ctx) ||
parseTCall(node, ctx) || parseTCall(node, ctx) ||
parseTCallBlock(node) || parseTCallBlock(node) ||
parseTTranslation(node, ctx) ||
parseTTranslationContext(node, ctx) ||
parseTKey(node, ctx) ||
parseTEscNode(node, ctx) || parseTEscNode(node, ctx) ||
parseTOutNode(node, ctx) || parseTOutNode(node, ctx) ||
parseTKey(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTTranslationContext(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
parseDOMNode(node, ctx) || parseDOMNode(node, ctx) ||
@@ -5049,29 +5040,19 @@ 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");
const content = parseNode(node, ctx); return {
const ast = {
type: 12 /* TDebug */, type: 12 /* TDebug */,
content, content: parseNode(node, ctx),
}; };
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");
const content = parseNode(node, ctx); return {
const ast = {
type: 13 /* TLog */, type: 13 /* TLog */,
expr, expr,
content, content: parseNode(node, ctx),
}; };
if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
return null; return null;
} }
@@ -5301,19 +5282,11 @@ 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 content = parseNode(node, ctx); const body = parseNode(node, ctx);
if (!content) { if (!body) {
return null; return null;
} }
const ast = { return { type: 10 /* TKey */, expr: key, content: body };
type: 10 /* TKey */,
expr: key,
content,
};
if (content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// t-call // t-call
@@ -5422,7 +5395,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, hasNoRepresentation: true }; return { type: 6 /* TSet */, name, value, defaultValue, body };
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Components // Components
@@ -5601,51 +5574,30 @@ 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");
const result = parseNode(node, ctx); return {
if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) { type: 16 /* TTranslation */,
const children = result.content.map(wrapInTTranslationAST); content: parseNode(node, ctx),
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");
const result = parseNode(node, ctx); return {
if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) { type: 17 /* TTranslationContext */,
const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx)); content: parseNode(node, ctx),
return makeASTMulti(children); translationCtx,
} };
return wrapInTTranslationContextAST(result, translationCtx);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Portal // Portal
@@ -5690,13 +5642,6 @@ 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.
@@ -5709,7 +5654,7 @@ function parseChildNodes(node, ctx) {
case 1: case 1:
return children[0]; return children[0];
default: default:
return makeASTMulti(children); return { type: 3 /* Multi */, content: children };
} }
} }
/** /**
@@ -5813,7 +5758,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.1"; const version = "2.7.0";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Scheduler // Scheduler
@@ -6284,6 +6229,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-09-23T07:17:45.055Z'; __info__.date = '2025-03-26T12:58:40.935Z';
__info__.hash = '5211116'; __info__.hash = 'e788e36';
__info__.url = 'https://github.com/odoo/owl'; __info__.url = 'https://github.com/odoo/owl';
+3233 -6113
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.1", "version": "2.7.0",
"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 node_modules/.bin/jest --runInBand --watch --testTimeout=5000000", "test:debug": "node --inspect-brk 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
@@ -1,18 +0,0 @@
# 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,28 +1,4 @@
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.hasNoRepresentation).length; const n = ast.content.filter((c) => c.type !== ASTType.TSet).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 forceNewBlock = !child.hasNoRepresentation; const isTSet = child.type === ASTType.TSet;
const subCtx = createContext(ctx, { const subCtx = createContext(ctx, {
block, block,
index, index,
forceNewBlock, forceNewBlock: !isTSet,
isLast: ctx.isLast && i === l - 1, isLast: ctx.isLast && i === l - 1,
}); });
this.compileAST(child, subCtx); this.compileAST(child, subCtx);
if (forceNewBlock) { if (!isTSet) {
index++; index++;
} }
} }
+40 -95
View File
@@ -31,17 +31,12 @@ export const enum ASTType {
TPortal, TPortal,
} }
export interface BaseAST { export interface ASTText {
type: ASTType;
hasNoRepresentation?: true;
}
export interface ASTText extends BaseAST {
type: ASTType.Text; type: ASTType.Text;
value: string; value: string;
} }
export interface ASTComment extends BaseAST { export interface ASTComment {
type: ASTType.Comment; type: ASTType.Comment;
value: string; value: string;
} }
@@ -57,7 +52,7 @@ interface TModelInfo {
specialInitTargetAttr: string | null; specialInitTargetAttr: string | null;
} }
export interface ASTDomNode extends BaseAST { export interface ASTDomNode {
type: ASTType.DomNode; type: ASTType.DomNode;
tag: string; tag: string;
content: AST[]; content: AST[];
@@ -70,24 +65,24 @@ export interface ASTDomNode extends BaseAST {
ns: string | null; ns: string | null;
} }
export interface ASTMulti extends BaseAST { export interface ASTMulti {
type: ASTType.Multi; type: ASTType.Multi;
content: AST[]; content: AST[];
} }
export interface ASTTEsc extends BaseAST { export interface ASTTEsc {
type: ASTType.TEsc; type: ASTType.TEsc;
expr: string; expr: string;
defaultValue: string; defaultValue: string;
} }
export interface ASTTOut extends BaseAST { export interface ASTTOut {
type: ASTType.TOut; type: ASTType.TOut;
expr: string; expr: string;
body: AST[] | null; body: AST[] | null;
} }
export interface ASTTif extends BaseAST { export interface ASTTif {
type: ASTType.TIf; type: ASTType.TIf;
condition: string; condition: string;
content: AST; content: AST;
@@ -95,16 +90,15 @@ export interface ASTTif extends BaseAST {
tElse: AST | null; tElse: AST | null;
} }
export interface ASTTSet extends BaseAST { export interface ASTTSet {
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 extends BaseAST { export interface ASTTForEach {
type: ASTType.TForEach; type: ASTType.TForEach;
collection: string; collection: string;
elem: string; elem: string;
@@ -117,13 +111,13 @@ export interface ASTTForEach extends BaseAST {
key: string | null; key: string | null;
} }
export interface ASTTKey extends BaseAST { export interface ASTTKey {
type: ASTType.TKey; type: ASTType.TKey;
expr: string; expr: string;
content: AST; content: AST;
} }
export interface ASTTCall extends BaseAST { export interface ASTTCall {
type: ASTType.TCall; type: ASTType.TCall;
name: string; name: string;
body: AST[] | null; body: AST[] | null;
@@ -138,7 +132,7 @@ interface SlotDefinition {
attrsTranslationCtx: Attrs | null; attrsTranslationCtx: Attrs | null;
} }
export interface ASTComponent extends BaseAST { export interface ASTComponent {
type: ASTType.TComponent; type: ASTType.TComponent;
name: string; name: string;
isDynamic: boolean; isDynamic: boolean;
@@ -149,7 +143,7 @@ export interface ASTComponent extends BaseAST {
slots: { [name: string]: SlotDefinition } | null; slots: { [name: string]: SlotDefinition } | null;
} }
export interface ASTSlot extends BaseAST { export interface ASTSlot {
type: ASTType.TSlot; type: ASTType.TSlot;
name: string; name: string;
attrs: Attrs | null; attrs: Attrs | null;
@@ -158,34 +152,34 @@ export interface ASTSlot extends BaseAST {
defaultContent: AST | null; defaultContent: AST | null;
} }
export interface ASTTCallBlock extends BaseAST { export interface ASTTCallBlock {
type: ASTType.TCallBlock; type: ASTType.TCallBlock;
name: string; name: string;
} }
export interface ASTDebug extends BaseAST { export interface ASTDebug {
type: ASTType.TDebug; type: ASTType.TDebug;
content: AST | null; content: AST | null;
} }
export interface ASTLog extends BaseAST { export interface ASTLog {
type: ASTType.TLog; type: ASTType.TLog;
expr: string; expr: string;
content: AST | null; content: AST | null;
} }
export interface ASTTranslation extends BaseAST { export interface ASTTranslation {
type: ASTType.TTranslation; type: ASTType.TTranslation;
content: AST | null; content: AST | null;
} }
export interface ASTTranslationContext extends BaseAST { export interface ASTTranslationContext {
type: ASTType.TTranslationContext; type: ASTType.TTranslationContext;
content: AST | null; content: AST | null;
translationCtx: string; translationCtx: string;
} }
export interface ASTTPortal extends BaseAST { export interface ASTTPortal {
type: ASTType.TPortal; type: ASTType.TPortal;
target: string; target: string;
content: AST; content: AST;
@@ -259,11 +253,11 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
parseTPortal(node, ctx) || parseTPortal(node, ctx) ||
parseTCall(node, ctx) || parseTCall(node, ctx) ||
parseTCallBlock(node, ctx) || parseTCallBlock(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTTranslationContext(node, ctx) ||
parseTKey(node, ctx) ||
parseTEscNode(node, ctx) || parseTEscNode(node, ctx) ||
parseTOutNode(node, ctx) || parseTOutNode(node, ctx) ||
parseTKey(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTTranslationContext(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
parseDOMNode(node, ctx) || parseDOMNode(node, ctx) ||
@@ -340,30 +334,20 @@ 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");
const content = parseNode(node, ctx); return {
const ast: ASTDebug = {
type: ASTType.TDebug, type: ASTType.TDebug,
content, content: parseNode(node, ctx),
}; };
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");
const content = parseNode(node, ctx); return {
const ast: ASTLog = {
type: ASTType.TLog, type: ASTType.TLog,
expr, expr,
content, content: parseNode(node, ctx),
}; };
if (content?.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
return null; return null;
} }
@@ -614,19 +598,11 @@ 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 content = parseNode(node, ctx); const body = parseNode(node, ctx);
if (!content) { if (!body) {
return null; return null;
} }
const ast: ASTTKey = { return { type: ASTType.TKey, expr: key, content: body };
type: ASTType.TKey,
expr: key,
content,
};
if (content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -748,7 +724,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, hasNoRepresentation: true }; return { type: ASTType.TSet, name, value, defaultValue, body };
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -940,55 +916,32 @@ 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");
const result = parseNode(node, ctx); return {
if (result?.type === ASTType.Multi) { type: ASTType.TTranslation,
const children = result.content.map(wrapInTTranslationAST); content: parseNode(node, ctx),
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");
const result = parseNode(node, ctx); return {
if (result?.type === ASTType.Multi) { type: ASTType.TTranslationContext,
const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx)); content: parseNode(node, ctx),
return makeASTMulti(children); translationCtx,
} };
return wrapInTTranslationContextAST(result, translationCtx);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -1037,14 +990,6 @@ 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.
@@ -1057,7 +1002,7 @@ function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
case 1: case 1:
return children[0]; return children[0];
default: default:
return makeASTMulti(children); return { type: ASTType.Multi, content: children };
} }
} }
-46
View File
@@ -1,46 +0,0 @@
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;
}
+32 -45
View File
@@ -1,13 +1,12 @@
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 { addAtomToContext, reactive, targets, withoutReactivity } from "./reactivity"; import { clearReactivesForCallback, getSubscriptions, reactive, targets } 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;
@@ -43,7 +42,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
@@ -55,7 +54,15 @@ function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>)
* @see reactive * @see reactive
*/ */
export function useState<T extends object>(state: T): T { export function useState<T extends object>(state: T): T {
return reactive(state); const node = getCurrent();
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);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -89,8 +96,6 @@ 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>,
@@ -104,15 +109,6 @@ 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) {
@@ -120,18 +116,16 @@ 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);
withoutReactivity(() => {
this.component.setup(); this.component.setup();
});
currentNode = null; currentNode = null;
} }
@@ -148,11 +142,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
const component = this.component; const component = this.component;
try { try {
let prom: Promise<any[]>; await Promise.all(this.willStart.map((f) => f.call(component)));
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;
@@ -268,18 +258,15 @@ 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;
let prom: Promise<any[]>; const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
withoutReactivity(() => { await prom;
prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
});
await prom!;
if (fiber !== this.fiber) { if (fiber !== this.fiber) {
return; return;
} }
@@ -397,8 +384,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
@@ -1,26 +0,0 @@
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,8 +3,6 @@ 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;
@@ -135,16 +133,12 @@ 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);
} }
} }
+5 -27
View File
@@ -1,6 +1,5 @@
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";
@@ -87,43 +86,22 @@ 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 = computeDependenciesWithContext(); dependencies = computeDependencies();
runEffect(); cleanup = effect(...dependencies);
}); });
onPatched(() => { onPatched(() => {
const newDeps = computeDependenciesWithContext(); const newDeps = computeDependencies();
const shouldReapply = newDeps.some((val: any, i: number) => val !== dependencies[i]); const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
if (shouldReapply) { if (shouldReapply) {
dependencies = newDeps; dependencies = newDeps;
if (cleanup) { if (cleanup) {
cleanup(); cleanup();
} }
runEffect(); cleanup = effect(...dependencies);
} }
}); });
+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, effect, withoutReactivity } from "./reactivity"; export { reactive, markRaw, toRaw } 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 {
+141 -270
View File
@@ -1,9 +1,13 @@
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"
@@ -51,8 +55,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) { function possiblyReactive(val: any, cb: Callback) {
return canBeMadeReactive(val) ? reactive(val) : val; return canBeMadeReactive(val) ? reactive(val, cb) : val;
} }
const skipped = new WeakSet<Target>(); const skipped = new WeakSet<Target>();
@@ -77,37 +81,7 @@ 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 targetToKeysToAtomItem = new WeakMap<Target, Map<PropertyKey, Atom>>(); const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>();
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.
@@ -117,73 +91,23 @@ export function addAtomToContext(atom: Atom, executionContext: ExecutionContext)
* 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 onReadTargetKey(target: Target, key: PropertyKey, receiver: any): void { function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
const executionContext = getExecutionContext(); if (callback === NO_CALLBACK) {
executionContext?.onReadAtom(getTargetKeyAtom(target, key)); return;
}
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.
@@ -193,21 +117,66 @@ function processAtoms() {
* @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 onWriteTargetKey(target: Target, key: PropertyKey): void { function notifyReactives(target: Target, key: PropertyKey): void {
const keyToAtomItem = targetToKeysToAtomItem.get(target)!; const keyToCallbacks = targetToKeysToCallbacks.get(target);
if (!keyToAtomItem) { if (!keyToCallbacks) {
return; return;
} }
const atom = keyToAtomItem.get(key); const callbacks = keyToCallbacks.get(key);
if (!atom) { if (!callbacks) {
return; return;
} }
scheduleAtom(atom); // Loop on copy because clearReactivesForCallback will modify the set in place
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, Reactive<Target>>(); const reactiveCache = new WeakMap<Target, WeakMap<Callback, 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
@@ -235,7 +204,7 @@ const reactiveCache = new WeakMap<Target, 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): T { export function reactive<T extends Target>(target: T, callback: Callback = NO_CALLBACK): 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`);
} }
@@ -244,130 +213,30 @@ export function reactive<T extends Target>(target: T): T {
} }
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); return reactive(targets.get(target) as T, callback);
return target;
} }
const reactive = reactiveCache.get(target)!; if (!reactiveCache.has(target)) {
if (reactive) return reactive as T; reactiveCache.set(target, new WeakMap());
}
const reactivesForTarget = reactiveCache.get(target)!;
if (!reactivesForTarget.has(callback)) {
const targetRawType = rawType(target); const targetRawType = rawType(target);
const handler = COLLECTION_RAW_TYPES.includes(targetRawType) const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
? collectionsProxyHandler(target as Collection, targetRawType as CollectionRawType) ? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
: basicProxyHandler<T>(); : basicProxyHandler<T>(callback);
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>; const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
reactivesForTarget.set(callback, proxy);
reactiveCache.set(target, 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>(): ProxyHandler<T> { function basicProxyHandler<T extends Target>(callback: Callback): 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
@@ -375,15 +244,15 @@ function basicProxyHandler<T extends Target>(): 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);
} }
onReadTargetKey(target, key); observeTargetKey(target, key, callback);
return possiblyReactive(Reflect.get(target, key, receiver)); return possiblyReactive(Reflect.get(target, key, receiver), callback);
}, },
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)) {
onWriteTargetKey(target, KEYCHANGES); notifyReactives(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
@@ -392,26 +261,26 @@ function basicProxyHandler<T extends Target>(): ProxyHandler<T> {
originalValue !== Reflect.get(target, key, receiver) || originalValue !== Reflect.get(target, key, receiver) ||
(key === "length" && Array.isArray(target)) (key === "length" && Array.isArray(target))
) { ) {
onWriteTargetKey(target, key); notifyReactives(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
onWriteTargetKey(target, KEYCHANGES); notifyReactives(target, KEYCHANGES);
onWriteTargetKey(target, key); notifyReactives(target, key);
return ret; return ret;
}, },
ownKeys(target) { ownKeys(target) {
onReadTargetKey(target, KEYCHANGES); observeTargetKey(target, KEYCHANGES, callback);
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.
onReadTargetKey(target, KEYCHANGES); observeTargetKey(target, KEYCHANGES, callback);
return Reflect.has(target, key); return Reflect.has(target, key);
}, },
} as ProxyHandler<T>; } as ProxyHandler<T>;
@@ -424,11 +293,11 @@ function basicProxyHandler<T extends Target>(): 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) { function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callback) {
return (key: any) => { return (key: any) => {
key = toRaw(key); key = toRaw(key);
onReadTargetKey(target, key); observeTargetKey(target, key, callback);
return possiblyReactive(target[methodName](key)); return possiblyReactive(target[methodName](key), callback);
}; };
} }
/** /**
@@ -441,15 +310,16 @@ function makeKeyObserver(methodName: "has" | "get", target: any) {
*/ */
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* () {
onReadTargetKey(target, KEYCHANGES); observeTargetKey(target, KEYCHANGES, callback);
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;
onReadTargetKey(target, key); observeTargetKey(target, key, callback);
yield possiblyReactive(item); yield possiblyReactive(item, callback);
} }
}; };
} }
@@ -461,16 +331,16 @@ function makeIteratorObserver(
* @param target @see reactive * @param target @see reactive
* @param callback @see reactive * @param callback @see reactive
*/ */
function makeForEachObserver(target: any) { function makeForEachObserver(target: any, callback: Callback) {
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) {
onReadTargetKey(target, KEYCHANGES); observeTargetKey(target, KEYCHANGES, callback);
target.forEach(function (val: any, key: any, targetObj: any) { target.forEach(function (val: any, key: any, targetObj: any) {
onReadTargetKey(target, key); observeTargetKey(target, key, callback);
forEachCb.call( forEachCb.call(
thisArg, thisArg,
possiblyReactive(val), possiblyReactive(val, callback),
possiblyReactive(key), possiblyReactive(key, callback),
possiblyReactive(targetObj) possiblyReactive(targetObj, callback)
); );
}, thisArg); }, thisArg);
}; };
@@ -497,10 +367,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) {
onWriteTargetKey(target, KEYCHANGES); notifyReactives(target, KEYCHANGES);
} }
if (originalValue !== target[getterName](key)) { if (originalValue !== target[getterName](key)) {
onWriteTargetKey(target, key); notifyReactives(target, key);
} }
return ret; return ret;
}; };
@@ -515,9 +385,9 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
return () => { return () => {
const allKeys = [...target.keys()]; const allKeys = [...target.keys()];
target.clear(); target.clear();
onWriteTargetKey(target, KEYCHANGES); notifyReactives(target, KEYCHANGES);
for (const key of allKeys) { for (const key of allKeys) {
onWriteTargetKey(target, key); notifyReactives(target, key);
} }
}; };
} }
@@ -529,40 +399,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) => ({ Set: (target: any, callback: Callback) => ({
has: makeKeyObserver("has", target), has: makeKeyObserver("has", target, callback),
add: delegateAndNotify("add", "has", target), add: delegateAndNotify("add", "has", target),
delete: delegateAndNotify("delete", "has", target), delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target), keys: makeIteratorObserver("keys", target, callback),
values: makeIteratorObserver("values", target), values: makeIteratorObserver("values", target, callback),
entries: makeIteratorObserver("entries", target), entries: makeIteratorObserver("entries", target, callback),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target), [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
forEach: makeForEachObserver(target), forEach: makeForEachObserver(target, callback),
clear: makeClearNotifier(target), clear: makeClearNotifier(target),
get size() { get size() {
onReadTargetKey(target, KEYCHANGES); observeTargetKey(target, KEYCHANGES, callback);
return target.size; return target.size;
}, },
}), }),
Map: (target: any) => ({ Map: (target: any, callback: Callback) => ({
has: makeKeyObserver("has", target), has: makeKeyObserver("has", target, callback),
get: makeKeyObserver("get", target), get: makeKeyObserver("get", target, callback),
set: delegateAndNotify("set", "get", target), set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target), delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target), keys: makeIteratorObserver("keys", target, callback),
values: makeIteratorObserver("values", target), values: makeIteratorObserver("values", target, callback),
entries: makeIteratorObserver("entries", target), entries: makeIteratorObserver("entries", target, callback),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target), [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
forEach: makeForEachObserver(target), forEach: makeForEachObserver(target, callback),
clear: makeClearNotifier(target), clear: makeClearNotifier(target),
get size() { get size() {
onReadTargetKey(target, KEYCHANGES); observeTargetKey(target, KEYCHANGES, callback);
return target.size; return target.size;
}, },
}), }),
WeakMap: (target: any) => ({ WeakMap: (target: any, callback: Callback) => ({
has: makeKeyObserver("has", target), has: makeKeyObserver("has", target, callback),
get: makeKeyObserver("get", target), get: makeKeyObserver("get", target, callback),
set: delegateAndNotify("set", "get", target), set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target), delete: delegateAndNotify("delete", "has", target),
}), }),
@@ -576,19 +446,20 @@ 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); const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
return Object.assign(basicProxyHandler(), { return Object.assign(basicProxyHandler(callback), {
// 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];
} }
onReadTargetKey(target, key); observeTargetKey(target, key, callback);
return possiblyReactive(target[key]); return possiblyReactive(target[key], callback);
}, },
}) as ProxyHandler<T>; }) as ProxyHandler<T>;
} }
-72
View File
@@ -1,72 +0,0 @@
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);
// });
// }
}
+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.1"; export const version = "2.7.0";
+109
View File
@@ -1,5 +1,51 @@
// 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
) { ) {
@@ -109,6 +155,69 @@ 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,27 +49,6 @@ 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
) { ) {
@@ -87,24 +66,3 @@ 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,18 +103,3 @@ 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,7 +10,8 @@ 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\`);
return text(ctx['label']); const b2 = text(ctx['label']);
return multi([b2]);
} }
}" }"
`; `;
@@ -93,23 +94,6 @@ 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
) { ) {
@@ -169,21 +153,6 @@ 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
) { ) {
@@ -295,23 +264,6 @@ 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
) { ) {
+1 -111
View File
@@ -692,7 +692,6 @@ describe("qweb parser", () => {
value: "value", value: "value",
defaultValue: null, defaultValue: null,
body: null, body: null,
hasNoRepresentation: true,
}); });
}); });
@@ -703,7 +702,6 @@ 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({
@@ -725,7 +723,6 @@ 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({
@@ -748,7 +745,6 @@ describe("qweb parser", () => {
}, },
{ type: ASTType.Text, value: "abc" }, { type: ASTType.Text, value: "abc" },
], ],
hasNoRepresentation: true,
}); });
}); });
@@ -762,7 +758,6 @@ 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,
@@ -788,14 +783,7 @@ describe("qweb parser", () => {
condition: "flag", condition: "flag",
content: { type: ASTType.Text, value: "1" }, content: { type: ASTType.Text, value: "1" },
tElif: null, tElif: null,
tElse: { tElse: { type: ASTType.TSet, name: "ourvar", value: "0", defaultValue: null, body: null },
type: ASTType.TSet,
name: "ourvar",
value: "0",
defaultValue: null,
body: null,
hasNoRepresentation: true,
},
}, },
], ],
}); });
@@ -1983,54 +1971,6 @@ describe("qweb parser", () => {
}); });
}); });
test('t-translation="off": interaction with t-esc', async () => {
expect(parse(`<span t-esc="a" t-translation="off"/>`)).toEqual({
type: ASTType.TTranslation,
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
defaultValue: "",
expr: "a",
type: ASTType.TEsc,
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "span",
type: ASTType.DomNode,
},
});
});
test('t-translation="off": interaction with t-out', async () => {
expect(parse(`<span t-out="a" t-translation="off"/>`)).toEqual({
type: ASTType.TTranslation,
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
body: null,
expr: "a",
type: ASTType.TOut,
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "span",
type: ASTType.DomNode,
},
});
});
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// t-translation-context // t-translation-context
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -2068,56 +2008,6 @@ describe("qweb parser", () => {
}); });
}); });
test("t-translation-context: interaction with t-esc", async () => {
expect(parse(`<span t-esc="a" t-translation-context="fr"/>`)).toEqual({
type: ASTType.TTranslationContext,
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
defaultValue: "",
expr: "a",
type: ASTType.TEsc,
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "span",
type: ASTType.DomNode,
},
translationCtx: "fr",
});
});
test("t-translation-context: interaction with t-out", async () => {
expect(parse(`<span t-out="a" t-translation-context="fr"/>`)).toEqual({
type: ASTType.TTranslationContext,
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
body: null,
expr: "a",
type: ASTType.TOut,
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "span",
type: ASTType.DomNode,
},
translationCtx: "fr",
});
});
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// t-translation-context-attr // t-translation-context-attr
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
-30
View File
@@ -38,34 +38,4 @@ 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,10 +63,4 @@ 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,21 +129,6 @@ 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`
@@ -185,22 +170,6 @@ 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", () => {
@@ -324,20 +293,4 @@ 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,36 +52,6 @@ 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
) { ) {
@@ -170,3 +140,39 @@ 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);
}
}"
`;
+7 -8
View File
@@ -1,28 +1,27 @@
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,
onRendered,
onWillPatch, onWillPatch,
onWillRender,
onWillStart, onWillStart,
onWillRender,
onRendered,
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,
nextAppError,
nextMicroTick,
nextTick, nextTick,
nextMicroTick,
snapshotEverything, snapshotEverything,
steps,
useLogLifecycle, useLogLifecycle,
nextAppError,
steps,
} from "../helpers"; } from "../helpers";
import { OwlError } from "../../src/common/owl_error";
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -648,7 +647,7 @@ describe("can catch errors", () => {
setup() { setup() {
onWillStart(() => { onWillStart(() => {
getCurrent(); this.state = useState({ value: 2 });
}); });
} }
} }
+4
View File
@@ -450,10 +450,14 @@ 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 () => {
+43 -38
View File
@@ -2,11 +2,12 @@ import {
Component, Component,
mount, mount,
onPatched, onPatched,
onWillPatch,
onWillRender, onWillRender,
onWillPatch,
onWillUnmount, onWillUnmount,
reactive, useState,
xml, xml,
toRaw,
} from "../../src"; } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers"; import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
@@ -19,36 +20,10 @@ 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 = reactive({ value: 42 }); counter = useState({ 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>");
@@ -61,7 +36,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 = reactive({ a: 5, b: 7 }); state = useState({ a: 5, b: 7 });
setup() { setup() {
onWillRender(() => n++); onWillRender(() => n++);
} }
@@ -82,7 +57,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 = reactive(new Map([["value", 42]])); counter = useState(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>");
@@ -97,7 +72,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 = reactive({ n: 2 }); state = useState({ n: 2 });
setup() { setup() {
onWillRender(() => { onWillRender(() => {
steps.push("render"); steps.push("render");
@@ -121,7 +96,7 @@ describe("reactivity in lifecycle", () => {
</div> </div>
`; `;
static components = { Child }; static components = { Child };
state = reactive({ val: 1, flag: true }); state = useState({ val: 1, flag: true });
} }
const parent = await mount(Parent, fixture); const parent = await mount(Parent, fixture);
expect(steps).toEqual(["render"]); expect(steps).toEqual(["render"]);
@@ -167,7 +142,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 = reactive({ val: 1 }); state = useState({ val: 1 });
setup() { setup() {
STATE = this.state; STATE = this.state;
onWillRender(() => { onWillRender(() => {
@@ -192,7 +167,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 = reactive({ renderChild: true, content: { a: 2 } }); state: any = useState({ renderChild: true, content: { a: 2 } });
setup() { setup() {
useLogLifecycle(); useLogLifecycle();
} }
@@ -230,8 +205,7 @@ describe("reactivity in lifecycle", () => {
`); `);
}); });
// todo: unskip it test("Component is automatically subscribed to reactive object received as prop", async () => {
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 {
@@ -244,7 +218,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 = reactive({ b: 2 }); reactiveObj = useState({ b: 2 });
setup() { setup() {
onWillRender(() => parentRenderCount++); onWillRender(() => parentRenderCount++);
} }
@@ -263,3 +237,34 @@ 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,8 +330,12 @@ 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
@@ -1,109 +0,0 @@
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,8 +458,10 @@ 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>');
@@ -470,8 +472,10 @@ 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",
+809 -759
View File
File diff suppressed because it is too large Load Diff