mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46095b7a96 | |||
| c3453d35b9 | |||
| b0dd0252a4 | |||
| e2b2e54e29 | |||
| 766c378ac9 | |||
| 022b29b6a0 | |||
| 367725d194 | |||
| e82ed7dc8c | |||
| 63c3d4f6b0 |
+50
-24
@@ -16,7 +16,6 @@
|
||||
- [Event Handling](#event-handling)
|
||||
- [Form Input Bindings](#form-input-bindings)
|
||||
- [`t-key` Directive](#t-key-directive)
|
||||
- [`t-mounted` Directive](#t-mounted-directive)
|
||||
- [Semantics](#semantics)
|
||||
- [Props Validation](#props-validation)
|
||||
- [References](#references)
|
||||
@@ -24,6 +23,7 @@
|
||||
- [Asynchronous Rendering](#asynchronous-rendering)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Functional Components](#functional-components)
|
||||
- [SVG components](#svg-components)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -328,9 +328,6 @@ before an actual DOM patch, and is only intended to be used to save some local
|
||||
DOM state. Also, it will not be called if the component is not in the DOM (this can
|
||||
happen with components with `t-keepalive`).
|
||||
|
||||
The return value of this method will be given as the first argument of the
|
||||
corresponding `patched` call.
|
||||
|
||||
#### `patched(snapshot)`
|
||||
|
||||
This hook is called whenever a component did actually update its DOM (most
|
||||
@@ -341,8 +338,6 @@ with the DOM (for example, through an external library) whenever the
|
||||
component was patched. Note that this hook will not be called if the compoent is
|
||||
not in the DOM (this can happen with components with `t-keepalive`).
|
||||
|
||||
The `snapshot` parameter is the result of the previous `willPatch` call.
|
||||
|
||||
Updating the compoent state in this hook is possible, but not encouraged.
|
||||
One need to be careful, because updates here will cause rerender, which in
|
||||
turn will cause other calls to patched. So, we need to be particularly
|
||||
@@ -760,24 +755,6 @@ There are three main use cases:
|
||||
- _animations_: give a different identity to a component. Ex: thread id with
|
||||
animations on add/remove message.
|
||||
|
||||
### `t-mounted` Directive
|
||||
|
||||
The `t-mounted` directive allows to register a callback to execute whenever the node
|
||||
is inserted into the DOM.
|
||||
|
||||
```xml
|
||||
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
|
||||
```
|
||||
|
||||
```js
|
||||
class MyComponent extends owl.Component {
|
||||
...
|
||||
focusMe() {
|
||||
this.refs.someInput.focus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Semantics
|
||||
|
||||
We give here an informal description of the way components are created/updated
|
||||
@@ -1198,3 +1175,52 @@ class MyComponent extends Component {
|
||||
The way this works is that sub templates are inlined, and have access to the
|
||||
ambient context. They can therefore access `props`, and any other part of the
|
||||
caller component.
|
||||
|
||||
### SVG Components
|
||||
|
||||
Owl components can be used to generate dynamic SVG graphs:
|
||||
|
||||
```js
|
||||
class Node extends Component {
|
||||
static template = xml`
|
||||
<g>
|
||||
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
|
||||
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
|
||||
<t t-set="childx" t-value="props.x + 100"/>
|
||||
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
|
||||
<t t-foreach="props.node.children || []" t-as="child">
|
||||
<t t-set="childy" t-value="props.y + child_index*height"/>
|
||||
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
|
||||
<Node x="childx" y="childy" node="child" height="height"/>
|
||||
</t>
|
||||
</g>
|
||||
`;
|
||||
static components = { Node };
|
||||
}
|
||||
|
||||
class RootNode extends Component {
|
||||
static template = xml`
|
||||
<svg height="180">
|
||||
<Node node="graph" x="10" y="20" height="180"/>
|
||||
</svg>
|
||||
`;
|
||||
static components = { Node };
|
||||
graph = {
|
||||
label: "a",
|
||||
children: [
|
||||
{ label: "b" },
|
||||
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
|
||||
{ label: "f", children: [{ label: "g" }] }
|
||||
]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This `RootNode` component will then display a live SVG representation of the
|
||||
graph described by the `graph` property. Note that there is a recursive structure
|
||||
here: the `Node` component uses itself as a subcomponent.
|
||||
|
||||
Note that since SVG needs to be handled in a specific way (its namespace needs
|
||||
to be properly set), there is a small constraint for Owl components: if an owl
|
||||
component is supposed to be a part of an svg graph, then its root node needs to
|
||||
be a `g` tag, so Owl can properly set the namespace.
|
||||
|
||||
+98
-12
@@ -3,13 +3,17 @@
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Example: Mouse Position](#example-mouse-position)
|
||||
- [Example: Autofocus](#example-autofocus)
|
||||
- [Reference](#reference)
|
||||
- [One Rule](#one-rule)
|
||||
- [`useState`](#usestate)
|
||||
- [`onMounted`](#onmounted)
|
||||
- [`onWillUnmount`](#onwillunmount)
|
||||
- [`onWillPatch`](#onwillpatch)
|
||||
- [`onPatched`](#onpatched)
|
||||
- [`useRef`](#useref)
|
||||
- [`useSubEnv`](#useSubEnv)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -28,7 +32,7 @@ Hooks works beautifully with Owl components: they solve the problems mentioned
|
||||
above, and in particular, they are the perfect way to make your component
|
||||
reactive.
|
||||
|
||||
## Example
|
||||
## Example: mouse position
|
||||
|
||||
Here is the classical example of a non trivial hook to track the mouse position.
|
||||
|
||||
@@ -57,9 +61,9 @@ function useMouse() {
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
static template = xml`
|
||||
<div t-name="App">
|
||||
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||
</div>`;
|
||||
<div t-name="App">
|
||||
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||
</div>`;
|
||||
|
||||
// this hooks is bound to the 'mouse' property.
|
||||
mouse = useMouse();
|
||||
@@ -69,6 +73,49 @@ class App extends owl.Component {
|
||||
Note that we use the prefix `use` for hooks, just like in React. This is just
|
||||
a convention.
|
||||
|
||||
## Example: autofocus
|
||||
|
||||
Hooks can be combined to create the desired effect. For example, the following
|
||||
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
|
||||
to create an easy way to focus an input whenever it appears in the DOM:
|
||||
|
||||
```js
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
let isInDom = false;
|
||||
function updateFocus() {
|
||||
if (!isInDom && ref.el) {
|
||||
isInDom = true;
|
||||
ref.el.focus();
|
||||
} else if (isInDom && !ref.el) {
|
||||
isInDom = false;
|
||||
}
|
||||
}
|
||||
onPatched(updateFocus);
|
||||
onMounted(updateFocus);
|
||||
}
|
||||
```
|
||||
|
||||
This hook takes the name of a valid `t-ref` directive, which should be present
|
||||
in the template. It then checks whenever the component is mounted or patched if
|
||||
the reference is not valid, and in this case, it will focus the node element.
|
||||
This hook can be used like this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input />
|
||||
<input t-ref="myinput"/>
|
||||
</div>`;
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
useAutofocus("myinput");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### One rule
|
||||
@@ -79,21 +126,21 @@ constructor (or in class fields):
|
||||
```js
|
||||
// ok
|
||||
class SomeComponent extends Component {
|
||||
state = useState({value: 0});
|
||||
state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
// also ok
|
||||
class SomeComponent extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = useState({value: 0});
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// not ok: this is executed after the constructor is called
|
||||
class SomeComponent extends Component {
|
||||
async willStart() {
|
||||
this.state = useState({value: 0});
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -125,16 +172,29 @@ class Counter extends owl.Component {
|
||||
|
||||
### `onMounted`
|
||||
|
||||
`onMounted` is not an hook, but is a building block designed to help make useful
|
||||
hooks. `onMounted` register a callback, which will be called when the component
|
||||
`onMounted` is not an user hook, but is a building block designed to help make useful
|
||||
abstractions. `onMounted` registers a callback, which will be called when the component
|
||||
is mounted (see example on top of this page).
|
||||
|
||||
### `onWillUnmount`
|
||||
|
||||
`onWillUnmount` is not an hook, but is a building block designed to help make useful
|
||||
hooks. `onWillUnmount` register a callback, which will be called when the component
|
||||
`onWillUnmount` is not an user hook, but is a building block designed to help make useful
|
||||
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
||||
is unmounted (see example on top of this page).
|
||||
|
||||
|
||||
### `onWillPatch`
|
||||
|
||||
`onWillPatch` is not an user hook, but is a building block designed to help make useful
|
||||
abstractions. `onWillPatch` registers a callback, which will be called just
|
||||
before the component patched.
|
||||
|
||||
### `onPatched`
|
||||
|
||||
`onPatched` is not an user hook, but is a building block designed to help make useful
|
||||
abstractions. `onPatched` registers a callback, which will be called just
|
||||
after the component patched.
|
||||
|
||||
### `useRef`
|
||||
|
||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||
@@ -187,3 +247,29 @@ this.ref2 = useRef("component_2");
|
||||
|
||||
References are only guaranteed to be active while the parent component is mounted.
|
||||
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
||||
|
||||
### `useSubEnv`
|
||||
|
||||
The environment is sometimes useful to share some common information between
|
||||
all components. But sometimes, we want to *scope* that knowledge to a subtree.
|
||||
|
||||
For example, if we have a form view component, maybe we would like to make some
|
||||
`model` object available to all sub component, but not to the whole application.
|
||||
This is where the `useSubEnv` hook may be useful: it let a component add some
|
||||
information to the environment in a way that only the component and its children
|
||||
can access it:
|
||||
|
||||
```js
|
||||
class FormComponent extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
const model = makeModel();
|
||||
useSubEnv({ model });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `useSubEnv` takes one argument: an object which contains some key/value that
|
||||
will be added to the parent environment. Note that it will extend, not replace
|
||||
the parent environment. And of course, the parent environment will not be
|
||||
affected.
|
||||
@@ -72,7 +72,6 @@ needs. Here is a list of all Owl specific directives:
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||
|
||||
|
||||
@@ -16,8 +16,11 @@ owl
|
||||
hooks
|
||||
onMounted
|
||||
onWillUnmount
|
||||
onWillPatch
|
||||
onPatched
|
||||
useState
|
||||
useRef
|
||||
useSubEnv
|
||||
router
|
||||
Link
|
||||
RouteComponent
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- [Development Mode](#development-mode)
|
||||
- [Playground](#playground)
|
||||
- [Benchmarks](#benchmarks)
|
||||
- [Single File Component](#single-file-component)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
+40
-57
@@ -49,7 +49,6 @@ export interface Fiber<Props> {
|
||||
patchQueue: Fiber<any>[];
|
||||
component: Component<any, any>;
|
||||
vnode: VNode | null;
|
||||
willPatchResult: any;
|
||||
props: Props;
|
||||
promise: Promise<VNode> | null;
|
||||
// handlers?: any;
|
||||
@@ -83,8 +82,10 @@ interface Internal<T extends Env, Props> {
|
||||
boundHandlers: { [key: number]: any };
|
||||
observer: Observer | null;
|
||||
render: CompiledTemplate | null;
|
||||
mountedHandlers: { [key: number]: Function };
|
||||
mountedCB: Function | null;
|
||||
willUnmountCB: Function | null;
|
||||
willPatchCB: Function | null;
|
||||
patchedCB: Function | null;
|
||||
classObj: { [key: string]: boolean } | null;
|
||||
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
|
||||
}
|
||||
@@ -185,8 +186,10 @@ export class Component<T extends Env, Props extends {}> {
|
||||
cmap: {},
|
||||
currentFiber: null,
|
||||
boundHandlers: {},
|
||||
mountedHandlers: {},
|
||||
mountedCB: null,
|
||||
willUnmountCB: null,
|
||||
willPatchCB: null,
|
||||
patchedCB: null,
|
||||
observer: null,
|
||||
render: null,
|
||||
classObj: null,
|
||||
@@ -237,8 +240,6 @@ export class Component<T extends Env, Props extends {}> {
|
||||
* It is not called on the initial render. This is useful to get some
|
||||
* information which are in the DOM. For example, the current position of the
|
||||
* scrollbar
|
||||
*
|
||||
* The return value of willPatch will be given to the patched function.
|
||||
*/
|
||||
willPatch(): any {}
|
||||
|
||||
@@ -254,10 +255,8 @@ export class Component<T extends Env, Props extends {}> {
|
||||
* One need to be careful, because updates here will cause rerender, which in
|
||||
* turn will cause other calls to updated. So, we need to be particularly
|
||||
* careful at avoiding endless cycles.
|
||||
*
|
||||
* The snapshot parameter is the result of the call to willPatch.
|
||||
*/
|
||||
patched(snapshot: any) {}
|
||||
patched() {}
|
||||
|
||||
/**
|
||||
* willUnmount is a hook that is called each time just before a component is
|
||||
@@ -352,25 +351,6 @@ export class Component<T extends Env, Props extends {}> {
|
||||
}
|
||||
}
|
||||
|
||||
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
|
||||
const fiber: Fiber<Props> = {
|
||||
force,
|
||||
scope,
|
||||
vars,
|
||||
rootFiber: null,
|
||||
isCancelled: false,
|
||||
component: this,
|
||||
vnode: null,
|
||||
patchQueue: parent ? parent.patchQueue : [],
|
||||
willPatchResult: null,
|
||||
props: this.props,
|
||||
promise: null
|
||||
};
|
||||
fiber.rootFiber = parent ? parent.rootFiber : fiber;
|
||||
this.__owl__.currentFiber = fiber;
|
||||
return fiber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the component. This operation is quite complex:
|
||||
* - it recursively destroy all children
|
||||
@@ -400,26 +380,6 @@ export class Component<T extends Env, Props extends {}> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the correct way to update the environment of a component. Doing
|
||||
* this will cause a full rerender of the component and its children, so this is
|
||||
* an operation that should not be done frequently.
|
||||
*
|
||||
* A good usecase for updating the environment would be to update some mostly
|
||||
* static config keys, such as a boolean to determine if we are in mobile
|
||||
* mode or not.
|
||||
*/
|
||||
async updateEnv(nextEnv: Partial<T>): Promise<void> {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.parent && __owl__.parent.env === this.env) {
|
||||
this.env = Object.create(this.env);
|
||||
}
|
||||
Object.assign(this.env, nextEnv);
|
||||
if (__owl__.isMounted) {
|
||||
await this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a custom event of type 'eventType' with the given 'payload' on the
|
||||
* component's el, if it exists. However, note that the event will only bubble
|
||||
@@ -441,6 +401,27 @@ export class Component<T extends Env, Props extends {}> {
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is a helper to create a fiber element.
|
||||
*/
|
||||
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
|
||||
const fiber: Fiber<Props> = {
|
||||
force,
|
||||
scope,
|
||||
vars,
|
||||
rootFiber: null,
|
||||
isCancelled: false,
|
||||
component: this,
|
||||
vnode: null,
|
||||
patchQueue: parent ? parent.patchQueue : [],
|
||||
props: this.props,
|
||||
promise: null
|
||||
};
|
||||
fiber.rootFiber = parent ? parent.rootFiber : fiber;
|
||||
this.__owl__.currentFiber = fiber;
|
||||
return fiber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper to perform a full destroy, from the point of view of an Owl
|
||||
* component. It does not remove the el (this is done only once on the top
|
||||
@@ -482,11 +463,10 @@ export class Component<T extends Env, Props extends {}> {
|
||||
}
|
||||
}
|
||||
__owl__.isMounted = true;
|
||||
const handlers = __owl__.mountedHandlers;
|
||||
try {
|
||||
this.mounted();
|
||||
for (let key in handlers) {
|
||||
handlers[key]();
|
||||
if (__owl__.mountedCB) {
|
||||
__owl__.mountedCB()
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler(e, this);
|
||||
@@ -603,7 +583,6 @@ export class Component<T extends Env, Props extends {}> {
|
||||
vnode = __owl__.render!(this, {
|
||||
promises,
|
||||
handlers: __owl__.boundHandlers,
|
||||
mountedHandlers: __owl__.mountedHandlers,
|
||||
fiber: fiber
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -686,9 +665,11 @@ export class Component<T extends Env, Props extends {}> {
|
||||
try {
|
||||
const patchLen = patchQueue.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
fiber.willPatchResult = component.willPatch();
|
||||
component = patchQueue[i].component;
|
||||
if (component.__owl__.willPatchCB) {
|
||||
component.__owl__.willPatchCB();
|
||||
}
|
||||
component.willPatch();
|
||||
}
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const fiber = patchQueue[i];
|
||||
@@ -696,9 +677,11 @@ export class Component<T extends Env, Props extends {}> {
|
||||
component.__patch(fiber.vnode);
|
||||
}
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
component.patched(fiber.willPatchResult);
|
||||
component = patchQueue[i].component;
|
||||
component.patched();
|
||||
if (component.__owl__.patchedCB) {
|
||||
component.__owl__.patchedCB();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler(e, component);
|
||||
|
||||
+50
-29
@@ -12,9 +12,11 @@ import { Observer } from "./core/observer";
|
||||
* - useRef
|
||||
*/
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useState
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* useState hook
|
||||
*
|
||||
* This is the main way a component can be made reactive. The useState hook
|
||||
* will return an observed object (or array). Changes to that value will then
|
||||
* trigger a rerendering of the current component.
|
||||
@@ -29,37 +31,41 @@ export function useState<T>(state: T): T {
|
||||
return __owl__.observer.observe(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounted hook. The callback will be called when the current component is
|
||||
* mounted. Note that the component mounted method is called first.
|
||||
*/
|
||||
let nextID = 1;
|
||||
|
||||
export function onMounted(cb) {
|
||||
const component: Component<any, any> = Component._current;
|
||||
component.__owl__.mountedHandlers[`h${nextID++}`] = cb;
|
||||
// -----------------------------------------------------------------------------
|
||||
// Life cycle hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
function makeLifecycleHook(method: string, reverse: boolean = false) {
|
||||
return function(cb) {
|
||||
const component: Component<any, any> = Component._current;
|
||||
if (component.__owl__[method]) {
|
||||
const current = component.__owl__[method];
|
||||
if (reverse) {
|
||||
component.__owl__[method] = function() {
|
||||
current.call(component);
|
||||
cb.call(component);
|
||||
};
|
||||
} else {
|
||||
component.__owl__[method] = function() {
|
||||
cb.call(component);
|
||||
current.call(component);
|
||||
};
|
||||
}
|
||||
} else {
|
||||
component.__owl__[method] = cb;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* willUnmount hook. The callback will be called when the current component is
|
||||
* willUnmounted. Note that the component mounted method is called last.
|
||||
*/
|
||||
export function onWillUnmount(cb) {
|
||||
const component: Component<any, any> = Component._current;
|
||||
if (component.__owl__.willUnmountCB) {
|
||||
const current = component.__owl__.willUnmountCB;
|
||||
component.__owl__.willUnmountCB = function() {
|
||||
cb.call(component);
|
||||
current.call(component);
|
||||
};
|
||||
} else {
|
||||
component.__owl__.willUnmountCB = cb;
|
||||
}
|
||||
}
|
||||
export const onMounted = makeLifecycleHook("mountedCB", true);
|
||||
export const onWillUnmount = makeLifecycleHook("willUnmountCB");
|
||||
export const onWillPatch = makeLifecycleHook("willPatchCB");
|
||||
export const onPatched = makeLifecycleHook("patchedCB", true);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useRef
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* useRef hook
|
||||
*
|
||||
* The purpose of this hook is to allow components to get a reference to a sub
|
||||
* html node or component.
|
||||
*/
|
||||
@@ -81,3 +87,18 @@ export function useRef(name: string): Ref {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useSubEnv
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* This hook is a simple way to let components use a sub environment. Note that
|
||||
* like for all hooks, it is important that this is only called in the
|
||||
* constructor method.
|
||||
*/
|
||||
export function useSubEnv(nextEnv) {
|
||||
const component = Component._current;
|
||||
component.env = Object.assign(Object.create(component.env), nextEnv);
|
||||
}
|
||||
@@ -183,42 +183,6 @@ QWeb.addDirective({
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-mounted
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "mounted",
|
||||
priority: 97,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const eventName = fullName.slice(5);
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handler = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)})()`;
|
||||
if (extraArgs) {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
||||
extraArgs
|
||||
)});`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
||||
);
|
||||
}
|
||||
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-slot
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
makeTestEnv,
|
||||
nextTick,
|
||||
patchNextFrame,
|
||||
renderToDOM,
|
||||
unpatchNextFrame
|
||||
@@ -265,28 +264,6 @@ describe("animations", () => {
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("t-transition combined with t-mounted", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = useState({ flag: false });
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(fixture);
|
||||
|
||||
patchNextFrame(cb => cb());
|
||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
||||
|
||||
widget.state.flag = true;
|
||||
await nextTick();
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
unpatchNextFrame();
|
||||
});
|
||||
|
||||
test("t-transition, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
|
||||
@@ -625,24 +625,6 @@ describe("lifecycle hooks", () => {
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("patched hook is called after updateEnv", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
state = useState({ a: 1 });
|
||||
|
||||
patched() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(n).toBe(0);
|
||||
|
||||
await widget.updateEnv({ isMobile: true });
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("shouldUpdate hook prevent rerendering", async () => {
|
||||
let shouldUpdate = false;
|
||||
class TestWidget extends Widget {
|
||||
@@ -725,10 +707,8 @@ describe("lifecycle hooks", () => {
|
||||
state = useState({ n: 1 });
|
||||
willPatch() {
|
||||
steps.push("parent:willPatch");
|
||||
return "leffe";
|
||||
}
|
||||
patched(snapshot) {
|
||||
expect(snapshot).toBe("leffe");
|
||||
patched() {
|
||||
steps.push("parent:patched");
|
||||
}
|
||||
}
|
||||
@@ -2817,107 +2797,6 @@ describe("async rendering", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("updating environment", () => {
|
||||
test("can update widget env", async () => {
|
||||
const widget = new Widget(env);
|
||||
expect(widget.env).toBe(env);
|
||||
await widget.updateEnv(<any>{ somekey: 4 });
|
||||
expect(widget.env).toBe(env);
|
||||
expect((<any>widget).env.somekey).toBe(4);
|
||||
});
|
||||
|
||||
test("updating widget env does not render widget (if not mounted)", async () => {
|
||||
let n = 0;
|
||||
class TestWidget extends Widget {
|
||||
__render(f) {
|
||||
n++;
|
||||
return super.__render(f);
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new TestWidget(env);
|
||||
expect(n).toBe(0);
|
||||
await widget.updateEnv(<any>{ somekey: 4 });
|
||||
expect(n).toBe(0);
|
||||
await widget.mount(fixture);
|
||||
expect(n).toBe(1);
|
||||
await widget.updateEnv(<any>{ somekey: 5 });
|
||||
expect(n).toBe(2);
|
||||
widget.unmount();
|
||||
expect(n).toBe(2);
|
||||
await widget.updateEnv(<any>{ somekey: 5 });
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("updating child env does not modify parent env", async () => {
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child"/></div>`);
|
||||
class ParentWidget extends Widget {
|
||||
static components = { child: Widget };
|
||||
}
|
||||
const parent = new ParentWidget(env);
|
||||
await parent.mount(fixture);
|
||||
const child = children(parent)[0];
|
||||
expect(child.env).toBe(parent.env);
|
||||
await child.updateEnv(<any>{ somekey: 4 });
|
||||
expect(child.env).not.toBe(parent.env);
|
||||
expect((<any>parent).env.somekey).toBeUndefined();
|
||||
});
|
||||
|
||||
test("updating parent env does modify child env", async () => {
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child"/></div>`);
|
||||
class ParentWidget extends Widget {
|
||||
static components = { child: Widget };
|
||||
}
|
||||
const parent = new ParentWidget(env);
|
||||
await parent.mount(fixture);
|
||||
const child = children(parent)[0];
|
||||
expect(child.env.somekey).toBeUndefined();
|
||||
await parent.updateEnv({ somekey: 4 });
|
||||
expect(child.env.somekey).toBe(4);
|
||||
});
|
||||
|
||||
test("updating parent env does modify child env, part 2", async () => {
|
||||
env.qweb.addTemplate("ParentWidget", `<div><Child/></div>`);
|
||||
class ParentWidget extends Widget {
|
||||
static components = { Child: Widget };
|
||||
}
|
||||
const parent = new ParentWidget(env);
|
||||
await parent.mount(fixture);
|
||||
const child = children(parent)[0];
|
||||
expect(child.env.somekey).toBeUndefined();
|
||||
await child.updateEnv({ somekey: 4 });
|
||||
await parent.updateEnv({ someotherkey: 4 });
|
||||
expect(child.env.someotherkey).toBe(4);
|
||||
});
|
||||
|
||||
test("updating env force a rerender", async () => {
|
||||
env.qweb.addTemplate("TestWidget", `<div><t t-esc="env.someKey"/></div>`);
|
||||
class TestWidget extends Widget {}
|
||||
(<any>env).someKey = "hey";
|
||||
const widget = new TestWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||
await widget.updateEnv(<any>{ someKey: "rerendered" });
|
||||
expect(fixture.innerHTML).toBe("<div>rerendered</div>");
|
||||
});
|
||||
|
||||
test("updating env force rerendering children", async () => {
|
||||
env.qweb.addTemplate("Parent", `<div><Child /></div>`);
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Child };
|
||||
}
|
||||
env.qweb.addTemplate("Child", `<div><t t-esc="env.someKey"/></div>`);
|
||||
(<any>env).someKey = "hey";
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
|
||||
await widget.updateEnv(<any>{ someKey: "rerendered" });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>rerendered</div></div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("widget and observable state", () => {
|
||||
test("widget is rerendered when its state is changed", async () => {
|
||||
env.qweb.addTemplate("TestWidget", `<div><t t-esc="state.drink"/></div>`);
|
||||
@@ -2961,71 +2840,6 @@ describe("widget and observable state", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-mounted directive", () => {
|
||||
test("callback is not called when not in DOM", async () => {
|
||||
env.qweb.addTemplate("TestWidget", `<div><input t-mounted="f"/></div>`);
|
||||
class TestWidget extends Widget {
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(document.createElement("div"));
|
||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("callback is called when in DOM", async () => {
|
||||
env.qweb.addTemplate("TestWidget", `<div><input t-mounted="f"/></div>`);
|
||||
class TestWidget extends Widget {
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(fixture);
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("callback with args is called when in DOM", async () => {
|
||||
env.qweb.addTemplate("TestWidget", `<div><input t-mounted="f(2)"/></div>`);
|
||||
class TestWidget extends Widget {
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(fixture);
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
expect(widget.f).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
test("combined with a t-if", async () => {
|
||||
env.qweb.addTemplate("TestWidget", `<div><input t-if="state.flag" t-mounted="f"/></div>`);
|
||||
class TestWidget extends Widget {
|
||||
state = useState({ flag: false });
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(fixture);
|
||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
||||
|
||||
widget.state.flag = true;
|
||||
await nextTick();
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("combined with a t-ref", async () => {
|
||||
env.qweb.addTemplate("TestWidget", `<div><input t-ref="input" t-mounted="f"/></div>`);
|
||||
class TestWidget extends Widget {
|
||||
input = useRef("input");
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(fixture);
|
||||
expect(widget.input.el).toBeDefined();
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can deduce template from name", () => {
|
||||
test("can find template if name of component", async () => {
|
||||
class ABC extends Widget {}
|
||||
|
||||
+208
-1
@@ -1,6 +1,14 @@
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { useState, onMounted, onWillUnmount, useRef } from "../src/hooks";
|
||||
import {
|
||||
useState,
|
||||
onMounted,
|
||||
onWillUnmount,
|
||||
useRef,
|
||||
onPatched,
|
||||
onWillPatch,
|
||||
useSubEnv
|
||||
} from "../src/hooks";
|
||||
import { xml } from "../src/tags";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -149,4 +157,203 @@ describe("hooks", () => {
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
|
||||
});
|
||||
|
||||
test("can use onPatched, onWillPatch", async () => {
|
||||
const steps: string[] = [];
|
||||
function useMyHook() {
|
||||
onWillPatch(() => {
|
||||
steps.push("willPatch");
|
||||
});
|
||||
onPatched(() => {
|
||||
steps.push("patched");
|
||||
});
|
||||
}
|
||||
|
||||
class MyComponent extends Component<any, any> {
|
||||
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
|
||||
state = useState({ flag: true });
|
||||
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
}
|
||||
|
||||
const component = new MyComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(component).not.toHaveProperty("patched");
|
||||
expect(component).not.toHaveProperty("willPatch");
|
||||
expect(steps).toEqual([]);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||
component.state.flag = false;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
|
||||
expect(steps).toEqual(["willPatch", "patched"]);
|
||||
});
|
||||
|
||||
test("patched, willPatch, onPatched, onWillPatch order", async () => {
|
||||
const steps: string[] = [];
|
||||
function useMyHook() {
|
||||
onPatched(() => {
|
||||
steps.push("hook:patched");
|
||||
});
|
||||
onWillPatch(() => {
|
||||
steps.push("hook:willPatch");
|
||||
});
|
||||
}
|
||||
class MyComponent extends Component<any, any> {
|
||||
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
|
||||
state = useState({ flag: true });
|
||||
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
willPatch() {
|
||||
steps.push("comp:willPatch");
|
||||
}
|
||||
patched() {
|
||||
steps.push("comp:patched");
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||
component.state.flag = false;
|
||||
await nextTick();
|
||||
|
||||
expect(steps).toEqual(["hook:willPatch", "comp:willPatch", "comp:patched", "hook:patched"]);
|
||||
});
|
||||
|
||||
test("two different call to willPatch/patched should work", async () => {
|
||||
const steps: string[] = [];
|
||||
function useMyHook(i) {
|
||||
onPatched(() => {
|
||||
steps.push("hook:patched" + i);
|
||||
});
|
||||
onWillPatch(() => {
|
||||
steps.push("hook:willPatch" + i);
|
||||
});
|
||||
}
|
||||
class MyComponent extends Component<any, any> {
|
||||
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
|
||||
state = useState({ value: 1 });
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useMyHook(1);
|
||||
useMyHook(2);
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>hey1</div>");
|
||||
component.state.value++;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>hey2</div>");
|
||||
|
||||
expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]);
|
||||
});
|
||||
|
||||
describe("autofocus hook", () => {
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
let isInDom = false;
|
||||
function updateFocus() {
|
||||
if (!isInDom && ref.el) {
|
||||
isInDom = true;
|
||||
ref.el.focus();
|
||||
} else if (isInDom && !ref.el) {
|
||||
isInDom = false;
|
||||
}
|
||||
}
|
||||
onPatched(updateFocus);
|
||||
onMounted(updateFocus);
|
||||
}
|
||||
|
||||
test("simple input", async () => {
|
||||
class SomeComponent extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input t-ref="input1"/>
|
||||
<input t-ref="input2"/>
|
||||
</div>`;
|
||||
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useAutofocus("input2");
|
||||
}
|
||||
}
|
||||
|
||||
const component = new SomeComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><input><input></div>");
|
||||
const input2 = fixture.querySelectorAll("input")[1];
|
||||
expect(input2).toBe(document.activeElement);
|
||||
});
|
||||
|
||||
test("input in a t-if", async () => {
|
||||
class SomeComponent extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input t-ref="input1"/>
|
||||
<t t-if="state.flag"><input t-ref="input2"/></t>
|
||||
</div>`;
|
||||
|
||||
state = useState({ flag: false });
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useAutofocus("input2");
|
||||
}
|
||||
}
|
||||
|
||||
const component = new SomeComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><input></div>");
|
||||
expect(document.activeElement).toBe(document.body);
|
||||
|
||||
component.state.flag = true;
|
||||
await nextTick();
|
||||
const input2 = fixture.querySelectorAll("input")[1];
|
||||
expect(input2).toBe(document.activeElement);
|
||||
});
|
||||
});
|
||||
|
||||
test("can use sub env", async () => {
|
||||
class TestComponent extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="env.val"/></div>`;
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useSubEnv({ val: 3 });
|
||||
}
|
||||
}
|
||||
const component = new TestComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||
expect(env).not.toHaveProperty("val");
|
||||
expect(component.env).toHaveProperty("val");
|
||||
});
|
||||
|
||||
test("parent and child env", async () => {
|
||||
class Child extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="env.val"/></div>`;
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useSubEnv({ val: 5 });
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
|
||||
static components = { Child}
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useSubEnv({ val: 3 });
|
||||
}
|
||||
}
|
||||
const component = new Parent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe( "<div>3<div>5</div></div>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,8 +173,10 @@ body {
|
||||
}
|
||||
|
||||
.right-pane .note {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
margin-top: 20px;
|
||||
padding: 5%;
|
||||
}
|
||||
|
||||
.right-pane .error {
|
||||
|
||||
+135
-1
@@ -5,7 +5,7 @@ class Greeter extends Component {
|
||||
state = useState({ word: 'Hello' });
|
||||
|
||||
toggle() {
|
||||
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
|
||||
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1625,6 +1625,135 @@ const WMS_CSS = `body {
|
||||
font-size: 20px;
|
||||
}`;
|
||||
|
||||
const MONKEYPATCH = `
|
||||
// In this example, we show a possible way components can be monkey patched.
|
||||
// This involves the functions patch/unpatch, which modifies the prototype of a
|
||||
// class (not only an owl component). Sadly, the patch code cannot make use of
|
||||
// the syntactic sugar that JS engines provides to classes, so it needs to use
|
||||
// the "this._super()" syntax instead of the standard "super.someMethod()".
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Patching Code
|
||||
//--------------------------------------------------------------------------
|
||||
const patchMap = new WeakMap();
|
||||
|
||||
function patch(C, patchName, patch) {
|
||||
let metadata = patchMap.get(C.prototype);
|
||||
if (!metadata) {
|
||||
metadata = {
|
||||
origMethods: {},
|
||||
patches: {},
|
||||
current: []
|
||||
};
|
||||
patchMap.set(C.prototype, metadata);
|
||||
}
|
||||
const proto = C.prototype;
|
||||
if (metadata.patches[patchName]) {
|
||||
throw new Error(\`Patch [\${patchName}] already exists\`);
|
||||
}
|
||||
metadata.patches[patchName] = patch;
|
||||
applyPatch(proto, patch);
|
||||
metadata.current.push(patchName);
|
||||
|
||||
function applyPatch(proto, patch) {
|
||||
Object.keys(patch).forEach(function(methodName) {
|
||||
const method = patch[methodName];
|
||||
if (typeof method === "function") {
|
||||
const original = proto[methodName];
|
||||
if (!(methodName in metadata.origMethods)) {
|
||||
metadata.origMethods[methodName] = original;
|
||||
}
|
||||
proto[methodName] = function(...args) {
|
||||
this._super = original;
|
||||
return method.call(this, ...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// we define here an unpatch function. This is mostly useful if we want to
|
||||
// remove a patch. For example, for testing purposes
|
||||
function unpatch(C, patchName) {
|
||||
const proto = C.prototype;
|
||||
let metadata = patchMap.get(proto);
|
||||
if (!metdata) {
|
||||
return;
|
||||
}
|
||||
patchMap.delete(proto);
|
||||
|
||||
// reset to original
|
||||
for (let k in metadata.origMethods) {
|
||||
proto[k] = metadata.origMethods[k];
|
||||
}
|
||||
|
||||
// apply other patches
|
||||
for (let name of metadata.current) {
|
||||
if (name !== patchName) {
|
||||
patch(C, name, metadata.patches[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Components
|
||||
//--------------------------------------------------------------------------
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class Counter extends Component {
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends Component {
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Patching code
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// In an Odoo application, this code would be located in another odoo module,
|
||||
// to customize some behaviour in an existing class/component
|
||||
patch(Counter, "double_value", {
|
||||
increment() {
|
||||
this._super();
|
||||
this.state.value = 2 * this.state.value;
|
||||
}
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Application setup
|
||||
//--------------------------------------------------------------------------
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
`;
|
||||
|
||||
const MONKEYPATCH_XML = `
|
||||
<templates>
|
||||
<button t-name="Counter" t-on-click="increment">
|
||||
Click [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
|
||||
<div t-name="App">
|
||||
<Counter />
|
||||
</div>
|
||||
</templates>
|
||||
`;
|
||||
|
||||
const MONKEYPATCH_CSS = `
|
||||
button {
|
||||
font-size: 20px;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
}`;
|
||||
|
||||
export const SAMPLES = [
|
||||
{
|
||||
description: "Components",
|
||||
@@ -1684,5 +1813,10 @@ export const SAMPLES = [
|
||||
code: ASYNC_COMPONENTS,
|
||||
xml: ASYNC_COMPONENTS_XML,
|
||||
css: ASYNC_COMPONENTS_CSS
|
||||
}, {
|
||||
description: "Monkey patching components",
|
||||
code: MONKEYPATCH,
|
||||
xml: MONKEYPATCH_XML,
|
||||
css: MONKEYPATCH_CSS
|
||||
}
|
||||
];
|
||||
|
||||
@@ -45,7 +45,11 @@
|
||||
<div>🦉 Odoo Web Library 🦉</div>
|
||||
<div>v<t t-esc="version"/></div>
|
||||
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
|
||||
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
|
||||
<div class="note">
|
||||
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
|
||||
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="state.error" class="error">
|
||||
<h3>Error</h3>
|
||||
|
||||
Reference in New Issue
Block a user