[IMP] component: add current key for easier hook creation

closes #319
This commit is contained in:
Géry Debongnie
2019-10-13 08:33:51 +02:00
committed by aab-odoo
parent 3e8f60cefa
commit 1337aa0107
6 changed files with 85 additions and 10 deletions
+5
View File
@@ -162,6 +162,11 @@ constructor.
} }
``` ```
There is another static property defined on the `Component` class: `current`.
This property is set to the currently being defined component (in the constructor).
This is the way [hooks](hooks.md) are able to get a reference to the target
component.
### Methods ### Methods
We explain here all the public methods of the `Component` class. We explain here all the public methods of the `Component` class.
+70
View File
@@ -15,6 +15,7 @@
- [`useContext`](#usecontext) - [`useContext`](#usecontext)
- [`useRef`](#useref) - [`useRef`](#useref)
- [`useSubEnv`](#usesubenv) - [`useSubEnv`](#usesubenv)
- [Making customized hooks](#making-customized-hooks)
## Overview ## Overview
@@ -146,6 +147,10 @@ class SomeComponent extends Component {
} }
``` ```
Hooks can get a reference to the currently being defined component with the
`Component.current` static property. This is why they need to be called in the
constructor.
### `useState` ### `useState`
The `useState` hook is certainly the most important hooks for Owl components: The `useState` hook is certainly the most important hooks for Owl components:
@@ -277,3 +282,68 @@ The `useSubEnv` takes one argument: an object which contains some key/value that
will be added to the parent environment. Note that it will extend, not replace will be added to the parent environment. Note that it will extend, not replace
the parent environment. And of course, the parent environment will not be the parent environment. And of course, the parent environment will not be
affected. affected.
### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature
instead of by lifecycle methods. They are like mixins, except that they can be
easily composed together.
But, like every good things in life, hooks should be used with moderation. They are
not the solution to every problem.
- they may be overkill: if your component needs to perform some action specific
to him (so, the specific code does not need to be shared), there is nothing
wrong with a simple class method:
```js
// maybe overkill
class A extends Component {
constructor(...args) {
super(...args);
useMySpecificHook()
}
}
// ok
class B extends Component {
constructor(...args) {
super(...args);
this.performSpecificTask()
}
}
```
Note that the second solution is easier to extend in sub components.
- they may be harder to test: if a customized hook inject some external side
effect dependency, then it is harder to test without doing some non obvious
manipulation. For example, assume that we want to give a reference to a
router in a `useRouter` hook. We could do this:
```js
const router = new Router(...);
function useRouter() {
return router;
}
```
As you can see, this does not *hook* into the internal of the component. It
simply returns a global object, which is difficult to mock.
A better way would be to do something like this: get the reference from the
environment.
```js
function useRouter() {
return Component.current.env.router;
}
```
This means that we give control to the application developer to create the
router, which is good, so they can set it up, subclass it, ... And then, to
test our components, we can just add a mock router in the environment.
Note: the code above makes use of the `Component.current` property. This is the
way hooks are able to get a reference to the component currently being created.
+1 -1
View File
@@ -59,7 +59,7 @@ export class Context extends EventBus {
* to context state changes. The `useContext` method returns the context state * to context state changes. The `useContext` method returns the context state
*/ */
export function useContext(ctx: Context): any { export function useContext(ctx: Context): any {
const component: Component<any, any> = Component._current; const component: Component<any, any> = Component.current!;
const __owl__ = component.__owl__; const __owl__ = component.__owl__;
const id = __owl__.id; const id = __owl__.id;
const mapping = ctx.mapping; const mapping = ctx.mapping;
+2 -2
View File
@@ -101,7 +101,7 @@ export class Component<T extends Env, Props extends {}> {
readonly __owl__: Internal<Env, Props>; readonly __owl__: Internal<Env, Props>;
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: Component<any,any> | null = null;
static components = {}; static components = {};
static props?: any; static props?: any;
static defaultProps?: any; static defaultProps?: any;
@@ -142,7 +142,7 @@ export class Component<T extends Env, Props extends {}> {
*/ */
constructor(parent: Component<T, any> | T, props?: Props) { constructor(parent: Component<T, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps; const defaultProps = (<any>this.constructor).defaultProps;
Component._current = this; Component.current = this;
if (defaultProps) { if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps); props = this.__applyDefaultProps(props, defaultProps);
} }
+6 -6
View File
@@ -22,7 +22,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();
@@ -37,7 +37,7 @@ export function useState<T>(state: T): T {
function makeLifecycleHook(method: string, reverse: boolean = false) { function makeLifecycleHook(method: string, reverse: boolean = false) {
if (reverse) { if (reverse) {
return function(cb) { return function(cb) {
const component: Component<any, any> = Component._current; const component: Component<any, any> = Component.current!;
if (component.__owl__[method]) { if (component.__owl__[method]) {
const current = component.__owl__[method]; const current = component.__owl__[method];
component.__owl__[method] = function() { component.__owl__[method] = function() {
@@ -50,7 +50,7 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
}; };
} else { } else {
return function(cb) { return function(cb) {
const component: Component<any, any> = Component._current; const component: Component<any, any> = Component.current!;
if (component.__owl__[method]) { if (component.__owl__[method]) {
const current = component.__owl__[method]; const current = component.__owl__[method];
component.__owl__[method] = function() { component.__owl__[method] = function() {
@@ -66,7 +66,7 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
function makeAsyncHook(method: string) { function makeAsyncHook(method: string) {
return function(cb) { return function(cb) {
const component: Component<any, any> = Component._current; const component: Component<any, any> = Component.current!;
if (component.__owl__[method]) { if (component.__owl__[method]) {
const current = component.__owl__[method]; const current = component.__owl__[method];
component.__owl__[method] = function(...args) { component.__owl__[method] = function(...args) {
@@ -100,7 +100,7 @@ interface Ref {
} }
export function useRef(name: string): Ref { export function useRef(name: string): Ref {
const __owl__ = Component._current.__owl__; const __owl__ = Component.current!.__owl__;
return { return {
get el(): HTMLElement | null { get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name]; const val = __owl__.refs && __owl__.refs[name];
@@ -123,6 +123,6 @@ export function useRef(name: string): Ref {
* constructor method. * constructor method.
*/ */
export function useSubEnv(nextEnv) { export function useSubEnv(nextEnv) {
const component = Component._current; const component = Component.current!;
component.env = Object.assign(Object.create(component.env), nextEnv); component.env = Object.assign(Object.create(component.env), nextEnv);
} }
+1 -1
View File
@@ -179,7 +179,7 @@ function deleteLocalSample() {
function useSamples() { function useSamples() {
const samples = loadSamples(); const samples = loadSamples();
const component = owl.Component._current; const component = owl.Component.current;
let interval; let interval;
onMounted(() => { onMounted(() => {