mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
@@ -48,7 +48,7 @@ const app = new App({ qweb: new QWeb() });
|
|||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the counter component is made reactive with the `useState` hook.
|
Note that the counter component is made reactive with the [`useState` hook](doc/hooks.md#usestate).
|
||||||
|
|
||||||
More interesting examples can be found on the
|
More interesting examples can be found on the
|
||||||
[playground](https://odoo.github.io/owl/playground) application.
|
[playground](https://odoo.github.io/owl/playground) application.
|
||||||
|
|||||||
+38
-27
@@ -47,7 +47,6 @@ exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in Q
|
|||||||
Rendering a component generates a virtual dom representation
|
Rendering a component generates a virtual dom representation
|
||||||
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
|
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
|
||||||
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Let us have a look at a simple component:
|
Let us have a look at a simple component:
|
||||||
@@ -79,7 +78,6 @@ Owl will use the component's name as template name. Here,
|
|||||||
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
|
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
|
||||||
[observed](observer.md), and any change to it will cause a rerendering.
|
[observed](observer.md), and any change to it will cause a rerendering.
|
||||||
|
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
An Owl component is a small class which represent a component or some UI element.
|
An Owl component is a small class which represent a component or some UI element.
|
||||||
@@ -122,9 +120,6 @@ constructor.
|
|||||||
in some case. Note that `props` are owned by the parent, not by the component.
|
in some case. Note that `props` are owned by the parent, not by the component.
|
||||||
As such, it should not ever be modified by the component!!
|
As such, it should not ever be modified by the component!!
|
||||||
|
|
||||||
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
|
|
||||||
or sub components defined by a `t-ref` directive in the component's template.
|
|
||||||
|
|
||||||
### Static Properties
|
### Static Properties
|
||||||
|
|
||||||
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
|
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
|
||||||
@@ -967,44 +962,60 @@ Examples:
|
|||||||
|
|
||||||
### References
|
### References
|
||||||
|
|
||||||
The `t-ref` directive helps a component keep reference to some inside part of it.
|
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||||
Like the `t-on` directive, it can work either on a DOM node, or on a component:
|
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||||
|
tagged by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
|
||||||
|
more detail.
|
||||||
|
|
||||||
|
As a short example, here is how we could set the focus on a given input:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div>
|
<div>
|
||||||
<div t-ref="someDiv"/>
|
<input t-ref="input"/>
|
||||||
<SubComponent t-ref="someComponent"/>
|
<button t-on-click="focusInput">Click</button>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the component will be able to access the `div` and the component
|
|
||||||
inside the special `refs` variable:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
this.refs.someDiv;
|
import { useRef } from "owl/hooks";
|
||||||
this.refs.someComponent;
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
inputRef = useRef("input");
|
||||||
|
|
||||||
|
focusInput() {
|
||||||
|
this.inputRef.el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This is useful for various usecases: for example, integrating with an external
|
The `useRef` hook can also be used to get a reference to an instance of a sub
|
||||||
library that needs to render itself inside an actual DOM node. Or for calling
|
component rendered by Owl. In that case, we need to access it with the `comp`
|
||||||
some method on a sub component.
|
property instead of `el`:
|
||||||
|
|
||||||
Note: if used on a component, the reference will be set in the `refs`
|
|
||||||
variable between `willPatch` and `patched`.
|
|
||||||
|
|
||||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
|
||||||
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
|
|
||||||
`t-component` directives). For example, if we have
|
|
||||||
`id` set to 44 in the rendering context,
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-ref="component_{{id}}"/>
|
<div>
|
||||||
|
<SubComponent t-ref="sub"/>
|
||||||
|
<button t-on-click="doSomething">Click</button>
|
||||||
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
this.refs.component_44;
|
import { useRef } from "owl/hooks";
|
||||||
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static components = { SubComponent };
|
||||||
|
subRef = useRef("sub");
|
||||||
|
|
||||||
|
doSomething() {
|
||||||
|
this.subRef.comp.doSomeThingElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that these two examples uses the suffix `ref` to name the reference. This
|
||||||
|
is not mandatory, but it is a useful convention, so we do not forget to access
|
||||||
|
it with the `el` or `comp` suffix.
|
||||||
|
|
||||||
### Slots
|
### Slots
|
||||||
|
|
||||||
To make generic components, it is useful to be able for a parent component to _inject_
|
To make generic components, it is useful to be able for a parent component to _inject_
|
||||||
|
|||||||
+63
-10
@@ -8,11 +8,13 @@
|
|||||||
- [One Rule](#one-rule)
|
- [One Rule](#one-rule)
|
||||||
- [`useState`](#usestate)
|
- [`useState`](#usestate)
|
||||||
- [`onMounted`](#onmounted)
|
- [`onMounted`](#onmounted)
|
||||||
- [`onWillUnmount`](#onWillUnmount)
|
- [`onWillUnmount`](#onwillunmount)
|
||||||
|
- [`useRef`](#useref)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Hooks were popularised by React as a way to solve the following issues:
|
Hooks were popularised by React as a way to solve the following issues:
|
||||||
|
|
||||||
- help reusing stateful logic between components
|
- help reusing stateful logic between components
|
||||||
- help organizing code by feature in complex components
|
- help organizing code by feature in complex components
|
||||||
- use state in functional components, without writing a class.
|
- use state in functional components, without writing a class.
|
||||||
@@ -22,33 +24,31 @@ Owl hooks serve the same purpose, except that they work for class components
|
|||||||
there seems to be the misconception that hooks are in opposition to class. This
|
there seems to be the misconception that hooks are in opposition to class. This
|
||||||
is clearly not true, as shown by Owl hooks).
|
is clearly not true, as shown by Owl hooks).
|
||||||
|
|
||||||
|
|
||||||
Hooks works beautifully with Owl components: they solve the problems mentioned
|
Hooks works beautifully with Owl components: they solve the problems mentioned
|
||||||
above, and in particular, they are the perfect way to make your component
|
above, and in particular, they are the perfect way to make your component
|
||||||
reactive.
|
reactive.
|
||||||
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Here is the classical example of a non trivial hook to track the mouse position.
|
Here is the classical example of a non trivial hook to track the mouse position.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const {useState, onMounted, onWillUnmount} = owl.hooks;
|
const { useState, onMounted, onWillUnmount } = owl.hooks;
|
||||||
|
|
||||||
// We define here a custom behaviour: this hook tracks the state of the mouse
|
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||||
// position
|
// position
|
||||||
function useMouse() {
|
function useMouse() {
|
||||||
const position = useState({x:0, y: 0});
|
const position = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
function update(e) {
|
function update(e) {
|
||||||
position.x = e.clientX;
|
position.x = e.clientX;
|
||||||
position.y = e.clientY;
|
position.y = e.clientY;
|
||||||
}
|
}
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('mousemove', update);
|
window.addEventListener("mousemove", update);
|
||||||
});
|
});
|
||||||
onWillUnmount(() => {
|
onWillUnmount(() => {
|
||||||
window.removeEventListener('mousemove', update);
|
window.removeEventListener("mousemove", update);
|
||||||
});
|
});
|
||||||
|
|
||||||
return position;
|
return position;
|
||||||
@@ -79,21 +79,21 @@ constructor (or in class fields):
|
|||||||
```js
|
```js
|
||||||
// ok
|
// ok
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
state = useState();
|
state = useState({value: 0});
|
||||||
}
|
}
|
||||||
|
|
||||||
// also ok
|
// also ok
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
this.state = useState();
|
this.state = useState({value: 0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// not ok: this is executed after the constructor is called
|
// not ok: this is executed after the constructor is called
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
async willStart() {
|
async willStart() {
|
||||||
this.state = useState();
|
this.state = useState({value: 0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -134,3 +134,56 @@ is mounted (see example on top of this page).
|
|||||||
`onWillUnmount` is not an hook, but is a building block designed to help make useful
|
`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
|
hooks. `onWillUnmount` register a callback, which will be called when the component
|
||||||
is unmounted (see example on top of this page).
|
is unmounted (see example on top of this page).
|
||||||
|
|
||||||
|
### `useRef`
|
||||||
|
|
||||||
|
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||||
|
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||||
|
tagged by the `t-ref` directive:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<div t-ref="someDiv"/>
|
||||||
|
<SubComponent t-ref="someComponent"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the component will be able to access the `div` and the component
|
||||||
|
`SubComponent` using the `useRef` hook:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Parent extends Component {
|
||||||
|
subRef = useRef("someComponent");
|
||||||
|
divRef = useRef("someDiv");
|
||||||
|
|
||||||
|
someMethod() {
|
||||||
|
// here, if component is mounted, refs are active:
|
||||||
|
// - this.divRef.el is the div HTMLElement
|
||||||
|
// - this.subRef.comp is the instance of the sub component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As shown by the example above, html elements are accessed by using the `el`
|
||||||
|
key, and components references are accessed with `comp`.
|
||||||
|
|
||||||
|
Note: if used on a component, the reference will be set in the `refs`
|
||||||
|
variable between `willPatch` and `patched`.
|
||||||
|
|
||||||
|
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||||
|
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
|
||||||
|
`t-component` directives). For example,
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the references needs to be set like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.ref1 = useRef("component_1");
|
||||||
|
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`.
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ For example, this code will display `update` in the console:
|
|||||||
```javascript
|
```javascript
|
||||||
const observer = new owl.Observer();
|
const observer = new owl.Observer();
|
||||||
observer.notifyCB = () => console.log("update");
|
observer.notifyCB = () => console.log("update");
|
||||||
const obj = observer.observe( { a: { b: 1 } });
|
const obj = observer.observe({ a: { b: 1 } });
|
||||||
|
|
||||||
obj.a.b = 2;
|
obj.a.b = 2;
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ owl
|
|||||||
onMounted
|
onMounted
|
||||||
onWillUnmount
|
onWillUnmount
|
||||||
useState
|
useState
|
||||||
|
useRef
|
||||||
router
|
router
|
||||||
Link
|
Link
|
||||||
RouteComponent
|
RouteComponent
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ helps you define inline templates, there is a VS Code addon `Comment tagged temp
|
|||||||
which, if installed, does exactly that. To enable it, you need to add a comment,
|
which, if installed, does exactly that. To enable it, you need to add a comment,
|
||||||
like this:
|
like this:
|
||||||
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// TEMPLATE
|
// TEMPLATE
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ interface Internal<T extends Env, Props> {
|
|||||||
render: CompiledTemplate | null;
|
render: CompiledTemplate | null;
|
||||||
mountedHandlers: { [key: number]: Function };
|
mountedHandlers: { [key: number]: Function };
|
||||||
classObj: { [key: string]: boolean } | null;
|
classObj: { [key: string]: boolean } | null;
|
||||||
|
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -97,6 +98,9 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
static template?: string | null = null;
|
static template?: string | null = null;
|
||||||
static _template?: string | null = null;
|
static _template?: string | null = null;
|
||||||
static _current?: any | null = null;
|
static _current?: any | null = null;
|
||||||
|
static components = {};
|
||||||
|
static props?: any;
|
||||||
|
static defaultProps?: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `el` is the root element of the component. Note that it could be null:
|
* The `el` is the root element of the component. Note that it could be null:
|
||||||
@@ -105,19 +109,10 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
get el(): HTMLElement | null {
|
get el(): HTMLElement | null {
|
||||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||||
}
|
}
|
||||||
static components = {};
|
|
||||||
|
|
||||||
env: T;
|
env: T;
|
||||||
props: Props;
|
props: Props;
|
||||||
|
|
||||||
// type of props is not easily representable in typescript...
|
|
||||||
static props?: any;
|
|
||||||
static defaultProps?: any;
|
|
||||||
|
|
||||||
refs: {
|
|
||||||
[key: string]: Component<T, any> | HTMLElement | undefined;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -192,7 +187,8 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
mountedHandlers: {},
|
mountedHandlers: {},
|
||||||
observer: null,
|
observer: null,
|
||||||
render: null,
|
render: null,
|
||||||
classObj: null
|
classObj: null,
|
||||||
|
refs: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -263,9 +263,10 @@ QWeb.addDirective({
|
|||||||
let refExpr = "";
|
let refExpr = "";
|
||||||
let refKey: string = "";
|
let refKey: string = "";
|
||||||
if (ref) {
|
if (ref) {
|
||||||
|
ctx.rootContext.shouldDefineRefs = true;
|
||||||
refKey = `ref${ctx.generateID()}`;
|
refKey = `ref${ctx.generateID()}`;
|
||||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
||||||
refExpr = `context.refs[${refKey}] = w${componentID};`;
|
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
|
||||||
}
|
}
|
||||||
let transitionsInsertCode = "";
|
let transitionsInsertCode = "";
|
||||||
if (transition) {
|
if (transition) {
|
||||||
@@ -273,7 +274,7 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
|
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
|
||||||
if (ref && !keepAlive) {
|
if (ref && !keepAlive) {
|
||||||
finalizeComponentCode += `delete context.refs[${refKey}];`;
|
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
|
||||||
}
|
}
|
||||||
if (transition) {
|
if (transition) {
|
||||||
finalizeComponentCode = `let finalize = () => {
|
finalizeComponentCode = `let finalize = () => {
|
||||||
|
|||||||
+29
-4
@@ -9,9 +9,9 @@ import { Observer } from "./core/observer";
|
|||||||
* - useState (reactive state)
|
* - useState (reactive state)
|
||||||
* - onMounted
|
* - onMounted
|
||||||
* - onWillUnmount
|
* - onWillUnmount
|
||||||
|
* - useRef
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useState hook
|
* useState hook
|
||||||
*
|
*
|
||||||
@@ -20,7 +20,7 @@ import { Observer } from "./core/observer";
|
|||||||
* trigger a rerendering of the current component.
|
* trigger a rerendering of the current component.
|
||||||
*/
|
*/
|
||||||
export function useState<T>(state: T): T {
|
export function useState<T>(state: T): T {
|
||||||
const component: Component<any,any> = Component._current;
|
const component: Component<any, any> = Component._current;
|
||||||
const __owl__ = component.__owl__;
|
const __owl__ = component.__owl__;
|
||||||
if (!__owl__.observer) {
|
if (!__owl__.observer) {
|
||||||
__owl__.observer = new Observer();
|
__owl__.observer = new Observer();
|
||||||
@@ -34,7 +34,7 @@ export function useState<T>(state: T): T {
|
|||||||
* mounted. Note that the component mounted method is called first.
|
* mounted. Note that the component mounted method is called first.
|
||||||
*/
|
*/
|
||||||
export function onMounted(cb) {
|
export function onMounted(cb) {
|
||||||
const component = Component._current;
|
const component: Component<any, any> = Component._current;
|
||||||
const current = component.mounted;
|
const current = component.mounted;
|
||||||
component.mounted = function() {
|
component.mounted = function() {
|
||||||
current.call(component);
|
current.call(component);
|
||||||
@@ -47,10 +47,35 @@ export function onMounted(cb) {
|
|||||||
* willUnmounted. Note that the component mounted method is called last.
|
* willUnmounted. Note that the component mounted method is called last.
|
||||||
*/
|
*/
|
||||||
export function onWillUnmount(cb) {
|
export function onWillUnmount(cb) {
|
||||||
const component = Component._current;
|
const component: Component<any, any> = Component._current;
|
||||||
const current = component.willUnmount;
|
const current = component.willUnmount;
|
||||||
component.willUnmount = function() {
|
component.willUnmount = function() {
|
||||||
cb();
|
cb();
|
||||||
current.call(component);
|
current.call(component);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useRef hook
|
||||||
|
*
|
||||||
|
* The purpose of this hook is to allow components to get a reference to a sub
|
||||||
|
* html node or component.
|
||||||
|
*/
|
||||||
|
interface Ref {
|
||||||
|
el: HTMLElement | null;
|
||||||
|
comp: Component<any, any> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRef(name: string): Ref {
|
||||||
|
const __owl__ = Component._current.__owl__;
|
||||||
|
return {
|
||||||
|
get el(): HTMLElement | null {
|
||||||
|
const val = __owl__.refs && __owl__.refs[name];
|
||||||
|
return val instanceof HTMLElement ? val : null;
|
||||||
|
},
|
||||||
|
get comp(): Component<any, any> | null {
|
||||||
|
const val = __owl__.refs && __owl__.refs[name];
|
||||||
|
return val instanceof Component ? val : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class Context {
|
|||||||
shouldDefineParent: boolean = false;
|
shouldDefineParent: boolean = false;
|
||||||
shouldDefineQWeb: boolean = false;
|
shouldDefineQWeb: boolean = false;
|
||||||
shouldDefineUtils: boolean = false;
|
shouldDefineUtils: boolean = false;
|
||||||
|
shouldDefineRefs: boolean = false;
|
||||||
shouldDefineResult: boolean = true;
|
shouldDefineResult: boolean = true;
|
||||||
shouldProtectContext: boolean = false;
|
shouldProtectContext: boolean = false;
|
||||||
shouldTrackScope: boolean = false;
|
shouldTrackScope: boolean = false;
|
||||||
@@ -62,6 +63,9 @@ export class Context {
|
|||||||
if (this.shouldDefineResult) {
|
if (this.shouldDefineResult) {
|
||||||
this.code.unshift(" let result;");
|
this.code.unshift(" let result;");
|
||||||
}
|
}
|
||||||
|
if (this.shouldDefineRefs) {
|
||||||
|
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
|
||||||
|
}
|
||||||
if (this.shouldDefineOwner) {
|
if (this.shouldDefineOwner) {
|
||||||
// this is necessary to prevent some directives (t-forach for ex) to
|
// this is necessary to prevent some directives (t-forach for ex) to
|
||||||
// pollute the rendering context by adding some keys in it.
|
// pollute the rendering context by adding some keys in it.
|
||||||
|
|||||||
@@ -79,9 +79,10 @@ QWeb.addDirective({
|
|||||||
name: "ref",
|
name: "ref",
|
||||||
priority: 95,
|
priority: 95,
|
||||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||||
|
ctx.rootContext.shouldDefineRefs = true;
|
||||||
const refKey = `ref${ctx.generateID()}`;
|
const refKey = `ref${ctx.generateID()}`;
|
||||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Env } from "../src/component/component";
|
import { Component, Env } from "../src/component/component";
|
||||||
import { QWeb } from "../src/qweb/index";
|
import { QWeb } from "../src/qweb/index";
|
||||||
import { useState } from "../src/hooks";
|
import { useState, useRef } from "../src/hooks";
|
||||||
import {
|
import {
|
||||||
makeDeferred,
|
makeDeferred,
|
||||||
makeTestFixture,
|
makeTestFixture,
|
||||||
@@ -150,6 +150,7 @@ describe("animations", () => {
|
|||||||
);
|
);
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
state = useState({ hide: false });
|
state = useState({ hide: false });
|
||||||
|
span = useRef("span");
|
||||||
}
|
}
|
||||||
const widget = new TestWidget(env);
|
const widget = new TestWidget(env);
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ describe("animations", () => {
|
|||||||
});
|
});
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
spanNode = widget.el!.children[0];
|
spanNode = widget.el!.children[0];
|
||||||
expect(widget.refs.span).toBe(spanNode);
|
expect(widget.span.el).toBe(spanNode);
|
||||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
|
|||||||
let QWeb = this.constructor;
|
let QWeb = this.constructor;
|
||||||
let parent = context;
|
let parent = context;
|
||||||
let owner = context;
|
let owner = context;
|
||||||
|
context.__owl__.refs = context.__owl__.refs || {};
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
@@ -327,7 +328,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
|
|||||||
w4 = new W4(parent, props4);
|
w4 = new W4(parent, props4);
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4.__prepare(extra.fiber, undefined, undefined);
|
def3 = w4.__prepare(extra.fiber, undefined, undefined);
|
||||||
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.__owl__.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.__owl__.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
} else {
|
} else {
|
||||||
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
|
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
@@ -359,6 +360,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
|
|||||||
let QWeb = this.constructor;
|
let QWeb = this.constructor;
|
||||||
let parent = context;
|
let parent = context;
|
||||||
let owner = context;
|
let owner = context;
|
||||||
|
context.__owl__.refs = context.__owl__.refs || {};
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
@@ -387,7 +389,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
|
|||||||
w4 = new W4(parent, props4);
|
w4 = new W4(parent, props4);
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4.__prepare(extra.fiber, undefined, undefined);
|
def3 = w4.__prepare(extra.fiber, undefined, undefined);
|
||||||
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.__owl__.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.__owl__.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
} else {
|
} else {
|
||||||
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
|
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
@@ -1552,6 +1554,7 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
|
context.__owl__.refs = context.__owl__.refs || {};
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = extra.parentNode;
|
let c1 = extra.parentNode;
|
||||||
Object.assign(context, extra.fiber.scope);
|
Object.assign(context, extra.fiber.scope);
|
||||||
@@ -1566,7 +1569,7 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
|
|||||||
const ref11 = \`myButton\`;
|
const ref11 = \`myButton\`;
|
||||||
p10.hook = {
|
p10.hook = {
|
||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.refs[ref11] = n.elm;
|
context.__owl__.refs[ref11] = n.elm;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
c10.push({text: \`do something\`});
|
c10.push({text: \`do something\`});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Env } from "../../src/component/component";
|
import { Component, Env } from "../../src/component/component";
|
||||||
import { QWeb } from "../../src/qweb/qweb";
|
import { QWeb } from "../../src/qweb/qweb";
|
||||||
import { xml } from "../../src/tags";
|
import { xml } from "../../src/tags";
|
||||||
import { useState } from "../../src/hooks";
|
import { useState, useRef } from "../../src/hooks";
|
||||||
import { EventBus } from "../../src/core/event_bus";
|
import { EventBus } from "../../src/core/event_bus";
|
||||||
import {
|
import {
|
||||||
makeDeferred,
|
makeDeferred,
|
||||||
@@ -989,13 +989,15 @@ describe("composition", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("t-refs on widget are components", async () => {
|
test("t-refs on widget are components", async () => {
|
||||||
env.qweb.addTemplate("WidgetC", `<div>Hello<t t-ref="mywidgetb" t-component="b"/></div>`);
|
|
||||||
class WidgetC extends Widget {
|
class WidgetC extends Widget {
|
||||||
static components = { b: WidgetB };
|
static template = xml`<div>Hello<WidgetB t-ref="mywidgetb" /></div>`;
|
||||||
|
static components = { WidgetB };
|
||||||
|
widget = useRef("mywidgetb");
|
||||||
}
|
}
|
||||||
|
|
||||||
const widget = new WidgetC(env);
|
const widget = new WidgetC(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
expect(widget.refs.mywidgetb instanceof WidgetB).toBe(true);
|
expect(widget.widget.comp).toBeInstanceOf(WidgetB);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-refs are bound at proper timing", async () => {
|
test("t-refs are bound at proper timing", async () => {
|
||||||
@@ -1008,11 +1010,12 @@ describe("composition", () => {
|
|||||||
`;
|
`;
|
||||||
static components = { Widget };
|
static components = { Widget };
|
||||||
state = useState({ list: <any>[] });
|
state = useState({ list: <any>[] });
|
||||||
|
child = useRef("child");
|
||||||
willPatch() {
|
willPatch() {
|
||||||
expect(this.refs.child).toBeUndefined();
|
expect(this.child.comp).toBeNull();
|
||||||
}
|
}
|
||||||
patched() {
|
patched() {
|
||||||
expect(this.refs.child).not.toBeUndefined();
|
expect(this.child.comp).not.toBeNull();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1035,29 +1038,31 @@ describe("composition", () => {
|
|||||||
class ParentWidget extends Widget {
|
class ParentWidget extends Widget {
|
||||||
static components = { Widget };
|
static components = { Widget };
|
||||||
state = useState({ child1: true, child2: false });
|
state = useState({ child1: true, child2: false });
|
||||||
|
child1 = useRef("child1");
|
||||||
|
child2 = useRef("child2");
|
||||||
count = 0;
|
count = 0;
|
||||||
mounted() {
|
mounted() {
|
||||||
expect(this.refs.child1).toBeDefined();
|
expect(this.child1.comp).toBeDefined();
|
||||||
expect(this.refs.child2).toBeUndefined();
|
expect(this.child2.comp).toBeNull();
|
||||||
}
|
}
|
||||||
willPatch() {
|
willPatch() {
|
||||||
if (this.count === 0) {
|
if (this.count === 0) {
|
||||||
expect(this.refs.child1).toBeDefined();
|
expect(this.child1.comp).toBeDefined();
|
||||||
expect(this.refs.child2).toBeUndefined();
|
expect(this.child2.comp).toBeNull();
|
||||||
}
|
}
|
||||||
if (this.count === 1) {
|
if (this.count === 1) {
|
||||||
expect(this.refs.child1).toBeDefined();
|
expect(this.child1.comp).toBeDefined();
|
||||||
expect(this.refs.child2).toBeDefined();
|
expect(this.child2.comp).toBeDefined();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
patched() {
|
patched() {
|
||||||
if (this.count === 0) {
|
if (this.count === 0) {
|
||||||
expect(this.refs.child1).toBeDefined();
|
expect(this.child1.comp).toBeDefined();
|
||||||
expect(this.refs.child2).toBeDefined();
|
expect(this.child2.comp).toBeDefined();
|
||||||
}
|
}
|
||||||
if (this.count === 1) {
|
if (this.count === 1) {
|
||||||
expect(this.refs.child1).toBeUndefined();
|
expect(this.child1.comp).toBeNull();
|
||||||
expect(this.refs.child2).toBeDefined();
|
expect(this.child2.comp).toBeDefined();
|
||||||
}
|
}
|
||||||
this.count++;
|
this.count++;
|
||||||
}
|
}
|
||||||
@@ -1095,12 +1100,19 @@ describe("composition", () => {
|
|||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
class ParentWidget extends Widget {
|
class ParentWidget extends Widget {
|
||||||
state = useState({ items: [1, 2, 3] });
|
|
||||||
static components = { Child: Widget };
|
static components = { Child: Widget };
|
||||||
|
elem1 = useRef("1");
|
||||||
|
elem2 = useRef("2");
|
||||||
|
elem3 = useRef("3");
|
||||||
|
elem4 = useRef("4");
|
||||||
|
state = useState({ items: [1, 2, 3] });
|
||||||
}
|
}
|
||||||
const parent = new ParentWidget(env);
|
const parent = new ParentWidget(env);
|
||||||
await parent.mount(fixture);
|
await parent.mount(fixture);
|
||||||
expect(Object.keys(parent.refs)).toEqual(["1", "2", "3"]);
|
expect(parent.elem1.comp).toBeDefined();
|
||||||
|
expect(parent.elem2.comp).toBeDefined();
|
||||||
|
expect(parent.elem3.comp).toBeDefined();
|
||||||
|
expect(parent.elem4.comp).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parent's elm for a children === children's elm, even after rerender", async () => {
|
test("parent's elm for a children === children's elm, even after rerender", async () => {
|
||||||
@@ -1217,36 +1229,37 @@ describe("composition", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("sub widget with t-ref and t-keepalive", async () => {
|
test("sub widget with t-ref and t-keepalive", async () => {
|
||||||
env.qweb.addTemplates(`
|
class ChildWidget extends Widget {
|
||||||
<templates>
|
static template = xml`<span>Hello</span>`;
|
||||||
<div t-name="ParentWidget">
|
}
|
||||||
|
class ParentWidget extends Widget {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
<t t-if="state.ok"><ChildWidget t-ref="child" t-keepalive="1"/></t>
|
<t t-if="state.ok"><ChildWidget t-ref="child" t-keepalive="1"/></t>
|
||||||
</div>
|
</div>
|
||||||
<span t-name="ChildWidget">Hello</span>
|
`;
|
||||||
</templates>`);
|
|
||||||
class ChildWidget extends Widget {}
|
|
||||||
class ParentWidget extends Widget {
|
|
||||||
state = useState({ ok: true });
|
|
||||||
static components = { ChildWidget };
|
static components = { ChildWidget };
|
||||||
|
state = useState({ ok: true });
|
||||||
|
child = useRef("child");
|
||||||
}
|
}
|
||||||
const widget = new ParentWidget(env);
|
const widget = new ParentWidget(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
let child = children(widget)[0];
|
let child = children(widget)[0];
|
||||||
|
|
||||||
expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>");
|
||||||
expect(widget.refs.child).toEqual(child);
|
expect(widget.child.comp).toEqual(child);
|
||||||
|
|
||||||
widget.state.ok = false;
|
widget.state.ok = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(fixture.innerHTML).toBe("<div></div>");
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
expect(widget.refs.child).toEqual(child);
|
expect(widget.child.comp).toEqual(child);
|
||||||
|
|
||||||
widget.state.ok = true;
|
widget.state.ok = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>");
|
||||||
expect(widget.refs.child).toEqual(child);
|
expect(widget.child.comp).toEqual(child);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sub components rendered in a loop", async () => {
|
test("sub components rendered in a loop", async () => {
|
||||||
@@ -1582,6 +1595,7 @@ describe("class and style attributes with t-component", () => {
|
|||||||
class ParentWidget extends Widget {
|
class ParentWidget extends Widget {
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
state = useState({ b: true });
|
state = useState({ b: true });
|
||||||
|
child = useRef("child");
|
||||||
}
|
}
|
||||||
const widget = new ParentWidget(env);
|
const widget = new ParentWidget(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
@@ -1593,7 +1607,7 @@ describe("class and style attributes with t-component", () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
expect(span.className).toBe("c d a");
|
expect(span.className).toBe("c d a");
|
||||||
|
|
||||||
(<any>widget.refs.child).state.d = false;
|
(widget.child.comp as Child).state.d = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(span.className).toBe("c a");
|
expect(span.className).toBe("c a");
|
||||||
|
|
||||||
@@ -1601,7 +1615,7 @@ describe("class and style attributes with t-component", () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
expect(span.className).toBe("c a b");
|
expect(span.className).toBe("c a b");
|
||||||
|
|
||||||
(<any>widget.refs.child).state.d = true;
|
(widget.child.comp as Child).state.d = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(span.className).toBe("c a b d");
|
expect(span.className).toBe("c a b d");
|
||||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||||
@@ -1623,6 +1637,7 @@ describe("class and style attributes with t-component", () => {
|
|||||||
class ParentWidget extends Widget {
|
class ParentWidget extends Widget {
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
state = useState({ b: true });
|
state = useState({ b: true });
|
||||||
|
child = useRef("child");
|
||||||
}
|
}
|
||||||
const widget = new ParentWidget(env);
|
const widget = new ParentWidget(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
@@ -1634,7 +1649,7 @@ describe("class and style attributes with t-component", () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
expect(span.className).toBe("c d a");
|
expect(span.className).toBe("c d a");
|
||||||
|
|
||||||
(<any>widget.refs.child).state.d = false;
|
(widget.child.comp as Child).state.d = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(span.className).toBe("c a");
|
expect(span.className).toBe("c a");
|
||||||
|
|
||||||
@@ -1642,7 +1657,7 @@ describe("class and style attributes with t-component", () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
expect(span.className).toBe("c a b");
|
expect(span.className).toBe("c a b");
|
||||||
|
|
||||||
(<any>widget.refs.child).state.d = true;
|
(widget.child.comp as Child).state.d = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(span.className).toBe("c a b d");
|
expect(span.className).toBe("c a b d");
|
||||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||||
@@ -3000,12 +3015,13 @@ describe("t-mounted directive", () => {
|
|||||||
test("combined with a t-ref", async () => {
|
test("combined with a t-ref", async () => {
|
||||||
env.qweb.addTemplate("TestWidget", `<div><input t-ref="input" t-mounted="f"/></div>`);
|
env.qweb.addTemplate("TestWidget", `<div><input t-ref="input" t-mounted="f"/></div>`);
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
|
input = useRef("input");
|
||||||
f() {}
|
f() {}
|
||||||
}
|
}
|
||||||
const widget = new TestWidget(env);
|
const widget = new TestWidget(env);
|
||||||
widget.f = jest.fn();
|
widget.f = jest.fn();
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
expect(widget.refs.input).toBeDefined();
|
expect(widget.input.el).toBeDefined();
|
||||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3233,21 +3249,21 @@ describe("t-slot directive", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("refs are properly bound in slots", async () => {
|
test("refs are properly bound in slots", async () => {
|
||||||
env.qweb.addTemplates(`
|
class Dialog extends Widget {
|
||||||
<templates>
|
static template = xml`<span><t t-slot="footer"/></span>`;
|
||||||
<div t-name="Parent">
|
}
|
||||||
|
class Parent extends Widget {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
<span class="counter"><t t-esc="state.val"/></span>
|
<span class="counter"><t t-esc="state.val"/></span>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
|
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
<span t-name="Dialog"><t t-slot="footer"/></span>
|
`;
|
||||||
</templates>
|
|
||||||
`);
|
|
||||||
class Dialog extends Widget {}
|
|
||||||
class Parent extends Widget {
|
|
||||||
static components = { Dialog };
|
static components = { Dialog };
|
||||||
state = useState({ val: 0 });
|
state = useState({ val: 0 });
|
||||||
|
button = useRef("myButton");
|
||||||
doSomething() {
|
doSomething() {
|
||||||
this.state.val++;
|
this.state.val++;
|
||||||
}
|
}
|
||||||
@@ -3259,7 +3275,7 @@ describe("t-slot directive", () => {
|
|||||||
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
|
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
|
||||||
);
|
);
|
||||||
|
|
||||||
(<any>parent.refs.myButton).click();
|
parent.button.el!.click();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
|
|||||||
+19
-1
@@ -1,6 +1,6 @@
|
|||||||
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||||
import { Component, Env } from "../src/component/component";
|
import { Component, Env } from "../src/component/component";
|
||||||
import { useState, onMounted, onWillUnmount } from "../src/hooks";
|
import { useState, onMounted, onWillUnmount, useRef } from "../src/hooks";
|
||||||
import { xml } from "../src/tags";
|
import { xml } from "../src/tags";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -129,4 +129,22 @@ describe("hooks", () => {
|
|||||||
"hook:willunmount1"
|
"hook:willunmount1"
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("useRef hook", async () => {
|
||||||
|
class Counter extends Component<any, any> {
|
||||||
|
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
|
||||||
|
button = useRef("button");
|
||||||
|
value = 0;
|
||||||
|
increment() {
|
||||||
|
this.value++;
|
||||||
|
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const counter = new Counter(env);
|
||||||
|
await counter.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
|
||||||
|
counter.increment();
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1915,6 +1915,7 @@ exports[`t-raw variable 1`] = `
|
|||||||
exports[`t-ref can get a dynamic ref on a node 1`] = `
|
exports[`t-ref can get a dynamic ref on a node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
context.__owl__.refs = context.__owl__.refs || {};
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
@@ -1925,7 +1926,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
|
|||||||
const ref3 = \`myspan\${context['id']}\`;
|
const ref3 = \`myspan\${context['id']}\`;
|
||||||
p2.hook = {
|
p2.hook = {
|
||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.refs[ref3] = n.elm;
|
context.__owl__.refs[ref3] = n.elm;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -1935,6 +1936,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
|
|||||||
exports[`t-ref can get a ref on a node 1`] = `
|
exports[`t-ref can get a ref on a node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
context.__owl__.refs = context.__owl__.refs || {};
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
@@ -1945,7 +1947,7 @@ exports[`t-ref can get a ref on a node 1`] = `
|
|||||||
const ref3 = \`myspan\`;
|
const ref3 = \`myspan\`;
|
||||||
p2.hook = {
|
p2.hook = {
|
||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.refs[ref3] = n.elm;
|
context.__owl__.refs[ref3] = n.elm;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -1955,6 +1957,7 @@ exports[`t-ref can get a ref on a node 1`] = `
|
|||||||
exports[`t-ref refs in a loop 1`] = `
|
exports[`t-ref refs in a loop 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
context.__owl__.refs = context.__owl__.refs || {};
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
@@ -1981,7 +1984,7 @@ exports[`t-ref refs in a loop 1`] = `
|
|||||||
const ref6 = (context['item']);
|
const ref6 = (context['item']);
|
||||||
p5.hook = {
|
p5.hook = {
|
||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.refs[ref6] = n.elm;
|
context.__owl__.refs[ref6] = n.elm;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
var _7 = context['item'];
|
var _7 = context['item'];
|
||||||
|
|||||||
@@ -1092,14 +1092,14 @@ describe("t-ref", () => {
|
|||||||
test("can get a ref on a node", () => {
|
test("can get a ref on a node", () => {
|
||||||
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
||||||
let refs: any = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs });
|
renderToDOM(qweb, "test", { __owl__: { refs } });
|
||||||
expect(refs.myspan.tagName).toBe("SPAN");
|
expect(refs.myspan.tagName).toBe("SPAN");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can get a dynamic ref on a node", () => {
|
test("can get a dynamic ref on a node", () => {
|
||||||
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
||||||
let refs: any = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs, id: 3 });
|
renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
|
||||||
expect(refs.myspan3.tagName).toBe("SPAN");
|
expect(refs.myspan3.tagName).toBe("SPAN");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1114,7 +1114,7 @@ describe("t-ref", () => {
|
|||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
let refs: any = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs, items: [1, 2, 3] });
|
renderToDOM(qweb, "test", { items: [1, 2, 3], __owl__: { refs } });
|
||||||
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
|
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1333,5 +1333,5 @@ describe("properly support svg", () => {
|
|||||||
expect(renderToString(qweb, "test")).toBe(
|
expect(renderToString(qweb, "test")).toBe(
|
||||||
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
|
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||||
|
|
||||||
const useState = owl.hooks.useState;
|
const { useState, useRef } = owl.hooks;
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Likes Counter Widget
|
// Likes Counter Widget
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -34,6 +34,7 @@ class Message extends owl.Component {
|
|||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
static components = { Message };
|
static components = { Message };
|
||||||
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
|
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
|
||||||
|
logRef = useRef("log");
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||||
@@ -130,12 +131,12 @@ class App extends owl.Component {
|
|||||||
div.classList.add("bold");
|
div.classList.add("bold");
|
||||||
}
|
}
|
||||||
div.textContent = `> ${str}`;
|
div.textContent = `> ${str}`;
|
||||||
this.refs.log.appendChild(div);
|
this.logRef.el.appendChild(div);
|
||||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearLog() {
|
clearLog() {
|
||||||
this.refs.log.innerHTML = "";
|
this.logRef.el.innerHTML = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SAMPLES } from "./samples.js";
|
import { SAMPLES } from "./samples.js";
|
||||||
const useState = owl.hooks.useState;
|
const {useState, useRef} = owl.hooks;
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Constants, helpers, utils
|
// Constants, helpers, utils
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -163,11 +163,11 @@ class TabbedEditor extends owl.Component {
|
|||||||
|
|
||||||
this.sessions = {};
|
this.sessions = {};
|
||||||
this._setupSessions(props);
|
this._setupSessions(props);
|
||||||
this.editor = null;
|
this.editorNode = useRef("editor");
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.editor = this.editor || ace.edit(this.refs.editor);
|
this.editor = this.editor || ace.edit(this.editorNode.el);
|
||||||
|
|
||||||
this.editor.setValue(this.props[this.state.currentTab], -1);
|
this.editor.setValue(this.props[this.state.currentTab], -1);
|
||||||
this.editor.setFontSize("12px");
|
this.editor.setFontSize("12px");
|
||||||
@@ -264,13 +264,14 @@ class App extends owl.Component {
|
|||||||
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
|
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
|
||||||
this.runCode = owl.utils.debounce(this.runCode, 250, true);
|
this.runCode = owl.utils.debounce(this.runCode, 250, true);
|
||||||
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
|
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
|
||||||
|
this.content = useRef("content");
|
||||||
}
|
}
|
||||||
|
|
||||||
displayError(error) {
|
displayError(error) {
|
||||||
this.state.error = error;
|
this.state.error = error;
|
||||||
if (error) {
|
if (error) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refs.content.innerHTML = "";
|
this.content.el.innerHTML = "";
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -296,8 +297,8 @@ class App extends owl.Component {
|
|||||||
} else {
|
} else {
|
||||||
this.state.error = false;
|
this.state.error = false;
|
||||||
}
|
}
|
||||||
this.refs.content.innerHTML = "";
|
this.content.el.innerHTML = "";
|
||||||
this.refs.content.appendChild(subiframe);
|
this.content.el.appendChild(subiframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSample(ev) {
|
setSample(ev) {
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
|
|||||||
// In this implementation, we use the owl Store class to manage the state. It
|
// In this implementation, we use the owl Store class to manage the state. It
|
||||||
// is very similar to the VueX store.
|
// is very similar to the VueX store.
|
||||||
const { Component, useState } = owl;
|
const { Component, useState } = owl;
|
||||||
|
const { useRef } = owl.hooks;
|
||||||
|
|
||||||
const ENTER_KEY = 13;
|
const ENTER_KEY = 13;
|
||||||
const ESC_KEY = 27;
|
const ESC_KEY = 27;
|
||||||
@@ -397,6 +398,7 @@ function makeStore() {
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class TodoItem extends Component {
|
class TodoItem extends Component {
|
||||||
state = useState({ isEditing: false });
|
state = useState({ isEditing: false });
|
||||||
|
inputRef = useRef("input");
|
||||||
|
|
||||||
removeTodo() {
|
removeTodo() {
|
||||||
this.env.store.dispatch("removeTodo", this.props.id);
|
this.env.store.dispatch("removeTodo", this.props.id);
|
||||||
@@ -411,9 +413,9 @@ class TodoItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusInput() {
|
focusInput() {
|
||||||
this.refs.input.value = "";
|
this.inputRef.el.value = "";
|
||||||
this.refs.input.focus();
|
this.inputRef.el.focus();
|
||||||
this.refs.input.value = this.props.title;
|
this.inputRef.el.value = this.props.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyup(ev) {
|
handleKeyup(ev) {
|
||||||
@@ -1380,6 +1382,7 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
|
|||||||
// - better heuristic for initial window position
|
// - better heuristic for initial window position
|
||||||
// - ...
|
// - ...
|
||||||
const { Component, useState } = owl;
|
const { Component, useState } = owl;
|
||||||
|
const { useRef } = owl.hooks;
|
||||||
|
|
||||||
class HelloWorld extends Component {}
|
class HelloWorld extends Component {}
|
||||||
|
|
||||||
@@ -1392,14 +1395,17 @@ class Counter extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Window extends Component {
|
class Window extends Component {
|
||||||
|
|
||||||
get style() {
|
get style() {
|
||||||
let { width, height, top, left, zindex } = this.props.info;
|
let { width, height, top, left, zindex } = this.props.info;
|
||||||
|
|
||||||
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
|
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.trigger("close-window", { id: this.props.info.id });
|
this.trigger("close-window", { id: this.props.info.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
startDragAndDrop(ev) {
|
startDragAndDrop(ev) {
|
||||||
this.updateZIndex();
|
this.updateZIndex();
|
||||||
this.el.classList.add('dragging');
|
this.el.classList.add('dragging');
|
||||||
@@ -1425,6 +1431,7 @@ class Window extends Component {
|
|||||||
self.trigger("set-window-position", options);
|
self.trigger("set-window-position", options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateZIndex() {
|
updateZIndex() {
|
||||||
this.trigger("update-z-index", { id: this.props.info.id });
|
this.trigger("update-z-index", { id: this.props.info.id });
|
||||||
}
|
}
|
||||||
@@ -1435,20 +1442,26 @@ class WindowManager extends Component {
|
|||||||
windows = [];
|
windows = [];
|
||||||
nextId = 1;
|
nextId = 1;
|
||||||
currentZindex = 1;
|
currentZindex = 1;
|
||||||
|
nextLeft = 0;
|
||||||
|
nextTop = 0;
|
||||||
|
|
||||||
addWindow(name) {
|
addWindow(name) {
|
||||||
const info = this.env.windows.find(w => w.name === name);
|
const info = this.env.windows.find(w => w.name === name);
|
||||||
|
this.nextLeft = this.nextLeft + 30;
|
||||||
|
this.nextTop = this.nextTop + 30;
|
||||||
this.windows.push({
|
this.windows.push({
|
||||||
id: this.nextId++,
|
id: this.nextId++,
|
||||||
title: info.title,
|
title: info.title,
|
||||||
width: info.defaultWidth,
|
width: info.defaultWidth,
|
||||||
height: info.defaultHeight,
|
height: info.defaultHeight,
|
||||||
top: 0,
|
top: this.nextTop,
|
||||||
left: 0,
|
left: this.nextLeft,
|
||||||
zindex: this.currentZindex++,
|
zindex: this.currentZindex++,
|
||||||
component: info.component
|
component: info.component
|
||||||
});
|
});
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeWindow(ev) {
|
closeWindow(ev) {
|
||||||
const id = ev.detail.id;
|
const id = ev.detail.id;
|
||||||
delete this.constructor.components[id];
|
delete this.constructor.components[id];
|
||||||
@@ -1456,12 +1469,14 @@ class WindowManager extends Component {
|
|||||||
this.windows.splice(index, 1);
|
this.windows.splice(index, 1);
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
setWindowPosition(ev) {
|
setWindowPosition(ev) {
|
||||||
const id = ev.detail.id;
|
const id = ev.detail.id;
|
||||||
const w = this.windows.find(w => w.id === id);
|
const w = this.windows.find(w => w.id === id);
|
||||||
w.top = ev.detail.top;
|
w.top = ev.detail.top;
|
||||||
w.left = ev.detail.left;
|
w.left = ev.detail.left;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateZIndex(ev) {
|
updateZIndex(ev) {
|
||||||
const id = ev.detail.id;
|
const id = ev.detail.id;
|
||||||
const w = this.windows.find(w => w.id === id);
|
const w = this.windows.find(w => w.id === id);
|
||||||
@@ -1472,9 +1487,10 @@ class WindowManager extends Component {
|
|||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
static components = { WindowManager };
|
static components = { WindowManager };
|
||||||
|
wmRef = useRef("wm");
|
||||||
|
|
||||||
addWindow(name) {
|
addWindow(name) {
|
||||||
this.refs.wm.addWindow(name);
|
this.wmRef.comp.addWindow(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user