mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
+6
-1
@@ -261,7 +261,12 @@ This comes from the fact that Owl 2 supports fragments (arbitrary content).
|
||||
Migration: if one need a reference to the root htmlelement of a template, it is
|
||||
suggested to simply add a `ref` on it, and access the reference as needed.
|
||||
|
||||
Documentation: [Refs](doc/reference/refs.md)
|
||||
Another way to get access to a root node or html element is the hook `useRoots`
|
||||
|
||||
Documentation:
|
||||
- [Refs](doc/reference/refs.md)
|
||||
- [useRoots](doc/reference/hooks.md#useroots)
|
||||
|
||||
|
||||
### 10. style/class on components are now regular props
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ Other hooks:
|
||||
- [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`)
|
||||
- [`useChildSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for child components)
|
||||
- [`useSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for current component and child components)
|
||||
- [`useRoots`](reference/hooks.md#useroots): returns an object that provides access to all root nodes or htmlelements
|
||||
|
||||
Utility/helpers:
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- [`useComponent`](#usecomponent)
|
||||
- [`useEnv`](#useenv)
|
||||
- [`useEffect`](#useeffect)
|
||||
- [`useRoots`](#useroots)
|
||||
- [Example: Mouse Position](#example-mouse-position)
|
||||
|
||||
## Overview
|
||||
@@ -288,6 +289,42 @@ class SomeComponent extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
### `useRoots`
|
||||
|
||||
`useRoots` is an alternative way to get a reference to the root notes or elements
|
||||
of a component. It may be useful in some cases where `useRef` cannot be applied,
|
||||
such as a higher order component, or a component with only text as content.
|
||||
|
||||
The return value of `useRoots` is an object with the following key/values:
|
||||
|
||||
- `node`: getter that evaluates to the first node of the component content (or null)
|
||||
- `elem`: getter that evaluates to the first HTMLElement of the component content (or null)
|
||||
- `nodes`: iterator that returns all content nodes
|
||||
- `elems`: iterator that returns all HTMLElement nodes
|
||||
|
||||
```js
|
||||
class Child extends Component {
|
||||
static template = xml`<p>some content</p>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child/>`;
|
||||
static components = { Child };
|
||||
|
||||
setup() {
|
||||
this.roots = useRoots();
|
||||
console.log(this.roots.elem); // null
|
||||
onMounted(() => {
|
||||
console.log(this.roots.elem); // log the `p` element from the child
|
||||
|
||||
for (let elem of this.roots.elems) {
|
||||
console.log(elem); // log the `p` element from the child
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: mouse position
|
||||
|
||||
Here is the classical example of a non trivial hook to track the mouse position.
|
||||
|
||||
@@ -517,6 +517,12 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
|
||||
return this.el!;
|
||||
}
|
||||
|
||||
*nodes() {
|
||||
if (this.el) {
|
||||
yield this.el;
|
||||
}
|
||||
}
|
||||
|
||||
moveBefore(other: Block | null, afterNode: Node | null) {
|
||||
const target = other ? other.el! : afterNode;
|
||||
nodeInsertBefore.call(this.parentEl, this.el!, target);
|
||||
|
||||
@@ -12,7 +12,6 @@ export function createCatcher(eventsSpec: EventsSpec): Catcher {
|
||||
child: VNode;
|
||||
handlerData: any[];
|
||||
handlerFns: any[] = [];
|
||||
|
||||
parentEl?: HTMLElement | undefined;
|
||||
afterNode: Text | null = null;
|
||||
|
||||
@@ -44,13 +43,10 @@ export function createCatcher(eventsSpec: EventsSpec): Catcher {
|
||||
const self = this;
|
||||
handler[idx] = function (ev: any) {
|
||||
const target = ev.target;
|
||||
let currentNode: any = self.child.firstNode();
|
||||
const afterNode = self.afterNode;
|
||||
while (currentNode !== afterNode) {
|
||||
if (currentNode.contains(target)) {
|
||||
for (let node of self.nodes()) {
|
||||
if (node.contains(target)) {
|
||||
return origFn.call(this, ev);
|
||||
}
|
||||
currentNode = currentNode.nextSibling;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -90,6 +86,10 @@ export function createCatcher(eventsSpec: EventsSpec): Catcher {
|
||||
return this.child.firstNode();
|
||||
}
|
||||
|
||||
nodes(): Generator<Node> {
|
||||
return this.child.nodes();
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.child.toString();
|
||||
}
|
||||
|
||||
@@ -78,6 +78,12 @@ class VHtml {
|
||||
return this.content[0]!;
|
||||
}
|
||||
|
||||
*nodes() {
|
||||
for (let elem of this.content) {
|
||||
yield elem;
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.html;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface VNode<T = any> {
|
||||
remove(): void;
|
||||
firstNode(): Node | undefined;
|
||||
|
||||
nodes(): Generator<Node>;
|
||||
|
||||
el?: undefined | HTMLElement | Text;
|
||||
parentEl?: undefined | HTMLElement;
|
||||
isOnlyChild?: boolean | undefined;
|
||||
|
||||
@@ -221,6 +221,12 @@ class VList {
|
||||
return child ? child.firstNode() : undefined;
|
||||
}
|
||||
|
||||
*nodes() {
|
||||
for (let child of this.children) {
|
||||
yield* child.nodes();
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.children.map((c) => c!.toString()).join("");
|
||||
}
|
||||
|
||||
@@ -124,6 +124,14 @@ export class VMulti {
|
||||
return child ? child.firstNode() : this.anchors![0];
|
||||
}
|
||||
|
||||
*nodes() {
|
||||
for (let child of this.children) {
|
||||
if (child) {
|
||||
yield* child.nodes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.children.map((c) => (c ? c!.toString() : "")).join("");
|
||||
}
|
||||
|
||||
@@ -38,6 +38,12 @@ abstract class VSimpleNode {
|
||||
return this.el!;
|
||||
}
|
||||
|
||||
*nodes(): Generator<Node> {
|
||||
if (this.el) {
|
||||
yield this.el;
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,10 @@ class VToggler {
|
||||
return this.child.firstNode();
|
||||
}
|
||||
|
||||
nodes() {
|
||||
return this.child.nodes();
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.child.toString();
|
||||
}
|
||||
|
||||
@@ -296,6 +296,12 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
return bdom ? bdom.firstNode() : undefined;
|
||||
}
|
||||
|
||||
*nodes(): Generator<Node> {
|
||||
if (this.bdom) {
|
||||
yield* this.bdom.nodes();
|
||||
}
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, anchor: ChildNode) {
|
||||
const bdom = this.fiber!.bdom!;
|
||||
this.bdom = bdom;
|
||||
|
||||
@@ -127,3 +127,39 @@ export function useExternalListener(
|
||||
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
|
||||
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useRoots
|
||||
// -----------------------------------------------------------------------------
|
||||
interface DomRangeObj {
|
||||
node: Node | null;
|
||||
elem: HTMLElement | null;
|
||||
elems: Iterable<HTMLElement>;
|
||||
nodes: Iterable<Node>;
|
||||
}
|
||||
|
||||
export function useRoots(): DomRangeObj {
|
||||
const cnode = getCurrent();
|
||||
|
||||
function* _elems(): Generator<HTMLElement> {
|
||||
for (let node of cnode.nodes()) {
|
||||
if (node.nodeType === 1) {
|
||||
yield node as HTMLElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
get node() {
|
||||
return cnode.nodes().next().value || null;
|
||||
},
|
||||
get elem() {
|
||||
return _elems().next().value || null;
|
||||
},
|
||||
get nodes() {
|
||||
return cnode.nodes();
|
||||
},
|
||||
get elems() {
|
||||
return _elems();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +40,15 @@ export type { ComponentConstructor } from "./component";
|
||||
export { useComponent, useState } from "./component_node";
|
||||
export { status } from "./status";
|
||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||
export {
|
||||
useEffect,
|
||||
useEnv,
|
||||
useExternalListener,
|
||||
useRef,
|
||||
useChildSubEnv,
|
||||
useSubEnv,
|
||||
useRoots,
|
||||
} from "./hooks";
|
||||
export { EventBus, whenReady, loadFile, markup } from "./utils";
|
||||
export {
|
||||
onWillStart,
|
||||
|
||||
@@ -405,3 +405,69 @@ exports[`hooks useSubEnv supports arbitrary descriptor 2`] = `
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`useRoots hook return a list of nodes and elems 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span>hey</span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`useRoots hook return a list of nodes and elems 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block3 = createBlock(\`<span>hey</span>\`);
|
||||
let block5 = createBlock(\`<p>bla</p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\` some text \`);
|
||||
const b3 = block3();
|
||||
const b4 = text(\` coucou \`);
|
||||
const b5 = block5();
|
||||
return multi([b2, b3, b4, b5]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`useRoots hook useRoots and lifecycle 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span>hey</span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`useRoots hook useRoots is up to date 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block2 = createBlock(\`<span>hey</span>\`);
|
||||
let block5 = createBlock(\`<p>paragraph</p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2,b3;
|
||||
if (ctx['state'].flag) {
|
||||
b2 = block2();
|
||||
} else {
|
||||
const b4 = text(\` coucou \`);
|
||||
const b5 = block5();
|
||||
b3 = multi([b4, b5]);
|
||||
}
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
xml,
|
||||
OwlError,
|
||||
} from "../../src/index";
|
||||
import { useRoots } from "../../src/runtime/hooks";
|
||||
import {
|
||||
elem,
|
||||
logStep,
|
||||
@@ -518,7 +519,7 @@ describe("hooks", () => {
|
||||
});
|
||||
|
||||
test("dependencies prevent effects from rerunning when unchanged", async () => {
|
||||
let steps = [];
|
||||
let steps: string[] = [];
|
||||
class MyComponent extends Component {
|
||||
state = useState({
|
||||
a: 0,
|
||||
@@ -673,3 +674,110 @@ describe("hooks", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("useRoots hook", () => {
|
||||
test("return a list of nodes and elems", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<span>hey</span>`;
|
||||
roots: any;
|
||||
|
||||
setup() {
|
||||
this.roots = useRoots();
|
||||
}
|
||||
}
|
||||
|
||||
const comp = await mount(MyComponent, fixture);
|
||||
|
||||
expect(comp.roots.node).toBeInstanceOf(HTMLSpanElement);
|
||||
expect(comp.roots.elem).toBeInstanceOf(HTMLSpanElement);
|
||||
expect([...comp.roots.nodes].map((n: any) => n.tagName)).toEqual(["SPAN"]);
|
||||
expect([...comp.roots.elems].map((n: any) => n.tagName)).toEqual(["SPAN"]);
|
||||
});
|
||||
|
||||
test("return a list of nodes and elems", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
some text
|
||||
<span>hey</span>
|
||||
coucou
|
||||
<p>bla</p>`;
|
||||
roots: any;
|
||||
|
||||
setup() {
|
||||
this.roots = useRoots();
|
||||
}
|
||||
}
|
||||
|
||||
const comp = await mount(MyComponent, fixture);
|
||||
|
||||
expect(comp.roots.node).toBeInstanceOf(Text);
|
||||
expect(comp.roots.elem).toBeInstanceOf(HTMLSpanElement);
|
||||
|
||||
expect([...comp.roots.elems].map((n: any) => n.tagName)).toEqual(["SPAN", "P"]);
|
||||
expect([...comp.roots.nodes].map((n: any) => n.nodeType)).toEqual([3, 1, 3, 1]);
|
||||
});
|
||||
|
||||
test("useRoots and lifecycle", async () => {
|
||||
expect.assertions(13);
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<span>hey</span>`;
|
||||
roots: any;
|
||||
|
||||
setup() {
|
||||
this.roots = useRoots();
|
||||
expect(this.roots.elem).toBe(null);
|
||||
expect(this.roots.node).toBe(null);
|
||||
expect([...this.roots.nodes]).toEqual([]);
|
||||
expect([...this.roots.elems]).toEqual([]);
|
||||
onWillStart(() => {
|
||||
expect(this.roots.elem).toBe(null);
|
||||
expect(this.roots.node).toBe(null);
|
||||
expect([...this.roots.nodes]).toEqual([]);
|
||||
expect([...this.roots.elems]).toEqual([]);
|
||||
});
|
||||
onMounted(() => {
|
||||
expect(this.roots.elem).toBeInstanceOf(HTMLSpanElement);
|
||||
expect(this.roots.node).toBeInstanceOf(HTMLSpanElement);
|
||||
expect([...this.roots.elems].map((n: any) => n.tagName)).toEqual(["SPAN"]);
|
||||
expect([...this.roots.nodes].map((n: any) => n.nodeType)).toEqual([1]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const comp = await mount(MyComponent, fixture);
|
||||
comp.__owl__.app.destroy();
|
||||
});
|
||||
|
||||
test("useRoots is up to date", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-if="state.flag">
|
||||
<span>hey</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
coucou
|
||||
<p>paragraph</p>
|
||||
</t>`;
|
||||
roots: any;
|
||||
state: any;
|
||||
|
||||
setup() {
|
||||
this.roots = useRoots();
|
||||
this.state = useState({ flag: true });
|
||||
onMounted(() => {
|
||||
expect(this.roots.elem).toBeInstanceOf(HTMLSpanElement);
|
||||
expect(this.roots.node).toBeInstanceOf(HTMLSpanElement);
|
||||
expect([...this.roots.elems].map((n: any) => n.tagName)).toEqual(["SPAN"]);
|
||||
expect([...this.roots.nodes].map((n: any) => n.nodeType)).toEqual([1]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const comp = await mount(MyComponent, fixture);
|
||||
comp.state.flag = false;
|
||||
await nextTick();
|
||||
expect(comp.roots.node.nodeType).toBe(3); // text node
|
||||
expect(comp.roots.node.textContent).toBe(" coucou "); // text node
|
||||
expect(comp.roots.elem).toBeInstanceOf(HTMLParagraphElement);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user