Compare commits

..

1 Commits

Author SHA1 Message Date
Lucas Perais (lpe) fb7d25ba0e [FIX] error_handling: onError works if handling happens between culprit and root
Have a component implementing onError that calls a callback from one of its parent.
That parent should not be the original source of the rendering (a parent above is).

Make the callback handle the error and trigger an render()

Before this commit, the parent did not see it was handling a subtree in error,
and did not have a chance to revert that error state in its rendering stack.

After this commit, this flow works.
2025-10-02 12:46:55 +02:00
24 changed files with 5182 additions and 8261 deletions
+3741 -6621
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -22,7 +22,7 @@
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
"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",
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
"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<
string,
(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;
};
-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;
}
+33 -46
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 { BDom, VNode } from "./blockdom";
import { makeTaskContext, TaskContext } from "./cancellableContext";
import { Component, ComponentConstructor, Props } from "./component";
import { fibersInError } from "./error_handling";
import { OwlError } from "../common/owl_error";
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 { batched, Callback } from "./utils";
let currentNode: ComponentNode | null = null;
@@ -43,7 +42,7 @@ function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>)
// 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.
* 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
*/
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[] = [];
patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = [];
taskContext: TaskContext;
executionContext: ExecutionContext;
constructor(
C: ComponentConstructor<P, E>,
@@ -104,15 +109,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
this.parent = parent;
this.props = props;
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;
props = Object.assign({}, props);
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;
this.childEnv = env;
// for (const key in props) {
// const prop = props[key];
// if (prop && typeof prop === "object" && targets.has(prop)) {
// props[key] = useState(prop);
// }
// }
for (const key in props) {
const prop = props[key];
if (prop && typeof prop === "object" && targets.has(prop)) {
props[key] = useState(prop);
}
}
this.component = new C(props, env, this);
const ctx = Object.assign(Object.create(this.component), { this: this.component });
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
withoutReactivity(() => {
this.component.setup();
});
this.component.setup();
currentNode = null;
}
@@ -148,11 +142,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
const component = this.component;
try {
let prom: Promise<any[]>;
withoutReactivity(() => {
prom = Promise.all(this.willStart.map((f) => f.call(component)));
});
await prom!;
await Promise.all(this.willStart.map((f) => f.call(component)));
} catch (e) {
this.app.handleError({ node: this, error: e });
return;
@@ -268,18 +258,15 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
currentNode = this;
// for (const key in props) {
// const prop = props[key];
// if (prop && typeof prop === "object" && targets.has(prop)) {
// props[key] = useState(prop);
// }
// }
for (const key in props) {
const prop = props[key];
if (prop && typeof prop === "object" && targets.has(prop)) {
props[key] = useState(prop);
}
}
currentNode = null;
let prom: Promise<any[]>;
withoutReactivity(() => {
prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
});
await prom!;
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
if (fiber !== this.fiber) {
return;
}
@@ -397,8 +384,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
return this.component.constructor.name;
}
// get subscriptions(): ReturnType<typeof getSubscriptions> {
// const render = batchedRenderFunctions.get(this);
// return render ? getSubscriptions(render) : [];
// }
get subscriptions(): ReturnType<typeof getSubscriptions> {
const render = batchedRenderFunctions.get(this);
return render ? getSubscriptions(render) : [];
}
}
+1
View File
@@ -55,6 +55,7 @@ export function handleError(params: ErrorParams) {
let current: Fiber | null = fiber;
do {
current.node.fiber = current;
fibersInError.set(current, error);
current = current.parent;
} while (current);
-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 { OwlError } from "../common/owl_error";
import { STATUS } from "./status";
import { popTaskContext, pushTaskContext } from "./cancellableContext";
import { popExecutionContext, pushExecutionContext } from "./executionContext";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber;
@@ -135,16 +133,12 @@ export class Fiber {
const node = this.node;
const root = this.root;
if (root) {
pushTaskContext(node.taskContext);
pushExecutionContext(node.executionContext);
try {
(this.bdom as any) = true;
this.bdom = node.renderFn();
} catch (e) {
node.app.handleError({ node, error: e });
}
popExecutionContext();
popTaskContext();
root.setCounter(root.counter - 1);
}
}
+5 -27
View File
@@ -1,6 +1,5 @@
import type { Env } from "./app";
import { getCurrent } from "./component_node";
import { popExecutionContext, pushExecutionContext } from "./executionContext";
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
import { inOwnerDocument } from "./utils";
@@ -87,43 +86,22 @@ export function useEffect<T extends unknown[]>(
effect: Effect<T>,
computeDependencies: () => [...T] = () => [NaN] as never
) {
const context = getCurrent().component.__owl__.executionContext;
let cleanup: (() => void) | void;
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(() => {
dependencies = computeDependenciesWithContext();
runEffect();
dependencies = computeDependencies();
cleanup = effect(...dependencies);
});
onPatched(() => {
const newDeps = computeDependenciesWithContext();
const shouldReapply = newDeps.some((val: any, i: number) => val !== dependencies[i]);
const newDeps = computeDependencies();
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
if (shouldReapply) {
dependencies = newDeps;
if (cleanup) {
cleanup();
}
runEffect();
cleanup = effect(...dependencies);
}
});
+1 -1
View File
@@ -39,7 +39,7 @@ export { Component } from "./component";
export type { ComponentConstructor } from "./component";
export { useComponent, useState } from "./component_node";
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 { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
export {
+141 -270
View File
@@ -1,9 +1,13 @@
import type { Callback } from "./utils";
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
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
// 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
* @returns a reactive for the given object when possible, the original otherwise
*/
function possiblyReactive(val: any) {
return canBeMadeReactive(val) ? reactive(val) : val;
function possiblyReactive(val: any, cb: Callback) {
return canBeMadeReactive(val) ? reactive(val, cb) : val;
}
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;
}
const targetToKeysToAtomItem = new WeakMap<Target, Map<PropertyKey, Atom>>();
const scheduledAtoms = new Set<Atom>();
function makeAtom(getValue: () => any): Atom {
const atom: Atom = {
executionContexts: new Set<ExecutionContext>(),
dependents: new Set<Atom>(),
// getValue,
};
return atom;
}
function getTargetKeyAtom(target: Target, key: PropertyKey): Atom {
let keyToAtomItem: Map<PropertyKey, Atom> = targetToKeysToAtomItem.get(target)!;
if (!keyToAtomItem) {
keyToAtomItem = new Map();
targetToKeysToAtomItem.set(target, keyToAtomItem);
}
let atom = keyToAtomItem.get(key)!;
if (!atom) {
atom = makeAtom(() => Reflect.get(target, key));
keyToAtomItem.set(key, atom);
}
return atom;
}
export function addAtomToContext(atom: Atom, executionContext: ExecutionContext) {
executionContext.atoms.add(atom);
atom.executionContexts.add(executionContext);
}
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>();
/**
* Observes a given key on a target with an callback. The callback will be
* called when the given key changes on the target.
@@ -117,73 +91,23 @@ export function addAtomToContext(atom: Atom, executionContext: ExecutionContext)
* or deletion)
* @param callback the function to call when the key changes
*/
function onReadTargetKey(target: Target, key: PropertyKey, receiver: any): void {
const executionContext = getExecutionContext();
executionContext?.onReadAtom(getTargetKeyAtom(target, key));
}
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 observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
if (callback === NO_CALLBACK) {
return;
}
}
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);
if (!targetToKeysToCallbacks.get(target)) {
targetToKeysToCallbacks.set(target, new Map());
}
for (const context of scheduledContexts) {
pushExecutionContext(context);
try {
context.update?.();
} finally {
popExecutionContext();
}
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
if (!keyToCallbacks.get(key)) {
keyToCallbacks.set(key, new Set());
}
}
/**
* Notify Reactives that are observing a given target that a key has changed on
}
});
};
for (const context of executionContexts) {
context.update();
keyToCallbacks.get(key)!.add(callback);
if (!callbacksToTargets.has(callback)) {
callbacksToTargets.set(callback, new Set());
}
callbacksToTargets.get(callback)!.add(target);
}
/**
* Notify Reactives that are observing a given target that a key has changed on
* the target.
@@ -193,21 +117,66 @@ function processAtoms() {
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
* or deleted)
*/
function onWriteTargetKey(target: Target, key: PropertyKey): void {
const keyToAtomItem = targetToKeysToAtomItem.get(target)!;
if (!keyToAtomItem) {
function notifyReactives(target: Target, key: PropertyKey): void {
const keyToCallbacks = targetToKeysToCallbacks.get(target);
if (!keyToCallbacks) {
return;
}
const atom = keyToAtomItem.get(key);
if (!atom) {
const callbacks = keyToCallbacks.get(key);
if (!callbacks) {
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
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
* 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
* @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)) {
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)) {
// target is reactive, create a reactive on the underlying object instead
// return reactive(targets.get(target) as T);
return target;
return reactive(targets.get(target) as T, callback);
}
const reactive = reactiveCache.get(target)!;
if (reactive) return reactive as T;
const targetRawType = rawType(target);
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
? collectionsProxyHandler(target as Collection, targetRawType as CollectionRawType)
: basicProxyHandler<T>();
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
reactiveCache.set(target, proxy);
targets.set(proxy, target);
return proxy;
if (!reactiveCache.has(target)) {
reactiveCache.set(target, new WeakMap());
}
const reactivesForTarget = reactiveCache.get(target)!;
if (!reactivesForTarget.has(callback)) {
const targetRawType = rawType(target);
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
: basicProxyHandler<T>(callback);
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
reactivesForTarget.set(callback, proxy);
targets.set(proxy, target);
}
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.
*
* @param callback @see reactive
* @returns a proxy handler object
*/
function basicProxyHandler<T extends Target>(): ProxyHandler<T> {
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> {
return {
get(target, key, receiver) {
// 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) {
return Reflect.get(target, key, receiver);
}
onReadTargetKey(target, key);
return possiblyReactive(Reflect.get(target, key, receiver));
observeTargetKey(target, key, callback);
return possiblyReactive(Reflect.get(target, key, receiver), callback);
},
set(target, key, value, receiver) {
const hadKey = objectHasOwnProperty.call(target, key);
const originalValue = Reflect.get(target, key, receiver);
const ret = Reflect.set(target, key, toRaw(value), receiver);
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
// 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) ||
(key === "length" && Array.isArray(target))
) {
onWriteTargetKey(target, key);
notifyReactives(target, key);
}
return ret;
},
deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key);
// TODO: only notify when something was actually deleted
onWriteTargetKey(target, KEYCHANGES);
onWriteTargetKey(target, key);
notifyReactives(target, KEYCHANGES);
notifyReactives(target, key);
return ret;
},
ownKeys(target) {
onReadTargetKey(target, KEYCHANGES);
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.ownKeys(target);
},
has(target, 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
// 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);
},
} as ProxyHandler<T>;
@@ -424,11 +293,11 @@ function basicProxyHandler<T extends Target>(): ProxyHandler<T> {
* @param target @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) => {
key = toRaw(key);
onReadTargetKey(target, key);
return possiblyReactive(target[methodName](key));
observeTargetKey(target, key, callback);
return possiblyReactive(target[methodName](key), callback);
};
}
/**
@@ -441,15 +310,16 @@ function makeKeyObserver(methodName: "has" | "get", target: any) {
*/
function makeIteratorObserver(
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
target: any
target: any,
callback: Callback
) {
return function* () {
onReadTargetKey(target, KEYCHANGES);
observeTargetKey(target, KEYCHANGES, callback);
const keys = target.keys();
for (const item of target[methodName]()) {
const key = keys.next().value;
onReadTargetKey(target, key);
yield possiblyReactive(item);
observeTargetKey(target, key, callback);
yield possiblyReactive(item, callback);
}
};
}
@@ -461,16 +331,16 @@ function makeIteratorObserver(
* @param target @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) {
onReadTargetKey(target, KEYCHANGES);
observeTargetKey(target, KEYCHANGES, callback);
target.forEach(function (val: any, key: any, targetObj: any) {
onReadTargetKey(target, key);
observeTargetKey(target, key, callback);
forEachCb.call(
thisArg,
possiblyReactive(val),
possiblyReactive(key),
possiblyReactive(targetObj)
possiblyReactive(val, callback),
possiblyReactive(key, callback),
possiblyReactive(targetObj, callback)
);
}, thisArg);
};
@@ -497,10 +367,10 @@ function delegateAndNotify(
const ret = target[setterName](key, value);
const hasKey = target.has(key);
if (hadKey !== hasKey) {
onWriteTargetKey(target, KEYCHANGES);
notifyReactives(target, KEYCHANGES);
}
if (originalValue !== target[getterName](key)) {
onWriteTargetKey(target, key);
notifyReactives(target, key);
}
return ret;
};
@@ -515,9 +385,9 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
return () => {
const allKeys = [...target.keys()];
target.clear();
onWriteTargetKey(target, KEYCHANGES);
notifyReactives(target, KEYCHANGES);
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.
*/
const rawTypeToFuncHandlers = {
Set: (target: any) => ({
has: makeKeyObserver("has", target),
Set: (target: any, callback: Callback) => ({
has: makeKeyObserver("has", target, callback),
add: delegateAndNotify("add", "has", target),
delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target),
values: makeIteratorObserver("values", target),
entries: makeIteratorObserver("entries", target),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
forEach: makeForEachObserver(target),
keys: makeIteratorObserver("keys", target, callback),
values: makeIteratorObserver("values", target, callback),
entries: makeIteratorObserver("entries", target, callback),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
forEach: makeForEachObserver(target, callback),
clear: makeClearNotifier(target),
get size() {
onReadTargetKey(target, KEYCHANGES);
observeTargetKey(target, KEYCHANGES, callback);
return target.size;
},
}),
Map: (target: any) => ({
has: makeKeyObserver("has", target),
get: makeKeyObserver("get", target),
Map: (target: any, callback: Callback) => ({
has: makeKeyObserver("has", target, callback),
get: makeKeyObserver("get", target, callback),
set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target),
values: makeIteratorObserver("values", target),
entries: makeIteratorObserver("entries", target),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
forEach: makeForEachObserver(target),
keys: makeIteratorObserver("keys", target, callback),
values: makeIteratorObserver("values", target, callback),
entries: makeIteratorObserver("entries", target, callback),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
forEach: makeForEachObserver(target, callback),
clear: makeClearNotifier(target),
get size() {
onReadTargetKey(target, KEYCHANGES);
observeTargetKey(target, KEYCHANGES, callback);
return target.size;
},
}),
WeakMap: (target: any) => ({
has: makeKeyObserver("has", target),
get: makeKeyObserver("get", target),
WeakMap: (target: any, callback: Callback) => ({
has: makeKeyObserver("has", target, callback),
get: makeKeyObserver("get", target, callback),
set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target),
}),
@@ -576,19 +446,20 @@ const rawTypeToFuncHandlers = {
*/
function collectionsProxyHandler<T extends Collection>(
target: T,
callback: Callback,
targetRawType: CollectionRawType
): ProxyHandler<T> {
// TODO: if performance is an issue we can create the special handlers lazily when each
// property is read.
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target);
return Object.assign(basicProxyHandler(), {
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
return Object.assign(basicProxyHandler(callback), {
// FIXME: probably broken when part of prototype chain since we ignore the receiver
get(target: any, key: PropertyKey) {
if (objectHasOwnProperty.call(specialHandlers, key)) {
return (specialHandlers as any)[key];
}
onReadTargetKey(target, key);
return possiblyReactive(target[key]);
observeTargetKey(target, key, callback);
return possiblyReactive(target[key], callback);
},
}) 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);
// });
// }
}
+109
View File
@@ -1,5 +1,51 @@
// 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`] = `
"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`] = `
"function anonymous(app, bdom, helpers
) {
@@ -94,6 +94,52 @@ exports[`basics no component catching error lead to full app destruction 2`] = `
}"
`;
exports[`basics render from above on error -- handler is not a Root or MountFiber 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Parent\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`basics render from above on error -- handler is not a Root or MountFiber 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Boom\`, true, false, false, []);
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2, b3;
if (ctx['error']) {
b2 = text(\`Error\`);
} else {
b3 = comp1({onError: (ctx['handleError']).bind(this)}, key + \`__1\`, node, this, null);
}
return block1([], [b2, b3]);
}
}"
`;
exports[`basics render from above on error -- handler is not a Root or MountFiber 3`] = `
"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['a'].b.c;
return block1([txt1]);
}
}"
`;
exports[`basics simple catchError 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -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`] = `
"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);
}
}"
`;
+45 -8
View File
@@ -1,28 +1,27 @@
import { App, Component, mount, onWillDestroy } from "../../src";
import { OwlError } from "../../src/common/owl_error";
import {
onError,
onMounted,
onPatched,
onRendered,
onWillPatch,
onWillRender,
onWillStart,
onWillRender,
onRendered,
onWillUnmount,
useState,
xml,
} from "../../src/index";
import { getCurrent } from "../../src/runtime/component_node";
import {
logStep,
makeTestFixture,
nextAppError,
nextMicroTick,
nextTick,
nextMicroTick,
snapshotEverything,
steps,
useLogLifecycle,
nextAppError,
steps,
} from "../helpers";
import { OwlError } from "../../src/common/owl_error";
let fixture: HTMLElement;
@@ -234,6 +233,44 @@ function(app, bdom, helpers) {
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("render from above on error -- handler is not a Root or MountFiber", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
setup() {
onError((err) => {
this.props.onError(err);
});
}
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Boom onError.bind="handleError"/>
</t>
</div>`;
static components = { Boom };
error: any = false;
handleError(err: Error) {
this.error = err;
this.render();
}
}
class GrandParent extends Component {
static template: string = xml`<Parent />`;
static components = { Parent };
}
await mount(GrandParent, fixture);
expect(fixture.innerHTML).toBe("<div>Error</div>");
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
});
describe("errors and promises", () => {
@@ -648,7 +685,7 @@ describe("can catch errors", () => {
setup() {
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(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Todo:willRender",
"Todo:rendered",
"Todo:willPatch",
"Todo:patched",
"Parent:willPatch",
"Parent:patched",
]
`);
});
+1 -1
View File
@@ -702,7 +702,7 @@ describe("props validation", () => {
const app = new App(Parent, { test: true });
await app.mount(fixture);
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 () => {
+43 -38
View File
@@ -2,11 +2,12 @@ import {
Component,
mount,
onPatched,
onWillPatch,
onWillRender,
onWillPatch,
onWillUnmount,
reactive,
useState,
xml,
toRaw,
} from "../../src";
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
@@ -19,36 +20,10 @@ beforeEach(() => {
});
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 () => {
class Counter extends Component {
static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = reactive({ value: 42 });
counter = useState({ value: 42 });
}
const counter = await mount(Counter, fixture);
expect(fixture.innerHTML).toBe("<div>42</div>");
@@ -61,7 +36,7 @@ describe("reactivity in lifecycle", () => {
let n = 0;
class Comp extends Component {
static template = xml`<div><t t-esc="state.a"/></div>`;
state = reactive({ a: 5, b: 7 });
state = useState({ a: 5, b: 7 });
setup() {
onWillRender(() => n++);
}
@@ -82,7 +57,7 @@ describe("reactivity in lifecycle", () => {
test("can use a state hook on Map", async () => {
class Counter extends Component {
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);
expect(fixture.innerHTML).toBe("<div>42</div>");
@@ -97,7 +72,7 @@ describe("reactivity in lifecycle", () => {
static template = xml`
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
`;
state = reactive({ n: 2 });
state = useState({ n: 2 });
setup() {
onWillRender(() => {
steps.push("render");
@@ -121,7 +96,7 @@ describe("reactivity in lifecycle", () => {
</div>
`;
static components = { Child };
state = reactive({ val: 1, flag: true });
state = useState({ val: 1, flag: true });
}
const parent = await mount(Parent, fixture);
expect(steps).toEqual(["render"]);
@@ -167,7 +142,7 @@ describe("reactivity in lifecycle", () => {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = reactive({ val: 1 });
state = useState({ val: 1 });
setup() {
STATE = this.state;
onWillRender(() => {
@@ -192,7 +167,7 @@ describe("reactivity in lifecycle", () => {
class Parent extends Component {
static template = xml`<Child t-if="state.renderChild" state="state"/>`;
static components = { Child };
state: any = reactive({ renderChild: true, content: { a: 2 } });
state: any = useState({ renderChild: true, content: { a: 2 } });
setup() {
useLogLifecycle();
}
@@ -230,8 +205,7 @@ describe("reactivity in lifecycle", () => {
`);
});
// todo: unskip it
test.skip("Component is automatically subscribed to reactive object received as prop", async () => {
test("Component is automatically subscribed to reactive object received as prop", async () => {
let childRenderCount = 0;
let parentRenderCount = 0;
class Child extends Component {
@@ -244,7 +218,7 @@ describe("reactivity in lifecycle", () => {
static template = xml`<Child obj="obj" reactiveObj="reactiveObj"/>`;
static components = { Child };
obj = { a: 1 };
reactiveObj = reactive({ b: 2 });
reactiveObj = useState({ b: 2 });
setup() {
onWillRender(() => parentRenderCount++);
}
@@ -263,3 +237,34 @@ describe("reactivity in lifecycle", () => {
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(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Parent:patched",
"Child:willPatch",
"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",
"child:mounted",
"parent:patched",
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched",
]);
expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>');
@@ -470,8 +472,10 @@ describe("Portal", () => {
"parent:willPatch",
"child:mounted",
"parent:patched",
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched",
"parent:willPatch",
"child:willUnmount",
"parent:patched",
+967 -917
View File
File diff suppressed because it is too large Load Diff