Compare commits

..

6 Commits

Author SHA1 Message Date
Simon Genin (ges) 9e2202c02a [IMP] devtools: add __owl__ access.
To be able to make devtools, we need access some inner state of a
component. We create a __owl_devtools__ variable on components.

Just like in Vue js, we attach it to the HTMLElement of a component.
It's how we give it to the "outside world", through the DOM.

For its update, we use the onMounted and onPatched hook.
2020-11-10 17:22:37 +01:00
Stephen Mugisha c8a27aa1a3 Fix minor grammatical errors
Correct minor grammatical mistakes in the `why_owl.md` file
2020-11-02 16:36:16 +01:00
Simon Genin 323cb61d2c [DOC] minor fix - missing this.
Proposed by @obayit with https://github.com/odoo/owl/pull/689
2020-11-02 16:33:02 +01:00
glovebx ac9cc91701 typo correct 2020-11-02 16:03:10 +01:00
Géry Debongnie d83cfc12ee [IMP] qweb: the t-slot directive is now dynamic
It is useful for some kind of components to be able to use a completely
dynamic slot expression.
2020-10-30 15:31:03 +01:00
Géry Debongnie cb07c99d40 [IMP] owl: add a new mount method 2020-10-30 15:31:03 +01:00
26 changed files with 318 additions and 148 deletions
+2 -3
View File
@@ -41,7 +41,7 @@ find some more additional information [here](doc/miscellaneous/comparison.md).
Here is a short example to illustrate interactive components:
```javascript
const { Component, useState } = owl;
const { Component, useState, mount } = owl;
const { xml } = owl.tags;
class Counter extends Component {
@@ -63,8 +63,7 @@ class App extends Component {
static components = { Counter };
}
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
```
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
+1 -2
View File
@@ -85,8 +85,7 @@ afterEach(() => {
describe("SomeComponent", () => {
test("component behaves as expected", async () => {
const props = {...}; // depends on the component
const comp = new SomeComponent(null, props);
await comp.mount(fixture);
const comp = await mount(SomeComponent, { target: fixture, props });
// do some assertions
expect(...).toBe(...);
+1 -1
View File
@@ -96,7 +96,7 @@ class OrderLine extends Component {
</div>`;
add() {
this.trigger("add-to-order", { line: props.line });
this.trigger("add-to-order", { line: this.props.line });
}
}
+8 -11
View File
@@ -54,7 +54,7 @@ Now, `index.html` should contain the following:
And `app.js` should look like this:
```js
const { Component } = owl;
const { Component, mount } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
@@ -65,8 +65,7 @@ class App extends Component {
// Setup code
function setup() {
const app = new App();
app.mount(document.body);
mount(App, target: { document.body })
}
whenReady(setup);
@@ -124,7 +123,7 @@ Here is the content of `app.js` and `main.js`:
```js
// app.js ----------------------------------------------------------------------
const { Component } = owl;
const { Component, mount } = owl;
const { xml } = owl.tags;
export class App extends Component {
@@ -135,8 +134,7 @@ export class App extends Component {
import { App } from "./app.js";
function setup() {
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
}
owl.utils.whenReady(setup);
@@ -240,12 +238,11 @@ export class App extends Component {
}
// src/main.js -----------------------------------------------------------------
import { utils } from "@odoo/owl";
import { utils, mount } from "@odoo/owl";
import { App } from "./components/App";
function setup() {
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
}
utils.whenReady(setup);
@@ -253,6 +250,7 @@ utils.whenReady(setup);
// tests/components/App.test.js ------------------------------------------------
import { App } from "../../src/components/App";
import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture;
@@ -266,8 +264,7 @@ afterEach(() => {
describe("App", () => {
test("Works as expected...", async () => {
const app = new App();
await app.mount(fixture);
await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div");
+8 -13
View File
@@ -83,7 +83,7 @@ a single root component. Let us start by defining an `App` component. Replace th
content of the function in `app.js` by the following code:
```js
const { Component } = owl;
const { Component, mount } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
@@ -94,8 +94,7 @@ class App extends Component {
// Setup code
function setup() {
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
}
whenReady(setup);
@@ -279,8 +278,7 @@ class App extends Component {
// -------------------------------------------------------------------------
function setup() {
owl.config.mode = "dev";
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
}
whenReady(setup);
@@ -639,8 +637,7 @@ function setup() {
owl.config.mode = "dev";
const store = new Store({ actions, state: initialState });
App.env.store = store;
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
}
whenReady(setup);
@@ -666,9 +663,8 @@ function makeStore() {
function setup() {
owl.config.mode = "dev";
App.env.store = makeStore();
const app = new App();
app.mount(document.body);
const env = {store = makeStore()};
mount(App, { target: document.body, env });
}
```
@@ -943,9 +939,8 @@ For reference, here is the final code:
function setup() {
owl.config.mode = "dev";
App.env.store = makeStore();
const app = new App();
app.mount(document.body);
const env = {store = makeStore()};
mount(App, { target: document.body, env });
}
whenReady(setup);
+12 -12
View File
@@ -61,14 +61,14 @@ because a lot of the state is hidden in their internals.
React or Vue have a huge community, and a lot of effort have been made into their
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
since the assets are totally dynamic (and could change whenever the user install
or remove an addon), we need to have all that kind of tooling on the production
since the assets are totally dynamic (and could change whenever the user installs
or removes an addon), we need to have all that kind of tooling on the production
servers. This is certainly not ideal.
Also, this makes it very complicated to setup Vue or React tools: Odoo code is
not a simple file that import other files. It changes all the time, assets
are bundled differently in different contexts. This is the reason why Odoo has
its own module system, which are resolve at runtime, by the browser. The
its own module system, which are resolved at runtime, by the browser. The
dynamic nature of Odoo means that we often need to delay work as late as possible
(in other word, we want a JIT user interface!)
@@ -78,12 +78,12 @@ deploy. Using React without JSX, or Vue without vue file is not very appealing.
At the same time, Owl is designed to solve this issue: it compiles templates
by the browser, it doesn't need much code for that, since we use the XML parser
built into each browser. Owl works with or without any additional tooling. It
can use template strings to write single file component, and is easy to integrate
can use template strings to write single file components, and is easy to integrate
in any html page, with a simple `<script>` tag.
## Template based
Odoo stores template as XML document in a database. This is very powerful, since
Odoo stores templates as XML documents in a database. This is very powerful, since
this allow the use of xpaths to customize other templates. This is a very
important feature of odoo, and one of the key to Odoo modularity.
@@ -104,12 +104,12 @@ awkward, and very confusing.
## Developer Experience
This brings us to the following point: developer experience. We see this choice
as an investment for the future, and we want to make onboarding developer as
as an investment for the future, and we want to make onboarding developers as
easy as possible.
While many javascript professionals clearly think that react/vue is not difficult
(which is true to some extent), it is alsy true that many non js specialists are
overwhelmed with the frontend world: functional component, hooks, and many other
overwhelmed with the frontend world: functional components, hooks, and many other
fancy words. Also, what is available in the compilation context may be difficult,
there is a lot of black magic going on in pretty much every framework. Vue
somehow join various namespaces into one, under the hood, and add various internal
@@ -135,7 +135,7 @@ needs: Odoo will fetch templates from the database and need to compile them only
at the last possible moment, so we can apply all necessary xpaths.
Even more: Odoo needs to be able to generate (and compile) templates at runtime.
Currently, Odoo form views interpret a xml description. But the form view code
Currently, Odoo form views interpret an xml description. But the form view code
then needs to do a lot of complicated operations. With Owl, we will be able to
transform a view description into a QWeb template, then compile that and use it
immediately.
@@ -147,16 +147,16 @@ For example, the reactivity system. We like the way Vue did it, but it has a
flaw: it is not really optional. There is actually a way to opt out of the reactivity
system by freezing the state, but then, it is freezed.
And there certainly are situations where we need a state, which is not readonly,
And there certainly are situations where we need a state, which is not read-only,
and not observed. For example, imagine a spreadsheet component. It may have a
very large internal state, and it knows exactly when it needs to be rendered
(basically, whenever the user perform some action). Then, observing its state
(basically, whenever the user performs some action). Then, observing its state
is a net performance loss, both for the CPU and the memory.
## Concurrency
Many applications are happy to simply display a spinner whenever a new asynchronous
action is performed, but Odoo want a different user experience: most asynchronous
action is performed, but Odoo wants a different user experience: most asynchronous
state changes are not displayed until ready. This is sometimes called a concurrent
mode: the UI is rendered in memory, and displayed only when it is ready (and
only if it has not been cancelled by subsequent user actions).
@@ -175,6 +175,6 @@ that current standard frameworks are not tailored to our needs. It is perfectly
fine, because they each chose a different set of tradeoffs.
However, we feel that there is still room in the framework world for something
that is different. For a framework that make choices compatible with Odoo.
that is different. For a framework that makes choices compatible with Odoo.
And that is why we built Owl 🦉.
+1
View File
@@ -27,6 +27,7 @@ provided by Owl.
- [Event Handling](reference/event_handling.md)
- [Error Handling](reference/error_handling.md)
- [Hooks](reference/hooks.md)
- [Mounting a component](reference/mounting.md)
- [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md)
- [Props](reference/props.md)
+4
View File
@@ -270,6 +270,10 @@ We explain here all the public methods of the `Component` class.
// app is now visible
```
Note that the normal way of mounting an application is by using the `mount`
method on a component class, not by creating the instance by hand. See the
documentation on [mounting applications](mounting.md).
* **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system.
+5 -4
View File
@@ -10,10 +10,11 @@ exported as `owl.core.EventBus`.
Component misc
Context AsyncRoot
QWeb Portal
Store router
useState Link
config RouteComponent
mode Router
mount router
Store Link
useState RouteComponent
config Router
mode
core tags
EventBus css
Observer xml
+4 -6
View File
@@ -49,15 +49,14 @@ The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created:
```js
App.env = {
const env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
const app = new App();
app.mount(document.body);
mount(App, { target: document.body, env });
```
It is also possible to simply share an environment between all root components,
@@ -121,9 +120,8 @@ async function myEnv() {
}
async function start() {
App.env = await myEnv();
const app = new App();
await app.mount(document.body);
const env = await myEnv();
mount(App, { target: document.body, env });
}
```
+2 -3
View File
@@ -43,7 +43,7 @@ workflow to help the user put in some data, which it could use later on.
JavaScript:
```js
const { Component } = owl;
const { Component, mount } = owl;
const { Portal } = owl.misc;
class TeleportedComponent extends Component {}
@@ -51,8 +51,7 @@ class App extends Component {
static components = { Portal, TeleportedComponent };
}
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
```
XML:
+60
View File
@@ -0,0 +1,60 @@
# 🦉 Mounting an application 🦉
## Content
- [Overview](#overview)
- [API](#api)
## Overview
Mounting an Owl application is done by using the `mount` method (available in
`owl.mount` if you are using the iife build, or it can be directly imported
from `owl` if you are using a module system):
```js
const mount = { owl }; // if owl is available as an object
const env = { ... };
const app = await mount(MyComponent, { target: document.body, env });
```
Another example:
```js
const config = {
env: ...,
props: ...,
target: document.body,
position: "self",
};
const app = await mount(App, config);
```
A common way to initialize an application is to first setup an environment,
then to call the `mount` method.
## API
Mount takes two parameters:
- `C`, which should be a component class (NOT instance),
- `params`, which is an object with the following keys:
- `target (HTMLElement | DocumentFragment)`: the target of the mount operation
- `env (optional, Env)` an environment
- `position (optional, "first-child" | "last-child" | "self")` the position
where it should be mounted (see below for more informations)
- `props (optional, any)`: some initial values that are given as props. Useful
when the root component is configurable, or when testing sub components
Here are the various positions supported by Owl:
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
The `mount` method returns a promise that resolves to the instance of the created
component.
+1 -1
View File
@@ -18,7 +18,7 @@ class Child extends Component {
}
class Parent extends Component {
static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`;
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
+17 -2
View File
@@ -25,6 +25,8 @@ some sub template, but still be the owner. For example, a generic dialog compone
will need to render some content, some footer, but with the parent as the
rendering context.
Slots are inserted with the `t-slot` directive:
```xml
<div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div>
@@ -62,7 +64,9 @@ This is deprecated and should no longer be used in new code.
## Reference
Default slot: the first element inside the component which is not a named slot will
### Default Slot
The first element inside the component which is not a named slot will
be considered the `default` slot. For example:
```xml
@@ -77,7 +81,9 @@ be considered the `default` slot. For example:
</div>
```
Default content: slots can define a default content, in case the parent did not define them:
### Default content
Slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
@@ -94,3 +100,12 @@ Rendering context: the content of the slots is actually rendered with the
rendering context corresponding to where it was defined, not where it is
positioned. This allows the user to define event handlers that will be bound
to the correct component (usually, the grandparent of the slot content).
### Dynamic Slots
The `t-slot` directive is actually able to use any expressions, using string
interplolation:
```xml
<t t-slot="{{current}}" />
```
+4 -4
View File
@@ -21,8 +21,8 @@ argument, it executes it as soon as the DOM ready (or directly).
```js
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
const qweb = new owl.QWeb({ templates });
const app = new App({ qweb });
app.mount(document.body);
const env = { qweb };
await mount(App, { env, target: document.body });
});
```
@@ -31,8 +31,8 @@ or alternatively:
```js
owl.utils.whenReady(function () {
const qweb = new owl.QWeb();
const app = new App({ qweb });
app.mount(document.body);
const env = { qweb };
await mount(App, { env, target: document.body });
});
```
+2 -2
View File
@@ -13,8 +13,8 @@
"node": ">=10.15.3"
},
"scripts": {
"build:bundle": "rollup -c",
"build": "npm run build:bundle",
"dev": "rollup -c",
"build": "NODE_ENV=production rollup -c",
"test": "jest",
"test:watch": "jest --watch",
"tools:serve": "python3 tools/server.py || python tools/server.py",
+69 -1
View File
@@ -8,6 +8,7 @@ import "./props_validation";
import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles";
import { Browser, browser } from "../browser";
import { onMounted, onPatched } from "../hooks";
/**
* Owl Component System
@@ -91,8 +92,24 @@ interface Internal<T extends Env> {
refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null;
}
interface DevToolsAccess {
props?: any;
defaultProps?: any;
template?: string | null;
state: Observer;
tag: String;
depth: number,
}
export const portalSymbol = Symbol("portal"); // FIXME
/**
* It is required for the dev tools to have access to the __owl__ element.
*/
interface HTMLElementWithDevToolsAccess extends HTMLElement {
__owl_devtools__: DevToolsAccess
}
//------------------------------------------------------------------------------
// Component
//------------------------------------------------------------------------------
@@ -110,11 +127,13 @@ export class Component<Props extends {} = any, T extends Env = Env> {
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
__devtools__: DevToolsAccess;
/**
* The `el` is the root element of the component. Note that it could be null:
* this is the case if the component is not mounted yet, or is destroyed.
*/
get el(): HTMLElement | null {
get el(): HTMLElementWithDevToolsAccess | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
@@ -208,6 +227,24 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (constr.style) {
this.__applyStyles(constr);
}
// DevTools hooks
onMounted(() => {
this.__devtools__ = {
depth: this.__owl__.depth,
state: this.__owl__.observer,
tag: this.constructor.name
};
this.__devtools__.defaultProps = defaultProps;
this.__devtools__.props = this.props;
this.__devtools__.template = template;
this.el.__owl_devtools__ = this.__devtools__;
})
onPatched(() => {
this.__devtools__.depth = this.__owl__.depth,
this.el.__owl_devtools__ = this.__devtools__;
})
}
/**
@@ -729,3 +766,34 @@ export class Component<Props extends {} = any, T extends Env = Env> {
}
}
}
interface MountParameters {
env?: Env;
target: HTMLElement | DocumentFragment;
props?: any;
position?: MountOptions["position"];
}
interface Type<T> extends Function {
new (...args: any[]): T;
}
export async function mount<T extends Type<Component>>(
C: T,
params: MountParameters
): Promise<InstanceType<T>> {
const { env, props, target } = params;
let origEnv = C.hasOwnProperty("env") ? (C as any).env : null;
if (env) {
((C as any) as typeof Component).env = env;
}
const component: Component = new C(null, props);
if (origEnv) {
(C as any).env = origEnv;
} else {
delete (C as any).env;
}
const position = params.position || "last-child";
await component.mount(target, { position });
return component as any;
}
-1
View File
@@ -21,7 +21,6 @@ export class Observer {
rev: number = 1;
allowMutations: boolean = true;
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
observe<T>(value: T, parent?: any): T {
+2 -1
View File
@@ -19,7 +19,7 @@ import { Link } from "./router/link";
import { RouteComponent } from "./router/route_component";
import { Router } from "./router/router";
export { Component } from "./component/component";
export { Component, mount } from "./component/component";
export { QWeb };
export { config };
@@ -37,4 +37,5 @@ export const hooks = Object.assign({}, _hooks, {
useGetters: _store.useGetters,
useStore: _store.useStore,
});
export const __info__ = {};
+3 -1
View File
@@ -1,4 +1,5 @@
import { VNode } from "../vdom/index";
import { INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb";
/**
@@ -228,8 +229,9 @@ QWeb.addDirective({
priority: 80,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID();
const valueExpr = value.match(INTERP_REGEXP) ? ctx.interpolate(value) : `'${value}'`;
ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + ${valueExpr}];`
);
ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`;
@@ -220,6 +220,25 @@ exports[`t-slot directive default slot work with text nodes 1`] = `
}"
`;
exports[`t-slot directive dynamic t-slot call 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c10 = [], p10 = {key:10,on:{}};
let vn10 = h('button', p10, c10);
extra.handlers['click__11__'] = extra.handlers['click__11__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['toggle'](e);};
p10.on['click'] = extra.handlers['click__11__'];
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
}
return vn10;
}"
`;
exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra
) {
+19 -20
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../../src/component/component";
import { Component, Env, mount } from "../../src/component/component";
import { EventBus } from "../../src/core/event_bus";
import { useRef, useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb/qweb";
@@ -69,18 +69,23 @@ describe("basic widget properties", () => {
class SomeWidget extends Component {
static template = xml`<div>content</div>`;
}
const widget = new SomeWidget();
widget.mount(fixture);
await nextTick();
await mount(SomeWidget, { target: fixture });
expect(fixture.innerHTML).toBe("<div>content</div>");
});
test("can be mounted with props", async () => {
class SomeWidget extends Component {
static template = xml`<div><t t-esc="props.content"/></div>`;
}
await mount(SomeWidget, { target: fixture, props: { content: "foo" } });
expect(fixture.innerHTML).toBe("<div>foo</div>");
});
test("can be mounted on a documentFragment", async () => {
class SomeWidget extends Component {
static template = xml`<div>content</div>`;
}
const widget = new SomeWidget();
await widget.mount(document.createDocumentFragment());
const widget = await mount(SomeWidget, { target: document.createDocumentFragment() });
expect(fixture.innerHTML).toBe("");
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>content</div>");
@@ -90,10 +95,9 @@ describe("basic widget properties", () => {
class SomeWidget extends Component {
static template = xml`<div>content</div>`;
}
const widget = new SomeWidget();
let error;
try {
await widget.mount(null as any);
await mount(SomeWidget, { target: null as any });
} catch (e) {
error = e;
}
@@ -107,10 +111,9 @@ describe("basic widget properties", () => {
class SomeWidget extends Component {
static template = xml`<t/>`;
}
const widget = new SomeWidget();
let error;
try {
await widget.mount(fixture);
await mount(SomeWidget, { target: fixture });
} catch (e) {
error = e;
}
@@ -137,8 +140,7 @@ describe("basic widget properties", () => {
});
}
const counter = new Counter();
counter.mount(fixture);
const counter = await mount(Counter, { target: fixture });
await nextTick();
expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
@@ -156,8 +158,7 @@ describe("basic widget properties", () => {
static components = { Child };
}
const parent = new Parent();
await parent.mount(fixture);
await mount(Parent, { target: fixture });
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><span></span></div>");
});
@@ -171,9 +172,8 @@ describe("basic widget properties", () => {
});
}
const counter = new Counter();
const target = document.createElement("div");
await counter.mount(target);
const counter = await mount(Counter, { target: target });
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
button.click();
@@ -188,8 +188,7 @@ describe("basic widget properties", () => {
<div style="font-weight:bold;" class="some-class">world</div>
`;
}
const widget = new StyledWidget();
await widget.mount(fixture);
await mount(StyledWidget, { target: fixture });
expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`);
});
@@ -212,8 +211,8 @@ describe("basic widget properties", () => {
steps.push("patched");
}
}
const widget = new TestW();
await widget.mount(fixture);
await mount(TestW, { target: fixture });
expect(steps).toEqual(["__render", "mounted"]);
});
+34 -1
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../../src/component/component";
import { Component, Env, mount } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
@@ -1085,4 +1085,37 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>");
});
test("dynamic t-slot call", async () => {
class Toggler extends Component {
static template = xml`<button t-on-click="toggle"><t t-slot="{{current.slot}}"/></button>`;
current = useState({ slot: "slot1" });
toggle() {
this.current.slot = this.current.slot === "slot1" ? "slot2" : "slot1";
}
}
class Parent extends Component {
static template = xml`
<div>
<Toggler>
<t t-set-slot="slot1"><p>slot1</p><span>content</span></t>
<t t-set-slot="slot2"><h1>slot2</h1></t>
</Toggler>
</div>`;
static components = { Toggler };
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div><button><p>slot1</p><span>content</span></button></div>");
fixture.querySelector<HTMLElement>("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button><h1>slot2</h1></button></div>");
fixture.querySelector<HTMLElement>("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button><p>slot1</p><span>content</span></button></div>");
expect(env.qweb.templates[Toggler.template].fn.toString()).toMatchSnapshot();
});
});
+9 -17
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../../src/component/component";
import { Component, Env, mount } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers";
@@ -37,8 +37,7 @@ describe("mount targets", () => {
div.innerHTML = `<p>pre-existing</p>`;
fixture.appendChild(div);
const app = new App();
await app.mount(div, { position: "self" });
const app = await mount(App, { target: div, position: "self" });
expect(fixture.innerHTML).toBe(
`<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
@@ -66,10 +65,9 @@ describe("mount targets", () => {
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
let error;
try {
await app.mount(div, { position: "self" });
await mount(App, { target: div, position: "self" });
} catch (e) {
error = e;
}
@@ -84,8 +82,7 @@ describe("mount targets", () => {
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "first-child" });
await mount(App, { target: fixture, position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
});
@@ -96,8 +93,7 @@ describe("mount targets", () => {
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "last-child" });
await mount(App, { target: fixture, position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
@@ -108,8 +104,7 @@ describe("mount targets", () => {
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture);
await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
});
@@ -133,8 +128,7 @@ describe("unmounting and remounting", () => {
}
}
const w = new MyWidget();
await w.mount(fixture);
const w = await mount(MyWidget, { target: fixture });
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
@@ -162,8 +156,7 @@ describe("unmounting and remounting", () => {
}
}
const w = new MyWidget();
await w.mount(fixture);
const w = await mount(MyWidget, { target: fixture });
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
@@ -203,8 +196,7 @@ describe("unmounting and remounting", () => {
state = useState({ val: 1, flag: true });
}
const widget = new Parent();
await widget.mount(fixture);
const widget = await mount(Parent, { target: fixture });
expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
widget.state.flag = false;
+4 -4
View File
@@ -1,5 +1,6 @@
import { SAMPLES } from "./samples.js";
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
const { mount, hooks } = owl;
const { useState, useRef, onMounted, onWillUnmount } = hooks;
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
@@ -419,9 +420,8 @@ async function start() {
owl.utils.whenReady()
]);
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
const app = new App();
app.mount(document.body);
const env = { qweb };
await mount(App, {target: document.body, env});
}
start();
+27 -38
View File
@@ -1,5 +1,5 @@
const COMPONENTS = `// In this example, we show how components can be defined and created.
const { Component, useState } = owl;
const { Component, useState, mount } = owl;
class Greeter extends Component {
constructor() {
@@ -22,8 +22,7 @@ class App extends Component {
App.components = { Greeter };
// Application setup
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
`;
const COMPONENTS_XML = `<templates>
@@ -50,7 +49,7 @@ const COMPONENTS_CSS = `.greeter {
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
// used to generate simple transition effects.
const { Component, useState } = owl;
const { Component, useState, mount } = owl;
class Counter extends Component {
constructor() {
@@ -80,8 +79,7 @@ class App extends Component {
}
App.components = { Counter };
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
`;
const ANIMATION_XML = `<templates>
@@ -193,7 +191,7 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
// methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what
// happens.
const { Component, useState } = owl;
const { Component, useState, mount } = owl;
class DemoComponent extends Component {
constructor() {
@@ -240,8 +238,7 @@ class App extends Component {
}
App.components = { DemoComponent };
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
`;
const LIFECYCLE_DEMO_XML = `<templates>
@@ -273,7 +270,8 @@ const LIFECYCLE_CSS = `button {
}`;
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
const {useState, onMounted, onWillUnmount} = owl.hooks;
const { hooks, mount } = owl;
const {useState, onMounted, onWillUnmount} = hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
@@ -312,8 +310,7 @@ class App extends owl.Component {
}
// Application setup
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
`;
const HOOKS_DEMO_XML = `<templates>
@@ -332,7 +329,7 @@ const HOOKS_CSS = `button {
const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext'
// hook to share information between them.
const { Component, Context } = owl;
const { Component, Context, mount } = owl;
const { useContext } = owl.hooks;
class ToolbarButton extends Component {
@@ -367,8 +364,7 @@ const themeContext = new Context({
});
// Add the themeContext the environment to make it available to all components
App.env.themeContext = themeContext;
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
`;
const CONTEXT_XML = `<templates>
@@ -395,7 +391,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 { Component, useState, mount } = owl;
const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks;
//------------------------------------------------------------------------------
@@ -568,8 +564,7 @@ function makeStore() {
}
TodoApp.env.store = makeStore();
const app = new TodoApp();
app.mount(document.body);
mount(TodoApp, { target: document.body });
`;
const TODO_APP_STORE_XML = `<templates>
@@ -1060,8 +1055,7 @@ function setupResponsivePlugin(env) {
//------------------------------------------------------------------------------
setupResponsivePlugin(App.env);
const app = new App();
app.mount(document.body);
owl.mount(App, { target: document.body });
`;
const RESPONSIVE_XML = `<templates>
@@ -1173,7 +1167,7 @@ const SLOTS = `// We show here how slots can be used to create generic component
//
// Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component
const { Component, useState } = owl;
const { Component, useState, mount } = owl;
class Card extends Component {
constructor() {
@@ -1211,8 +1205,8 @@ class App extends Component {
App.components = {Card, Counter};
// Application setup
const app = new App();
app.mount(document.body);`;
mount(App, { target: document.body });
`;
const SLOTS_XML = `<templates>
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
@@ -1298,7 +1292,7 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
// However, we don't want renderings of the other sub component to be delayed
// because of the slow component. We use the AsyncRoot component for this
// purpose. Try removing it to see the difference.
const { Component, useState } = owl;
const { Component, useState, mount } = owl;
const { AsyncRoot } = owl.misc;
class SlowComponent extends Component {
@@ -1329,8 +1323,7 @@ class App extends Component {
}
App.components = {SlowComponent, NotificationList, AsyncRoot};
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
`;
const ASYNC_COMPONENTS_XML = `<templates>
@@ -1385,7 +1378,7 @@ const FORM = `// This example illustrate how the t-model directive can be used t
// data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally
// synchronized.
const { Component, useState } = owl;
const { Component, useState, mount } = owl;
class Form extends Component {
constructor() {
@@ -1401,8 +1394,7 @@ class Form extends Component {
}
// Application setup
const form = new Form();
form.mount(document.body);
mount(Form, { target: document.body });
`;
const FORM_XML = `<templates>
@@ -1448,7 +1440,7 @@ const PORTAL_COMPONENTS = `
// This shows the expected use case of Portal
// which is to implement something similar
// to bootstrap modal
const { Component, useState } = owl;
const { Component, useState, mount } = owl;
const { Portal } = owl.misc;
class Modal extends Component {}
@@ -1470,8 +1462,7 @@ class App extends Component {
App.components = { Dialog , Interstellar };
// Application setup
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
`;
const PORTAL_XML = `
@@ -1559,7 +1550,7 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - minimal width/height
// - better heuristic for initial window position
// - ...
const { Component, useState } = owl;
const { Component, useState, mount } = owl;
const { useRef } = owl.hooks;
class HelloWorld extends Component {}
@@ -1699,8 +1690,7 @@ const windows = [
];
App.env.windows = windows;
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
`;
const WMS_XML = `<templates>
@@ -1818,7 +1808,7 @@ const SFC = `// This example illustrates how Owl enables single file components,
// Note that this example has no external xml or css file, everything is
// contained in a single js file.
const { Component, useState, tags } = owl;
const { Component, useState, tags, mount } = owl;
const { xml, css } = tags;
// Counter component
@@ -1850,8 +1840,7 @@ App.template = APP_TEMPLATE;
App.components = { Counter };
// Application setup
const app = new App();
app.mount(document.body);
mount(App, { target: document.body });
`;
export const SAMPLES = [