mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5c07d918b |
@@ -74,6 +74,7 @@ Are you new to Owl? This is the place to start!
|
||||
- [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md)
|
||||
- [How to start an Owl project](doc/learning/quick_start.md)
|
||||
- [How to test Components](doc/learning/how_to_test.md)
|
||||
- [How to organize Owl code](doc/miscellaneous/organizing_code.md)
|
||||
|
||||
### Reference
|
||||
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
# 🦉 Organising Owl code 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Reactivity](#reactivity)
|
||||
- [Asynchrony](#asynchrony)
|
||||
- [Basic design: Components and Props](#basic-design-components-and-props)
|
||||
- [More advanced design: using the environment](#more-advanced-design-using-the-environment)
|
||||
- [Even more advanced: completely separating model management](#even-more-advanced-completely-separating-model-management)
|
||||
- [Special case: bypassing reactivity](#special-case-bypassing-reactivity)
|
||||
|
||||
## Overview
|
||||
|
||||
The topic of this document is to discuss how one could organize an Owl
|
||||
application/component/feature/codebase. Clearly, good practices for a system depends
|
||||
on its properties/semantics. For Owl 2.x applications, the most important
|
||||
ideas to have in mind are the following:
|
||||
|
||||
- Owl provides a way to structure a component tree,
|
||||
- communication between a parent and a child component is mostly done with props
|
||||
(and callback props to go from child to parent),
|
||||
- there is an additional communication channel: information can be propagated
|
||||
through the environment,
|
||||
- Owl implements a reactivity system, which tracks each components and values,
|
||||
so that it is able to only rerender the relevant subset of components,
|
||||
- asynchrony: owl components can delay renderings with `onWillStart`/`onWillUpdateProps`
|
||||
- renderings are batched (10 calls to render in the same call stack will only
|
||||
result in 1 actual rendering)
|
||||
|
||||
## Reactivity
|
||||
|
||||
With the reactivity system, each value created with the `useState` or the `reactive`
|
||||
function is a proxy that allows Owl to trask which keys/values have been read
|
||||
by which components. This means that Owl can then only update the components that
|
||||
are impacted by a state change.
|
||||
|
||||
There are two important reactivity primitives:
|
||||
|
||||
- `useState`: a hook to turn a value into a reactive value, linked to a component
|
||||
(so, each time the value is updated, the component is rerendered)
|
||||
- `reactive`: a function to create a "standalone" reactive value
|
||||
|
||||
Also, it is good to know that one can call `useState` on a reactive value: this will
|
||||
create another reactive value, connected to the same source, but linked to a
|
||||
different component.
|
||||
|
||||
Let us consider the following component tree:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A-->B
|
||||
A-->C
|
||||
B-->D
|
||||
B-->E
|
||||
B-->F
|
||||
C-->G
|
||||
C-->H
|
||||
```
|
||||
|
||||
It is important to know how renderings are applied by Owl:
|
||||
|
||||
- if a render is initiated in A, it will render its template
|
||||
- whenever it encounter a child component (here, B and C), it will shallow
|
||||
compare the before and after props. If they are different, Owl will render the
|
||||
corresponding child component, otherwise it will stop.
|
||||
- each props object received by a (child) component is turned into a reactive
|
||||
object.
|
||||
|
||||
Each (non primitive) value given by `A` to `B` and `C` will be transformed into
|
||||
a reactive object.
|
||||
basically, owl does a useState on props at each level, and compare
|
||||
shallowly values
|
||||
|
||||
Notes:
|
||||
|
||||
- each component can be independently rendered (so, for example, only B and D)
|
||||
- one can create reactive objects (just like useState, but not linked to any
|
||||
component) with the `reactive` function
|
||||
- escape hatch: one can mark an object as "raw" (non reactive) with `markRaw`, and get the target
|
||||
object (with `toRaw`)
|
||||
- note that it is bad practice to keep reference to the raw object: doing so means
|
||||
that some => may miss
|
||||
rendering
|
||||
|
||||
- note: reactivity is not free. It may be expensive for some large objects.
|
||||
|
||||
## Asynchrony
|
||||
|
||||
explain: onWillStart and onWillUpdateProps are async => can delay render
|
||||
|
||||
good usecase for lazyloading libs
|
||||
|
||||
note: most of the time, update should be done in an atomic way:
|
||||
|
||||
```js
|
||||
// bad => will cause extra rendering, possibly corrupt
|
||||
this.state.something = someValue;
|
||||
const otherValue = await this.fetchSomething();
|
||||
this.state.otherValue = otherValue;
|
||||
```
|
||||
|
||||
```js
|
||||
// good
|
||||
const otherValue = await this.fetchSomething();
|
||||
this.state.something = someValue;
|
||||
this.state.otherValue = otherValue;
|
||||
```
|
||||
|
||||
```js
|
||||
// bad => will cause extra rendering, possibly corrupt
|
||||
const someValue = getsomeValue();
|
||||
const otherValue = await this.fetchSomething();
|
||||
Object.assign(this.state, {
|
||||
someValue,
|
||||
otherValue,
|
||||
});
|
||||
```
|
||||
|
||||
other note: minimize async stuff
|
||||
|
||||
```js
|
||||
setup() {
|
||||
// strategy 1: have a loading screen
|
||||
this.state = useState({loading: true});
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<t t-if="state.loading">
|
||||
</t>
|
||||
...
|
||||
```
|
||||
|
||||
// other strategy: not display a loading indicator, and update the UI atomically
|
||||
|
||||
```js
|
||||
setup() {
|
||||
onWillStart(() => this.loadData());
|
||||
onWillUpdateProps(() => this.updateData());
|
||||
}
|
||||
```
|
||||
|
||||
## Baseline design: Components and props
|
||||
|
||||
Here is what I suppose many Owl systems will look like: `useState` in some components,
|
||||
state is propagated down the tree with props, and potentially callbacks as well
|
||||
(to communicate from child to parent)
|
||||
|
||||
A
|
||||
|
||||
/ \
|
||||
B C
|
||||
/ | \ | \
|
||||
D E F G H
|
||||
|
||||
```js
|
||||
// in A (and maybe some/all other components)
|
||||
this.state = useState(...);
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- in A -->
|
||||
<div>
|
||||
<t t-if="state.message">
|
||||
<span t-esc="state.message"/>
|
||||
</t>
|
||||
<B record="state.records[3]" updateValue.bind="updateValue">
|
||||
</div>
|
||||
```
|
||||
|
||||
Notice the `.bind` suffix here, very convenient.
|
||||
|
||||
```xml
|
||||
<!-- in B. It is now subscribed to the 'some_field' value -->
|
||||
<span t-esc="props.record.some_field"/>
|
||||
```
|
||||
|
||||
With reactivity, any update to the state will only rerender the affected components.
|
||||
So, if some code in `A` update the value of `some_field` (without reading it!),
|
||||
only B will be rerendered.
|
||||
|
||||
## More advanced design: passing stuff through the environment
|
||||
|
||||
It may be tedious/impractical to pass props all along the tree when we have a deep
|
||||
component tree.
|
||||
|
||||
One can then use the environment to communicate to children. Only issue with that
|
||||
is that stuff in the environment bypass the reactivity system: if it comes from
|
||||
a `useState` in `A` => only `A` will react to state changes, not the children that
|
||||
reads it.
|
||||
|
||||
To solve that, one need to use `useState` on it:
|
||||
|
||||
```js
|
||||
// in root component A
|
||||
class A extends Component {
|
||||
setup() {
|
||||
this.posModel = {
|
||||
state: useState(this.state),
|
||||
updateSomeValue: () => this.updateSomeValue(),
|
||||
doSomethingElse: () => this.doSomethingElse(),
|
||||
};
|
||||
useSubEnv({ posModel: this.posModel });
|
||||
}
|
||||
}
|
||||
|
||||
// in some child component D
|
||||
class D extends Component {
|
||||
setup() {
|
||||
// this one is wrong! this.posModel.state is stated
|
||||
this.posModel = this.env.posModel;
|
||||
|
||||
// this.posModel.state === the one in A, linked to the component A
|
||||
|
||||
// this one is ok!
|
||||
this.posModel = useState(this.env.posModel);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that we can slightly simplify the use of the model by a hook:
|
||||
|
||||
```js
|
||||
function usePosModel() {
|
||||
const env = useEnv();
|
||||
const model = env.posModel;
|
||||
return useState(model);
|
||||
}
|
||||
```
|
||||
|
||||
This is not much simpler, but it makes sure that one does not forget to call
|
||||
`useState` on the model from the environment.
|
||||
|
||||
## Even more advanced: completely separating model management
|
||||
|
||||
The baseline design above it nice for many usecases, but we sometimes may want
|
||||
to separate the "model" management from the UI, especially as the model is doing
|
||||
more complicated stuff. Useful to code some complex transitions.
|
||||
|
||||
Here is how we could use the environment and reactive object to organize code:
|
||||
|
||||
```js
|
||||
// in my_model.js
|
||||
class MyModel {
|
||||
...
|
||||
constructor() {
|
||||
this.complicatedCacheObject = {};
|
||||
|
||||
markRaw(this.complicatedCacheObject);
|
||||
}
|
||||
|
||||
async load(...) {
|
||||
|
||||
}
|
||||
|
||||
async update(...) {
|
||||
|
||||
}
|
||||
|
||||
async doSomeComplexThing() {
|
||||
this.loading = true;
|
||||
|
||||
// coordinate here some thing
|
||||
// fetch stuff...
|
||||
|
||||
this.loading = false;
|
||||
this.data = ...;
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
// in root component A:
|
||||
|
||||
class A extends Component {
|
||||
setup() {
|
||||
const model = useState(new MyModel(...));
|
||||
useSubEnv({ model });
|
||||
}
|
||||
}
|
||||
|
||||
// in some child component:
|
||||
class D extends Component {
|
||||
setup() {
|
||||
// notice the useState: this is necessary to bind model changes to the
|
||||
// component D
|
||||
this.model = useState(this.env.model);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- model could be created elsewhere, in a service, in the start code, ...
|
||||
- one may want to mark some internal stuff as 'raw'...
|
||||
- if created elsewhere, it should probably be done with `reactive`
|
||||
- the call to `reactive` could be done in the model constructor directly:
|
||||
|
||||
```js
|
||||
class MyModel {
|
||||
constructor() {
|
||||
...
|
||||
return reactive(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- one could slightly simplify the model use with a hook:
|
||||
|
||||
```js
|
||||
function useModel() {
|
||||
const env = useEnv();
|
||||
const model = env.model;
|
||||
return useState(model);
|
||||
}
|
||||
```
|
||||
|
||||
This can be simply used like this in a child component:
|
||||
|
||||
```js
|
||||
setup() {
|
||||
this.model = useModel();
|
||||
}
|
||||
```
|
||||
|
||||
## Special case: bypassing reactivity
|
||||
|
||||
Sometimes, reactivity is not what we want. For example, the o_spreadsheet
|
||||
library has a huge extremely complex model, and each state transition may
|
||||
potentially update the full UI, so it needs to be fast anyway, and reactivity
|
||||
has a cost that may outweight the benefits.
|
||||
|
||||
The strategy here is:
|
||||
|
||||
- mark the model as raw (not necessary in all cases, but potentially important
|
||||
if model is passed through some props)
|
||||
- find a way to be notified on update change (for example, by an event triggered
|
||||
by an event bus)
|
||||
- perform a deep rendering: render(true), at the root component.
|
||||
|
||||
Here is how one could organize the code then:
|
||||
|
||||
```js
|
||||
class MainModel extends EventBus{
|
||||
constructor() {
|
||||
markRaw(this); // make sure this object will never be turned into a reactive
|
||||
}
|
||||
|
||||
doSomething() {
|
||||
...
|
||||
// need to notify the outside world that a change occurred
|
||||
this.trigger("UPDATE");
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
// in root component:
|
||||
class A extends Component {
|
||||
setup() {
|
||||
const model = new MainModel();
|
||||
|
||||
// notice the 'render(true)', to do a deep render
|
||||
model.addEventListener("UPDATE", () => this.render(true));
|
||||
useSubEnv({ model });
|
||||
}
|
||||
}
|
||||
|
||||
// in child components:
|
||||
class E extends Component {
|
||||
setup() {
|
||||
this.model = this.env.model; // not really necessary, but maybe nice
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.model.doSomething();
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -117,5 +117,3 @@ Dev mode activates some additional checks and developer amenities:
|
||||
- [Props validation](./props.md#props-validation) is performed
|
||||
- [t-foreach](./templates.md#loops) loops check for key unicity
|
||||
- Lifecycle hooks are wrapped to report their errors in a more developer-friendly way
|
||||
- onWillStart and onWillUpdateProps will emit a warning in the console when they
|
||||
take longer than 3 seconds in an effort to ease debugging the presence of deadlocks
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
|
||||
import { compile, Template, TemplateFunction } from "../compiler";
|
||||
import { compile, Template } from "../compiler";
|
||||
import { markRaw } from "../reactivity";
|
||||
import { Portal } from "../portal";
|
||||
import { component, getCurrent } from "../component/component_node";
|
||||
import { helpers } from "./template_helpers";
|
||||
import { globalTemplates } from "../utils";
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
|
||||
|
||||
@@ -47,7 +48,7 @@ function makeHelpers(getTemplate: (name: string) => Template): any {
|
||||
markRaw,
|
||||
getTemplate,
|
||||
call: (owner: any, subTemplate: string, ctx: any, parent: any, key: any) => {
|
||||
const template = typeof subTemplate === "string" ? getTemplate(subTemplate) : subTemplate;
|
||||
const template = getTemplate(subTemplate);
|
||||
return toggler(subTemplate, template.call(owner, ctx, parent, key));
|
||||
},
|
||||
});
|
||||
@@ -62,12 +63,11 @@ export interface TemplateSetConfig {
|
||||
|
||||
export class TemplateSet {
|
||||
dev: boolean;
|
||||
rawTemplates: any = {}; //typeof globalTemplates = Object.create(globalTemplates);
|
||||
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
||||
templates: { [name: string]: Template } = {};
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
helpers: any;
|
||||
bdom: any = bdom;
|
||||
|
||||
constructor(config: TemplateSetConfig = {}) {
|
||||
this.dev = config.dev || false;
|
||||
@@ -102,11 +102,8 @@ export class TemplateSet {
|
||||
}
|
||||
}
|
||||
|
||||
getTemplate(name: string | any): Template {
|
||||
getTemplate(name: string): Template {
|
||||
if (!(name in this.templates)) {
|
||||
if (typeof name === "function") {
|
||||
return name(this);
|
||||
}
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
let extraInfo = "";
|
||||
@@ -129,7 +126,7 @@ export class TemplateSet {
|
||||
return this.templates[name];
|
||||
}
|
||||
|
||||
_compileTemplate(name: string, template: string | Element): TemplateFunction {
|
||||
_compileTemplate(name: string, template: string | Element) {
|
||||
return compile(template, {
|
||||
name,
|
||||
dev: this.dev,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { ComponentNode } from "./component_node";
|
||||
import type { TemplateFunction } from "../compiler/index";
|
||||
import type { App } from "..";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Component Class
|
||||
@@ -22,7 +20,7 @@ export type ComponentConstructor<P extends Props = any, E = any> = (new (
|
||||
StaticComponentProperties;
|
||||
|
||||
export class Component<Props = any, Env = any> {
|
||||
static template: string | ((app: App) => TemplateFunction) = "";
|
||||
static template: string = "";
|
||||
static props?: any;
|
||||
static defaultProps?: any;
|
||||
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
import { getCurrent } from "./component_node";
|
||||
import { nodeErrorHandlers } from "./error_handling";
|
||||
|
||||
const TIMEOUT = Symbol("timeout");
|
||||
function wrapError(fn: (...args: any[]) => any, hookName: string) {
|
||||
const error = new Error(`The following error occurred in ${hookName}: `) as Error & {
|
||||
cause: any;
|
||||
};
|
||||
const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
|
||||
const node = getCurrent();
|
||||
return (...args: any[]) => {
|
||||
try {
|
||||
const result = fn(...args);
|
||||
if (result instanceof Promise) {
|
||||
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
|
||||
const fiber = node.fiber;
|
||||
Promise.race([
|
||||
result,
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber) {
|
||||
console.warn(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.catch((cause) => {
|
||||
error.cause = cause;
|
||||
if (cause instanceof Error) {
|
||||
|
||||
+5
-33
@@ -75,41 +75,13 @@ export function markup(value: any) {
|
||||
// -----------------------------------------------------------------------------
|
||||
// xml tag helper
|
||||
// -----------------------------------------------------------------------------
|
||||
export const globalTemplates: { [key: string]: string | Element } = {};
|
||||
|
||||
export function xml(...args: Parameters<typeof String.raw>): any {
|
||||
const subTemplates: any = {};
|
||||
let hasSubTemplates = false;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (typeof arg === "function") {
|
||||
hasSubTemplates = true;
|
||||
const id = `__template__${xml.nextId++}`;
|
||||
subTemplates[id] = arg;
|
||||
args[i] = `{{__owl__.subTemplates.${id}(__owl__.app)}}`;
|
||||
}
|
||||
}
|
||||
export function xml(...args: Parameters<typeof String.raw>) {
|
||||
const name = `__template__${xml.nextId++}`;
|
||||
const value = String.raw(...args);
|
||||
|
||||
let cache = new WeakMap();
|
||||
|
||||
return (app: any) => {
|
||||
debugger;
|
||||
let renderFn = cache.get(app);
|
||||
if (!renderFn) {
|
||||
let templateFn = app._compileTemplate("", value);
|
||||
renderFn = templateFn(app.bdom, app.helpers);
|
||||
if (hasSubTemplates) {
|
||||
let origRenderFn = renderFn;
|
||||
renderFn = (context: any, vnode: any, key?: any) => {
|
||||
debugger;
|
||||
context.__owl__.subTemplates = subTemplates;
|
||||
return origRenderFn(context, vnode, key);
|
||||
};
|
||||
}
|
||||
cache.set(app, renderFn);
|
||||
}
|
||||
return renderFn;
|
||||
};
|
||||
globalTemplates[name] = value;
|
||||
return name;
|
||||
}
|
||||
|
||||
xml.nextId = 1;
|
||||
|
||||
@@ -169,11 +169,11 @@ exports[`basics can inject values in tagged templates 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { call } = helpers;
|
||||
let { getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__999\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__1(ctx['__owl__'].app));
|
||||
return call(this, template1, ctx, node, key + \`__1\`);
|
||||
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -658,43 +658,6 @@ exports[`lifecycle hooks sub widget (inside sub node): hooks are correctly calle
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const props1 = {prop: ctx['state'].prop};
|
||||
helpers.validateProps(\`Child\`, props1, ctx);
|
||||
return component(\`Child\`, props1, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks willPatch, patched hook are called on subsubcomponents, in proper order 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -249,11 +249,11 @@ exports[`slots can use t-call in default-content of t-slot 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { callSlot, call } = helpers;
|
||||
let { callSlot, getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__999\`);
|
||||
|
||||
function defaultContent1(ctx, node, key = \\"\\") {
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__3(ctx['__owl__'].app));
|
||||
return call(this, template1, ctx, node, key + \`__1\`);
|
||||
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
@@ -772,12 +772,12 @@ exports[`slots mix of slots, t-call, t-call with body, and giving own props chil
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { capture, call, markRaw } = helpers;
|
||||
let { capture, getTemplate, markRaw } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__1002\`);
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`[A]\`);
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__5(ctx['__owl__'].app));
|
||||
const b3 = call(this, template1, ctx, node, key + \`__1\`);
|
||||
const b3 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
|
||||
@@ -792,34 +792,8 @@ exports[`slots mix of slots, t-call, t-call with body, and giving own props chil
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`[B]\`);
|
||||
const b3 = component(\`C\`, {slots: ctx['props'].slots}, key + \`__1\`, node, ctx);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 5`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { callSlot } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`[C]\`);
|
||||
const b3 = callSlot(ctx, node, key, 'default', false, null);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 6`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue, call } = helpers;
|
||||
let { isBoundary, withDefault, setContextValue, getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__1001\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
@@ -829,14 +803,13 @@ exports[`slots mix of slots, t-call, t-call with body, and giving own props chil
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1;
|
||||
setContextValue(ctx, \\"v\\", ctx['props'].number);
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__4(ctx['__owl__'].app));
|
||||
const b3 = call(this, template1, ctx, node, key + \`__1\`);
|
||||
const b3 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 7`] = `
|
||||
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 5`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
@@ -850,6 +823,33 @@ exports[`slots mix of slots, t-call, t-call with body, and giving own props chil
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 6`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`[B]\`);
|
||||
const b3 = component(\`C\`, {slots: ctx['props'].slots}, key + \`__1\`, node, ctx);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 7`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { callSlot } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`[C]\`);
|
||||
const b3 = callSlot(ctx, node, key, 'default', false, null);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots multiple roots are allowed in a default slot 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
@@ -1486,13 +1486,13 @@ exports[`slots slot and (inline) t-call 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { capture, call, markRaw } = helpers;
|
||||
let { capture, getTemplate, markRaw } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__999\`);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__2(ctx['__owl__'].app));
|
||||
return call(this, template1, ctx, node, key + \`__1\`);
|
||||
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
@@ -1505,6 +1505,19 @@ exports[`slots slot and (inline) t-call 1`] = `
|
||||
|
||||
exports[`slots slot and (inline) t-call 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<p>sokka</p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots slot and (inline) t-call 3`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { callSlot } = helpers;
|
||||
@@ -1518,30 +1531,17 @@ exports[`slots slot and (inline) t-call 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots slot and (inline) t-call 3`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<p>sokka</p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots slot and t-call 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { capture, call, markRaw } = helpers;
|
||||
let { capture, getTemplate, markRaw } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__999\`);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__1(ctx['__owl__'].app));
|
||||
return call(this, template1, ctx, node, key + \`__1\`);
|
||||
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
@@ -1556,13 +1556,11 @@ exports[`slots slot and t-call 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { callSlot } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<span><block-child-0/></span>\`);
|
||||
let block1 = createBlock(\`<p>sokka</p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = callSlot(ctx, node, key, 'default', false, null);
|
||||
return block1([], [b2]);
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -1571,11 +1569,13 @@ exports[`slots slot and t-call 3`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { callSlot } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<p>sokka</p>\`);
|
||||
let block1 = createBlock(\`<span><block-child-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
const b2 = callSlot(ctx, node, key, 'default', false, null);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -116,13 +116,13 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { call } = helpers;
|
||||
let { getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__999\`);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__1(ctx['__owl__'].app));
|
||||
const b2 = call(this, template1, ctx, node, key + \`__1\`);
|
||||
const b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
let txt1 = ctx['counter'];
|
||||
return block1([txt1], [b2]);
|
||||
}
|
||||
@@ -147,13 +147,13 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { call } = helpers;
|
||||
let { getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__999\`);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__6(ctx['__owl__'].app));
|
||||
const b2 = call(this, template1, ctx, node, key + \`__1\`);
|
||||
const b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -178,13 +178,13 @@ exports[`t-call parent is set within t-call 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { call } = helpers;
|
||||
let { getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__999\`);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__3(ctx['__owl__'].app));
|
||||
const b2 = call(this, template1, ctx, node, key + \`__1\`);
|
||||
const b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -218,11 +218,11 @@ exports[`t-call parent is set within t-call with no parentNode 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { call } = helpers;
|
||||
let { getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__999\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__5(ctx['__owl__'].app));
|
||||
return call(this, template1, ctx, node, key + \`__1\`);
|
||||
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -353,7 +353,8 @@ exports[`t-call t-call in t-foreach and children component 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, call, withKey } = helpers;
|
||||
let { prepareList, getTemplate, withKey } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`__template__999\`);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -367,8 +368,7 @@ exports[`t-call t-call in t-foreach and children component 1`] = `
|
||||
ctx[\`val_index\`] = i1;
|
||||
ctx[\`val_value\`] = k_block2[i1];
|
||||
const key1 = ctx['val'];
|
||||
const template1 = (ctx['__owl__'].subTemplates.__template__4(ctx['__owl__'].app));
|
||||
c_block2[i1] = withKey(call(this, template1, ctx, node, key + \`__1__\${key1}\`), key1);
|
||||
c_block2[i1] = withKey(callTemplate_1.call(this, ctx, node, key + \`__1__\${key1}\`), key1);
|
||||
}
|
||||
const b2 = list(c_block2);
|
||||
return block1([], [b2]);
|
||||
|
||||
@@ -444,7 +444,6 @@ describe("hooks", () => {
|
||||
let cleanupRun = 0;
|
||||
let steps = [];
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div/>`;
|
||||
state = useState({
|
||||
value: 0,
|
||||
});
|
||||
@@ -456,6 +455,7 @@ describe("hooks", () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div/>`;
|
||||
|
||||
const component = await mount(MyComponent, fixture);
|
||||
|
||||
@@ -512,7 +512,6 @@ describe("hooks", () => {
|
||||
test("dependencies prevent effects from rerunning when unchanged", async () => {
|
||||
let steps = [];
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div/>`;
|
||||
state = useState({
|
||||
a: 0,
|
||||
b: 0,
|
||||
@@ -541,6 +540,7 @@ describe("hooks", () => {
|
||||
);
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div/>`;
|
||||
steps.push("before mount");
|
||||
const component = await mount(MyComponent, fixture);
|
||||
steps.push("after mount");
|
||||
@@ -597,7 +597,6 @@ describe("hooks", () => {
|
||||
test("effect with empty dependency list never reruns", async () => {
|
||||
let steps = [];
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div t-esc="state.value"/>`;
|
||||
state = useState({
|
||||
value: 0,
|
||||
});
|
||||
@@ -611,6 +610,7 @@ describe("hooks", () => {
|
||||
);
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div t-esc="state.value"/>`;
|
||||
|
||||
const component = await mount(MyComponent, fixture);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
logStep,
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
nextMicroTick,
|
||||
nextTick,
|
||||
snapshotEverything,
|
||||
useLogLifecycle,
|
||||
@@ -105,83 +104,6 @@ describe("lifecycle hooks", () => {
|
||||
await mount(Test, fixture);
|
||||
});
|
||||
|
||||
test("timeout in onWillStart emits a warning", async () => {
|
||||
const { warn } = console;
|
||||
let warnArgs: any[];
|
||||
console.warn = jest.fn((...args) => (warnArgs = args));
|
||||
const { setTimeout } = window;
|
||||
let timeoutCbs: any = {};
|
||||
let timeoutId = 0;
|
||||
window.setTimeout = ((cb: any) => {
|
||||
timeoutCbs[++timeoutId] = cb;
|
||||
return timeoutId;
|
||||
}) as any;
|
||||
class Test extends Component {
|
||||
static template = xml`<span/>`;
|
||||
setup() {
|
||||
onWillStart(() => new Promise(() => {}));
|
||||
}
|
||||
}
|
||||
mount(Test, fixture, { test: true });
|
||||
nextTick();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(warnArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
||||
console.warn = warn;
|
||||
window.setTimeout = setTimeout;
|
||||
});
|
||||
|
||||
test("timeout in onWillUpdateProps emits a warning", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml``;
|
||||
setup() {
|
||||
onWillUpdateProps(() => new Promise(() => {}));
|
||||
}
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child prop="state.prop"/>`;
|
||||
static components = { Child };
|
||||
state = useState({ prop: 1 });
|
||||
}
|
||||
const parent = await mount(Parent, fixture, { test: true });
|
||||
|
||||
const { warn } = console;
|
||||
let warnArgs: any[];
|
||||
console.warn = jest.fn((...args) => (warnArgs = args));
|
||||
const { setTimeout } = window;
|
||||
let timeoutCbs: any = {};
|
||||
let timeoutId = 0;
|
||||
window.setTimeout = ((cb: any) => {
|
||||
timeoutCbs[++timeoutId] = cb;
|
||||
return timeoutId;
|
||||
}) as any;
|
||||
|
||||
parent.state.prop = 2;
|
||||
let tick = nextTick();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await tick;
|
||||
tick = nextTick();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await tick;
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(warnArgs![0]!.message).toBe(
|
||||
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
|
||||
);
|
||||
console.warn = warn;
|
||||
window.setTimeout = setTimeout;
|
||||
});
|
||||
|
||||
test("mounted hook is called if mounted in DOM", async () => {
|
||||
let mounted = false;
|
||||
class Test extends Component {
|
||||
|
||||
+9
-19
@@ -13,13 +13,13 @@ import {
|
||||
onWillUpdateProps,
|
||||
status,
|
||||
useComponent,
|
||||
xml,
|
||||
} from "../src";
|
||||
import { helpers } from "../src/app/template_helpers";
|
||||
import { TemplateSet } from "../src/app/template_set";
|
||||
import { BDom } from "../src/blockdom";
|
||||
import { compile } from "../src/compiler";
|
||||
import { Portal } from "../src/portal";
|
||||
// import { globalTemplates } from "../src/utils";
|
||||
import { globalTemplates } from "../src/utils";
|
||||
|
||||
const mount = blockDom.mount;
|
||||
|
||||
@@ -39,6 +39,9 @@ export function makeTestFixture() {
|
||||
return fixture;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
xml.nextId = 999;
|
||||
});
|
||||
export async function nextTick(): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
@@ -46,7 +49,7 @@ export async function nextTick(): Promise<void> {
|
||||
|
||||
interface Deferred extends Promise<any> {
|
||||
resolve(val?: any): void;
|
||||
reject(val?: any): void;
|
||||
reject(): void;
|
||||
}
|
||||
|
||||
export function makeDeferred(): Deferred {
|
||||
@@ -114,29 +117,16 @@ export function snapshotEverything() {
|
||||
// this function has already been called
|
||||
return;
|
||||
}
|
||||
const globalTemplateNames = new Set(Object.keys(globalTemplates));
|
||||
shouldSnapshot = true;
|
||||
beforeEach(() => {
|
||||
snapshottedTemplates.clear();
|
||||
});
|
||||
|
||||
const getTemplate = TemplateSet.prototype.getTemplate;
|
||||
TemplateSet.prototype.getTemplate = function (this: any, name: string) {
|
||||
if (name === Portal.template) {
|
||||
this.skipSnapshot = true;
|
||||
}
|
||||
const result = getTemplate.call(this, name);
|
||||
this.skipSnapshot = false;
|
||||
return result;
|
||||
};
|
||||
|
||||
const originalCompileTemplate = TemplateSet.prototype._compileTemplate;
|
||||
TemplateSet.prototype._compileTemplate = function (
|
||||
this: any,
|
||||
name: string,
|
||||
template: string | Element
|
||||
) {
|
||||
TemplateSet.prototype._compileTemplate = function (name: string, template: string | Element) {
|
||||
const fn = originalCompileTemplate.call(this, "", template);
|
||||
if (!this.skipSnapshot) {
|
||||
if (!globalTemplateNames.has(name)) {
|
||||
expect(fn.toString()).toMatchSnapshot();
|
||||
}
|
||||
return fn;
|
||||
|
||||
Reference in New Issue
Block a user