[IMP] hooks: implement useRef hook

closes #194
This commit is contained in:
Géry Debongnie
2019-09-26 21:56:01 +02:00
parent 91b9e78182
commit 5cdaa7b473
20 changed files with 321 additions and 171 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ const app = new App({ qweb: new QWeb() });
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
[playground](https://odoo.github.io/owl/playground) application.
+50 -39
View File
@@ -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
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
## Example
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
[observed](observer.md), and any change to it will cause a rerendering.
## Reference
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.
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
- **`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
@@ -517,24 +512,24 @@ class ParentComponent {
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
@@ -967,44 +962,60 @@ Examples:
### References
The `t-ref` directive helps a component keep reference to some inside part of it.
Like the `t-on` directive, it can work either on a DOM node, or on a component:
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. 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
<div>
<div t-ref="someDiv"/>
<SubComponent t-ref="someComponent"/>
<input t-ref="input"/>
<button t-on-click="focusInput">Click</button>
</div>
```
In this example, the component will be able to access the `div` and the component
inside the special `refs` variable:
```js
this.refs.someDiv;
this.refs.someComponent;
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
inputRef = useRef("input");
focusInput() {
this.inputRef.el.focus();
}
}
```
This is useful for various usecases: for example, integrating with an external
library that needs to render itself inside an actual DOM node. Or for calling
some method on a sub component.
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,
The `useRef` hook can also be used to get a reference to an instance of a sub
component rendered by Owl. In that case, we need to access it with the `comp`
property instead of `el`:
```xml
<div t-ref="component_{{id}}"/>
<div>
<SubComponent t-ref="sub"/>
<button t-on-click="doSomething">Click</button>
</div>
```
```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
To make generic components, it is useful to be able for a parent component to _inject_
@@ -1163,19 +1174,19 @@ env.qweb.on("error", null, function(error) {
### Functional Components
Owl does not exactly have functional components. However, there is an extremely
Owl does not exactly have functional components. However, there is an extremely
close alternative: calling sub templates.
A stateless functional component in react is usually some kind of function that
maps props to a virtual dom (often with `jsx`). So, basically, almost like a
template rendered with `props`. In Owl, this can be done by
template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object:
```js
const Welcome = xml`<h1>Hello, {props.name}</h1>`;
class MyComponent extends Component {
static template = xml`
static template = xml`
<div>
<t t-call=${Welcome}/>
<div>something</div>
@@ -1186,4 +1197,4 @@ 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.
caller component.
+86 -33
View File
@@ -5,14 +5,16 @@
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [One Rule](#one-rule)
- [`useState`](#usestate)
- [`onMounted`](#onmounted)
- [`onWillUnmount`](#onWillUnmount)
- [One Rule](#one-rule)
- [`useState`](#usestate)
- [`onMounted`](#onmounted)
- [`onWillUnmount`](#onwillunmount)
- [`useRef`](#useref)
## Overview
Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components
- help organizing code by feature in complex components
- use state in functional components, without writing a class.
@@ -22,51 +24,49 @@ 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
is clearly not true, as shown by Owl hooks).
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
Here is the classical example of a non trivial hook to track the mouse position.
```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
// position
function useMouse() {
const position = useState({x:0, y: 0});
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onWillUnmount(() => {
window.removeEventListener('mousemove', update);
});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener("mousemove", update);
});
onWillUnmount(() => {
window.removeEventListener("mousemove", update);
});
return position;
return position;
}
// Main root component
class App extends owl.Component {
static template = xml`
static template = xml`
<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();
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
Note that we use the prefix `use` for hooks, just like in React. This is just
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
## Reference
@@ -79,22 +79,22 @@ constructor (or in class fields):
```js
// ok
class SomeComponent extends Component {
state = useState();
state = useState({value: 0});
}
// also ok
class SomeComponent extends Component {
constructor(...args) {
super(...args);
this.state = useState();
}
constructor(...args) {
super(...args);
this.state = useState({value: 0});
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState();
}
async willStart() {
this.state = useState({value: 0});
}
}
```
@@ -126,11 +126,64 @@ 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
hooks. `onMounted` register 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
hooks. `onWillUnmount` register a callback, which will be called when the component
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`.
+2 -2
View File
@@ -11,10 +11,10 @@ For example, this code will display `update` in the console:
```javascript
const observer = new owl.Observer();
observer.notifyCB = () => console.log("update");
const obj = observer.observe( { a: { b: 1 } });
const obj = observer.observe({ a: { b: 1 } });
obj.a.b = 2;
```
The observer is implemented with the native `Proxy` object. Note that this
The observer is implemented with the native `Proxy` object. Note that this
means that it will not work on older browsers.
+1
View File
@@ -17,6 +17,7 @@ owl
onMounted
onWillUnmount
useState
useRef
router
Link
RouteComponent
+5 -6
View File
@@ -61,10 +61,9 @@ useful to compare various performance metrics on some tasks.
If you want to have `xml` syntax highlighting while using the `xml` helper which
helps you define inline templates, there is a VS Code addon `Comment tagged template`
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:
```js
// -----------------------------------------------------------------------------
// TEMPLATE
@@ -79,9 +78,9 @@ const TEMPLATE = xml/* xml */ `
// CODE
// -----------------------------------------------------------------------------
class MyComponent extends Component {
static template = TEMPLATE;
static components = { Sidebar, Content };
static template = TEMPLATE;
static components = { Sidebar, Content };
// rest of component...
// rest of component...
}
```
```
+6 -10
View File
@@ -85,6 +85,7 @@ interface Internal<T extends Env, Props> {
render: CompiledTemplate | null;
mountedHandlers: { [key: number]: Function };
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 _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:
@@ -105,19 +109,10 @@ export class Component<T extends Env, Props extends {}> {
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
static components = {};
env: T;
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
//--------------------------------------------------------------------------
@@ -192,7 +187,8 @@ export class Component<T extends Env, Props extends {}> {
mountedHandlers: {},
observer: null,
render: null,
classObj: null
classObj: null,
refs: null
};
}
+3 -2
View File
@@ -263,9 +263,10 @@ QWeb.addDirective({
let refExpr = "";
let refKey: string = "";
if (ref) {
ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${componentID};`;
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
}
let transitionsInsertCode = "";
if (transition) {
@@ -273,7 +274,7 @@ QWeb.addDirective({
}
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) {
finalizeComponentCode += `delete context.refs[${refKey}];`;
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
+29 -4
View File
@@ -9,9 +9,9 @@ import { Observer } from "./core/observer";
* - useState (reactive state)
* - onMounted
* - onWillUnmount
* - useRef
*/
/**
* useState hook
*
@@ -20,7 +20,7 @@ import { Observer } from "./core/observer";
* trigger a rerendering of the current component.
*/
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__;
if (!__owl__.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.
*/
export function onMounted(cb) {
const component = Component._current;
const component: Component<any, any> = Component._current;
const current = component.mounted;
component.mounted = function() {
current.call(component);
@@ -47,10 +47,35 @@ export function onMounted(cb) {
* willUnmounted. Note that the component mounted method is called last.
*/
export function onWillUnmount(cb) {
const component = Component._current;
const component: Component<any, any> = Component._current;
const current = component.willUnmount;
component.willUnmount = function() {
cb();
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;
}
};
}
+4
View File
@@ -20,6 +20,7 @@ export class Context {
shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
@@ -62,6 +63,9 @@ export class Context {
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
+2 -1
View File
@@ -79,9 +79,10 @@ QWeb.addDirective({
name: "ref",
priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineRefs = true;
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
}
});
+3 -2
View File
@@ -1,6 +1,6 @@
import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb/index";
import { useState } from "../src/hooks";
import { useState, useRef } from "../src/hooks";
import {
makeDeferred,
makeTestFixture,
@@ -150,6 +150,7 @@ describe("animations", () => {
);
class TestWidget extends Widget {
state = useState({ hide: false });
span = useRef("span");
}
const widget = new TestWidget(env);
@@ -164,7 +165,7 @@ describe("animations", () => {
});
await widget.mount(fixture);
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");
await def; // wait for the mocked repaint to be done
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 parent = context;
let owner = context;
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h;
let c1 = [], p1 = {key:1};
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);
parent.__owl__.cmap[4] = w4.__owl__.id;
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 {
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;});
@@ -359,6 +360,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
let QWeb = this.constructor;
let parent = context;
let owner = context;
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h;
let c1 = [], p1 = {key:1};
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);
parent.__owl__.cmap[4] = w4.__owl__.id;
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 {
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;});
@@ -1552,6 +1554,7 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
"function anonymous(context,extra
) {
let owner = context;
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h;
let c1 = extra.parentNode;
Object.assign(context, extra.fiber.scope);
@@ -1566,7 +1569,7 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
const ref11 = \`myButton\`;
p10.hook = {
create: (_, n) => {
context.refs[ref11] = n.elm;
context.__owl__.refs[ref11] = n.elm;
},
};
c10.push({text: \`do something\`});
+60 -44
View File
@@ -1,7 +1,7 @@
import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState } from "../../src/hooks";
import { useState, useRef } from "../../src/hooks";
import { EventBus } from "../../src/core/event_bus";
import {
makeDeferred,
@@ -989,13 +989,15 @@ describe("composition", () => {
});
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 {
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);
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 () => {
@@ -1008,11 +1010,12 @@ describe("composition", () => {
`;
static components = { Widget };
state = useState({ list: <any>[] });
child = useRef("child");
willPatch() {
expect(this.refs.child).toBeUndefined();
expect(this.child.comp).toBeNull();
}
patched() {
expect(this.refs.child).not.toBeUndefined();
expect(this.child.comp).not.toBeNull();
}
}
@@ -1035,29 +1038,31 @@ describe("composition", () => {
class ParentWidget extends Widget {
static components = { Widget };
state = useState({ child1: true, child2: false });
child1 = useRef("child1");
child2 = useRef("child2");
count = 0;
mounted() {
expect(this.refs.child1).toBeDefined();
expect(this.refs.child2).toBeUndefined();
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
willPatch() {
if (this.count === 0) {
expect(this.refs.child1).toBeDefined();
expect(this.refs.child2).toBeUndefined();
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
if (this.count === 1) {
expect(this.refs.child1).toBeDefined();
expect(this.refs.child2).toBeDefined();
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
}
patched() {
if (this.count === 0) {
expect(this.refs.child1).toBeDefined();
expect(this.refs.child2).toBeDefined();
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
if (this.count === 1) {
expect(this.refs.child1).toBeUndefined();
expect(this.refs.child2).toBeDefined();
expect(this.child1.comp).toBeNull();
expect(this.child2.comp).toBeDefined();
}
this.count++;
}
@@ -1095,12 +1100,19 @@ describe("composition", () => {
</div>`
);
class ParentWidget extends Widget {
state = useState({ items: [1, 2, 3] });
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);
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 () => {
@@ -1217,36 +1229,37 @@ describe("composition", () => {
});
test("sub widget with t-ref and t-keepalive", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<t t-if="state.ok"><ChildWidget t-ref="child" t-keepalive="1"/></t>
</div>
<span t-name="ChildWidget">Hello</span>
</templates>`);
class ChildWidget extends Widget {}
class ChildWidget extends Widget {
static template = xml`<span>Hello</span>`;
}
class ParentWidget extends Widget {
state = useState({ ok: true });
static template = xml`
<div>
<t t-if="state.ok"><ChildWidget t-ref="child" t-keepalive="1"/></t>
</div>
`;
static components = { ChildWidget };
state = useState({ ok: true });
child = useRef("child");
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
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;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(widget.refs.child).toEqual(child);
expect(widget.child.comp).toEqual(child);
widget.state.ok = true;
await nextTick();
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 () => {
@@ -1582,6 +1595,7 @@ describe("class and style attributes with t-component", () => {
class ParentWidget extends Widget {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
@@ -1593,7 +1607,7 @@ describe("class and style attributes with t-component", () => {
await nextTick();
expect(span.className).toBe("c d a");
(<any>widget.refs.child).state.d = false;
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
@@ -1601,7 +1615,7 @@ describe("class and style attributes with t-component", () => {
await nextTick();
expect(span.className).toBe("c a b");
(<any>widget.refs.child).state.d = true;
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
@@ -1623,6 +1637,7 @@ describe("class and style attributes with t-component", () => {
class ParentWidget extends Widget {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
@@ -1634,7 +1649,7 @@ describe("class and style attributes with t-component", () => {
await nextTick();
expect(span.className).toBe("c d a");
(<any>widget.refs.child).state.d = false;
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
@@ -1642,7 +1657,7 @@ describe("class and style attributes with t-component", () => {
await nextTick();
expect(span.className).toBe("c a b");
(<any>widget.refs.child).state.d = true;
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
@@ -3000,12 +3015,13 @@ describe("t-mounted directive", () => {
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.refs.input).toBeDefined();
expect(widget.input.el).toBeDefined();
expect(widget.f).toHaveBeenCalledTimes(1);
});
});
@@ -3233,21 +3249,21 @@ describe("t-slot directive", () => {
});
test("refs are properly bound in slots", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
class Dialog extends Widget {
static template = xml`<span><t t-slot="footer"/></span>`;
}
class Parent extends Widget {
static template = xml`
<div>
<span class="counter"><t t-esc="state.val"/></span>
<Dialog>
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
</Dialog>
</div>
<span t-name="Dialog"><t t-slot="footer"/></span>
</templates>
`);
class Dialog extends Widget {}
class Parent extends Widget {
`;
static components = { Dialog };
state = useState({ val: 0 });
button = useRef("myButton");
doSomething() {
this.state.val++;
}
@@ -3259,7 +3275,7 @@ describe("t-slot directive", () => {
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
);
(<any>parent.refs.myButton).click();
parent.button.el!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
+19 -1
View File
@@ -1,6 +1,6 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
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";
//------------------------------------------------------------------------------
@@ -129,4 +129,22 @@ describe("hooks", () => {
"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>");
});
});
+6 -3
View File
@@ -1915,6 +1915,7 @@ exports[`t-raw variable 1`] = `
exports[`t-ref can get a dynamic ref on a node 1`] = `
"function anonymous(context,extra
) {
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h;
let c1 = [], p1 = {key:1};
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']}\`;
p2.hook = {
create: (_, n) => {
context.refs[ref3] = n.elm;
context.__owl__.refs[ref3] = n.elm;
},
};
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`] = `
"function anonymous(context,extra
) {
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
@@ -1945,7 +1947,7 @@ exports[`t-ref can get a ref on a node 1`] = `
const ref3 = \`myspan\`;
p2.hook = {
create: (_, n) => {
context.refs[ref3] = n.elm;
context.__owl__.refs[ref3] = n.elm;
},
};
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`] = `
"function anonymous(context,extra
) {
context.__owl__.refs = context.__owl__.refs || {};
context = Object.create(context);
var h = this.h;
let c1 = [], p1 = {key:1};
@@ -1981,7 +1984,7 @@ exports[`t-ref refs in a loop 1`] = `
const ref6 = (context['item']);
p5.hook = {
create: (_, n) => {
context.refs[ref6] = n.elm;
context.__owl__.refs[ref6] = n.elm;
},
};
var _7 = context['item'];
+4 -4
View File
@@ -1092,14 +1092,14 @@ describe("t-ref", () => {
test("can get a ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
let refs: any = {};
renderToDOM(qweb, "test", { refs });
renderToDOM(qweb, "test", { __owl__: { refs } });
expect(refs.myspan.tagName).toBe("SPAN");
});
test("can get a dynamic ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
let refs: any = {};
renderToDOM(qweb, "test", { refs, id: 3 });
renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
expect(refs.myspan3.tagName).toBe("SPAN");
});
@@ -1114,7 +1114,7 @@ describe("t-ref", () => {
</div>`
);
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"]);
});
});
@@ -1333,5 +1333,5 @@ describe("properly support svg", () => {
expect(renderToString(qweb, "test")).toBe(
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
);
});
});
});
+5 -4
View File
@@ -1,6 +1,6 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const useState = owl.hooks.useState;
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
@@ -34,6 +34,7 @@ class Message extends owl.Component {
class App extends owl.Component {
static components = { Message };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
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.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
this.logRef.el.innerHTML = "";
}
}
+7 -6
View File
@@ -1,5 +1,5 @@
import { SAMPLES } from "./samples.js";
const useState = owl.hooks.useState;
const {useState, useRef} = owl.hooks;
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
@@ -163,11 +163,11 @@ class TabbedEditor extends owl.Component {
this.sessions = {};
this._setupSessions(props);
this.editor = null;
this.editorNode = useRef("editor");
}
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.setFontSize("12px");
@@ -264,13 +264,14 @@ class App extends owl.Component {
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("content");
}
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.refs.content.innerHTML = "";
this.content.el.innerHTML = "";
});
return;
}
@@ -296,8 +297,8 @@ class App extends owl.Component {
} else {
this.state.error = false;
}
this.refs.content.innerHTML = "";
this.refs.content.appendChild(subiframe);
this.content.el.innerHTML = "";
this.content.el.appendChild(subiframe);
}
setSample(ev) {
+22 -6
View File
@@ -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
// is very similar to the VueX store.
const { Component, useState } = owl;
const { useRef } = owl.hooks;
const ENTER_KEY = 13;
const ESC_KEY = 27;
@@ -397,6 +398,7 @@ function makeStore() {
//------------------------------------------------------------------------------
class TodoItem extends Component {
state = useState({ isEditing: false });
inputRef = useRef("input");
removeTodo() {
this.env.store.dispatch("removeTodo", this.props.id);
@@ -411,9 +413,9 @@ class TodoItem extends Component {
}
focusInput() {
this.refs.input.value = "";
this.refs.input.focus();
this.refs.input.value = this.props.title;
this.inputRef.el.value = "";
this.inputRef.el.focus();
this.inputRef.el.value = this.props.title;
}
handleKeyup(ev) {
@@ -1380,6 +1382,7 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - better heuristic for initial window position
// - ...
const { Component, useState } = owl;
const { useRef } = owl.hooks;
class HelloWorld extends Component {}
@@ -1392,14 +1395,17 @@ class Counter extends Component {
}
class Window extends Component {
get style() {
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}\`;
}
close() {
this.trigger("close-window", { id: this.props.info.id });
}
startDragAndDrop(ev) {
this.updateZIndex();
this.el.classList.add('dragging');
@@ -1425,6 +1431,7 @@ class Window extends Component {
self.trigger("set-window-position", options);
}
}
updateZIndex() {
this.trigger("update-z-index", { id: this.props.info.id });
}
@@ -1435,20 +1442,26 @@ class WindowManager extends Component {
windows = [];
nextId = 1;
currentZindex = 1;
nextLeft = 0;
nextTop = 0;
addWindow(name) {
const info = this.env.windows.find(w => w.name === name);
this.nextLeft = this.nextLeft + 30;
this.nextTop = this.nextTop + 30;
this.windows.push({
id: this.nextId++,
title: info.title,
width: info.defaultWidth,
height: info.defaultHeight,
top: 0,
left: 0,
top: this.nextTop,
left: this.nextLeft,
zindex: this.currentZindex++,
component: info.component
});
this.render();
}
closeWindow(ev) {
const id = ev.detail.id;
delete this.constructor.components[id];
@@ -1456,12 +1469,14 @@ class WindowManager extends Component {
this.windows.splice(index, 1);
this.render();
}
setWindowPosition(ev) {
const id = ev.detail.id;
const w = this.windows.find(w => w.id === id);
w.top = ev.detail.top;
w.left = ev.detail.left;
}
updateZIndex(ev) {
const id = ev.detail.id;
const w = this.windows.find(w => w.id === id);
@@ -1472,9 +1487,10 @@ class WindowManager extends Component {
class App extends Component {
static components = { WindowManager };
wmRef = useRef("wm");
addWindow(name) {
this.refs.wm.addWindow(name);
this.wmRef.comp.addWindow(name);
}
}