[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
+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>`
);
});
});
});