From 19a47a7001a90ca6fd6a1a668b7107e5af4c74bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 30 Oct 2020 09:33:49 +0100 Subject: [PATCH] [IMP] component: add setup lifecycle hook --- doc/reference/component.md | 20 +++++++++- doc/reference/hooks.md | 9 ++++- src/component/component.ts | 12 ++++++ tests/component/component.test.ts | 15 +++----- tools/playground/samples.js | 63 +++++++++++-------------------- 5 files changed, 67 insertions(+), 52 deletions(-) diff --git a/doc/reference/component.md b/doc/reference/component.md index 30973007..9e4c32b0 100644 --- a/doc/reference/component.md +++ b/doc/reference/component.md @@ -11,6 +11,7 @@ - [Methods](#methods) - [Lifecycle](#lifecycle) - [`constructor(parent, props)`](#constructorparent-props) + - [`setup()`](#setup) - [`willStart()`](#willstart) - [`mounted()`](#mounted) - [`willUpdateProps(nextProps)`](#willupdatepropsnextprops) @@ -324,7 +325,7 @@ a owl component: | Method | Description | | ------------------------------------------------ | ----------------------------------------------------------- | -| **[constructor](#constructorparent-props)** | constructor | +| **[setup](#setup)** | setup | | **[willStart](#willstart)** | async, before first rendering | | **[mounted](#mounted)** | just after component is rendered and added to the DOM | | **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update | @@ -369,6 +370,23 @@ class ClickCounter extends owl.Component { } ``` +Hook functions can be called in the constructor. + +#### `setup()` + +_setup_ is run just after the component is constructed. It is a lifecycle method, +very similar to the _constructor_, except that it does not receive any argument. + +It is a valid method to call hook functions. Note that one of the main reason to +have the `setup` hook in the component lifecycle is to make it possible to +monkey patch it. It is a common need in the Odoo ecosystem. + +```javascript +setup() { + useSetupAutofocus(); +} +``` + #### `willStart()` willStart is an asynchronous hook that can be implemented to diff --git a/doc/reference/hooks.md b/doc/reference/hooks.md index b60600e1..c27fd56b 100644 --- a/doc/reference/hooks.md +++ b/doc/reference/hooks.md @@ -131,7 +131,7 @@ class SomeComponent extends Component { ### One rule There is only one rule: every hook for a component has to be called in the -constructor (or in class fields): +constructor, in the _setup_ method, or in class fields: ```js // ok @@ -147,6 +147,13 @@ class SomeComponent extends Component { } } +// also ok +class SomeComponent extends Component { + setup() { + this.state = useState({ value: 0 }); + } +} + // not ok: this is executed after the constructor is called class SomeComponent extends Component { async willStart() { diff --git a/src/component/component.ts b/src/component/component.ts index b7e68cda..24cb1a35 100644 --- a/src/component/component.ts +++ b/src/component/component.ts @@ -209,8 +209,20 @@ export class Component { if (constr.style) { this.__applyStyles(constr); } + this.setup(); } + /** + * setup is run just after the component is constructed. This is the standard + * location where the component can setup its hooks. It has some advantages + * over the constructor: + * - it can be patched (useful in odoo ecosystem) + * - it does not need to propagate the arguments to the super call + * + * Note: this method should not be called manually. + */ + setup() {} + /** * willStart is an asynchronous hook that can be implemented to perform some * action before the initial rendering of a component. diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index 33a03420..af2ade97 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -661,9 +661,8 @@ describe("lifecycle hooks", () => { class ChildWidget extends Component { static template = xml`
`; - constructor(parent) { - super(parent); - steps.push("init"); + setup() { + steps.push("setup"); } async willStart() { steps.push("willstart"); @@ -687,10 +686,10 @@ describe("lifecycle hooks", () => { const widget = new ParentWidget(); await widget.mount(fixture); - expect(steps).toEqual(["init", "willstart", "mounted"]); + expect(steps).toEqual(["setup", "willstart", "mounted"]); widget.state.ok = false; await nextTick(); - expect(steps).toEqual(["init", "willstart", "mounted", "willunmount"]); + expect(steps).toEqual(["setup", "willstart", "mounted", "willunmount"]); }); test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => { @@ -738,8 +737,7 @@ describe("lifecycle hooks", () => { class ChildWidget extends Component { static template = xml`
`; - constructor(parent) { - super(parent); + setup() { steps.push("c init"); } async willStart() { @@ -755,8 +753,7 @@ describe("lifecycle hooks", () => { class ParentWidget extends Component { static template = xml`
`; static components = { child: ChildWidget }; - constructor(parent?) { - super(parent); + setup() { steps.push("p init"); } async willStart() { diff --git a/tools/playground/samples.js b/tools/playground/samples.js index b4cda13d..962229e6 100644 --- a/tools/playground/samples.js +++ b/tools/playground/samples.js @@ -2,8 +2,7 @@ const COMPONENTS = `// In this example, we show how components can be defined an const { Component, useState, mount } = owl; class Greeter extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ word: 'Hello' }); } @@ -14,10 +13,9 @@ class Greeter extends Component { // Main root component class App extends Component { - constructor() { - super(...arguments); - this.state = useState({ name: 'World'}); - } + setup() { + this.state = useState({ name: 'World'}); + } } App.components = { Greeter }; @@ -52,8 +50,7 @@ const ANIMATION = `// The goal of this component is to see how the t-transition const { Component, useState, mount } = owl; class Counter extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ value: 0 }); } @@ -63,8 +60,7 @@ class Counter extends Component { } class App extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ flag: false, componentFlag: false, numbers: [] }); } @@ -194,10 +190,9 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks const { Component, useState, mount } = owl; class DemoComponent extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ n: 0 }); - console.log("constructor"); + console.log("setup"); } async willStart() { console.log("willstart"); @@ -223,8 +218,7 @@ class DemoComponent extends Component { } class App extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ n: 0, flag: true }); } @@ -295,8 +289,7 @@ function useMouse() { // Main root component class App extends owl.Component { - constructor() { - super(...arguments); + setup() { // simple state hook (reactive object) this.counter = useState({ value: 0 }); @@ -333,8 +326,7 @@ const { Component, Context, mount } = owl; const { useContext } = owl.hooks; class ToolbarButton extends Component { - constructor() { - super(...arguments); + setup() { this.theme = useContext(this.env.themeContext); } @@ -468,8 +460,7 @@ const actions = { // TodoItem //------------------------------------------------------------------------------ class TodoItem extends Component { - constructor() { - super(...arguments); + setup() { useAutofocus("input"); this.state = useState({ isEditing: false }); this.dispatch = useDispatch(); @@ -499,8 +490,7 @@ class TodoItem extends Component { // TodoApp //------------------------------------------------------------------------------ class TodoApp extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ filter: "all" }); this.todos = useStore(state => state.todos); this.dispatch = useDispatch(); @@ -1026,8 +1016,7 @@ class FormView extends owl.Component {} FormView.components = { AdvancedComponent }; class Chatter extends owl.Component { - constructor() { - super(...arguments); + setup() { this.messages = Array.from(Array(100).keys()); } } @@ -1170,8 +1159,7 @@ const SLOTS = `// We show here how slots can be used to create generic component const { Component, useState, mount } = owl; class Card extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ showContent: true }); } @@ -1181,8 +1169,7 @@ class Card extends Component { } class Counter extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({val: 1}); } @@ -1193,8 +1180,7 @@ class Counter extends Component { // Main root component class App extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({a: 1, b: 3}); } @@ -1306,8 +1292,7 @@ class SlowComponent extends Component { class NotificationList extends Component {} class App extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ value: 0, notifs: [] }); } @@ -1381,8 +1366,7 @@ const FORM = `// This example illustrate how the t-model directive can be used t const { Component, useState, mount } = owl; class Form extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ text: "", othertext: "", @@ -1556,8 +1540,7 @@ const { useRef } = owl.hooks; class HelloWorld extends Component {} class Counter extends Component { - constructor() { - super(...arguments); + setup() { this.state = useState({ value: 0 }); } @@ -1610,8 +1593,7 @@ class Window extends Component { } class WindowManager extends Component { - constructor() { - super(...arguments); + setup() { this.windows = []; this.nextId = 1; this.currentZindex = 1; @@ -1661,8 +1643,7 @@ class WindowManager extends Component { WindowManager.components = { Window }; class App extends Component { - constructor() { - super(...arguments); + setup() { this.wmRef = useRef("wm"); }