Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9f5a9aa47 | |||
| a568ca6687 | |||
| 5c324fa075 | |||
| 3945c009ed | |||
| 5b1132f01a | |||
| f014b1a33e | |||
| 3f09a953b0 | |||
| b8e7ee5ff7 | |||
| 7fdca35a74 | |||
| 489e20843c | |||
| 34b781aeed | |||
| e8b0f31da6 | |||
| d160c4a628 | |||
| 4542171a31 | |||
| 0b1c4dd4ef | |||
| f4da50d350 | |||
| f2921abda8 | |||
| 107200fd94 | |||
| 0f8c859d5f | |||
| cca8438d38 | |||
| a727347d60 | |||
| f19de73b0f | |||
| a8ffcafd23 | |||
| a2f01ef4ad | |||
| d7de25e867 | |||
| 424bc480e2 | |||
| 421f400766 | |||
| c37f3445f6 | |||
| 4cde06d685 | |||
| 7b95315b87 | |||
| 09761ddd8e | |||
| b3c6ec2b48 | |||
| b20462ada6 | |||
| 25000b36be | |||
| 144d4a253c | |||
| 9d48bda227 | |||
| c8db663869 | |||
| 247200194f | |||
| 60b8817ac0 | |||
| e008966291 | |||
| e2fedc6eff | |||
| b3fb9a35bf | |||
| 240259568e | |||
| 18fb67eaad | |||
| e946967867 | |||
| 777b2aae5e | |||
| 0831bb54e3 | |||
| 03f34a38dc | |||
| dcefd26bee | |||
| 922dab8e8f | |||
| 99740f9993 | |||
| 0b79b1c8fd | |||
| 5b0dce94cf | |||
| a1d435c5a5 | |||
| 13c3178760 | |||
| 2b565ce1b4 | |||
| cfcf2c6714 | |||
| 92174559a1 | |||
| 840348892f | |||
| b988a68b6f | |||
| 7869a1f2c6 | |||
| 9e81d14d50 | |||
| 0e4a55ba09 | |||
| 6e9b68dafa | |||
| ba7c9063c0 | |||
| 3a361876ad | |||
| 58b8572f0a | |||
| c23637e6d8 | |||
| 4a4b1fbba5 | |||
| 536d9e1762 | |||
| 6ed68372a6 | |||
| efd934d2b1 | |||
| dabc24cee3 | |||
| 7f49796a07 | |||
| 757dffefac | |||
| bdfe058279 | |||
| 345c44b952 | |||
| ea74739d46 | |||
| 93b53d8017 | |||
| c2284bc6f5 | |||
| 48650da62e | |||
| 2edd6cb8f3 | |||
| 4b0a37c542 | |||
| 50c0e30936 | |||
| e97335d7ab | |||
| 6ea40a66da | |||
| 2806cda420 | |||
| cb0b525f32 | |||
| 30d28670e8 | |||
| 7b761a2f8b | |||
| d834bb2ac4 | |||
| 6ae92fa4b3 | |||
| 3ceb118ace | |||
| fa426b7fc0 | |||
| d7403871fc | |||
| 6aeff21d9a | |||
| 429e145b68 | |||
| fb6aab8b70 | |||
| 9e285e1e0a | |||
| 63bfdfb8db | |||
| d4c4fe853e | |||
| 9163bb1e08 | |||
| a187a376a0 | |||
| 06ea6d2490 | |||
| 04334e23d7 | |||
| 4efe1ae166 | |||
| ad4673cb36 | |||
| e611c20ab4 | |||
| d60a5a414e | |||
| d47dcf6be2 | |||
| 3e70b17da1 | |||
| c718d8e6c6 | |||
| a122a94180 | |||
| ca139166ab | |||
| ae71db28e4 | |||
| 779fc6b02b | |||
| 84913593b2 | |||
| 8d4eb21536 | |||
| 4bdfaf96f3 | |||
| 233c953243 | |||
| ee5ad354ff | |||
| 4e4b85e2a9 | |||
| 6479631983 | |||
| 2a0c410014 | |||
| bb4aecf638 | |||
| a1adfd5a1b | |||
| dd4848f602 | |||
| b25f476c22 | |||
| 90cdb97b49 | |||
| 7d7568d254 | |||
| 06d207f60e | |||
| ff564421da | |||
| c3695ec8db | |||
| 6e0834ce5e | |||
| d1425c7100 | |||
| e01fe301c0 | |||
| ba2fe3ff55 | |||
| 700d574314 | |||
| ba32772a92 | |||
| e4a0277f68 | |||
| abe3748825 | |||
| ee64de8b3f | |||
| ed831320db | |||
| 5f1e1e189f | |||
| 55ef9ca116 | |||
| 642ecf0ccd | |||
| c9cc789f1d | |||
| b8649f1add | |||
| e2d98e4c26 | |||
| e6bb4ef286 | |||
| 7ac20f4fc2 |
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es2022": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"root": true,
|
||||
"rules": {
|
||||
"no-restricted-globals": ["error", "event", "self"],
|
||||
"no-const-assign": ["error"],
|
||||
"no-debugger": ["error"],
|
||||
"no-dupe-class-members": ["error"],
|
||||
"no-dupe-keys": ["error"],
|
||||
"no-dupe-args": ["error"],
|
||||
"no-dupe-else-if": ["error"],
|
||||
"no-unsafe-negation": ["error"],
|
||||
"no-duplicate-imports": ["error"],
|
||||
"valid-typeof": ["error"],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false, "caughtErrors": "all" }],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "MemberExpression[object.name='test'][property.name='only']",
|
||||
"message": "test.only(...) is forbidden",
|
||||
},
|
||||
{
|
||||
"selector": "MemberExpression[object.name='describe'][property.name='only']",
|
||||
"message": "describe.only(...) is forbidden",
|
||||
}
|
||||
],
|
||||
},
|
||||
"globals": {
|
||||
"describe": true,
|
||||
"expect": true,
|
||||
"test": true,
|
||||
"beforeEach": true,
|
||||
"beforeAll": true,
|
||||
"afterEach": true,
|
||||
"afterAll": true,
|
||||
"jest": true,
|
||||
},
|
||||
}
|
||||
@@ -5,7 +5,7 @@ name: Node.js CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ master, owl-next ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x, 22.x]
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -22,8 +22,6 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm install
|
||||
- run: npm run test
|
||||
- run: npm run check-formatting
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
|
||||
@@ -14,22 +14,21 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
#ide's
|
||||
**/.vscode/*
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
|
||||
# Extras temp file
|
||||
/tools/owl.js
|
||||
|
||||
release-notes.md
|
||||
|
||||
.rpt2_cache
|
||||
|
||||
# useful in some cases
|
||||
/temp
|
||||
|
||||
# owl-vision
|
||||
*/owl-vision/out/
|
||||
*/owl-vision/.vs/
|
||||
**/*.vsix
|
||||
!*/owl-vision/.vscode/launch.json
|
||||
!*/owl-vision/.vscode/tasks.json
|
||||
/temp
|
||||
@@ -8,16 +8,13 @@ patching the `setup` method of `Component` to auto register all the lifecycle
|
||||
methods as hooks). This will be done for the transition period, but will be
|
||||
removed after.
|
||||
|
||||
## From Owl 1.x to Owl 2.0
|
||||
|
||||
All changes are documented here in no particular order.
|
||||
## Changes
|
||||
|
||||
**Components**
|
||||
|
||||
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
|
||||
- breaking: component.el is removed ([details](#9-componentel-is-removed))
|
||||
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
||||
- new `onWillDestroy`, `onWillRender` and `onRendered` hooks ([doc](doc/reference/component.md#lifecycle))
|
||||
- new `useEffect` hook
|
||||
- new `onDestroyed`, `onWillRender` and `onRendered` hooks
|
||||
- breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed))
|
||||
- breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element))
|
||||
- breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler))
|
||||
@@ -25,77 +22,47 @@ All changes are documented here in no particular order.
|
||||
- breaking: components can no longer be unmounted/remounted ([details](#6-components-can-no-longer-be-unmountedremounted))
|
||||
- breaking: template name is no longer inferred from the class name ([details](#7-template-name-is-no-longer-inferred-from-the-class-name))
|
||||
- breaking: components no longer have a `shouldUpdate` method ([details](#8-components-no-longer-have-a-shouldupdate-method))
|
||||
- breaking: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
|
||||
- breaking: `render` method does not return a promise anymore ([details](#35-render-method-does-not-return-a-promise-anymore))
|
||||
- breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook))
|
||||
- breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed))
|
||||
- new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation))
|
||||
- breaking: prop validation system does not allow default prop on a mandatory (not optional) prop ([doc](doc/reference/props.md#props-validation))
|
||||
- breaking: rendering a component does not necessarily render child components ([details](#40-rendering-a-component-does-not-necessarily-render-child-components))
|
||||
|
||||
|
||||
|
||||
**Templates**
|
||||
|
||||
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
|
||||
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
|
||||
- breaking: `t-raw` directive has been removed (replaced by `t-out`) ([details](#38-t-raw-directive-has-been-removed-replaced-by-t-out))
|
||||
- new: add support for synthetic events ([doc](doc/reference/event_handling.md#synthetic-events))
|
||||
- breaking: component.el may be a text node, and is no longer `null` ([details](#9-componentel-may-be-a-text-node-and-is-no-longer-null))
|
||||
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
|
||||
- new: components can use the `.bind` suffix to bind function props ([doc](doc/reference/props.md#binding-function-props))
|
||||
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
|
||||
- new: an error is thrown if an handler defined in a `t-on-` directive is not a function (failed silently previously in some cases)
|
||||
- breaking: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
|
||||
- breaking: `t-on` does not work on components any more ([details](#12-t-on-does-not-work-on-components-any-more))
|
||||
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
|
||||
- new: the `this` variable in template expressions is now bound to the component
|
||||
|
||||
|
||||
**Reactivity**
|
||||
|
||||
- finer grained reactivity: owl 2 tracks change per key/component
|
||||
- finer grained reactivity: sub components can reobserve state ([doc](doc/reference/reactivity.md))
|
||||
- new: `reactive` function: create reactive state (without being linked to a component) ([doc](doc/reference/reactivity.md#reactive))
|
||||
- new: `markRaw` function: mark an object or array so that it is ignored by the reactivity system ([doc](doc/reference/reactivity.md#markraw))
|
||||
- new: `toRaw` function: given a reactive objet, return the raw (non reactive) underlying object ([doc](doc/reference/reactivity.md#toraw))
|
||||
|
||||
|
||||
**Slots**
|
||||
|
||||
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
|
||||
- slots capabilities have been improved ([doc](doc/reference/slots.md))
|
||||
- params can be give to slot content (to pass information from slot owner to slot user)
|
||||
- slots are given as a `prop` (and can be manipulated/propagated to sub components )
|
||||
- slots can define scopes (to pass information from slot user to slot owner)
|
||||
|
||||
|
||||
**Portal**
|
||||
|
||||
- Portal are now defined with `t-portal` ([details](#33-portal-are-now-defined-with-t-portal))
|
||||
- portals can now have arbitrary content (no longer restricted to one single child)
|
||||
- breaking: does no longer transfer dom events ([details](#13-portal-does-no-longer-transfer-dom-events))
|
||||
- breaking: does render as an empty text node instead of `<portal/>` ([details](#14-portal-does-render-as-an-empty-text-node-instead-of-portal))
|
||||
|
||||
**Slots**
|
||||
|
||||
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
|
||||
|
||||
**Miscellaneous**
|
||||
|
||||
- improved performance
|
||||
- much simpler code
|
||||
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
|
||||
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
||||
- finer grained reactivity: owl 2 tracks change per key/component
|
||||
- finer grained reactivity: sub components can reobserve state
|
||||
- new App class to encapsulate a root Owl component (with the config for that application)
|
||||
- new `Memo` component
|
||||
- breaking: `Context` is removed ([details](#15-context-is-removed))
|
||||
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
|
||||
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
|
||||
- new hook: `useChildSubEnv` (only applies to child components) ([details](#27-usechildsubenv-only-applies-to-child-components))
|
||||
- breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level))
|
||||
- breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes))
|
||||
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
|
||||
- breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget))
|
||||
- breaking: `Store` is removed ([details](#22-store-is-removed))
|
||||
- breaking: `Router` is removed ([details](#23-router-is-removed))
|
||||
- breaking: transition system is removed ([details](#24-transition-system-is-removed))
|
||||
- breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates))
|
||||
- breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed))
|
||||
- breaking: `useSubEnv` only applies to child components ([details](#27-usesubenv-only-applies-to-child-components))
|
||||
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
|
||||
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
|
||||
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
|
||||
- breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
|
||||
- breaking: `debounce` utility function has been removed ([details](#34-debounce-utility-function-has-been-removed))
|
||||
- breaking: `browser` object has been removed ([details](#39-browser-object-has-been-removed))
|
||||
|
||||
|
||||
## Details/Rationale/Migration
|
||||
|
||||
@@ -129,8 +96,6 @@ class MyComponent extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
Documentation: [Component Lifecycle](doc/reference/component.md#lifecycle)
|
||||
|
||||
### 2. components can no longer be mounted in a detached dom element
|
||||
|
||||
Nor document fragment.
|
||||
@@ -182,8 +147,6 @@ because real applications will need to configure the templates, the translations
|
||||
and other stuff. All complex usecases need to go through the new `App` class,
|
||||
that encapsulates the root of an owl application.
|
||||
|
||||
Documentation: [Mounting a component](doc/reference/app.md#mount-helper)
|
||||
|
||||
### 5. components can no longer be instantiated and mounted by hand
|
||||
|
||||
In Owl 1, it was possible to instantiate a component by hand:
|
||||
@@ -247,21 +210,40 @@ Rationale: `shouldUpdate` is a dangerous method to use, that may cause a lot of
|
||||
issues. Vue does not have such a mechanism (see https://github.com/vuejs/vue/issues/4255),
|
||||
because the reactivity system in Vue is smart enough to only rerender the minimal
|
||||
subset of components that is subscribed to a piece of state. Now, Owl 2 features
|
||||
a much more powerful reactivity system, so the same rationale applies: in a way,
|
||||
it's like each Owl 2 component has a `shouldUpdate` method that precisely tracks
|
||||
every value used by the component.
|
||||
a much more powerful reactivity system.
|
||||
|
||||
Migration code: remove the `shouldUpdate` methods, and it should work as well
|
||||
as before.
|
||||
Migration code: remove the `shouldUpdate` methods. Then, maybe the following
|
||||
ideas may help:
|
||||
|
||||
### 9. component.el is removed
|
||||
- try to organize the state/architecture to minimize the number of state updates
|
||||
- take advantage of the finer reactivity system. For example, if we have a list
|
||||
of items, with a component for each item, we can write this:
|
||||
|
||||
This comes from the fact that Owl 2 supports fragments (arbitrary content).
|
||||
```js
|
||||
class Item extends Component {
|
||||
setup() {
|
||||
this.item = useState(this.props.item); // and only use this, not props.item
|
||||
}
|
||||
}
|
||||
```
|
||||
Doing so will make it that each `Item` component will register itself as an
|
||||
observer of its own item, and will be the only component being rerendered when
|
||||
its item object is updated.
|
||||
- use the `Memo` component to wrap some piece of template. `Memo` memoize its
|
||||
content, and only update itself if its props are different (shallow comparison):
|
||||
|
||||
```xml
|
||||
<Memo a="state.a" b="state.b">
|
||||
<t t-esc="state.a"/>
|
||||
<t t-esc="state.b"/>
|
||||
<t t-esc="state.c"/>
|
||||
</Memo>
|
||||
```
|
||||
|
||||
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.
|
||||
### 9. component.el may be a text node, and is no longer `null`
|
||||
|
||||
Documentation: [Refs](doc/reference/refs.md)
|
||||
This comes from the fact that Owl 2 supports fragments (arbitrary content). When
|
||||
it is not defined, it was `null` in Owl 1 and is `undefined` in owl 2.
|
||||
|
||||
### 10. style/class on components are now regular props
|
||||
|
||||
@@ -304,13 +286,34 @@ compatible with the fact that a component can have a root `<div>` then later,
|
||||
change it to something else, or even a text node.
|
||||
|
||||
Migration: no real way to do the same. Owl application needs to be appended or
|
||||
prepended in something, maybe a `div`. Remember that you the root component
|
||||
can have multiple roots
|
||||
prepended in something, maybe a `div`.
|
||||
|
||||
Documentation:
|
||||
- [Fragments](doc/reference/templates.md#fragments)
|
||||
- [Mounting a component](doc/reference/app.md#mount-helper)
|
||||
### 12. `t-on` does not work on components any more
|
||||
|
||||
In owl 1, it was possible to bind an event listener on a component tag in a
|
||||
template:
|
||||
|
||||
```xml
|
||||
<SomeComponent t-on-some-event="doSomething"/>
|
||||
```
|
||||
|
||||
This does not work any more.
|
||||
|
||||
Rationale: with the support of fragments, there is no longer a canonical html
|
||||
element that we can refer. So, this makes it difficult to implement correctly
|
||||
and efficiently. Also, we noticed in practice that the event system was an issue
|
||||
in some cases, when components need to communicate before they are mounted. In
|
||||
those cases, the better solution is to directly use a callback. Also, another
|
||||
conceptual issue with this is that it kind of breaks the component encapsulation.
|
||||
The child component kind of leak its own implementation to the outside world.
|
||||
|
||||
Migration: a quick fix that may work in some cases is to simply bind the event
|
||||
handler on a parent htmlelement. A better way to do it, if possible, is to change
|
||||
the component API to accept explicitely a callback as props.
|
||||
|
||||
```xml
|
||||
<SomeComponent onSomeEvent="doSomething"/>
|
||||
```
|
||||
|
||||
### 13. Portal does no longer transfer DOM events
|
||||
|
||||
@@ -369,8 +372,6 @@ Migration: there is no proper way to get an equivalent. The closest is to get
|
||||
a reference to the root App using `this.__owl__.app`. If you need to do this,
|
||||
let us know. If this is a legitimate usecase, we may add a `useApp` hook.
|
||||
|
||||
Documentation: [Environment](doc/reference/environment.md)
|
||||
|
||||
### 17. `t-component` no longer accepts strings
|
||||
|
||||
In owl 1, we could write this:
|
||||
@@ -396,8 +397,6 @@ the implementation is slightly simpler.
|
||||
Migration: simply using `constructor.components.Coucou` instead of `Coucou` will
|
||||
do the trick.
|
||||
|
||||
Documentation: [Component](doc/reference/component.md#dynamic-sub-components)
|
||||
|
||||
### 18. most exports are exported at top level
|
||||
|
||||
Most exports are flattened: for ex, `onMounted` is in owl, not in `owl.hooks`.
|
||||
@@ -454,8 +453,6 @@ rewritten like this: `bus.addEventListener("event-type", (({detail: info}) => {.
|
||||
|
||||
Do not forget to similarly replace `bus.off(...)` by `bus.removeEventListener(...)`
|
||||
|
||||
Documentation: [EventBus](doc/reference/utils.md#eventbus)
|
||||
|
||||
### 22. `Store` is removed
|
||||
|
||||
The Store system had been abandoned in owl 2.
|
||||
@@ -517,12 +514,14 @@ either a fallback when the data is not ready, or the actual component with data
|
||||
as props. If there is no escape, and `AsyncRoot` is needed, please reach out to
|
||||
us so we can study this usecase.
|
||||
|
||||
### 27. `useChildSubEnv` (only applies to child components)
|
||||
### 27. `useSubEnv` only applies to child components
|
||||
|
||||
In Owl 1, a call to `useSubEnv` would define a new environment for the children
|
||||
AND the component. It now only defines an environment for the children.
|
||||
|
||||
Rationale: This was a subtle cause for bugs: some code had to be rrun
|
||||
before the call to `useSubEnv`, otherwise it could interfere with the sub environment.
|
||||
|
||||
In Owl, a call to `useSubEnv` would define a new environment for the children
|
||||
AND the component. It is very useful, but in some cases, one only need to update
|
||||
the children component environment. This can now be done with a new hook:
|
||||
[`useChildSubEnv`](doc/reference/hooks.md#usesubenv-and-usechildsubenv)
|
||||
|
||||
### 28. `env` is now frozen
|
||||
|
||||
@@ -537,8 +536,6 @@ components. This use case still works with `useSubEnv`.
|
||||
Migration: use `useSubEnv` instead of writing directly to the env. Also, note
|
||||
that the environment given to the App can initially contain anything.
|
||||
|
||||
Documentation: [Environment](doc/reference/environment.md)
|
||||
|
||||
### 29. `t-ref` does not work on component
|
||||
|
||||
Before, `t-ref` could be used to get a reference to a child component. It no
|
||||
@@ -579,8 +576,6 @@ adapted like this:
|
||||
<button t-on-click="() => this.someFunction(someVar)">blabla</button>
|
||||
```
|
||||
|
||||
Documentation: [Event Handling](doc/reference/event_handling.md)
|
||||
|
||||
### 31. components can now have arbitrary content
|
||||
|
||||
Before Owl 2, components had to limit themselves to one single htmlelement as
|
||||
@@ -593,8 +588,6 @@ So, the following template works for components:
|
||||
hello
|
||||
```
|
||||
|
||||
Documentation: [Fragments](doc/reference/templates.md#fragments)
|
||||
|
||||
### 32. `renderToString` on QWeb has been removed
|
||||
|
||||
Rationale: the `renderToString` function was a qweb method, which made sense because
|
||||
@@ -608,159 +601,12 @@ Also, this can easily be done in userspace, by mounting a component in a div. F
|
||||
export async function renderToString(template, context) {
|
||||
class C extends Component {
|
||||
static template = template;
|
||||
setup () {
|
||||
Object.assign(this, context);
|
||||
}
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
const app = new App(C);
|
||||
await app.mount(div);
|
||||
const component = await mount(C, div);
|
||||
const result = div.innerHTML;
|
||||
app.destroy();
|
||||
div.remove();
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
The function above works for most cases, but is asynchronous. An alternative
|
||||
function could look like this:
|
||||
|
||||
```js
|
||||
const { App, blockDom } = owl;
|
||||
const app = new App(Component); // act as a template repository
|
||||
|
||||
function renderToString(template, context = {}) {
|
||||
app.addTemplate(template, template, { allowDuplicate: true });
|
||||
const templateFn = app.getTemplate(template);
|
||||
const bdom = templateFn(context, {});
|
||||
const div = document.createElement('div')
|
||||
blockDom.mount(bdom, div);
|
||||
return div.innerHTML;
|
||||
}
|
||||
```
|
||||
|
||||
This is a synchronous function, so it will not work with components, but it should
|
||||
be useful for most simple templates.
|
||||
|
||||
Also note that these two examples do not translate their templates. To do that,
|
||||
they need to be modified to pass the proper translate function to the `App`
|
||||
configuration.
|
||||
|
||||
### 33. Portal are now defined with `t-portal`
|
||||
|
||||
Before Owl 2, one could use the `Portal` component by importing it and using it.
|
||||
Now, it is no longer available. Instead, we can simply use the `t-portal` directive:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
some content
|
||||
<span t-portal="'body'">
|
||||
portalled content
|
||||
</span>
|
||||
<div>
|
||||
```
|
||||
|
||||
Rationale: it makes it slightly simpler to use (just need the directive, instead
|
||||
of having to import and use a sub component), it makes the implementation slightly
|
||||
simpler as well. Also, it prevents subclassing the Portal component, which could
|
||||
be dangerous, since it is really doing weird stuff under the hood, and could
|
||||
easily be broken inadvertendly.
|
||||
|
||||
### 34. `debounce` utility function has been removed
|
||||
|
||||
Rationale: it did not really help that much, is available as utility function
|
||||
elsewhere, so, we decided to have a smaller footprint by focusing Owl on what
|
||||
it does best.
|
||||
|
||||
### 35. `render` method does not return a promise anymore
|
||||
|
||||
Rationale: using the `render` method directly and waiting for it to complete
|
||||
was slightly un-declarative. Also, it can be done using the lifecycle hooks
|
||||
any way.
|
||||
|
||||
Migration: if necessary, one can use the lifecycle hooks to execute code after
|
||||
the next mounted/patched operation.
|
||||
|
||||
### 36. `catchError` method is replaced by `onError` hook
|
||||
|
||||
The `catchError` method was used to provide a way to components to handle errors
|
||||
occurring during the component lifecycle. This has been replaced by a `onError`
|
||||
hook, with a similar API.
|
||||
|
||||
Rationale: `catchError` felt a little big awkward, when most of the way we
|
||||
interact with componentss is via hooks. Using hooks felt more natural and
|
||||
consistent.
|
||||
|
||||
Migration: mostly replace all `catchError` methods by `onError` hooks in the
|
||||
`setup` method.
|
||||
|
||||
Documentation: [Error Handling](doc/reference/error_handling.md)
|
||||
|
||||
|
||||
## 37. Support for inline css (`css` tag and static `style`) has been removed
|
||||
|
||||
Rationale: Owl tries to focus on what it does best, and supporting inline css
|
||||
was not a priority. It used to support some simplified scss language, but it
|
||||
was feared that it would cause more trouble than it was worth. Also, it seems
|
||||
like it can be done in userspace.
|
||||
|
||||
Migration: it seems possible to implement an equivalent solution using hooks. A
|
||||
simple implementation could look like this:
|
||||
|
||||
```js
|
||||
let cache = {};
|
||||
|
||||
function useStyle(css) {
|
||||
if (!css in cache) {
|
||||
const sheet = document.createElement("style");
|
||||
sheet.innerHTML = css;
|
||||
cache[css] = sheet;
|
||||
document.head.appendChild(sheet);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 38. `t-raw` directive has been removed (replaced by `t-out`)
|
||||
|
||||
To match the Odoo qweb server implementation, Owl does no longer implement `t-raw`.
|
||||
It is replaced by the `t-out` directive, which is safer: it requires the data
|
||||
to be marked explicitely as markup if it is to be inserted without escaping.
|
||||
Otherwise, it will be escaped (just like `t-esc`).
|
||||
|
||||
Migration: replace all `t-raw` uses by `t-out`, and uses the `markup` function
|
||||
to mark all the js values.
|
||||
|
||||
Documentation: [Outputting data](doc/reference/templates.md#outputting-data)
|
||||
|
||||
## 39. `browser` object has been removed
|
||||
|
||||
Rationale: the `browser` object caused more trouble than it was worth. Also, it
|
||||
seems like this should be done in user space, not at the framework level.
|
||||
|
||||
Migration: code should just be adapted to either use another browser object,
|
||||
or to use native browser function (and then, just mock them directly).
|
||||
|
||||
## 40. Rendering a component does not necessarily render child components
|
||||
|
||||
Before, if one had the following component tree:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
```
|
||||
|
||||
when `A` would render, it would also render `B` and `C`. Now, in Owl 2, it will
|
||||
(shallow) compare the before and after props, and `B` or `C` will only be rerendered
|
||||
if their props have changed.
|
||||
|
||||
Now, the question is what happens if the props have changed, but in a deeper way?
|
||||
In that case, Owl will know, because each props are now reactive. So, if some
|
||||
inner value read by `B` was changed, then only `B` will be updated.
|
||||
|
||||
Rationale: This was just not possible in Owl 1, but it now possible. This is
|
||||
due to the rewriteof the underlying rendering engine and the reactivity
|
||||
system. The goal is to have a big performance boost in large screen with many
|
||||
components: now Owl only rerender what is strictly useful.
|
||||
|
||||
}
|
||||
@@ -16,10 +16,8 @@ framework, written in Typescript, taking the best ideas from React and Vue in a
|
||||
simple and consistent way. Owl's main features are:
|
||||
|
||||
- a declarative component system,
|
||||
- a fine grained reactivity system similar to Vue,
|
||||
- hooks
|
||||
- fragments
|
||||
- asynchronous rendering
|
||||
- a reactivity system based on hooks,
|
||||
- concurrent mode by default,
|
||||
|
||||
Owl components are defined with ES6 classes and xml templates, uses an
|
||||
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
|
||||
@@ -40,26 +38,28 @@ const { Component, useState, mount, xml } = owl;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="() => state.value = state.value + props.increment">
|
||||
<button t-on-click="() => state.value++">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
class App extends Component {
|
||||
static template = xml`
|
||||
<span>Hello Owl</span>
|
||||
<Counter increment="2"/>`;
|
||||
<div>
|
||||
<span>Hello Owl</span>
|
||||
<Counter />
|
||||
</div>`;
|
||||
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
mount(Root, document.body);
|
||||
mount(App, document.body);
|
||||
```
|
||||
|
||||
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
|
||||
Also, all examples here uses the [`xml` helper](doc/reference/templates.md#inline-templates) to define inline templates.
|
||||
Also, all examples here uses the [`xml` helper](doc/reference/tags.md#xml-tag) to define inline templates.
|
||||
But this is not mandatory, many applications will load templates separately.
|
||||
|
||||
More interesting examples can be found on the
|
||||
@@ -74,46 +74,47 @@ Are you new to Owl? This is the place to start!
|
||||
- [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md)
|
||||
- [How to start an Owl project](doc/learning/quick_start.md)
|
||||
- [How to test Components](doc/learning/how_to_test.md)
|
||||
- [How to write Single File Components](doc/learning/how_to_write_sfc.md)
|
||||
|
||||
### Reference
|
||||
|
||||
- [Overview](doc/readme.md)
|
||||
- [App](doc/reference/app.md)
|
||||
You will find here a complete reference of every feature, class or object
|
||||
provided by Owl.
|
||||
|
||||
- [Animations](doc/reference/animations.md)
|
||||
- [Browser](doc/reference/browser.md)
|
||||
- [Component](doc/reference/component.md)
|
||||
- [Component Lifecycle](doc/reference/component.md#lifecycle)
|
||||
- [Content](doc/reference/content.md)
|
||||
- [Concurrency Model](doc/reference/concurrency_model.md)
|
||||
- [Dev mode](doc/reference/app.md#dev-mode)
|
||||
- [Dynamic sub components](doc/reference/component.md#dynamic-sub-components)
|
||||
- [Configuration](doc/reference/config.md)
|
||||
- [Context](doc/reference/context.md)
|
||||
- [Environment](doc/reference/environment.md)
|
||||
- [Error Handling](doc/reference/error_handling.md)
|
||||
- [Event Bus](doc/reference/event_bus.md)
|
||||
- [Event Handling](doc/reference/event_handling.md)
|
||||
- [Form Input Bindings](doc/reference/input_bindings.md)
|
||||
- [Fragments](doc/reference/templates.md#fragments)
|
||||
- [Error Handling](doc/reference/error_handling.md)
|
||||
- [Hooks](doc/reference/hooks.md)
|
||||
- [Loading Templates](doc/reference/app.md#loading-templates)
|
||||
- [Mounting a component](doc/reference/app.md#mount-helper)
|
||||
- [Portal](doc/reference/portal.md)
|
||||
- [Precompiling templates](doc/reference/precompiling_templates.md)
|
||||
- [Mounting a component](doc/reference/mounting.md)
|
||||
- [Miscellaneous Components](doc/reference/misc.md)
|
||||
- [Observer](doc/reference/observer.md)
|
||||
- [Props](doc/reference/props.md)
|
||||
- [Props Validation](doc/reference/props.md#props-validation)
|
||||
- [Reactivity](doc/reference/reactivity.md)
|
||||
- [Rendering SVG](doc/reference/templates.md#rendering-svg)
|
||||
- [Refs](doc/reference/refs.md)
|
||||
- [Props Validation](doc/reference/props_validation.md)
|
||||
- [QWeb Templating Language](doc/reference/qweb_templating_language.md)
|
||||
- [QWeb Engine](doc/reference/qweb_engine.md)
|
||||
- [Slots](doc/reference/slots.md)
|
||||
- [Sub components](doc/reference/component.md#sub-components)
|
||||
- [Sub templates](doc/reference/templates.md#sub-templates)
|
||||
- [Templates (Qweb)](doc/reference/templates.md)
|
||||
- [Translations](doc/reference/translations.md)
|
||||
- [Tags](doc/reference/tags.md)
|
||||
- [Utils](doc/reference/utils.md)
|
||||
|
||||
### Other Topics
|
||||
|
||||
- [Notes On Owl Architecture](doc/miscellaneous/architecture.md)
|
||||
This section provides miscellaneous document that explains some topics
|
||||
which cannot be considered either a tutorial, or reference documentation.
|
||||
|
||||
- [Owl architecture: the Virtual DOM](doc/miscellaneous/vdom.md)
|
||||
- [Owl architecture: the rendering pipeline](doc/miscellaneous/rendering.md)
|
||||
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
|
||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
||||
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
|
||||
- [Owl devtools extension](doc/tools/devtools.md)
|
||||
- [Why did Odoo built Owl?](doc/miscellaneous/why_owl.md)
|
||||
|
||||
|
||||
|
||||
## Installing Owl
|
||||
|
||||
@@ -122,28 +123,8 @@ Owl is available on `npm` and can be installed with the following command:
|
||||
```
|
||||
npm install @odoo/owl
|
||||
```
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl](https://github.com/odoo/owl/releases/latest)
|
||||
|
||||
## Installing Owl devtools
|
||||
|
||||
The Owl devtools browser extension is also available in the [release](https://github.com/odoo/owl/releases/latest):
|
||||
Unzip the owl-devtools.zip file and follow the instructions depending on your browser:
|
||||
|
||||
### Chrome
|
||||
|
||||
Go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
|
||||
Select the devtools-chrome folder and that's it, your extension is active!
|
||||
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
|
||||
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
|
||||
|
||||
### Firefox
|
||||
Go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
|
||||
Select any file in the devtools-firefox folder and that's it, your extension is active!
|
||||
Here, you can use the reload button to refresh the extension.
|
||||
|
||||
Note that you may have to open another window or reload your tab to see the extension working.
|
||||
Also note that the extension will only be active on pages that have a sufficient version of owl.
|
||||
|
||||
- [owl-1.4.7](https://github.com/odoo/owl/releases/tag/v1.4.7)
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# 🦉 How to write Single File Components 🦉
|
||||
|
||||
It is very useful to group code by feature instead of by type of file. It makes
|
||||
it easier to scale application to larger size.
|
||||
|
||||
To do so, Owl has two small helpers that make it easy to define a
|
||||
template or a stylesheet inside a javascript (or typescript) file: the
|
||||
[`xml`](../reference/tags.md#xml-tag) and [`css`](../reference/tags.md#css-tag)
|
||||
helper.
|
||||
|
||||
This means that the template, the style and the javascript code can be defined in
|
||||
the same file. For example:
|
||||
|
||||
```js
|
||||
const { Component } = owl;
|
||||
const { xml, css } = owl.tags;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// TEMPLATE
|
||||
// -----------------------------------------------------------------------------
|
||||
const TEMPLATE = xml/* xml */ `
|
||||
<div class="main">
|
||||
<Sidebar/>
|
||||
<Content />
|
||||
</div>`;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// STYLE
|
||||
// -----------------------------------------------------------------------------
|
||||
const STYLE = css/* css */ `
|
||||
.main {
|
||||
display: grid;
|
||||
grid-template-columns: 200px auto;
|
||||
}
|
||||
`;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// CODE
|
||||
// -----------------------------------------------------------------------------
|
||||
class Main extends Component {
|
||||
static template = TEMPLATE;
|
||||
static style = STYLE;
|
||||
static components = { Sidebar, Content };
|
||||
|
||||
// rest of component...
|
||||
}
|
||||
```
|
||||
|
||||
Note that the above example has an inline xml comment, just after the `xml` call.
|
||||
This is useful for some editor plugins, such as the VS Code addon
|
||||
`Comment tagged template`, which, if installed, add syntax highlighting to the
|
||||
content of the template string.
|
||||
@@ -116,7 +116,7 @@ class App extends Component {}
|
||||
App.template = xml`<div>todo app</div>`;
|
||||
```
|
||||
|
||||
Note 3: writing inline templates with the [`xml` helper](../reference/templates.md#inline-templates)
|
||||
Note 3: writing inline templates with the [`xml` helper](../reference/tags.md#xml-tag)
|
||||
is nice, but there is no syntax highlighting, and this makes it very easy to
|
||||
have malformed xml. Some editors support syntax highlighting for this situation.
|
||||
For example, VS Code has an addon `Comment tagged template`, which, if installed,
|
||||
@@ -173,14 +173,14 @@ class Root extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
The template contains a [`t-foreach`](../reference/templates.md#loops) loop to iterate
|
||||
through the tasks. It can find the `tasks` list from the component, since the rendering
|
||||
context contains the properties of the component. Note that we use the `id` of each task
|
||||
as a `t-key`, which is very common. There are two css classes: `task-list` and `task`,
|
||||
The template contains a [`t-foreach`](../reference/qweb_templating_language.md#loops) loop to iterate
|
||||
through the tasks. It can find the `tasks` list from the component, since the
|
||||
component is the rendering context. Note that we use the `id` of each task as a
|
||||
`t-key`, which is very common. There are two css classes: `task-list` and `task`,
|
||||
that we will use in the next section.
|
||||
|
||||
Finally, notice the use of the `t-att-checked` attribute:
|
||||
prefixing an attribute by [`t-att`](../reference/templates.md#dynamic-attributes) makes
|
||||
prefixing an attribute by [`t-att`](../reference/qweb_templating_language.md#dynamic-attributes) makes
|
||||
it dynamic. Owl will evaluate the expression and set it as the value of the
|
||||
attribute.
|
||||
|
||||
@@ -240,7 +240,7 @@ class Task extends Component {
|
||||
static template = xml /* xml */`
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
|
||||
<span><t t-esc="props.task.text"/></span>
|
||||
<span><t t-esc="props.task.title"/></span>
|
||||
</div>`;
|
||||
static props = ["task"];
|
||||
}
|
||||
@@ -277,10 +277,10 @@ A lot of stuff happened here:
|
||||
- the `Task` component has a `props` key: this is only useful for validation
|
||||
purpose. It says that each `Task` should be given exactly one prop, named
|
||||
`task`. If this is not the case, Owl will throw an
|
||||
[error](../reference/props.md#props-validation). This is extremely
|
||||
[error](../reference/props_validation.md). This is extremely
|
||||
useful when refactoring components
|
||||
- finally, to activate the props validation, we need to set Owl's
|
||||
[mode](../reference/app.md#configuration) to `dev`. This is done in the last argument
|
||||
[mode](../reference/config.md#mode) to `dev`. This is done in the last argument
|
||||
of the `mount` function. Note that this should be removed when an app is used in a real
|
||||
production environment, since `dev` mode is slightly slower, due to extra
|
||||
checks and validations.
|
||||
@@ -502,10 +502,6 @@ deleteTask(task) {
|
||||
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
|
||||
suffix that makes sure the function callback is bound to the component.
|
||||
|
||||
Notice also that we have two functions named `deleteTask`. The one in the Task
|
||||
component just delegates the work to the Root component that owns the task list
|
||||
via the `onDelete` property.
|
||||
|
||||
## 10. Using a store
|
||||
|
||||
Looking at the code, it is apparent that all the code handling tasks is scattered
|
||||
@@ -747,7 +743,7 @@ the user experience.
|
||||
```xml
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"
|
||||
t-att-id="props.task.id"
|
||||
t-on-click="() => store.toggleTask(props.task)"/>
|
||||
t-on-click="dispatch('toggleTask', props.task.id)"/>
|
||||
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
|
||||
```
|
||||
|
||||
@@ -774,11 +770,10 @@ For reference, here is the final code:
|
||||
<meta charset="UTF-8" />
|
||||
<title>OWL Todo App</title>
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="owl.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# 🦉 Notes On Owl Architecture 🦉
|
||||
|
||||
We explain here how Owl is designed
|
||||
|
||||
Warning: these notes are technical by nature, and intended for people working
|
||||
on Owl (or interested in understanding its design).
|
||||
|
||||
## Overview
|
||||
|
||||
Roughly speaking, Owl has 5 main parts:
|
||||
|
||||
- a virtual dom system (in `src/blockdom`)
|
||||
- a component system (in `src/component`)
|
||||
- a template compiler (located in the `src/compiler` folder)
|
||||
- a small runtime code to tie them together (in `src/app`)
|
||||
- a reactivity system (in `src/reactivity.ts`)
|
||||
|
||||
There are some other files, but the core of Owl can be understood with these
|
||||
five main parts.
|
||||
|
||||
The virtual dom is an optimized virtual dom based on blocks, which supports
|
||||
multi blocks (for fragments). Everything that owl renders is internally
|
||||
represented by a virtual node. The job of the virtual dom is to efficiently
|
||||
represent the current state of the application, and to build an actual DOM
|
||||
representation when needed, or update the DOM whenever it is needed.
|
||||
|
||||
- some other helpers/smaller scale stuff
|
||||
A rendering occurs in two phases:
|
||||
|
||||
- virtual rendering: this generates the virtual dom in memory, asynchronously
|
||||
- patch: applies a virtual tree to the screen (synchronously)
|
||||
|
||||
There are several classes involved in a rendering:
|
||||
|
||||
- components
|
||||
- a scheduler
|
||||
- fibers: small objects containing some metadata, associated with a rendering of
|
||||
a specific component
|
||||
|
||||
Components are organized in a dynamic component tree, visible in the user
|
||||
interface. Whenever a rendering is initiated in a component `C`:
|
||||
|
||||
- a fiber is created on `C` with the rendering props information
|
||||
- the virtual rendering phase starts on C (will asynchronously render all the
|
||||
child components)
|
||||
- the fiber is added to the scheduler, which will poll continuously, every
|
||||
animation frame, if the fiber is done
|
||||
- once it is done, the scheduler will call the task callback, which will apply
|
||||
the patch (if it was not cancelled in the meantime).
|
||||
|
||||
# 🦉 VDom 🦉
|
||||
|
||||
Owl is a declarative component system: we declare the structure of the component
|
||||
tree, and Owl will translate that to a list of imperative operations. This
|
||||
translation is done by a virtual dom. This is the low level layer of Owl, most
|
||||
developer will not need to call directly the virtual dom functions.
|
||||
|
||||
The main idea behind a virtual dom is to keep a in-memory representation of the
|
||||
DOM (called a virtual node), and whenever some change is needed, to regenerate
|
||||
a new representation, compute the difference between the old and the new, then
|
||||
apply the changes.
|
||||
|
||||
`vdom` exports two functions:
|
||||
|
||||
- `h`: create a new virtual node
|
||||
- `patch`: compare two virtual nodes, and apply the difference.
|
||||
|
||||
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
@@ -78,7 +78,7 @@ additional tools, we made a lot of effort to make the most of the web platform.
|
||||
|
||||
For example, Owl uses the standard `xml` parser that comes with every browser.
|
||||
Because of that, Owl did not have to write its own template parser. Another
|
||||
example is the [`xml`](../reference/templates.md#inline-templates) tag helper function, which makes use of
|
||||
example is the [`xml`](../reference/tags.md#xml-tag) tag helper function, which makes use of
|
||||
native template literals to allow in a natural way to write `xml` templates
|
||||
directly in the javascript code. This can be easily integrated with editor
|
||||
plugins to have autocompletion inside the template.
|
||||
@@ -126,7 +126,7 @@ structured than a template language. Note that the tooling is quite impressive:
|
||||
there is a syntax highlighter for jsx here on github!
|
||||
|
||||
By comparison, here is the equivalent Owl component, written with the
|
||||
[`xml`](../reference/templates.md#inline-templates) tag helper:
|
||||
[`xml`](../reference/tags.md#xml-tag) tag helper:
|
||||
|
||||
```js
|
||||
class Clock extends Component {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# 🦉 Notes On Owl Compiled Templates 🦉
|
||||
|
||||
This page will explain what an Owl compiled template look like. This is a
|
||||
technical document intended for developers interested in understanding how Owl
|
||||
works internally.
|
||||
|
||||
Broadly speaking, Owl compiles templates into a javascript function (a closure)
|
||||
that returns a function (the "render" function). The point of the closure is to
|
||||
have a place to store all values specific to the template (in particular, "blocks").
|
||||
Once a template is compiled, its closure function is called once to get the
|
||||
render function, and from then on, only the render function is used.
|
||||
|
||||
The render function takes some context (and some additional information) and
|
||||
return a virtual dom representation of the rendered template, as a block tree.
|
||||
A block tree is a very light weight representation that only contains the dynamic
|
||||
part of the template, and its structure. It is actually independant of the
|
||||
static part of the templates (which are contained in the blocks captured by the
|
||||
closure). This means that the work performed at render time is only to collect
|
||||
dynamic data, and to describe the block structure of the result.
|
||||
|
||||
It looks like this, in pseudo code:
|
||||
|
||||
```js
|
||||
function closure(bdom, helpers) {
|
||||
// here is some place to put stuff specific to the template, such as
|
||||
// blocks
|
||||
...
|
||||
|
||||
return function render(context, node, key) {
|
||||
// only build here all dynamic parts of the template
|
||||
// build a block tree
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, let us see an example. Consider the following template:
|
||||
|
||||
```xml
|
||||
<div class="some-class">
|
||||
<div class="blabla">
|
||||
<span><t t-esc="state.value"/></span>
|
||||
</div>
|
||||
<t t-if="state.info">
|
||||
<p class="info" t-att-class="someAttribute">
|
||||
<t t-esc="state.info"/>
|
||||
</p>
|
||||
</t>
|
||||
<SomeComponent value="value"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
If you look carefully, there are 5 dynamic things:
|
||||
|
||||
- a text value (the first `t-esc`),
|
||||
- a sub block (the `t-if`),
|
||||
- a dynamic attribute (the `t-att-class` attribute),
|
||||
- another text value (the second `t-esc`),
|
||||
- and finally, a sub component
|
||||
|
||||
Here is the compiled code for this template:
|
||||
|
||||
```js
|
||||
function closure(bdom, helpers) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(
|
||||
`<div class="some-class"><div class="blabla"><span><block-text-0/></span></div><block-child-0/><block-child-1/></div>`
|
||||
);
|
||||
let block2 = createBlock(`<p class="info" block-attribute-0="class"><block-text-1/></p>`);
|
||||
|
||||
return function render(ctx, node, key = "") {
|
||||
let b2, b3;
|
||||
let txt1 = ctx["state"].value;
|
||||
if (ctx["state"].info) {
|
||||
let attr1 = ctx["someAttribute"];
|
||||
let txt2 = ctx["state"].info;
|
||||
b2 = block2([attr1, txt2]);
|
||||
}
|
||||
b3 = component(`SomeComponent`, { value: ctx["value"] }, key + `__1`, node, ctx);
|
||||
return block1([txt1], [b2, b3]);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The values captured in the closure capture the static part of the template: we
|
||||
define here two blocks (which contains a template node, that can be deep cloned
|
||||
whenever a block is mounted). Then the render function only describes the block
|
||||
tree structure of the result, depending on the context. This means that we
|
||||
minimize the amount of work done at render time.
|
||||
|
||||
Then, when we want to patch the dom, Owl will uses the `patch` function from
|
||||
blockdom, which then will diff the block tree, and deep clone new blocks whenever
|
||||
a new block is inserted, keep track of dynamic parts of each block, and update
|
||||
them accordingly.
|
||||
|
||||
With this design, the cost of rendering a template is proportional to the number
|
||||
of dynamic values, and not to the size of the template.
|
||||
@@ -0,0 +1,32 @@
|
||||
# 🦉 Rendering Pipeline 🦉
|
||||
|
||||
We explain here how Owl is designed, from the perspective of its rendering
|
||||
pipeline.
|
||||
|
||||
Warning: these notes are technical by nature, and intended for people working
|
||||
on Owl (or interested in understanding its design).
|
||||
|
||||
## Overview
|
||||
|
||||
A rendering occurs in two phases:
|
||||
|
||||
- virtual rendering: this generates the virtual dom in memory, asynchronously
|
||||
- patch: applies a virtual tree to the screen (synchronously)
|
||||
|
||||
There are several classes involved in a rendering:
|
||||
|
||||
- components
|
||||
- a scheduler
|
||||
- fibers: small objects containing some metadata, associated with a rendering of
|
||||
a specific component
|
||||
|
||||
Components are organized in a dynamic component tree, visible in the user
|
||||
interface. Whenever a rendering is initiated in a component `C`:
|
||||
|
||||
- a fiber is created on `C` with the rendering props information
|
||||
- the virtual rendering phase starts on C (will asynchronously render all the
|
||||
child components)
|
||||
- the fiber is added to the scheduler, which will poll continuously, every
|
||||
animation frame, if the fiber is done
|
||||
- once it is done, the scheduler will call the task callback, which will apply
|
||||
the patch (if it was not cancelled in the meantime).
|
||||
@@ -0,0 +1,18 @@
|
||||
# 🦉 VDom 🦉
|
||||
|
||||
Owl is a declarative component system: we declare the structure of the component
|
||||
tree, and Owl will translate that to a list of imperative operations. This
|
||||
translation is done by a virtual dom. This is the low level layer of Owl, most
|
||||
developer will not need to call directly the virtual dom functions.
|
||||
|
||||
The main idea behind a virtual dom is to keep a in-memory representation of the
|
||||
DOM (called a virtual node), and whenever some change is needed, to regenerate
|
||||
a new representation, compute the difference between the old and the new, then
|
||||
apply the changes.
|
||||
|
||||
`vdom` exports two functions:
|
||||
|
||||
- `h`: create a new virtual node
|
||||
- `patch`: compare two virtual nodes, and apply the difference.
|
||||
|
||||
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
@@ -1,50 +0,0 @@
|
||||
# 🦉 Owl overview 🦉
|
||||
|
||||
Here is a list of everything exported by the Owl library:
|
||||
|
||||
Main entities:
|
||||
|
||||
- [`App`](reference/app.md): represent an Owl application (mainly a root component,a set of templates, and a config)
|
||||
- [`Component`](reference/component.md): the main class to define a concrete Owl component
|
||||
- [`mount`](reference/app.md#mount-helper): main entry point for most application: mount a component to a target
|
||||
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
|
||||
|
||||
Reactivity
|
||||
|
||||
- [`useState`](reference/reactivity.md#usestate): create a reactive object (hook, linked to a specific component)
|
||||
- [`reactive`](reference/reactivity.md#reactive): create a reactive object (not linked to any component)
|
||||
- [`markRaw`](reference/reactivity.md#markraw): mark an object or array so that it is ignored by the reactivity system
|
||||
- [`toRaw`](reference/reactivity.md#toraw): given a reactive objet, return the raw (non reactive) underlying object
|
||||
|
||||
Lifecycle hooks:
|
||||
|
||||
- [`onWillStart`](reference/component.md#willstart): hook to define asynchronous code that should be executed before component is rendered
|
||||
- [`onMounted`](reference/component.md#mounted): hook to define code that should be executed when component is mounted
|
||||
- [`onWillPatch`](reference/component.md#willpatch): hook to define code that should be executed before component is patched
|
||||
- [`onWillUpdateProps`](reference/component.md#willupdateprops): hook to define code that should be executed before component is updated
|
||||
- [`onPatched`](reference/component.md#patched): hook to define code that should be executed when component is patched
|
||||
- [`onWillRender`](reference/component.md#willrender): hook to define code that should be executed before component is rendered
|
||||
- [`onRendered`](reference/component.md#rendered): hook to define code that should be executed after component is rendered
|
||||
- [`onWillUnmount`](reference/component.md#willunmount): hook to define code that should be executed before component is unmounted
|
||||
- [`onWillDestroy`](reference/component.md#willdestroy): hook to define code that should be executed before component is destroyed
|
||||
- [`onError`](reference/component.md#onerror): hook to define a Owl error handler
|
||||
|
||||
Other hooks:
|
||||
|
||||
- [`useComponent`](reference/hooks.md#usecomponent): return a reference to the current component (useful to create derived hooks)
|
||||
- [`useEffect`](reference/hooks.md#useeffect): define an effect with its dependencies
|
||||
- [`useEnv`](reference/hooks.md#useenv): return a reference to the current env
|
||||
- [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM
|
||||
- [`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)
|
||||
|
||||
Utility/helpers:
|
||||
|
||||
- [`EventBus`](reference/utils.md#eventbus): a simple event bus
|
||||
- [`loadFile`](reference/utils.md#loadfile): an helper to load a file from the server
|
||||
- [`markup`](reference/templates.md#outputting-data): utility function to define strings that represent html (should not be escaped)
|
||||
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
||||
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
||||
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
||||
- [`batched`](reference/utils.md#batched): utility function to batch function calls
|
||||
@@ -0,0 +1,127 @@
|
||||
# 🦉 Animations 🦉
|
||||
|
||||
Animation is a complex topic. There are many different use cases, and many
|
||||
solutions and technologies. Owl only supports some basic use cases.
|
||||
|
||||
## Simple CSS effects
|
||||
|
||||
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
|
||||
necessary: it just needs to render a DOM element with a specific class. For
|
||||
example:
|
||||
|
||||
```xml
|
||||
<a class="btn flash" t-on-click="doSomething">Click</a>
|
||||
```
|
||||
|
||||
with the following CSS:
|
||||
|
||||
```css
|
||||
btn {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.flash {
|
||||
transition: background 0.5s;
|
||||
}
|
||||
|
||||
.flash:active {
|
||||
background-color: #41454a;
|
||||
transition: background 0s;
|
||||
}
|
||||
```
|
||||
|
||||
will produce a nice flash effect whenever the user clicks (or activates with the
|
||||
keyboard) the button.
|
||||
|
||||
## CSS Transitions
|
||||
|
||||
A more complex situation occurs when we want to transition an element in or out
|
||||
of the page. For example, we may want a fade-in and fade-out effect.
|
||||
|
||||
The `t-transition` directive is here to help us. It works on html elements and
|
||||
on components, by adding and removing some css classes.
|
||||
|
||||
To perform useful transition effects, whenever an element appears or disappears,
|
||||
it is necessary to add/remove some css style or class at some precise moment in
|
||||
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
|
||||
directive is there to help.
|
||||
|
||||
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
||||
sequence of events will happen:
|
||||
|
||||
At node insertion:
|
||||
|
||||
- the css classes `name-enter` and `name-enter-active` will be added directly
|
||||
when the node is inserted into the DOM.
|
||||
- on the next animation frame: the css class `name-enter` will be removed and the
|
||||
class `name-enter-to` will be added (so they can be used to trigger css
|
||||
transition effects).
|
||||
- at the end of the transition, `name-enter-to` and `name-enter-active` will be removed.
|
||||
|
||||
At node destruction:
|
||||
|
||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||
node is removed to the DOM.
|
||||
- on the next animation frame: the css class `name-leave` will be removed and the
|
||||
class `name-leave-to` will be added (so they can be used to trigger css
|
||||
transition effects).
|
||||
- at the end of the transition, `name-leave-to` and `name-leave-active` will be removed.
|
||||
|
||||
For example, a simple fade in/out effect can be done with this:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The `t-transition` directive can be applied on a node element or on a component.
|
||||
|
||||
Notes:
|
||||
|
||||
Owl does not support more than one transition on a single node, so the
|
||||
`t-transition` expression must be a single value (i.e. no space allowed).
|
||||
|
||||
## SCSS Mixins
|
||||
|
||||
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
|
||||
|
||||
```scss
|
||||
@mixin animation-fade($time, $name) {
|
||||
.#{$name}_fade-enter-active,
|
||||
.#{$name}_fade-active {
|
||||
transition: all $time;
|
||||
}
|
||||
|
||||
.#{$name}_fade-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.#{$name}_fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```scss
|
||||
@include animation-fade(0.5s, "o_notification");
|
||||
```
|
||||
|
||||
You can now have in your template:
|
||||
|
||||
```xml
|
||||
<SomeTag t-transition="o_notification_fade"/>
|
||||
```
|
||||
@@ -1,156 +0,0 @@
|
||||
# 🦉 App 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [API](#api)
|
||||
- [Configuration](#configuration)
|
||||
- [`mount` helper](#mount-helper)
|
||||
- [Roots](#roots)
|
||||
- [Loading templates](#loading-templates)
|
||||
|
||||
## Overview
|
||||
|
||||
Every Owl application has a root element, a set of templates, an environment and
|
||||
possibly a few other settings. The `App` class is a simple class that represents
|
||||
all of these elements. Here is an example:
|
||||
|
||||
```js
|
||||
const {Component, App } = owl;
|
||||
|
||||
class MyComponent extends Component { ... }
|
||||
|
||||
const app = new App(MyComponent, { props: {...}, templates: "..."});
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
The basic workflow is: create an `App` instance configured with the root
|
||||
component, the templates, and possibly other settings. Then, we mount that
|
||||
instance somewhere in the DOM.
|
||||
|
||||
## API
|
||||
|
||||
- **`constructor(Root[, config])`**: first argument should be a component class (not
|
||||
an instance), and the optional second argument is a configuration object (see below).
|
||||
|
||||
- **`mount(target, options)`**: first argument is an html element, and the optional
|
||||
second argument is an object with mounting options (see below). Mount the app
|
||||
to a target in the DOM. Note that this is an asynchronous operation: the `mount`
|
||||
method returns a promise that resolves to the component instance whenever it
|
||||
is complete.
|
||||
|
||||
The `option` object is an object with the following keys:
|
||||
|
||||
- **`position (string)`**: either `first-child` or `last-child`. This option determines
|
||||
the position of the application in the target: either first or last child.
|
||||
|
||||
- **`destroy()`**: destroys the application
|
||||
|
||||
## Configuration
|
||||
|
||||
The `config` object is an object with some of the following keys:
|
||||
|
||||
- **`env (object)`**: if given, this will be the shared `env` given to each component
|
||||
- **`props (object)`**: the props given to the root component
|
||||
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in
|
||||
[`dev` mode](#dev-mode);
|
||||
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
|
||||
that Owl will not log a message to warn that Owl is in `dev` mode.
|
||||
- **`translatableAttributes (string[])`**: a list of additional attributes that should
|
||||
be translated (see [translations](translations.md))
|
||||
- **`translateFn (function)`**: a function that will be called by owl to translate
|
||||
templates (see [translations](translations.md))
|
||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||
the components created by the application.
|
||||
- **`getTemplate ((s: string) => Element | Function | string | void)`**: a function that will be called by owl when it
|
||||
needs a template. If undefined is returned, owl looks into the app templates.
|
||||
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
|
||||
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
||||
- **`customDirectives (object)`**: if given, the corresponding function on the object will be called
|
||||
on the template custom directives: `t-custom-*` (see [Custom Directives](templates.md#custom-directives)).
|
||||
- **`globalValues (object)`**: Global object of elements available at compilations.
|
||||
|
||||
## `mount` helper
|
||||
|
||||
Note that there is a `mount` helper to do that in just a line:
|
||||
|
||||
```js
|
||||
const { mount, Component } = owl;
|
||||
|
||||
class MyComponent extends Component {
|
||||
...
|
||||
}
|
||||
|
||||
mount(MyComponent, document.body, { props: {...}, templates: "..."});
|
||||
```
|
||||
|
||||
Here is the `mount` function signature:
|
||||
|
||||
**`mount(Component, target, config)`** with the following arguments:
|
||||
|
||||
- **`Component`**: a component class (Root component of the app)
|
||||
- **`target`**: an html element, where the component will be mounted as last child
|
||||
- **`config (optional)`**: a config object (the same as the App config object)
|
||||
|
||||
Most of the time, the `mount` helper is more convenient, but whenever one needs
|
||||
a reference to the actual Owl App, then using the `App` class directly is
|
||||
possible.
|
||||
|
||||
## Roots
|
||||
|
||||
An application can have multiple roots. It is sometimes useful to instantiate
|
||||
sub components in places that are not managed by Owl, such as an html editor
|
||||
with dynamic content (the Knowledge application in Odoo).
|
||||
|
||||
To create a root, one can use the `createRoot` method, which takes two arguments:
|
||||
|
||||
- **`Component`**: a component class (Root component of the app)
|
||||
- **`config (optional)`**: a config object that may contain a `props` object or a
|
||||
`env` object.
|
||||
|
||||
The `createRoot` method returns an object with a `mount` method (same API as
|
||||
the `App.mount` method), and a `destroy` method.
|
||||
|
||||
```js
|
||||
const root = app.createRoot(MyComponent, { props: { someProps: true } });
|
||||
await root.mount(targetElement);
|
||||
|
||||
// later
|
||||
root.destroy();
|
||||
```
|
||||
|
||||
Note that, like with owl `App`, it is the responsibility of the code that created
|
||||
the root to properly destroy it (before it has been removed from the DOM!). Owl
|
||||
has no way of doing it itself.
|
||||
|
||||
## Loading templates
|
||||
|
||||
Most applications will need to load templates whenever they start. Here is
|
||||
what it could look like in practice:
|
||||
|
||||
```js
|
||||
// in the main js file:
|
||||
const { loadFile, mount } = owl;
|
||||
|
||||
// async, so we can use async/await
|
||||
(async function setup() {
|
||||
const templates = await loadFile(`/some/endpoint/that/return/templates`);
|
||||
const env = {
|
||||
_t: someTranslateFn,
|
||||
templates,
|
||||
// possibly other stuff
|
||||
};
|
||||
|
||||
mount(Root, document.body, { env });
|
||||
})();
|
||||
```
|
||||
|
||||
## Dev mode
|
||||
|
||||
Dev mode activates some additional checks and developer amenities:
|
||||
|
||||
- [Props validation](./props.md#props-validation) is performed
|
||||
- [t-foreach](./templates.md#loops) loops check for key unicity
|
||||
- Lifecycle hooks are wrapped to report their errors in a more developer-friendly way
|
||||
- onWillStart and onWillUpdateProps will emit a warning in the console when they
|
||||
take longer than 3 seconds in an effort to ease debugging the presence of deadlocks
|
||||
@@ -0,0 +1,33 @@
|
||||
# 🦉 Browser 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Browser Content](#browser-content)
|
||||
|
||||
## Overview
|
||||
|
||||
The browser object contains some browser native APIs, such as `setTimeout`, that
|
||||
are used by Owl and its utility functions. They are exposed with the intent of
|
||||
making them mockable if necessary.
|
||||
|
||||
```js
|
||||
owl.browser.setTimeout === window.setTimeout; // return true
|
||||
```
|
||||
|
||||
For now, this object contains some functions that are not used by Owl. They
|
||||
will eventually be removed in Owl 2.0.
|
||||
|
||||
## Browser Content
|
||||
|
||||
More specifically, the `browser` object contains the following methods and objects:
|
||||
|
||||
- `setTimeout`
|
||||
- `clearTimeout`
|
||||
- `setInterval`
|
||||
- `clearInterval`
|
||||
- `requestAnimationFrame`
|
||||
- `random`
|
||||
- `Date`
|
||||
- `fetch`
|
||||
- `localStorage`
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
Owl was designed from the very beginning with asynchronous components. This comes
|
||||
from the `willStart` and the `willUpdateProps` lifecycle hooks. With these
|
||||
asynchronous hooks, it is possible to build complex highly concurrent applications.
|
||||
methods, it is possible to build complex highly concurrent applications.
|
||||
|
||||
Owl concurrent mode has several benefits: it makes it possible to delay the
|
||||
rendering until some asynchronous operation is complete, it makes it possible
|
||||
@@ -35,8 +35,7 @@ two phases: _virtual rendering_ and _patching_.
|
||||
|
||||
### Virtual rendering
|
||||
|
||||
This phase represent the process of rendering a template, in memory, which creates
|
||||
a virtual representation of the desired component html). The output of this phase is a
|
||||
This phase represent the process of rendering a template, in memory, which create a virtual representation of the desired component html). The output of this phase is a
|
||||
virtual DOM.
|
||||
|
||||
It is asynchronous: each subcomponents needs to either be created (so, `willStart`
|
||||
@@ -95,7 +94,7 @@ component (with some code like `app.mount(document.body)`).
|
||||
5. The method `mounted` is called recursively on all components in the following
|
||||
order: `E`, `D`, `C`, `B`, `A`.
|
||||
|
||||
**Scenario 2: updating a component**. Now, let's assume that the user clicked on some
|
||||
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
|
||||
button in `C`, and this results in a state update, which is supposed to:
|
||||
|
||||
- update `D`,
|
||||
@@ -137,7 +136,8 @@ Here is what Owl will do:
|
||||
6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C`
|
||||
|
||||
Tags are very small helpers to make it easy to write inline templates. There is
|
||||
only one currently available tag: `xml`.
|
||||
only one currently available tag: `xml`, but we plan to add other tags later,
|
||||
such as a `css` tag, which will be used to write [single file components](../learning/how_to_write_sfc.md).
|
||||
|
||||
### Asynchronous Rendering
|
||||
|
||||
@@ -160,4 +160,22 @@ Here are a few tips on how to work with asynchronous components:
|
||||
1. Minimize the use of asynchronous components!
|
||||
2. Lazy loading external libraries is a good use case for async rendering. This
|
||||
is mostly fine, because we can assume that it will only takes a fraction of a
|
||||
second, and only once.
|
||||
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
|
||||
3. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
|
||||
this component is met, a new rendering
|
||||
sub tree is created, such that the rendering of that component (and its
|
||||
children) is not tied to the rendering of the rest of the interface. It can
|
||||
be used on an asynchronous component, to prevent it from delaying the
|
||||
rendering of the whole interface, or on a synchronous one, such that its
|
||||
rendering isn't delayed by other (asynchronous) components. Note that this
|
||||
directive has no effect on the first rendering, but only on subsequent ones
|
||||
(triggered by state or props changes).
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<SyncChild />
|
||||
<AsyncRoot>
|
||||
<AsyncChild/>
|
||||
</AsyncRoot>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# 🦉 Config 🦉
|
||||
|
||||
The Owl framework is designed to work in many situations. However, it is
|
||||
sometimes necessary to customize some behaviour. This is done by using the
|
||||
global `config` object. It provides two settings:
|
||||
|
||||
- [`mode`](#mode) (default value: `prod`),
|
||||
- [`enableTransitions`](#enabletransitions) (default value: `true`).
|
||||
|
||||
## Mode
|
||||
|
||||
By default, Owl is in _production_ mode, this means that it will try to do its
|
||||
job fast, and skip some expensive operations. However, it is sometimes necessary
|
||||
to have better information on what is going on, this is the purpose
|
||||
of the `dev` mode.
|
||||
|
||||
Owl has a mode flag, in `owl.config.mode`. Its default value is `prod`, but
|
||||
it can be set to `dev`:
|
||||
|
||||
```js
|
||||
owl.config.mode = "dev";
|
||||
```
|
||||
|
||||
Note that templates compiled with the `prod` settings will not be recompiled.
|
||||
So, changing this setting is best done at startup.
|
||||
|
||||
An important job done by the `dev` mode is to validate props for each component
|
||||
creation and update. Also, extra props will cause an error.
|
||||
|
||||
## `enableTransitions`
|
||||
|
||||
Transitions are usually nice, but they can cause issues in some specific cases,
|
||||
such as automated tests. It is uncomfortable having to wait for a transition
|
||||
to end before moving to the next step.
|
||||
|
||||
To solve this issue, Owl can be configured to ignore the `t-transition` directive.
|
||||
To do that, one only needs to set the `enableTransitions` flag to false:
|
||||
|
||||
```js
|
||||
owl.config.enableTransitions = false;
|
||||
```
|
||||
|
||||
Note that it suffers from the same drawback as the "dev" mode: all compiled
|
||||
templates, if any, will keep their current behaviours.
|
||||
@@ -0,0 +1,36 @@
|
||||
# 🦉 Owl Content 🦉
|
||||
|
||||
Here is a complete visual representation of everything exported by the `owl`
|
||||
global object.
|
||||
|
||||
For example, `Component` is available at `owl.Component` and `EventBus` is
|
||||
exported as `owl.core.EventBus`.
|
||||
|
||||
```
|
||||
browser
|
||||
Component misc
|
||||
Context AsyncRoot
|
||||
QWeb Portal
|
||||
mount
|
||||
useState tags
|
||||
config css
|
||||
mode xml
|
||||
core utils
|
||||
EventBus debounce
|
||||
Observer escape
|
||||
hooks loadJS
|
||||
onWillStart loadFile
|
||||
onMounted shallowEqual
|
||||
onWillUpdateProps whenReady
|
||||
onWillPatch
|
||||
onPatched
|
||||
onWillUnmount
|
||||
useContext
|
||||
useState
|
||||
useRef
|
||||
useComponent
|
||||
useEnv
|
||||
useSubEnv
|
||||
```
|
||||
|
||||
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
||||
@@ -0,0 +1,105 @@
|
||||
# 🦉 Context 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [`Context`](#context)
|
||||
- [`useContext`](#usecontext)
|
||||
|
||||
## Overview
|
||||
|
||||
The `Context` object provides a way to share data between an arbitrary number
|
||||
of components. Usually, data is passed from a parent to its children component,
|
||||
but when we have to deal with some mostly global information, this can be
|
||||
annoying, since each component will need to pass the information to each children,
|
||||
even though some or most of them will not use the information.
|
||||
|
||||
With a `Context` object, each component can subscribe (with the `useContext` hook)
|
||||
to its state, and will be updated whenever the context state is updated.
|
||||
|
||||
## Example
|
||||
|
||||
Assume that we have an application with various components which needs to render
|
||||
differently depending on the size of the device. Here is how we could proceed
|
||||
to make sure that the information is properly shared. First, let us create a
|
||||
context, and add it to the environment:
|
||||
|
||||
```js
|
||||
const deviceContext = new Context({ isMobile: true });
|
||||
App.env.deviceContext = deviceContext;
|
||||
```
|
||||
|
||||
If we want to make it completely responsive, we need to update its value whenever
|
||||
the size of the screen is updated:
|
||||
|
||||
```js
|
||||
const isMobile = () => window.innerWidth <= 768;
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
owl.utils.debounce(() => {
|
||||
const state = deviceContext.state;
|
||||
if (state.isMobile !== isMobile()) {
|
||||
state.isMobile = !state.isMobile;
|
||||
}
|
||||
}, 15)
|
||||
);
|
||||
```
|
||||
|
||||
Then, each component that want can subscribe and render differently depending on the
|
||||
fact that we are in a mobile or desktop mode.
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if=device.isMobile>
|
||||
some simplified user interface
|
||||
</t>
|
||||
<t t-else="">
|
||||
a more advanced user interface
|
||||
</t>
|
||||
</div>`;
|
||||
device = useContext(this.env.deviceContext);
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### `Context`
|
||||
|
||||
A `Context` object should be created with a state object:
|
||||
|
||||
```js
|
||||
const someContext = new Context({ some: "key" });
|
||||
```
|
||||
|
||||
Its state is now available in the `state` key:
|
||||
|
||||
```js
|
||||
someContext.state.some = "other key";
|
||||
```
|
||||
|
||||
This is the way some global code (such as the responsive code above) should
|
||||
read and update the context state. However, components should not ever read the
|
||||
context state directly from the context, they should instead use the `useContext`
|
||||
hook to properly register themselves to state changes.
|
||||
|
||||
Note that the `Context` hook is different from the React version. For example,
|
||||
there is no concept of provider/consumer. So, the `Context` feature does not
|
||||
by itself allow the use of a different context state depending on the component
|
||||
place in the component tree. However, this functionality can be obtained, if
|
||||
necessary, with the use of sub environment.
|
||||
|
||||
### `useContext`
|
||||
|
||||
The `useContext` hook is the normal way for a component to register themselve
|
||||
to context state changes. The `useContext` method returns the context state:
|
||||
|
||||
```js
|
||||
device = useContext(this.env.deviceContext);
|
||||
```
|
||||
|
||||
It is a simple observed state (with an owl `Observer`), which contains the shared
|
||||
information.
|
||||
@@ -6,15 +6,15 @@
|
||||
- [Setting an Environment](#setting-an-environment)
|
||||
- [Using a sub environment](#using-a-sub-environment)
|
||||
- [Content of an Environment](#content-of-an-environment)
|
||||
- [Special keys](#special-keys)
|
||||
|
||||
## Overview
|
||||
|
||||
An environment is a shared object given to all components in a tree. It is not
|
||||
used by Owl itself, but it is useful for application developers to provide a
|
||||
simple communication channel between components (in addition to the props).
|
||||
|
||||
The `env` given to the [`App`](app.md) is assigned to the `env` component
|
||||
property.
|
||||
An environment is an object which contains a [`QWeb` instance](qweb_engine.md). Whenever
|
||||
a root component is created, it is assigned an environment (see
|
||||
[below](#setting-an-environment) for more info on this). This environment is
|
||||
then automatically given to each sub component (and accessible in the `this.env`
|
||||
property).
|
||||
|
||||
```
|
||||
Root
|
||||
@@ -22,14 +22,31 @@ property.
|
||||
A B
|
||||
```
|
||||
|
||||
Also, the `env` object is frozen when the application is started. This is done
|
||||
to ensure a simpler mental model of what's happening in runtime. Note that it
|
||||
is only shallowly frozen, so sub objects can be modified.
|
||||
This way, all components share the same `QWeb` instance. Owl internally requires
|
||||
that the environment has a `qweb` key which maps to a
|
||||
[`QWeb`](qweb_engine.md) instance. This is the QWeb instance that will be used to
|
||||
render each templates in this specific component tree. Note that if no `QWeb`
|
||||
instance is provided, Owl will simply generate it on the fly.
|
||||
|
||||
The environment is mostly static. Each application is free to add anything to
|
||||
the environment, which is very useful, since this can be accessed by each sub
|
||||
component.
|
||||
|
||||
## Setting an environment
|
||||
|
||||
The correct way to customize an environment is to simply give it to the `App`,
|
||||
whenever it is created.
|
||||
An Owl application needs an [environment](environment.md) to be executed. The
|
||||
environment has an important key: the [QWeb](qweb_engine.md) instance, which will render
|
||||
all templates.
|
||||
|
||||
Whenever a root component `App` is mounted, Owl will setup a valid environment by
|
||||
following the next steps:
|
||||
|
||||
- take the `env` object defined on `App.env` (if no `env` was explicitly setup,
|
||||
this will return the empty `env` object defined on `Component`)
|
||||
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
|
||||
|
||||
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
|
||||
const env = {
|
||||
@@ -39,38 +56,81 @@ const env = {
|
||||
...
|
||||
},
|
||||
};
|
||||
|
||||
new App(Root, { env }).mount(document.body);
|
||||
|
||||
// or alternatively
|
||||
mount(App, document.body, { env });
|
||||
mount(App, { target: document.body, env });
|
||||
```
|
||||
|
||||
It is also possible to simply share an environment between all root components,
|
||||
by simply doing this:
|
||||
|
||||
```js
|
||||
Component.env = myEnv; // will be the default env for all components
|
||||
```
|
||||
|
||||
Note that this environment is the global owl environment for an application. The
|
||||
next section explains how to extend an environment for a specific sub component
|
||||
and its children.
|
||||
|
||||
## Using a sub environment
|
||||
|
||||
It is sometimes useful to add one (or more) specific keys to the environment,
|
||||
from the perspective of a specific component and its children. In that case, the
|
||||
solution presented above will not work, since it sets the global environment.
|
||||
|
||||
There are two hooks for this situation: [`useSubEnv` and `useChildSubEnv`](hooks.md#usesubenv-and-usechildsubenv).
|
||||
There is a hook for this situation: [`useSubEnv`](hooks.md#usesubenv).
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
setup() {
|
||||
useSubEnv({ myKey: someValue }); // myKey is now available for all child components
|
||||
class FormComponent extends Component {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
useSubEnv({ myKey: someValue });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Content of an Environment
|
||||
|
||||
The `env` object content is totally up to the application developer. However,
|
||||
some good use cases for additional keys in the environment are:
|
||||
Some good use cases for additional keys in the environment are:
|
||||
|
||||
- some configuration keys,
|
||||
- session information,
|
||||
- generic services (such as doing rpcs).
|
||||
- other utility functions that one want to inject, such as a translation function.
|
||||
|
||||
Doing it this way means that components are easily testable: we can simply
|
||||
create a test environment with mock services.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
async function myEnv() {
|
||||
const templates = await loadTemplates();
|
||||
const qweb = new QWeb({ templates });
|
||||
const session = getSession();
|
||||
|
||||
return {
|
||||
_t: myTranslateFunction,
|
||||
session: session,
|
||||
qweb: qweb,
|
||||
services: {
|
||||
localStorage: localStorage,
|
||||
rpc: rpc,
|
||||
},
|
||||
debug: false,
|
||||
inMobileMode: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const env = await myEnv();
|
||||
mount(App, { target: document.body, env });
|
||||
}
|
||||
```
|
||||
|
||||
## Special Keys
|
||||
|
||||
There are two special key/value added by Owl if not provided in the environment:
|
||||
the `QWeb` instance and a `browser` object:
|
||||
|
||||
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
|
||||
for Owl to be able to render anything
|
||||
- `browser`: this is an object that contains some common access points to the
|
||||
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0.
|
||||
|
||||
@@ -3,30 +3,60 @@
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Managing Errors](#managing-errors)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
|
||||
## Overview
|
||||
|
||||
By default, whenever an error occurs in the rendering of an Owl application, we
|
||||
destroy the whole application. Otherwise, we cannot offer any guarantee on the
|
||||
state of the resulting component tree. It might be hopelessly corrupted, but
|
||||
without any user-visible feedback.
|
||||
without any user-visible state.
|
||||
|
||||
Clearly, it is usually a little bit extreme to destroy the application. This
|
||||
is why we need a mechanism to handle rendering errors (and errors coming
|
||||
from lifecycle hooks): the `onError` hook.
|
||||
Clearly, it sometimes is a little bit extreme to destroy the application. This
|
||||
is why we have a builtin mechanism to handle rendering errors (and errors coming
|
||||
from lifecycle hooks): the `catchError` hook.
|
||||
|
||||
The main idea is that the `onError` hook register a function that will be called
|
||||
with the error. This function need to handle the situation, most of the time by
|
||||
updating some state and rerendering itself, so the application can return to a
|
||||
normal state.
|
||||
## Example
|
||||
|
||||
## Managing Errors
|
||||
For example, here is how we could implement an `ErrorBoundary` component:
|
||||
|
||||
Whenever the `onError` lifecycle hook is used, all errors coming from
|
||||
```xml
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">
|
||||
Error handled
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-slot="default" />
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ErrorBoundary extends Component {
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using the `ErrorBoundary` is then extremely simple:
|
||||
|
||||
```xml
|
||||
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
|
||||
```
|
||||
|
||||
Note that we need to be careful here: the fallback UI should not throw any
|
||||
error, otherwise we risk going into an infinite loop (also, see the page on
|
||||
[slots](slots.md) for more information on the `t-slot` directive).
|
||||
|
||||
## Reference
|
||||
|
||||
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
|
||||
sub components rendering and/or lifecycle method calls will be caught and given
|
||||
to the `onError` method. This allows us to properly handle the error, and to
|
||||
to the `catchError` method. This allows us to properly handle the error, and to
|
||||
not break the application.
|
||||
|
||||
There are important things to know:
|
||||
@@ -35,41 +65,17 @@ There are important things to know:
|
||||
Owl will destroy the full application. This is done on purpose, because Owl
|
||||
cannot guarantee that the state is not corrupted from this point on.
|
||||
|
||||
- errors coming from event handlers are NOT managed by `onError` or any other
|
||||
- errors coming from event handlers are NOT managed by `catchError` or any other
|
||||
owl mechanism. This is up to the application developer to properly recover
|
||||
from an error
|
||||
|
||||
- if an error handler is unable to properly handle an error, it can just rethrow
|
||||
an error, and Owl will try looking for another error handler up the component
|
||||
tree.
|
||||
|
||||
## Example
|
||||
|
||||
For example, here is how we could implement a generic component `ErrorBoundary`
|
||||
that render its content, and a fallback if an error happened.
|
||||
Also, it may be useful to know that whenever an error is caught, it is then
|
||||
broadcasted to the application by an event on the `qweb` instance. It may be
|
||||
useful, for example, to log the error somewhere.
|
||||
|
||||
```js
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<t t-if="state.error" t-slot="fallback">An error occurred</t>
|
||||
<t t-else="" t-slot="default"/>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ error: false });
|
||||
onError(() => (this.state.error = true));
|
||||
}
|
||||
}
|
||||
env.qweb.on("error", null, function (error) {
|
||||
// do something
|
||||
// react to the error
|
||||
});
|
||||
```
|
||||
|
||||
Using the `ErrorBoundary` is then simple simple:
|
||||
|
||||
```xml
|
||||
<ErrorBoundary>
|
||||
<SomeOtherComponent/>
|
||||
<t t-set-slot="fallback">Some specific error message</t>
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
Note that we need to be careful here: the fallback UI should not throw any
|
||||
error, otherwise we risk going into an infinite loop (also, see the page on
|
||||
[slots](slots.md) for more information on the `t-slot` directive).
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# 🦉 Event Bus 🦉
|
||||
|
||||
It is sometimes useful to use a `Bus` to communicate informations between various
|
||||
parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
||||
triggering events, and callbacks.
|
||||
|
||||
```js
|
||||
const bus = new owl.core.EventBus();
|
||||
|
||||
bus.on("some-event", null, function (...args) {
|
||||
console.log(...args);
|
||||
});
|
||||
|
||||
bus.trigger("some-event", 1, 2, 3);
|
||||
// [1,2,3] will be logged to the console
|
||||
```
|
||||
|
||||
Its API is:
|
||||
|
||||
| Method | Description |
|
||||
| -------------------------------- | --------------------------------- |
|
||||
| `on(eventType, owner, callback)` | add a listener |
|
||||
| `off(eventType, owner)` | remove all listeners for an owner |
|
||||
| `trigger(eventType, ...args)` | trigger an event |
|
||||
| `clear` | remove all subscriptions |
|
||||
@@ -3,15 +3,22 @@
|
||||
## Content
|
||||
|
||||
- [Event Handling](#event-handling)
|
||||
- [Business DOM Events](#business-dom-events)
|
||||
- [Inline Event Handlers](#inline-event-handlers)
|
||||
- [Modifiers](#modifiers)
|
||||
- [Synthetic Events](#synthetic-events)
|
||||
- [On Components](#on-components)
|
||||
|
||||
## Event Handling
|
||||
|
||||
In a component's template, it is useful to be able to register handlers on DOM
|
||||
elements to some specific events. This is what makes a template _alive_. This
|
||||
is done with the `t-on` directive. For example:
|
||||
elements to some specific events. This is what makes a template _alive_. There
|
||||
are four different use cases.
|
||||
|
||||
1. Register an event handler on a DOM node (_pure_ DOM event)
|
||||
2. Register an event handler on a component (_pure_ DOM event)
|
||||
3. Register an event handler on a DOM node (_business_ DOM event)
|
||||
4. Register an event handler on a component (_business_ DOM event)
|
||||
|
||||
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod">Do something</button>
|
||||
@@ -24,28 +31,93 @@ button.addEventListener("click", component.someMethod.bind(component));
|
||||
```
|
||||
|
||||
The suffix (`click` in this example) is simply the name of the actual DOM
|
||||
event. The value of the `t-on` expression should be a valid javascript expression
|
||||
that evaluates to a function in the context of the current component. So, one
|
||||
can get a reference to the event, or pass some additional arguments. For example,
|
||||
all the following expressions are valid:
|
||||
event.
|
||||
|
||||
## Business DOM Events
|
||||
|
||||
A _business_ DOM event is triggered by a call to `trigger` on a component.
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod">Do something</button>
|
||||
<button t-on-click="() => this.increment(3)">Add 3</button>
|
||||
<button t-on-click="ev => this.doStuff(ev, 'value')">Do something</button>
|
||||
<MyComponent t-on-menu-loaded="someMethod" />
|
||||
```
|
||||
|
||||
Notice the use of the `this` keyword in the lambda function: this is the
|
||||
correct way to call a method on the component in a lambda function.
|
||||
```js
|
||||
class MyComponent {
|
||||
someWhere() {
|
||||
const payload = ...;
|
||||
this.trigger('menu-loaded', payload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
One could use the following expression:
|
||||
The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
|
||||
with an additional attribute `originalComponent` (the component that triggered
|
||||
the event). The generated event is of type `menu-loaded` and dispatches it on
|
||||
the component's DOM element (`this.el`). The event bubbles and is cancelable.
|
||||
The parent component listening to event `menu-loaded` will receive the payload
|
||||
in its `someMethod` handler (in the `detail` property of the event), whenever
|
||||
the event is triggered.
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
someMethod(ev) {
|
||||
const payload = ev.detail;
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By convention, we use KebabCase for the name of _business_ events.
|
||||
|
||||
The `t-on` directive allows to prebind its arguments. For example,
|
||||
|
||||
```xml
|
||||
<button t-on-click="() => increment(3)">Add 3</button>
|
||||
<button t-on-click="someMethod(expr)">Do something</button>
|
||||
```
|
||||
|
||||
But then, the increment function may be unbound (unless the component binds it
|
||||
in its setup function, for example).
|
||||
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
|
||||
from the rendering context.
|
||||
|
||||
### Type Hinting
|
||||
|
||||
Note that if you work with Typescript, the `trigger` method is generic on the type of the payload.
|
||||
|
||||
You can then describe the type of the event, so you will see typing errors...
|
||||
|
||||
```typescript
|
||||
this.trigger<MyCustomPayload>("my-custom-event", payload);
|
||||
```
|
||||
|
||||
```typescript
|
||||
myCustomEventHandler(ev: OwlEvent<MyCustomPayload>) { ... }
|
||||
```
|
||||
|
||||
## Inline Event Handlers
|
||||
|
||||
One can also directly specify inline statements. For example,
|
||||
|
||||
```xml
|
||||
<button t-on-click="state.counter++">Increment counter</button>
|
||||
```
|
||||
|
||||
Here, `state` must be defined in the rendering context (typically the component)
|
||||
as it will be translated to:
|
||||
|
||||
```js
|
||||
button.addEventListener("click", () => {
|
||||
context.state.counter++;
|
||||
});
|
||||
```
|
||||
|
||||
Warning: inline expressions are evaluated in the context of the template. This
|
||||
means that they can access the component methods and properties. But if they set
|
||||
a key, the inline statement will actually not modify the component, but a key in
|
||||
a sub scope.
|
||||
|
||||
```xml
|
||||
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
|
||||
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
|
||||
```
|
||||
|
||||
## Modifiers
|
||||
|
||||
@@ -53,13 +125,12 @@ In order to remove the DOM event details from the event handlers (like calls to
|
||||
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
|
||||
specified as additional suffixes of the `t-on` directive.
|
||||
|
||||
| Modifier | Description |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `.stop` | calls `event.stopPropagation()` before calling the method |
|
||||
| `.prevent` | calls `event.preventDefault()` before calling the method |
|
||||
| `.self` | calls the method only if the `event.target` is the element itself |
|
||||
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
|
||||
| `.synthetic` | define a synthetic event handler (see below) |
|
||||
| Modifier | Description |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `.stop` | calls `event.stopPropagation()` before calling the method |
|
||||
| `.prevent` | calls `event.preventDefault()` before calling the method |
|
||||
| `.self` | calls the method only if the `event.target` is the element itself |
|
||||
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
|
||||
|
||||
```xml
|
||||
<button t-on-click.stop="someMethod">Do something</button>
|
||||
@@ -78,42 +149,3 @@ modifiers. For example,
|
||||
```
|
||||
|
||||
This will simply stop the propagation of the event.
|
||||
|
||||
## Synthetic Events
|
||||
|
||||
In some cases, attaching an event handler for each element of large lists has
|
||||
a non trivial cost. Owl provides a way to efficiently improve the performance:
|
||||
with synthetic event, it actually adds only one handler on the document body,
|
||||
and will properly call the handler, just as expected.
|
||||
|
||||
The only difference with regular events is that the event is caught at the document
|
||||
body, so it cannot be stopped before it actually gets there. Since it may be
|
||||
surprising in some cases, it is not enabled by default.
|
||||
|
||||
To enable it, one can just use the `.synthetic` suffix:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<t t-foreach="largeList" t-as="elem" t-key="elem.id">
|
||||
<button t-on-click.synthetic="doSomething" ...>
|
||||
<!-- some content -->
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
## On Components
|
||||
|
||||
The `t-on` directive also works on a child component:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
in some template
|
||||
<Child t-on-click="dosomething"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
This will catch all click events on any html element contained in the `Child`
|
||||
sub component. Note that if the child component is reduced to one (or more) text
|
||||
nodes, then clicking on it will not call the handler, since the event will be
|
||||
dispatched by the browser on the parent element (a `div` in this case).
|
||||
|
||||
@@ -3,17 +3,24 @@
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [The Hook Rule](#the-hook-rule)
|
||||
- [Lifecycle hooks](#lifecycle-hooks)
|
||||
- [Other hooks](#other-hooks)
|
||||
- [Example: Mouse Position](#example-mouse-position)
|
||||
- [Example: Autofocus](#example-autofocus)
|
||||
- [Reference](#reference)
|
||||
- [One Rule](#one-rule)
|
||||
- [`useState`](#usestate)
|
||||
- [`onMounted`](#onmounted)
|
||||
- [`onWillUnmount`](#onwillunmount)
|
||||
- [`onWillPatch`](#onwillpatch)
|
||||
- [`onPatched`](#onpatched)
|
||||
- [`onWillStart`](#onwillstart)
|
||||
- [`onWillUpdateProps`](#onwillupdateprops)
|
||||
- [`useContext`](#usecontext)
|
||||
- [`useRef`](#useref)
|
||||
- [`useSubEnv` and `useChildSubEnv`](#usesubenv-and-usechildsubenv)
|
||||
- [`useSubEnv`](#usesubenv)
|
||||
- [`useExternalListener`](#useexternallistener)
|
||||
- [`useComponent`](#usecomponent)
|
||||
- [`useEnv`](#useenv)
|
||||
- [`useEffect`](#useeffect)
|
||||
- [Example: Mouse Position](#example-mouse-position)
|
||||
- [Making customized hooks](#making-customized-hooks)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -32,9 +39,96 @@ Hooks work beautifully with Owl components: they solve the problems mentioned
|
||||
above, and in particular, they are the perfect way to make your component
|
||||
reactive.
|
||||
|
||||
## The Hook Rule
|
||||
## Example: mouse position
|
||||
|
||||
There is only one rule: every hook for a component has to be called in the _setup_ method, or in class fields:
|
||||
Here is the classical example of a non trivial hook to track the mouse position.
|
||||
|
||||
```js
|
||||
const { useState, onMounted, onWillUnmount } = owl.hooks;
|
||||
|
||||
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||
// position
|
||||
function useMouse() {
|
||||
const position = useState({ x: 0, y: 0 });
|
||||
|
||||
function update(e) {
|
||||
position.x = e.clientX;
|
||||
position.y = e.clientY;
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener("mousemove", update);
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
window.removeEventListener("mousemove", update);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
static template = xml`
|
||||
<div t-name="App">
|
||||
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||
</div>`;
|
||||
|
||||
// this hooks is bound to the 'mouse' property.
|
||||
mouse = useMouse();
|
||||
}
|
||||
```
|
||||
|
||||
Note that we use the prefix `use` for hooks, just like in React. This is just
|
||||
a convention.
|
||||
|
||||
## Example: autofocus
|
||||
|
||||
Hooks can be combined to create the desired effect. For example, the following
|
||||
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
|
||||
to create an easy way to focus an input whenever it appears in the DOM:
|
||||
|
||||
```js
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
let isInDom = false;
|
||||
function updateFocus() {
|
||||
if (!isInDom && ref.el) {
|
||||
isInDom = true;
|
||||
ref.el.focus();
|
||||
} else if (isInDom && !ref.el) {
|
||||
isInDom = false;
|
||||
}
|
||||
}
|
||||
onPatched(updateFocus);
|
||||
onMounted(updateFocus);
|
||||
}
|
||||
```
|
||||
|
||||
This hook takes the name of a valid `t-ref` directive, which should be present
|
||||
in the template. It then checks whenever the component is mounted or patched if
|
||||
the reference is not valid, and in this case, it will focus the node element.
|
||||
This hook can be used like this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input />
|
||||
<input t-ref="myinput"/>
|
||||
</div>`;
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
useAutofocus("myinput");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### One rule
|
||||
|
||||
There is only one rule: every hook for a component has to be called in the
|
||||
constructor, in the _setup_ method, or in class fields:
|
||||
|
||||
```js
|
||||
// ok
|
||||
@@ -42,6 +136,14 @@ class SomeComponent extends Component {
|
||||
state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
// also ok
|
||||
class SomeComponent extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// also ok
|
||||
class SomeComponent extends Component {
|
||||
setup() {
|
||||
@@ -57,24 +159,15 @@ class SomeComponent extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Hooks
|
||||
As you can see, the `useState` hook does not need to be given a reference to
|
||||
the component. This is possible because there is a way to get a reference to the
|
||||
current component: the `Component.current` static property is the reference to the
|
||||
component instance that is currently being created.
|
||||
|
||||
All lifecycle hooks are documented in detail in their specific [section](component.md#lifecycle).
|
||||
|
||||
| Hook | Description |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| **[onWillStart](component.md#willstart)** | async, before first rendering |
|
||||
| **[onWillRender](component.md#willrender)** | just before component is rendered |
|
||||
| **[onRendered](component.md#rendered)** | just after component is rendered |
|
||||
| **[onMounted](component.md#mounted)** | just after component is rendered and added to the DOM |
|
||||
| **[onWillUpdateProps](component.md#willupdateprops)** | async, before props update |
|
||||
| **[onWillPatch](component.md#willpatch)** | just before the DOM is patched |
|
||||
| **[onPatched](component.md#patched)** | just after the DOM is patched |
|
||||
| **[onWillUnmount](component.md#willunmount)** | just before removing component from DOM |
|
||||
| **[onWillDestroy](component.md#willdestroy)** | just before component is destroyed |
|
||||
| **[onError](component.md#onerror)** | catch and handle errors (see [error handling page](error_handling.md)) |
|
||||
|
||||
## Other Hooks
|
||||
Hooks need to be called in the constructor to ensure that this reference is
|
||||
properly set. This is also a good thing for performance reasons (Owl can use
|
||||
this to optimize its implementation), and for a clean architecture (this makes
|
||||
it easier for developers to understand what is really happening in a component).
|
||||
|
||||
### `useState`
|
||||
|
||||
@@ -85,9 +178,9 @@ The `useState` hook has to be given an object or an array, and will return
|
||||
an observed version of it (using a `Proxy`).
|
||||
|
||||
```javascript
|
||||
const { useState, Component } = owl;
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class Counter extends Component {
|
||||
class Counter extends owl.Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
@@ -104,99 +197,179 @@ class Counter extends Component {
|
||||
It is important to remember that `useState` only works with objects or arrays. It
|
||||
is necessary, since Owl needs to react to a change in state.
|
||||
|
||||
### `onMounted`
|
||||
|
||||
`onMounted` is not a user hook, but is a building block designed to help make useful
|
||||
abstractions. `onMounted` registers a callback, which will be called when the component
|
||||
is mounted (see example on top of this page).
|
||||
|
||||
### `onWillUnmount`
|
||||
|
||||
`onWillUnmount` is not a user hook, but is a building block designed to help make useful
|
||||
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
||||
is unmounted (see example on top of this page).
|
||||
|
||||
### `onWillPatch`
|
||||
|
||||
`onWillPatch` is not a user hook, but is a building block designed to help make useful
|
||||
abstractions. `onWillPatch` registers a callback, which will be called just
|
||||
before the component patched.
|
||||
|
||||
### `onPatched`
|
||||
|
||||
`onPatched` is not a user hook, but is a building block designed to help make useful
|
||||
abstractions. `onPatched` registers a callback, which will be called just
|
||||
after the component patched.
|
||||
|
||||
### `onWillStart`
|
||||
|
||||
`onWillStart` is an asynchronous hook. This means that the function registered
|
||||
in the hook will be run just before the component is first rendered and can return a
|
||||
promise, to express the fact that it is an asynchronous operation.
|
||||
|
||||
Note that if there are more than one `onWillStart` registered callback, then they
|
||||
will all be run in parallel.
|
||||
|
||||
It can be used to load some initial data. For example, the following hook will
|
||||
automatically load some data from the server, and return an object that will
|
||||
be ready whenever the component is rendered:
|
||||
|
||||
```js
|
||||
function useLoader() {
|
||||
const component = Component.current;
|
||||
const record = useState({});
|
||||
onWillStart(async () => {
|
||||
const recordId = component.props.id;
|
||||
Object.assign(record, await fetchSomeRecord(recordId));
|
||||
});
|
||||
return record;
|
||||
}
|
||||
```
|
||||
|
||||
Note that this example does not update the record value whenever props are
|
||||
updated. For that situation, we need to use the `onWillUpdateProps` hook.
|
||||
|
||||
### `onWillUpdateProps`
|
||||
|
||||
Just like `onWillStart`, `onWillUpdateProps` is an asynchronous hook. It is
|
||||
designed to be run whenever the component props are updated. This could be
|
||||
useful to perform some asynchronous task such as fetching updated data.
|
||||
|
||||
```js
|
||||
function useLoader() {
|
||||
const component = Component.current;
|
||||
const record = useState({});
|
||||
|
||||
async function updateRecord(id) {
|
||||
Object.assign(record, await fetchSomeRecord(id));
|
||||
}
|
||||
|
||||
onWillStart(() => updateRecord(component.props.id));
|
||||
onWillUpdateProps((nextProps) => updateRecord(nextProps.id));
|
||||
|
||||
return record;
|
||||
}
|
||||
```
|
||||
|
||||
Note that if there are more than one `onWillUpdateProps` registered callback,
|
||||
then they will all be run in parallel.
|
||||
|
||||
### `useContext`
|
||||
|
||||
See [`useContext`](context.md#usecontext) for reference documentation.
|
||||
|
||||
### `useRef`
|
||||
|
||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||
of a component, rendered by Owl. It only work on a html element tagged by the
|
||||
`t-ref` directive:
|
||||
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||
tagged by the `t-ref` directive:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-ref="someInput"/>
|
||||
<span>hello</span>
|
||||
<div t-ref="someDiv"/>
|
||||
<SubComponent t-ref="someComponent"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the component will be able to access the `input` with the `useRef` hook:
|
||||
In this example, the component will be able to access the `div` and the component
|
||||
`SubComponent` using the `useRef` hook:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
inputRef = useRef("someInput");
|
||||
subRef = useRef("someComponent");
|
||||
divRef = useRef("someDiv");
|
||||
|
||||
someMethod() {
|
||||
// here, if component is mounted, refs are active:
|
||||
// - this.inputRef.el is the input HTMLElement
|
||||
// - this.divRef.el is the div HTMLElement
|
||||
// - this.subRef.comp is the instance of the sub component
|
||||
// - this.subRef.el is the root HTML node of the sub component (i.e. this.subRef.comp.el)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As shown by the example above, the actual HTMLElement instance is accessed with
|
||||
the `el` key.
|
||||
As shown by the example above, html elements are accessed by using the `el`
|
||||
key, and components references are accessed with `comp`.
|
||||
|
||||
Notes:
|
||||
|
||||
- if used on a component, the reference will be set in the `refs`
|
||||
variable between `willPatch` and `patched`,
|
||||
- on a component, accessing `ref.el` will get the root node of the component.
|
||||
|
||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](templates.md#dynamic-attributes) and
|
||||
(like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) and
|
||||
`t-component` directives). For example,
|
||||
|
||||
```xml
|
||||
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>
|
||||
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||
```
|
||||
|
||||
Here, the references need to be set like this:
|
||||
|
||||
```js
|
||||
this.ref1 = useRef("div_1");
|
||||
this.ref2 = useRef("div_2");
|
||||
this.ref1 = useRef("component_1");
|
||||
this.ref2 = useRef("component_2");
|
||||
```
|
||||
|
||||
References are only guaranteed to be active while the parent component is mounted.
|
||||
If this is not the case, accessing `el` on it will return `null`.
|
||||
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
||||
|
||||
### `useSubEnv` and `useChildSubEnv`
|
||||
### `useSubEnv`
|
||||
|
||||
The environment is sometimes useful to share some common information between
|
||||
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
|
||||
|
||||
For example, if we have a form view component, maybe we would like to make some
|
||||
`model` object available to all sub components, but not to the whole application.
|
||||
This is where the `useChildSubEnv` hook may be useful: it lets a component add some
|
||||
information to the environment in a way that only its children
|
||||
This is where the `useSubEnv` hook may be useful: it lets a component add some
|
||||
information to the environment in a way that only the component and its children
|
||||
can access it:
|
||||
|
||||
```js
|
||||
class FormComponent extends Component {
|
||||
setup() {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
const model = makeModel();
|
||||
// model will be available on this.env for this component and all children
|
||||
useSubEnv({ model });
|
||||
// someKey will be available on this.env for all children
|
||||
useChildSubEnv({ someKey: "value" });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `useSubEnv` and `useChildSubEnv` hooks take one argument: an object which
|
||||
contains some key/value that will be added to the current environment. These hooks
|
||||
will create a new env object with the new information:
|
||||
|
||||
- `useSubEnv` will assign this new `env` to itself and to all children components
|
||||
- `useChildSubEnv` will only assign this new `env` to all children components.
|
||||
|
||||
As usual in Owl, [environments](environment.md) created with these two hooks are
|
||||
frozen, to prevent unwanted modifications.
|
||||
|
||||
Note that both these hooks can be called an arbitrary number of times. The `env`
|
||||
will then be updated accordingly.
|
||||
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
|
||||
the parent environment. And of course, the parent environment will not be
|
||||
affected.
|
||||
|
||||
### `useExternalListener`
|
||||
|
||||
The `useExternalListener` hook helps solve a very common problem: adding and removing
|
||||
a listener on some target whenever a component is mounted/unmounted. It takes a target
|
||||
as its first argument, forwards the other arguments to `addEventListener`. For example,
|
||||
a listener on some target whenever a component is mounted/unmounted. For example,
|
||||
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
|
||||
to be closed:
|
||||
|
||||
```js
|
||||
useExternalListener(window, "click", this.closeMenu, { capture: true });
|
||||
useExternalListener(window, "click", this.closeMenu);
|
||||
```
|
||||
|
||||
### `useComponent`
|
||||
@@ -204,122 +377,70 @@ useExternalListener(window, "click", this.closeMenu, { capture: true });
|
||||
The `useComponent` hook is useful as a building block for some customized hooks,
|
||||
that may need a reference to the component calling them.
|
||||
|
||||
```js
|
||||
function useSomething() {
|
||||
const component = useComponent();
|
||||
// now, component is bound to the instance of the current component
|
||||
}
|
||||
```
|
||||
|
||||
### `useEnv`
|
||||
|
||||
The `useEnv` hook is useful as a building block for some customized hooks,
|
||||
that may need a reference to the env of the component calling them.
|
||||
|
||||
```js
|
||||
function useSomething() {
|
||||
const env = useEnv();
|
||||
// now, env is bound to the env of the current component
|
||||
}
|
||||
```
|
||||
### Making customized hooks
|
||||
|
||||
### `useEffect`
|
||||
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.
|
||||
|
||||
This hook will run a callback when a component is mounted and patched, and
|
||||
will run a cleanup function before patching and before unmounting the
|
||||
the component (only if some dependencies have changed).
|
||||
But, like every good things in life, hooks should be used with moderation. They are
|
||||
not the solution to every problem.
|
||||
|
||||
It has almost the same API as the React `useEffect` hook, except that the dependencies
|
||||
are defined by a function instead of just the dependencies.
|
||||
- they may be overkill: if your component needs to perform some action specific
|
||||
to itself (so, the specific code does not need to be shared), there is nothing
|
||||
wrong with a simple class method:
|
||||
|
||||
The `useEffect` hook takes two function: the effect function and the dependency
|
||||
function. The effect function perform some task and return (optionally) a cleanup
|
||||
function. The dependency function returns a list of dependencies, these dependencies
|
||||
are passed as parameters in the effect function . If any of these
|
||||
dependencies changes, then the current effect will be cleaned up and reexecuted.
|
||||
|
||||
Here is an example without any dependencies:
|
||||
|
||||
```js
|
||||
useEffect(
|
||||
() => {
|
||||
window.addEventListener("mousemove", someHandler);
|
||||
return () => window.removeEventListener("mousemove", someHandler);
|
||||
},
|
||||
() => []
|
||||
);
|
||||
```
|
||||
|
||||
In the example above, the dependency list is empty, so the effect is only cleaned
|
||||
up when the component is unmounted.
|
||||
|
||||
If the dependency function is skipped, then the effect will be cleaned up and
|
||||
rerun at every patch.
|
||||
|
||||
Here is another example, of how one could implement a `useAutofocus` hook with
|
||||
the `useEffect` hook:
|
||||
|
||||
```js
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
useEffect(
|
||||
(el) => el && el.focus(),
|
||||
() => [ref.el]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This hook takes the name of a valid `t-ref` directive, which should be present
|
||||
in the template. It then checks whenever the component is mounted or patched if
|
||||
the reference is not valid, and in this case, it will focus the node element.
|
||||
This hook can be used like this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input />
|
||||
<input t-ref="myinput"/>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
useAutofocus("myinput");
|
||||
```js
|
||||
// maybe overkill
|
||||
class A extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
useMySpecificHook();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: mouse position
|
||||
|
||||
Here is the classical example of a non trivial hook to track the mouse position.
|
||||
|
||||
```js
|
||||
const { useState, onWillDestroy, Component } = owl;
|
||||
|
||||
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||
// position
|
||||
function useMouse() {
|
||||
const position = useState({ x: 0, y: 0 });
|
||||
|
||||
function update(e) {
|
||||
position.x = e.clientX;
|
||||
position.y = e.clientY;
|
||||
// ok
|
||||
class B extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.performSpecificTask();
|
||||
}
|
||||
}
|
||||
window.addEventListener("mousemove", update);
|
||||
onWillDestroy(() => {
|
||||
window.removeEventListener("mousemove", update);
|
||||
});
|
||||
```
|
||||
|
||||
return position;
|
||||
}
|
||||
Note that the second solution is easier to extend in sub components.
|
||||
|
||||
// Main root component
|
||||
class Root extends Component {
|
||||
static template = xml`<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>`;
|
||||
- they may be harder to test: if a customized hook injects 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:
|
||||
|
||||
// this hooks is bound to the 'mouse' property.
|
||||
mouse = useMouse();
|
||||
}
|
||||
```
|
||||
```js
|
||||
const router = new Router(...);
|
||||
|
||||
Note that we use the prefix `use` for hooks, just like in React. This is just
|
||||
a convention.
|
||||
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() {
|
||||
const env = useEnv();
|
||||
return 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.
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# 🦉 Form Input Bindings 🦉
|
||||
|
||||
It is very common to need to be able to read the value out of an html `input` (or
|
||||
`textarea`, or `select`) in order to use it (note: it does not need to be in a
|
||||
form!). A possible way to do this is to do it by hand:
|
||||
|
||||
```js
|
||||
class Form extends owl.Component {
|
||||
state = useState({ text: "" });
|
||||
|
||||
_updateInputValue(event) {
|
||||
this.state.text = event.target.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-on-input="_updateInputValue" />
|
||||
<span t-esc="state.text" />
|
||||
</div>
|
||||
```
|
||||
|
||||
This works. However, this requires a little bit of _plumbing_ code. Also, the
|
||||
plumbing code is slightly different if you need to interact with a checkbox,
|
||||
or with radio buttons, or with select tags.
|
||||
|
||||
To help with this situation, Owl has a builtin directive `t-model`: its value
|
||||
should be an observed value in the component (usually `state.someValue`). With
|
||||
the `t-model` directive, we can write a shorter code, equivalent to the previous
|
||||
example:
|
||||
|
||||
```js
|
||||
class Form extends owl.Component {
|
||||
state = { text: "" };
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-model="state.text" />
|
||||
<span t-esc="state.text" />
|
||||
</div>
|
||||
```
|
||||
|
||||
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
|
||||
`<input type="radio">`, `<textarea>` and `<select>`:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div>Text in an input: <input t-model="state.someVal"/></div>
|
||||
<div>Textarea: <textarea t-model="state.otherVal"/></div>
|
||||
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
|
||||
<div>Selection:
|
||||
<select t-model="state.color">
|
||||
<option value="">Select a color</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
Selection with radio buttons:
|
||||
<span>
|
||||
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
|
||||
<label for="red">Red</label>
|
||||
</span>
|
||||
<span>
|
||||
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
|
||||
<label for="blue">Blue</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Like event handling, the `t-model` directive accepts the following modifiers:
|
||||
|
||||
| Modifier | Description |
|
||||
| --------- | -------------------------------------------------------------------- |
|
||||
| `.lazy` | update the value on the `change` event (default is on `input` event) |
|
||||
| `.number` | try to parse the value to a number (using `parseFloat`) |
|
||||
| `.trim` | trim the resulting value |
|
||||
|
||||
For example:
|
||||
|
||||
```xml
|
||||
<input t-model.lazy="state.someVal" />
|
||||
```
|
||||
|
||||
These modifiers can be combined. For instance, `t-model.lazy.number` will only
|
||||
update a number whenever the change is done.
|
||||
|
||||
Note: the online playground has an example to show how it works.
|
||||
@@ -0,0 +1,133 @@
|
||||
# 🦉 Miscellaneous 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Portal](#portal)
|
||||
- [AsyncRoot](#asyncroot)
|
||||
|
||||
## `Portal`
|
||||
|
||||
### Overview
|
||||
|
||||
The component `Portal` is meant to be used as a transparent way to 'teleport' a piece
|
||||
of DOM to the node represented by its sole `target` props.
|
||||
|
||||
This component aims at helping the implementation of the needed infrastructure
|
||||
for modals (as in `bootstrap-modal`).
|
||||
|
||||
### Usage
|
||||
|
||||
The content it will teleport is defined within the `<Portal>` node and
|
||||
internally uses the `default` [Slot](slots.md).
|
||||
|
||||
This slot must contain only **one** node, which in turn can have as many children as necessary.
|
||||
|
||||
The element under which the content will be teleported is represented as a selector
|
||||
by the `target` props which only accepts a string as value.
|
||||
|
||||
The `target` props only supports static selector, and is not meant to be passed to `Portal`
|
||||
as a variable. Namely, `<Portal target="'body'" />` is the intended use.
|
||||
By contrast, `<Portal target="state.target" />` is not supported.
|
||||
|
||||
The component `Portal` has no particular state, rather it is meant to be a slave to its parent,
|
||||
and ultimately just a way for the parent to teleport a piece of its own DOM elsewhere.
|
||||
|
||||
The `Portal`'s root node is always `<portal/>` and is placed where the teleported content
|
||||
_would have_ been. It is this element that the [teleported events](#expected-behaviors) are re-directed on.
|
||||
|
||||
### Example
|
||||
|
||||
The canonic use-case is to implement a Dialog, where a Component may choose to break the natural
|
||||
workflow to help the user put in some data, which it could use later on.
|
||||
|
||||
JavaScript:
|
||||
|
||||
```js
|
||||
const { Component, mount } = owl;
|
||||
const { Portal } = owl.misc;
|
||||
|
||||
class TeleportedComponent extends Component {}
|
||||
class App extends Component {
|
||||
static components = { Portal, TeleportedComponent };
|
||||
}
|
||||
|
||||
mount(App, { target: document.body });
|
||||
```
|
||||
|
||||
XML:
|
||||
|
||||
```xml
|
||||
<templates>
|
||||
<div t-name="TeleportedComponent">
|
||||
<span>I will move soon enough</span>
|
||||
</div>
|
||||
|
||||
<div t-name="App">
|
||||
<span>I am like the rest of us</span>
|
||||
<Portal target="'body'">
|
||||
<TeleportedComponent />
|
||||
</Portal>
|
||||
</div>
|
||||
</templates>
|
||||
```
|
||||
|
||||
In this example, the `Portal` component will teleport the `TeleportedComponent`'s `div` as a child of the `body`.
|
||||
`TeleportedComponent` is acting as a Dialog here.
|
||||
|
||||
The resulting DOM will look like:
|
||||
|
||||
```xml
|
||||
<body>
|
||||
<div>
|
||||
<span>I am like the rest of us</span>
|
||||
<portal></portal>
|
||||
</div>
|
||||
<div>
|
||||
<span>I will move soon enough</span>
|
||||
</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
### Expected Behaviors
|
||||
|
||||
The teleported piece is updated as any other `Component`'s DOM and in the same sequence.
|
||||
Namely the teleported piece will be updated in function of its parents components, and patched as
|
||||
a normal child.
|
||||
|
||||
The [_business_ events](event_handling.md#business-dom-events) triggered by a child component will be stopped
|
||||
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
|
||||
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
|
||||
|
||||
Beware that those re-directed events are copies of the original event.
|
||||
They have:
|
||||
|
||||
- The same payload.
|
||||
- The same `originalComponent` than their original counterpart,
|
||||
that is the actual Component that triggered it.
|
||||
- A **different** `target` property than their original counterpart.
|
||||
The `target` of a re-directed event is necessarily the `Portal`'s root node.
|
||||
|
||||
Pure DOM events do not follow this pattern and are free to bubble their natural, unaltered way
|
||||
up to the `body`.
|
||||
|
||||
## `AsyncRoot`
|
||||
|
||||
When this component is used, a new rendering sub tree is created, such that the
|
||||
rendering of that component (and its children) is not tied to the rendering of
|
||||
the rest of the interface. It can be used on an asynchronous component, to
|
||||
prevent it from delaying the rendering of the whole interface, or on a
|
||||
synchronous one, such that its rendering isn't delayed by other (asynchronous)
|
||||
components. Note that this directive has no effect on the first rendering, but
|
||||
only on subsequent ones (triggered by state or props changes).
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<SyncChild />
|
||||
<AsyncRoot>
|
||||
<AsyncChild/>
|
||||
</AsyncRoot>
|
||||
</div>
|
||||
```
|
||||
|
||||
The `AsyncRoot` assumes that there is exactly one root node inside it. It can
|
||||
be a dom node or a component.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,52 @@
|
||||
# 🦉 Observer 🦉
|
||||
|
||||
Owl needs to be able to react to state changes. For example, whenever the state
|
||||
of a component is changed, Owl needs to rerender it. To help with that, there is
|
||||
an Observer class. Its job is to observe the state of an object (or array), and
|
||||
to react to any change. The observer is implemented with the native `Proxy`
|
||||
object. Note that this means that it will not work on older browsers.
|
||||
|
||||
Note that the `Observer` is used by the `useState` and `useContext` hooks. This
|
||||
is the way most Owl applications will create observers. For the majority of
|
||||
use cases, there is no need to directly instantiate an observer.
|
||||
|
||||
## Example
|
||||
|
||||
For example, this code will display `update` in the console:
|
||||
|
||||
```javascript
|
||||
const observer = new owl.core.Observer();
|
||||
observer.notifyCB = () => console.log("update");
|
||||
const obj = observer.observe({ a: { b: 1 } });
|
||||
|
||||
obj.a.b = 2;
|
||||
```
|
||||
|
||||
This example shows that an observer can observe nested properties.
|
||||
|
||||
## Reference
|
||||
|
||||
**observe** An observer can observe multiple values with the `observe` method.
|
||||
This method takes an object or an array as its argument and will return a proxy
|
||||
(which is mapped to the initial object/array). With this proxy, the observer
|
||||
can detect whenever any internal value is changed.
|
||||
|
||||
**Registering a callback** Whenever an observer sees a state change, it will
|
||||
call its `notifyCB` method. No additional information is given to the callback.
|
||||
|
||||
**deepRevNumber** Each observed value has an internal revision number, which
|
||||
is incremented every time the value is observed. Sometimes, it can be useful
|
||||
to obtain that number:
|
||||
|
||||
```js
|
||||
const observer = new owl.core.Observer();
|
||||
const obj = observer.observe({ a: { b: 1 } });
|
||||
|
||||
observer.revNumber(obj.a); // 1
|
||||
obj.a.b = 2;
|
||||
|
||||
observer.revNumber(obj.a); // 2
|
||||
```
|
||||
|
||||
The `revNumber` can also return 0, which indicates that the value is not
|
||||
observed.
|
||||
@@ -1,17 +0,0 @@
|
||||
# 🦉 Portal 🦉
|
||||
|
||||
It is sometimes useful to be able to render some content outside the boundaries
|
||||
of a component. To do that, Owl provides a special directive: `t-portal`:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>this is inside the component</div>
|
||||
<div t-portal="'body'">and this is outside</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
The `t-portal` directive takes a valid css selector as argument. The content of
|
||||
the portalled template will be mounted at the corresponding location. Note that
|
||||
Owl need to insert an empty text node at the location of the portalled content.
|
||||
@@ -1,30 +0,0 @@
|
||||
# 🦉 Precompiling templates 🦉
|
||||
|
||||
Owl is designed to be used by the Odoo javascript framework. Since Odoo handles
|
||||
its assets in its own non standard way, it was decided/assumed that Owl would
|
||||
compile templates at runtime.
|
||||
|
||||
However, in some cases, it is not optimal, or even worse, not possible to do that.
|
||||
For example, browser extensions do not allow javascript code to create a new
|
||||
function (using the `new Function(...)` syntax).
|
||||
|
||||
Therefore, in these cases, it is required to compile templates ahead of time. It
|
||||
is possible to do that in Owl, but the tooling is still rough. For now, the
|
||||
process is the following:
|
||||
|
||||
1. write your templates in xml files (with a `t-name` directive to declare the name
|
||||
of the template)
|
||||
2. Compile them in a `templates.js` file
|
||||
3. get the `owl.iife.runtime.js` file (which is a owl build without the compiler)
|
||||
4. bundle `owl.iife.runtime.js` and `template.js` with your assets (owl needs to
|
||||
be positioned before the templates)
|
||||
|
||||
Here is a more detailed explanation on how to compile xml files into a js file:
|
||||
|
||||
1. clone the owl repository locally
|
||||
2. `npm install` to install all the required tooling
|
||||
3. `npm run build:runtime` to build the `owl.iife.runtime.js` file
|
||||
4. `npm run build:compiler` to build the template compiler
|
||||
5. `npm run compile_templates -- path/to/your/templates` will scan your target
|
||||
folder, find all xml files, get all templates, compile them, and generate a
|
||||
`templates.js` file.
|
||||
@@ -4,12 +4,8 @@
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Definition](#definition)
|
||||
- [Props comparison](#props-comparison)
|
||||
- [Binding function props](#binding-function-props)
|
||||
- [Dynamic Props](#dynamic-props)
|
||||
- [Default Props](#default-props)
|
||||
- [Props validation](#props-validation)
|
||||
- [Good Practices](#good-practices)
|
||||
- [Dynamic Props](#dynamic-props)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -42,6 +38,8 @@ The `props` object is made of every attributes defined on the template, with the
|
||||
following exceptions:
|
||||
|
||||
- every attribute starting with `t-` are not props (they are QWeb directives),
|
||||
- `style` and `class` attributes are excluded as well (they are applied by Owl on
|
||||
the root element of the component).
|
||||
|
||||
In the following example:
|
||||
|
||||
@@ -49,6 +47,7 @@ In the following example:
|
||||
<div>
|
||||
<ComponentA a="state.a" b="'string'"/>
|
||||
<ComponentB t-if="state.flag" model="model"/>
|
||||
<ComponentC style="color:red;" class="left-pane" />
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -56,269 +55,7 @@ the `props` object contains the following keys:
|
||||
|
||||
- for `ComponentA`: `a` and `b`,
|
||||
- for `ComponentB`: `model`,
|
||||
|
||||
## Props comparison
|
||||
|
||||
Whenever Owl encounters a subcomponent in a template, it performs a shallow
|
||||
comparison of all props. If they are all referentially equal, then the subcomponent
|
||||
will not even be updated. Otherwise, if at least one props has changed, then
|
||||
Owl will update it.
|
||||
|
||||
However, in some cases, we know that two values are different, but they have the
|
||||
same effect, and should not be considered different by Owl. For example, anonymous
|
||||
functions in a template are always different, but most of them should not be
|
||||
considered different:
|
||||
|
||||
```xml
|
||||
<t t-foreach="todos" t-as="todo" t-key="todo.id">
|
||||
<Todo todo="todo" onDelete="() => deleteTodo(todo.id)" />
|
||||
</t>
|
||||
```
|
||||
|
||||
In that case, one can use the `.alike` suffix:
|
||||
|
||||
```xml
|
||||
<t t-foreach="todos" t-as="todo" t-key="todo.id">
|
||||
<Todo todo="todo" onDelete.alike="() => deleteTodo(todo.id)" />
|
||||
</t>
|
||||
```
|
||||
|
||||
This tells Owl that this specific prop should always be considered equivalent
|
||||
(or, in other words, should be removed from the list of comparable props).
|
||||
|
||||
Note that even if most anonymous functions should probably be considered `alike`,
|
||||
it is not necessarily true in all cases. It depends on what values are captured
|
||||
by the anonymous function. The following example shows a case where it is probably
|
||||
wrong to use `.alike`.
|
||||
|
||||
```xml
|
||||
<t t-foreach="todos" t-as="todo" t-key="todo.id">
|
||||
<!-- Probably wrong! todo.isCompleted may change -->
|
||||
<Todo todo="todo" toggle.alike="() => toggleTodo(todo.isCompleted)" />
|
||||
</t>
|
||||
```
|
||||
|
||||
## Binding function props
|
||||
|
||||
It is common to have the need to pass a callback as a prop. Since Owl components
|
||||
are class based, the callback frequently needs to be bound to its owner component.
|
||||
So, one can do this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child callback="doSomething"/>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
this.doSomething = this.doSomething.bind(this);
|
||||
}
|
||||
|
||||
doSomething() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, this is such a common use case that Owl provides a special suffix to do
|
||||
just that: `.bind`. This looks like this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child callback.bind="doSomething"/>
|
||||
</div>`;
|
||||
|
||||
doSomething() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `.bind` suffix also implies `.alike`, so these props will not cause additional
|
||||
renderings.
|
||||
|
||||
## Translatable props
|
||||
|
||||
When you need to pass a user-facing string to a subcomponent, you likely want it
|
||||
to be translated. Unfortunately, because props are arbitrary expressions, it wouldn't
|
||||
be practical for Owl to find out which parts of the expression are strings and translate
|
||||
them, and it also makes it difficult for tooling to extract these strings to generate
|
||||
terms to translate. While you can work around this issue by doing the translation in
|
||||
JavaScript, or by using `t-set` with a body (the body of `t-set` is translated),
|
||||
and passing the variable as a prop, this is a sufficiently common use case that Owl
|
||||
provides a suffix for this purpose: `.translate`.
|
||||
|
||||
```xml
|
||||
<t t-name="ParentComponent">
|
||||
<Child someProp.translate="some message"/>
|
||||
</t>
|
||||
```
|
||||
|
||||
Note that the content of this attribute is _NOT_ treated as a JavaScript expression:
|
||||
it is treated as a string, as if it was an attribute on an HTML element, and translated
|
||||
before being passed to the component. If you need to interpolate some data into the
|
||||
string, you will still have to do this in JavaScript.
|
||||
|
||||
## Dynamic Props
|
||||
|
||||
The `t-props` directive can be used to specify totally dynamic props:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<Child t-props="some.obj"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
static components = { Child };
|
||||
some = { obj: { a: 1, b: 2 } };
|
||||
}
|
||||
```
|
||||
|
||||
## Default Props
|
||||
|
||||
If the static `defaultProps` property is defined, it will be used to complete
|
||||
props received by the parent, if missing.
|
||||
|
||||
```js
|
||||
class Counter extends owl.Component {
|
||||
static defaultProps = {
|
||||
initialValue: 0,
|
||||
};
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, the `initialValue` props is now by default set to 0.
|
||||
|
||||
## Props Validation
|
||||
|
||||
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
|
||||
|
||||
- hard to tell how a component should be used, by looking at its code.
|
||||
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
|
||||
|
||||
A props type system solves both issues, by describing the types and shapes
|
||||
of the props. Here is how it works in Owl:
|
||||
|
||||
- `props` key is a static key (so, different from `this.props` in a component instance)
|
||||
- it is optional: it is ok for a component to not define a `props` key.
|
||||
- props are validated whenever a component is created/updated
|
||||
- props are only validated in `dev` mode (see [how to configure an app](app.md#configuration))
|
||||
- if a key does not match the description, an error is thrown
|
||||
- it validates keys defined in (static) `props`. Additional keys given by the
|
||||
parent will cause an error (unless the special prop `*` is present).
|
||||
- it is an object or a list of strings
|
||||
- a list of strings is a simplified props definition, which only lists the name
|
||||
of the props. Also, if the name ends with `?`, it is considered optional.
|
||||
- all props are by default required, unless they are defined with `optional: true`
|
||||
(in that case, it is only done if there is a value)
|
||||
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
|
||||
constructor functions (so, if you have a `Person` class, it can be used as a type)
|
||||
- arrays are homogeneous (all elements have the same type/shape)
|
||||
|
||||
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
|
||||
|
||||
- a boolean: indicate that the props exists, and is mandatory.
|
||||
- a constructor: this should describe the type, for example: `id: Number` describe
|
||||
the props `id` as a number
|
||||
- an object describing a value as type. This is done by using the `value` key. For example, `{value: false}` specifies that the corresponding value should be equal to false.
|
||||
- a list of constructors. In that case, this means that we allow more than one
|
||||
type. For example, `id: [Number, String]` means that `id` can be either a string
|
||||
or a number.
|
||||
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
|
||||
- `type`: the main type of the prop being validated
|
||||
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
|
||||
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
|
||||
- `values`: if the type was `Object`, then the `values` key describes the interface of values in the object, this allows validating objects that are used as mappings, where keys are not known in advance but the shape of the values is.
|
||||
- `validate`: this is a function which should return a boolean to determine if
|
||||
the value is valid or not. Useful for custom validation logic.
|
||||
- `optional`: if true, the prop is not mandatory
|
||||
|
||||
There is a special `*` prop that means that additional prop are allowed. This is
|
||||
sometimes useful for generic components that will propagate some or all their
|
||||
props to their child components.
|
||||
|
||||
Note that default values cannot be defined for a mandatory props. Doing so will
|
||||
result in a prop validation error.
|
||||
|
||||
Examples:
|
||||
|
||||
```js
|
||||
class ComponentA extends owl.Component {
|
||||
static props = ['id', 'url'];
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
class ComponentB extends owl.Component {
|
||||
static props = {
|
||||
count: {type: Number},
|
||||
messages: {
|
||||
type: Array,
|
||||
element: {type: Object, shape: {id: Boolean, text: String }}
|
||||
},
|
||||
date: Date,
|
||||
combinedVal: [Number, Boolean],
|
||||
optionalProp: { type: Number, optional: true }
|
||||
};
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// only the existence of those 3 keys is documented
|
||||
static props = ['message', 'id', 'date'];
|
||||
```
|
||||
|
||||
```js
|
||||
// only the existence of those 3 keys is documented. any other key is allowed.
|
||||
static props = ['message', 'id', 'date', '*'];
|
||||
```
|
||||
|
||||
```js
|
||||
// size is optional
|
||||
static props = ['message', 'size?'];
|
||||
```
|
||||
|
||||
```js
|
||||
static props = {
|
||||
messageIds: {type: Array, element: Number}, // list of number
|
||||
otherArr: {type: Array}, // just array. no validation is made on sub elements
|
||||
otherArr2: Array, // same as otherArr
|
||||
someObj: {type: Object}, // just an object, no internal validation
|
||||
someObj2: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: Number,
|
||||
name: {type: String, optional: true},
|
||||
url: String
|
||||
}
|
||||
}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
someObj3: {
|
||||
type: Object,
|
||||
values: { type: Array, element: String },
|
||||
}, // object with arbitary keys where values are arrays of strings
|
||||
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||
someVal: [Boolean, Date], // either a boolean or a date
|
||||
otherValue: true, // indicates that it is a prop
|
||||
kindofsmallnumber: {
|
||||
type: Number,
|
||||
validate: n => (0 <= n && n <= 10)
|
||||
},
|
||||
size: {
|
||||
validate: e => ["small", "medium", "large"].includes(e)
|
||||
},
|
||||
someId: [Number, {value: false}], // either a number or false
|
||||
};
|
||||
```
|
||||
|
||||
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
||||
- for `ComponentC`: empty object
|
||||
|
||||
## Good Practices
|
||||
|
||||
@@ -341,3 +78,20 @@ sent to the parent (for example, with an event).
|
||||
Any value can go in a props. Strings, objects, classes, or even callbacks could
|
||||
be given to a child component (but then, in the case of callbacks, communicating
|
||||
with events seems more appropriate).
|
||||
|
||||
## Dynamic Props
|
||||
|
||||
The `t-props` directive can be used to specify totally dynamic props:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<Child t-props="some.obj"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
static components = { Child };
|
||||
some = { obj: { a: 1, b: 2 } };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# 🦉 Props Validation 🦉
|
||||
|
||||
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
|
||||
|
||||
- hard to tell how a component should be used, by looking at its code.
|
||||
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
|
||||
|
||||
A props type system solves both issues, by describing the types and shapes
|
||||
of the props. Here is how it works in Owl:
|
||||
|
||||
- `props` key is a static key (so, different from `this.props` in a component instance)
|
||||
- it is optional: it is ok for a component to not define a `props` key.
|
||||
- props are validated whenever a component is created/updated
|
||||
- props are only validated in `dev` mode (see [config page](config.md#mode))
|
||||
- if a key does not match the description, an error is thrown
|
||||
- it validates keys defined in (static) `props`. Additional keys given by the
|
||||
parent will cause an error.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
class ComponentA extends owl.Component {
|
||||
static props = ['id', 'url'];
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
class ComponentB extends owl.Component {
|
||||
static props = {
|
||||
count: {type: Number},
|
||||
messages: {
|
||||
type: Array,
|
||||
element: {type: Object, shape: {id: Boolean, text: String }
|
||||
},
|
||||
date: Date,
|
||||
combinedVal: [Number, Boolean]
|
||||
};
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
- it is an object or a list of strings
|
||||
- a list of strings is a simplified props definition, which only lists the name
|
||||
of the props. Also, if the name ends with `?`, it is considered optional.
|
||||
- all props are by default required, unless they are defined with `optional: true`
|
||||
(in that case, validation is only done if there is a value)
|
||||
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
|
||||
constructor functions (so, if you have a `Person` class, it can be used as a type)
|
||||
- arrays are homogeneous (all elements have the same type/shape)
|
||||
|
||||
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
|
||||
|
||||
- a boolean: indicate that the props exists, and is mandatory.
|
||||
- a constructor: this should describe the type, for example: `id: Number` describe
|
||||
the props `id` as a number
|
||||
- a list of constructors. In that case, this means that we allow more than one
|
||||
type. For example, `id: [Number, String]` means that `id` can be either a string
|
||||
or a number.
|
||||
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
|
||||
- `type`: the main type of the prop being validated
|
||||
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
|
||||
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
|
||||
- `validate`: this is a function which should return a boolean to determine if
|
||||
the value is valid or not. Useful for custom validation logic.
|
||||
|
||||
Examples:
|
||||
|
||||
```js
|
||||
// only the existence of those 3 keys is documented
|
||||
static props = ['message', 'id', 'date'];
|
||||
```
|
||||
|
||||
```js
|
||||
// size is optional
|
||||
static props = ['message', 'size?'];
|
||||
```
|
||||
|
||||
```js
|
||||
static props = {
|
||||
messageIds: {type: Array, element: Number}, // list of number
|
||||
otherArr: {type: Array}, // just array. no validation is made on sub elements
|
||||
otherArr2: Array, // same as otherArr
|
||||
someObj: {type: Object}, // just an object, no internal validation
|
||||
someObj2: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: Number,
|
||||
name: {type: String, optional: true},
|
||||
url: String
|
||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||
someVal: [Boolean, Date], // either a boolean or a date
|
||||
otherValue: true, // indicates that it is a prop
|
||||
kindofsmallnumber: {
|
||||
type: Number,
|
||||
validate: n => (0 <= n && n <= 10)
|
||||
},
|
||||
size: {
|
||||
validate: e => ["small", "medium", "large"].includes(e)
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,153 @@
|
||||
# 🦉 QWeb Engine 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Reference](#reference)
|
||||
|
||||
## Overview
|
||||
|
||||
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
|
||||
templating engine used by Odoo. The QWeb class in the OWL project is an
|
||||
implementation of that specification with a few interesting points:
|
||||
|
||||
- it compiles templates into functions that output a virtual DOM instead of a
|
||||
string. This is necessary for the component system.
|
||||
- it has a few extra directives: `t-component`, `t-on`, ...
|
||||
|
||||
We present in this section the engine, not the templating language.
|
||||
|
||||
## Reference
|
||||
|
||||
This section is about the javascript code that implements the `QWeb` specification.
|
||||
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
|
||||
instantiated:
|
||||
|
||||
```js
|
||||
const qweb = new owl.QWeb();
|
||||
```
|
||||
|
||||
Its API is quite simple:
|
||||
|
||||
- **`constructor(config)`**: constructor. Takes an optional configuration object
|
||||
with an optional `templates` string to add initial
|
||||
templates (see `addTemplates` for more information on format of the string)
|
||||
and an optional `translateFn` translate function (see the section on
|
||||
[translations](#translations)).
|
||||
|
||||
```js
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
|
||||
```
|
||||
|
||||
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
|
||||
|
||||
```js
|
||||
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
||||
```
|
||||
|
||||
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
|
||||
ignore templates added for a second time. Otherwise, `QWeb` will crash.
|
||||
|
||||
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
||||
attribute).
|
||||
|
||||
```js
|
||||
const TEMPLATES = `
|
||||
<templates>
|
||||
<div t-name="App" class="main">main</div>
|
||||
<div t-name="OtherComponent">other component</div>
|
||||
</templates>`;
|
||||
qweb.addTemplates(TEMPLATES);
|
||||
```
|
||||
|
||||
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
|
||||
which is a virtual representation of the DOM (see [vdom doc](../miscellaneous/vdom.md)).
|
||||
|
||||
```js
|
||||
const vnode = qweb.render("App", component);
|
||||
```
|
||||
|
||||
- **`renderToString(name, context)`**: renders a template, but returns an html
|
||||
string.
|
||||
|
||||
```js
|
||||
const str = qweb.renderToString("someTemplate", somecontext);
|
||||
```
|
||||
|
||||
- **`registerTemplate(name, template)`**: static function to register a global
|
||||
QWeb template. This is useful for commonly used components accross the
|
||||
application, and for making a template available to an application without
|
||||
having a reference to the actual QWeb instance.
|
||||
|
||||
```js
|
||||
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
|
||||
```
|
||||
|
||||
- **`registerComponent(name, Component)`**: static function to register an OWL Component
|
||||
to QWeb's global registry. Globally registered Components can be used in
|
||||
templates (see the `t-component` directive). This is useful for commonly used
|
||||
components accross the application.
|
||||
|
||||
```js
|
||||
class Dialog extends owl.Component { ... }
|
||||
QWeb.registerComponent("Dialog", Dialog);
|
||||
|
||||
...
|
||||
|
||||
class ParentComponent extends owl.Component { ... }
|
||||
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
|
||||
```
|
||||
|
||||
In some way, a `QWeb` instance is the core of an Owl application. It is the only
|
||||
mandatory element of an [environment](environment.md). As such, it
|
||||
has an extra responsibility: it can act as an event bus for internal communication
|
||||
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
|
||||
|
||||
### Translations
|
||||
|
||||
take care of this and "cherry-pick" 8464a1b04e7469434f9dcb3d68a543f58cb61b8e
|
||||
|
||||
If properly setup, Owl QWeb engine can translate all rendered templates. To do
|
||||
so, it needs a translate function, which takes a string and returns a string.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
const translations = {
|
||||
hello: "bonjour",
|
||||
yes: "oui",
|
||||
no: "non",
|
||||
};
|
||||
const translateFn = (str) => translations[str] || str;
|
||||
|
||||
const qweb = new QWeb({ translateFn });
|
||||
```
|
||||
|
||||
Once setup, all rendered templates will be translated using `translateFn`:
|
||||
|
||||
- each text node will be replaced with its translation,
|
||||
- each of the following attribute values will be translated as well: `title`,
|
||||
`placeholder`, `label` and `alt`,
|
||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||
if its value is `off`.
|
||||
|
||||
So, with the above `translateFn`, the following templates:
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
<div t-translation="off">hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="hello" other="yes"/>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<div>bonjour</div>
|
||||
<div>hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="bonjour" other="yes"/>
|
||||
```
|
||||
|
||||
Note that the translation is done during the compilation of the template, not
|
||||
when it is rendered.
|
||||
@@ -1,11 +1,12 @@
|
||||
# 🦉 Templates 🦉
|
||||
# 🦉 QWeb Templating Language🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Directives](#directives)
|
||||
- [QWeb Template reference](#qweb-template-reference)
|
||||
- [Reference](#reference)
|
||||
- [White Spaces](#white-spaces)
|
||||
- [Root Nodes](#root-nodes)
|
||||
- [Expression Evaluation](#expression-evaluation)
|
||||
- [Static html Nodes](#static-html-nodes)
|
||||
- [Outputting Data](#outputting-data)
|
||||
@@ -15,21 +16,17 @@
|
||||
- [Dynamic Class Attribute](#dynamic-class-attribute)
|
||||
- [Dynamic Tag Names](#dynamic-tag-names)
|
||||
- [Loops](#loops)
|
||||
- [Sub Templates](#sub-templates)
|
||||
- [Rendering Sub Templates](#rendering-sub-templates)
|
||||
- [Dynamic Sub Templates](#dynamic-sub-templates)
|
||||
- [Translations](#translations)
|
||||
- [Debugging](#debugging)
|
||||
- [Custom Directives](#custom-directives)
|
||||
- [Fragments](#fragments)
|
||||
- [Inline templates](#inline-templates)
|
||||
- [Rendering svg](#rendering-svg)
|
||||
- [Restrictions](#restrictions)
|
||||
|
||||
## Overview
|
||||
|
||||
Owl templates are describe using the [QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) specification. It is based on the XML format, and used
|
||||
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
|
||||
templating engine used by Odoo. It is based on the XML format, and used
|
||||
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
|
||||
generate a virtual dom representation of the HTML. Also, since Owl is a live
|
||||
component system, there are additional directives specific to Owl (such as `t-on`).
|
||||
generate a virtual dom representation of the HTML.
|
||||
|
||||
```xml
|
||||
<div>
|
||||
@@ -56,34 +53,34 @@ extensions.
|
||||
|
||||
For reference, here is a list of all standard QWeb directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------ | --------------------------------------------------------------- |
|
||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-translation` | [Disabling the translation of a node](translations.md) |
|
||||
| Name | Description |
|
||||
| ------------------------------ | -------------------------------------------------------------- |
|
||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
|
||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-translation` | [Disabling the translation of a node](#translations) |
|
||||
| `t-name` | [Defining a template (not really a directive)](qweb_engine.md) |
|
||||
|
||||
The component system in Owl requires additional directives, to express various
|
||||
needs. Here is a list of all Owl specific directives:
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------------------- | --------------------------------------------------------------- |
|
||||
| `t-component`, `t-props` | [Defining a sub component](component.md#sub-components) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](refs.md) |
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
|
||||
| `t-on-*` | [Event handling](event_handling.md) |
|
||||
| `t-portal` | [Portal](portal.md) |
|
||||
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
|
||||
| `t-model` | [Form input bindings](input_bindings.md) |
|
||||
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
||||
| `t-custom-*` | [Rendering nodes with custom directives](#custom-directives) |
|
||||
| Name | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------------- |
|
||||
| `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
|
||||
| `t-on-*` | [Event handling](event_handling.md) |
|
||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||
| `t-slot` | [Rendering a slot](slots.md) |
|
||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
||||
|
||||
## QWeb Template Reference
|
||||
## Reference
|
||||
|
||||
### White Spaces
|
||||
|
||||
@@ -93,6 +90,32 @@ White spaces in a template are handled in a special way:
|
||||
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||
- the previous rules do not apply if we are in a `<pre>` tag
|
||||
|
||||
### Root Nodes
|
||||
|
||||
For many reasons, Owl QWeb templates should have a single root node. More
|
||||
precisely, the result of a template rendering should have a single root node:
|
||||
|
||||
```xml
|
||||
<!–– not ok: two root nodes ––>
|
||||
<t>
|
||||
<div>foo</div>
|
||||
<div>bar</div>
|
||||
</t>
|
||||
|
||||
<!–– ok: result has one single root node ––>
|
||||
<t>
|
||||
<div t-if="someCondition">foo</div>
|
||||
<span t-else="">bar</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
Extra root nodes will actually be ignored (even though they will be rendered
|
||||
in memory).
|
||||
|
||||
Note: this does not apply to subtemplates (see the `t-call` directive). In that
|
||||
case, they will be inlined in the main template, and can actually have many
|
||||
root nodes.
|
||||
|
||||
### Expression Evaluation
|
||||
|
||||
QWeb expressions are strings that will be processed at compile time. Each variable in
|
||||
@@ -117,7 +140,7 @@ It is useful to explain the various rules that apply on these expressions:
|
||||
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||
```
|
||||
|
||||
2. it can use anything in the rendering context (which typically contains the properties of the component):
|
||||
2. it can use anything in the rendering context (typically, the component):
|
||||
|
||||
```xml
|
||||
<p t-if="user.birthday === today()">Happy bithday!</p>
|
||||
@@ -167,29 +190,24 @@ rendered with the value `value` set to `42` in the rendering context yields:
|
||||
<p>42</p>
|
||||
```
|
||||
|
||||
The `t-out` directive is almost the same as `t-esc`, but possibly without the
|
||||
escaping. The difference is that the value received by the `t-out` directive
|
||||
will only be not-escaped if it has been marked as such, using the `markup`
|
||||
utility function:
|
||||
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
|
||||
This is mostly useful to inject a raw html string somewhere. Obviously, this
|
||||
is unsafe to do in general, and should only be used for strings known to be safe.
|
||||
|
||||
For example, in the following component:
|
||||
|
||||
```js
|
||||
const { markup, Component, xml } = owl;
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-out="value1"/>
|
||||
<t t-out="value2"/>`;
|
||||
|
||||
value1 = "<div>some text 1</div>";
|
||||
value2 = markup("<div>some text 2</div>");
|
||||
}
|
||||
```xml
|
||||
<p><t t-raw="value"/></p>
|
||||
```
|
||||
|
||||
The first `t-out` will act as a `t-esc` directive, which means that the content
|
||||
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
||||
this will be injected as html.
|
||||
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
|
||||
|
||||
```html
|
||||
<p><span>foo</span></p>
|
||||
```
|
||||
|
||||
Note that since the content of the expression is not known beforehand, the `t-raw`
|
||||
directive has to parse the html (and convert it to a virtual dom structure) for
|
||||
each rendering. So, it will be much slower than a regular template. It is
|
||||
therefore advised to limit the use of `t-raw` whenever possible.
|
||||
|
||||
### Setting Variables
|
||||
|
||||
@@ -292,11 +310,10 @@ If an expression evaluates to a falsy value, it will not be set at all:
|
||||
|
||||
It is sometimes convenient to format an attribute with string interpolation. In
|
||||
that case, the `t-attf-` directive can be used. It is useful when we need to mix
|
||||
literal and dynamic elements, such as css classes. The dynamic elements can be
|
||||
specified with either `{{...}}` or `#{...}`:
|
||||
literal and dynamic elements, such as css classes.
|
||||
|
||||
```xml
|
||||
<div t-attf-foo="a {{value1}} is #{value2} of {{value3}} ]"/>
|
||||
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
|
||||
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
||||
```
|
||||
|
||||
@@ -351,7 +368,7 @@ collection to iterate on, and a second parameter `t-as` providing the name to us
|
||||
for the current item of the iteration:
|
||||
|
||||
```xml
|
||||
<t t-foreach="[1, 2, 3]" t-as="i" t-key="i">
|
||||
<t t-foreach="[1, 2, 3]" t-as="i">
|
||||
<p><t t-esc="i"/></p>
|
||||
</t>
|
||||
```
|
||||
@@ -367,27 +384,22 @@ will be rendered as:
|
||||
Like conditions, `t-foreach` applies to the element bearing the directive’s attribute, and
|
||||
|
||||
```xml
|
||||
<p t-foreach="[1, 2, 3]" t-as="i" t-key="i">
|
||||
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||
<t t-esc="i"/>
|
||||
</p>
|
||||
```
|
||||
|
||||
is equivalent to the previous example.
|
||||
|
||||
An important difference should be made with the usual `QWeb` behaviour: Owl
|
||||
requires the presence of a `t-key` directive, to be able to properly reconcile
|
||||
renderings.
|
||||
`t-foreach` can iterate on an array (the current item will be the current value)
|
||||
or an object (the current item will be the current key).
|
||||
|
||||
`t-foreach` can iterate on any iterable, and also has special support for objects
|
||||
and maps, it will expose the key of the current iteration as the contents of the
|
||||
`t-as`, and the corresponding value with the same name and the suffix `_value`.
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||
variables for various data points (note: `$as` will be replaced with the name
|
||||
passed to `t-as`):
|
||||
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other useful
|
||||
variables (note: `$as` will be replaced with the name passed to `t-as`):
|
||||
|
||||
- `$as_value`: the current iteration value, identical to `$as` for arrays and
|
||||
other iterables, but for objects and maps, it provides the value (where `$as`
|
||||
provides the key)
|
||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||
integers, but for objects, it provides the value (where `$as` provides the key)
|
||||
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
|
||||
- `$as_first`: whether the current item is the first of the iteration
|
||||
(equivalent to `$as_index == 0`)
|
||||
@@ -404,7 +416,7 @@ into the global context.
|
||||
<t t-set="existing_variable" t-value="false"/>
|
||||
<!-- existing_variable now False -->
|
||||
|
||||
<p t-foreach="Array(3)" t-as="i" t-key="i">
|
||||
<p t-foreach="Array(3)" t-as="i">
|
||||
<t t-set="existing_variable" t-value="true"/>
|
||||
<t t-set="new_variable" t-value="true"/>
|
||||
<!-- existing_variable and new_variable now true -->
|
||||
@@ -418,14 +430,17 @@ Even though Owl tries to be as declarative as possible, the DOM does not fully
|
||||
expose its state declaratively in the DOM tree. For example, the scrolling state,
|
||||
the current user selection, the focused element or the state of an input are not
|
||||
set as attribute in the DOM tree. This is why we use a virtual dom
|
||||
algorithm to make sure we keep the actual DOM node instead of replacing it with
|
||||
a new one.
|
||||
algorithm to keep the actual DOM node as much as possible.
|
||||
|
||||
However, in some situations, this is not enough, and we need to help Owl decide
|
||||
if an element is actually the same, or is a different element with the same
|
||||
properties.
|
||||
|
||||
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
|
||||
and we render them in this template:
|
||||
|
||||
```xml
|
||||
<p t-foreach="items" t-as="item" t-key="item_index"><t t-esc="item.text"/></p>
|
||||
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
|
||||
```
|
||||
|
||||
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
|
||||
@@ -480,13 +495,13 @@ not work with other iterables, such as `Set`. However, it is only a matter of
|
||||
using the `...` javascript operator. For example:
|
||||
|
||||
```xml
|
||||
<t t-foreach="[...items]" t-as="item">...</t>
|
||||
<t t-foreach="...items" t-as="item">...</t>
|
||||
```
|
||||
|
||||
The `...` operator will convert the `Set` (or any other iterables) into a list,
|
||||
which will work with Owl QWeb.
|
||||
|
||||
### Sub Templates
|
||||
### Rendering Sub Templates
|
||||
|
||||
QWeb templates can be used for top level rendering, but they can also be used
|
||||
from within another template (to avoid duplication or give names to parts of
|
||||
@@ -543,14 +558,6 @@ This can be used to define variables scoped to a sub template:
|
||||
<!-- "var" does not exist here -->
|
||||
```
|
||||
|
||||
Note: by default, the rendering context for a sub template is simply the current
|
||||
rendering context. However, it may be useful to be able to specify a specific
|
||||
object as context. This can be done by using the `t-call-context` directive:
|
||||
|
||||
```xml
|
||||
<t t-call="other-template" t-call-context="obj"/>
|
||||
```
|
||||
|
||||
### Dynamic sub templates
|
||||
|
||||
The `t-call` directive can also be used to dynamically call a sub template,
|
||||
@@ -567,6 +574,21 @@ using string interpolation. For example:
|
||||
Here, the name of the template is obtained from the `template` value in the
|
||||
template rendering context.
|
||||
|
||||
### Translations
|
||||
|
||||
By default, QWeb specify that templates should be translated. If this behaviour
|
||||
is not wanted, there is a `t-translation` directive which can turn off
|
||||
translations (if it is set to the `off` value), with the following rules:
|
||||
|
||||
- each text node will be replaced with its translation,
|
||||
- each of the following attribute values will be translated as well: `title`,
|
||||
`placeholder`, `label` and `alt`,
|
||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||
if its value is `off`.
|
||||
|
||||
See [here](qweb_engine.md#translations) for more information on how to setup a
|
||||
translate function in Owl QWeb.
|
||||
|
||||
### Debugging
|
||||
|
||||
The javascript QWeb implementation provides two useful debugging directives:
|
||||
@@ -589,146 +611,3 @@ will stop execution if the browser dev tools are open.
|
||||
```
|
||||
|
||||
will print 42 to the console.
|
||||
|
||||
### Custom Directives
|
||||
|
||||
Owl 2 supports the declaration of custom directives. To use them, an Object of functions needs to be configured on the owl APP:
|
||||
|
||||
```js
|
||||
new App(..., {
|
||||
customDirectives: {
|
||||
test_directive: function (el, value) {
|
||||
el.setAttribute("t-on-click", value);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The functions will be called when a custom directive with the name of the
|
||||
function is found. The original element will be replaced with the one
|
||||
modified by the function.
|
||||
This :
|
||||
|
||||
```xml
|
||||
<div t-custom-test_directive="click" />
|
||||
```
|
||||
|
||||
will be replaced by :
|
||||
|
||||
```xml
|
||||
<div t-on-click="value"/>
|
||||
```
|
||||
|
||||
## Fragments
|
||||
|
||||
Owl 2 supports templates with an arbitrary number of root elements, or even just
|
||||
a text node. So, the following templates are all valid:
|
||||
|
||||
```xml
|
||||
hello owl. This is just a text node!
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
<div>ola</div>
|
||||
```
|
||||
|
||||
```xml
|
||||
<div t-if="someCondition"><SomeChildComponent/></div>
|
||||
```
|
||||
|
||||
```xml
|
||||
<t t-if="someCondition"><SomeChildComponent/></t>
|
||||
```
|
||||
|
||||
## Inline templates
|
||||
|
||||
Most real applications will define their templates in a XML file, to benefit
|
||||
from the XML ecosystem, and to do some additional processing, such as translating
|
||||
them. However, in some cases, it is convenient to be able to define a template
|
||||
inline. To do so, one can use the `xml` helper function:
|
||||
|
||||
```js
|
||||
const { Component, xml } = owl;
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
mount(MyComponent, document.body);
|
||||
```
|
||||
|
||||
This function simply generates an unique string id, and register the template
|
||||
under that id in the internals of Owl, then return the id.
|
||||
|
||||
## Rendering svg
|
||||
|
||||
Owl components can be used to generate dynamic SVG graphs:
|
||||
|
||||
```js
|
||||
class Node extends Component {
|
||||
static template = xml`
|
||||
<g>
|
||||
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
|
||||
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
|
||||
<t t-set="childx" t-value="props.x + 100"/>
|
||||
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
|
||||
<t t-foreach="props.node.children || []" t-as="child">
|
||||
<t t-set="childy" t-value="props.y + child_index*height"/>
|
||||
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
|
||||
<Node x="childx" y="childy" node="child" height="height"/>
|
||||
</t>
|
||||
</g>
|
||||
`;
|
||||
static components = { Node };
|
||||
}
|
||||
|
||||
class RootNode extends Component {
|
||||
static template = xml`
|
||||
<svg height="180">
|
||||
<Node node="graph" x="10" y="20" height="180"/>
|
||||
</svg>
|
||||
`;
|
||||
static components = { Node };
|
||||
graph = {
|
||||
label: "a",
|
||||
children: [
|
||||
{ label: "b" },
|
||||
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
|
||||
{ label: "f", children: [{ label: "g" }] },
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This `RootNode` component will then display a live SVG representation of the
|
||||
graph described by the `graph` property. Note that there is a recursive structure
|
||||
here: the `Node` component uses itself as a subcomponent.
|
||||
|
||||
**Important note:** Owl needs to properly set the namespace for each svg elements.
|
||||
Since Owl compile each template separately, it is not able to determine easily
|
||||
if a template is supposed to be included in a svg namespace or not. Therefore,
|
||||
Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will
|
||||
be considered as svg. In practice, this means that each component or each sub
|
||||
templates (included with `t-call`) should have one of these tag as root tag.
|
||||
|
||||
## Restrictions
|
||||
|
||||
Note that Owl templates forbid the use of tag and or attributes starting with
|
||||
the `block-` string. This restriction prevents name collision with the internal
|
||||
code of Owl.
|
||||
|
||||
```xml
|
||||
<div><block-1>this will not be accepted by Owl</block-1></div>
|
||||
```
|
||||
@@ -1,395 +0,0 @@
|
||||
# 🦉 Reactivity 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [`useState`](#usestate)
|
||||
- [`reactive`](#reactive)
|
||||
- [`Escape hatches`](#escape-hatches)
|
||||
- [`Advanced usage`](#advanced-usage)
|
||||
|
||||
## Introduction
|
||||
|
||||
Reactivity is a big topic in javascript frameworks. The goal is to provide a
|
||||
simple way to manipulate state, in such a way that the interface updates automatically
|
||||
according to state changes, and to do so in a performant manner.
|
||||
|
||||
To this end, Owl provides a proxy-based reactivity system, based on the `reactive` primitive.
|
||||
The `reactive` function takes an object as a first argument, and an optional callback as its second
|
||||
argument, it returns a proxy of the object. This proxy tracks what properties are read
|
||||
through the proxy, and calls the provided callback whenever one of these properties is changed
|
||||
through any reactive version of the same object. It does so in depth, by returning reactive versions
|
||||
of the subobjects when they are read.
|
||||
|
||||
## `useState`
|
||||
|
||||
While the `reactive` primitive is very powerful, its usage in components follow a very standard pattern:
|
||||
components want to be rerendered when part of the state which they depend on for rendering changes. To
|
||||
this end, owl provides a standard hook: `useState`. To put it simply, this hook simply calls reactive
|
||||
with the provided object, and the current component's render function as its callback. This will cause
|
||||
it to rerender whenever any part of the state object that has been read by this component is modified.
|
||||
|
||||
Here is a simple example of how `useState` can be used:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="() => this.state.value++">
|
||||
<t t-esc="state.value"/>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This component reads `state.value` when it renders, subscribing it to changes to that key. Whenever
|
||||
the value changes, Owl will update the component. Note that there is nothing special about the
|
||||
`state` property, you can name your state variables whatever you want, and you can have multiple of
|
||||
them on the same component if it makes sense to do so. This also allows `useState` to be used in custom
|
||||
hooks that may require state that is specific to that hook.
|
||||
|
||||
### Reactive props
|
||||
|
||||
Since version 2.0, Owl renders are no longer "deep" by default: a component is only rerendered by its
|
||||
parent if its props have changed (using a simple equality test). What if the contents of a props have
|
||||
changed in a deeper property? If that prop is reactive, owl will rerender the child components that
|
||||
need to be updated automatically, and only those components, it does so by reobserving reactive
|
||||
objects passed as props to components. Consider the following example:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="() => props.state.value++">
|
||||
<t t-esc="props.state.value"/>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Counter state="this.state"/>
|
||||
<button t-on-click="() => this.state.value = 0">Reset counter</button>
|
||||
<button t-on-click="() => this.state.test++" t-esc="this.state.test"/>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ value: 0, test: 1 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When clicking on the counter button, only the Counter rerenders, because the Parent has never read
|
||||
the "value" key in the state. When clicking on the "Reset Counter" button, the same thing happens:
|
||||
only the Counter component rerenders. What matters is not _where_ the state is updated, but which
|
||||
parts of the state are updated, and which components depend on them. This is achieved by Owl by
|
||||
automatically calling `useState` on reactive objects passed as props to a child component.
|
||||
|
||||
When clicking on the last button, the parent is rerendered, but the child does not care about the
|
||||
`test` key: it has not read it. The props that we give it (`this.state`) have also not changed,
|
||||
as such, the parent updates but the child doesn't.
|
||||
|
||||
For most day-to-day operations, `useState` should cover all of your needs. If
|
||||
you are curious about more advanced use cases and technical details, read on.
|
||||
|
||||
### Debugging subscriptions
|
||||
|
||||
Owl provides a way to show which reactive objects and keys a component is subscribed to: you can
|
||||
look at `component.__owl__.subscriptions`. Note that this is on the internal `__owl__` field, and
|
||||
should not be used in any type of production code as the name of this property or any of its properties
|
||||
or methods are subject to change at any point, even in stable versions of Owl, and may become available
|
||||
only in debug mode in the future.
|
||||
|
||||
## `reactive`
|
||||
|
||||
The `reactive` function is the basic reactivity primitive. It takes an object
|
||||
or an array as first argument, and optionally, a function as the second argument.
|
||||
The function is called whenever any tracked value is updated.
|
||||
|
||||
```js
|
||||
const obj = reactive({ a: 1 }, () => console.log("changed"));
|
||||
|
||||
obj.a = 2; // does not log anything: the 'a' key has not been read yet
|
||||
console.log(obj.a); // logs 2 and reads the 'a' key => it is now tracked
|
||||
obj.a = 3; // logs 'changed' because we updated a tracked value
|
||||
```
|
||||
|
||||
An important property of reactive objects is that they can be reobserved: this
|
||||
will create an independent proxy that tracks another set of keys:
|
||||
|
||||
```js
|
||||
const obj1 = reactive({ a: 1, b: 2 }, () => console.log("observer 1"));
|
||||
const obj2 = reactive(obj1, () => console.log("observer 2"));
|
||||
|
||||
console.log(obj1.a); // logs 1, and reads the 'a' key => it is now tracked by observer 1
|
||||
console.log(obj2.b); // logs 2, and 'b' is now tracked by observer 2
|
||||
obj2.a = 3; // only logs 'observer1', because observer2 does not track a
|
||||
obj2.b = 3; // only logs 'observer2', because observer1 does not track b
|
||||
console.log(obj2.a, obj1.b); // logs 3 and 3, while the object is observed independently, it is still a single object
|
||||
```
|
||||
|
||||
Because `useState` returns a normal reactive object, it is possible to call `reactive` on the result
|
||||
of a `useState` to observe changes to that object while outside the context of a component, or to
|
||||
call `useState` on reactive objects created outside of components. In those cases, one needs to be
|
||||
careful with regards to the lifetime of those reactive objects, as holding references to these
|
||||
objects may prevent garbage collection of the component and its data even if Owl has destroyed it.
|
||||
|
||||
### Subscriptions are ephemereal
|
||||
|
||||
Subscription to state changes are ephemereal, whenever an observer is notified that a state object
|
||||
has changed, all of its subscriptions are cleared, meaning that if it still cares about it, it
|
||||
should read the properties it cares about again. For example:
|
||||
|
||||
```js
|
||||
const obj = reactive({ a: 1 }, () => console.log("observer called"));
|
||||
|
||||
console.log(obj.a); // logs 1, and reads the 'a' key => it is now tracked by the observer
|
||||
obj.a = 3; // logs 'observer1' and clears the subscriptions of the observer
|
||||
obj.a = 4; // doesn't log anything, the key is no longer observed
|
||||
```
|
||||
|
||||
This may seem counter-intuitive, but it makes perfect sense in the context of components:
|
||||
|
||||
```js
|
||||
class DoubleCounter extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="'selected: ' + state.selected + ', value: ' + state[state.selected]"/>
|
||||
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
||||
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
||||
<button t-on-click="changeCounter">Switch counter</button>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ selected: "count1", count1: 0, count2: 0 });
|
||||
}
|
||||
|
||||
changeCounter() {
|
||||
this.state.selected = this.state.selected === "count1" ? "count2" : "count1";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this component, if we increment the value of the second counter, the component will not rerender,
|
||||
which makes sense as rerendering will have no effect, as the second counter is not displayed. If we
|
||||
toggle the component to display the second counter, we now no longer want the component to rerender
|
||||
when the value of the first counter changes, and this is what happens: a component only rerenders
|
||||
when there are changes to pieces of state that have been read during or after the previous render.
|
||||
If a piece of state has not been read in the last render, we know that its value won't influence the
|
||||
rendered output, and so we can ignore it.
|
||||
|
||||
### reactive `Map` and `Set`
|
||||
|
||||
The reactivity system has special support built-in for the standard container types `Map` and `Set`.
|
||||
They behave like one would expect: reading a key subscribes the observer to that key, adding or
|
||||
removing an item to them notifies observers that have used any of the iterators on that reactive
|
||||
object, such as `.entries()` or `.keys()`, likewise with clearing them.
|
||||
|
||||
## Escape hatches
|
||||
|
||||
Sometimes, it is desirable to bypass the reactivity system. Creating proxies when interacting with
|
||||
reactive objects is expensive, and while on the whole, the performance benefit that we get by
|
||||
rerendering only the parts of the interface that need it outweighs that cost, in some cases, we want
|
||||
to be able to opt out of creating them in the first place. This is the purpose of `markRaw`:
|
||||
|
||||
### `markRaw`
|
||||
|
||||
Marks an object so that it is ignored by the reactivity system, meaning that if this object is ever
|
||||
part of a reactive object, it will be returned as is, and no keys in that object will be
|
||||
observed.
|
||||
|
||||
```js
|
||||
const someObject = markRaw({ b: 1 });
|
||||
const state = useState({
|
||||
a: 1,
|
||||
obj: someObject,
|
||||
});
|
||||
console.log(state.obj.b); // attempt to subscribe to the "b" key in someObject
|
||||
state.obj.b = 2; // No rerender will occur here
|
||||
console.log(someObject === state.obj); // true
|
||||
```
|
||||
|
||||
This is useful in some rare cases. One such example would be if you want to use an array of objects
|
||||
that is potentially large to render a list, but those objects are known to be immutable:
|
||||
|
||||
```js
|
||||
this.items = useState([
|
||||
{ label: "some text", value: 42 },
|
||||
// ... 1000 total objects
|
||||
]);
|
||||
```
|
||||
|
||||
in the template:
|
||||
|
||||
```xml
|
||||
<t t-foreach="items" t-as="item" t-key="item.label" t-esc="item.label + item.value"/>
|
||||
```
|
||||
|
||||
Here, on every render, we go and read one thousand keys from a reactive object, which causes
|
||||
one thousand reactive objects to be created. If we know that the content of these objects
|
||||
cannot change, this is wasted work. If instead all of these objects are marked as raw, we avoid
|
||||
all of this work while keeping the ability to lean on the reactivity to track the presence and
|
||||
identity of these objects:
|
||||
|
||||
```js
|
||||
this.items = useState([
|
||||
markRaw({ label: "some text", value: 42 }),
|
||||
// ... 1000 total objects
|
||||
]);
|
||||
```
|
||||
|
||||
However, use this function with caution: this is an escape hatch from the reactivity
|
||||
system, and as such, using it may cause subtle and unintended issues! For example:
|
||||
|
||||
```js
|
||||
// This will cause a rerender
|
||||
this.items.push(markRaw({ label: "another label", value: 1337 }));
|
||||
|
||||
// THIS WILL NOT CAUSE A RENDER!
|
||||
this.items[17].value = 3;
|
||||
// The UI is now desynced from component's state until the next render caused by something else
|
||||
```
|
||||
|
||||
In short: only use `markRaw` if your application is slowing down noticeably and profiling reveals
|
||||
that a lot of time is spent creating useless reactive objects.
|
||||
|
||||
### `toRaw`
|
||||
|
||||
While `markRaw` marks an object so that it is never made reactive, `toRaw` takes an object and
|
||||
returns the underlying non-reactive object. It can be useful in some niche cases. In particular,
|
||||
because the reactivity system returns a proxy, the returned object does not compare equal to the
|
||||
original object:
|
||||
|
||||
```js
|
||||
const obj = {};
|
||||
const reactiveObj = reactive(obj);
|
||||
console.log(obj === reactiveObj); // false
|
||||
console.log(obj === toRaw(reactiveObj)); // true
|
||||
```
|
||||
|
||||
It can also be useful during debugging, as unfolding proxies recursively in debuggers can be confusing.
|
||||
|
||||
## Advanced usage
|
||||
|
||||
The following is a collection of small snippets that leverage the reactivity system in
|
||||
"non-standard" ways to help you understand its power and where using it might make your code simpler.
|
||||
|
||||
### Notification manager
|
||||
|
||||
Showing notifications is a pretty common need in web applications, you may want to show a
|
||||
notification from any other component within the application, and the notifications should stack on
|
||||
top of one another regardless of which component spawned them, here is how we can leverage the
|
||||
reactivity to accomplish this:
|
||||
|
||||
```js
|
||||
let notificationId = 1;
|
||||
const notifications = reactive({});
|
||||
class NotificationContainer extends Component {
|
||||
static template = xml`
|
||||
<t t-foreach="notifications" t-as="notification" t-key="notification_key" t-esc="notification"/>
|
||||
`;
|
||||
setup() {
|
||||
this.notifications = useState(notifications);
|
||||
}
|
||||
}
|
||||
|
||||
export function addNotification(label) {
|
||||
const id = notificationId++;
|
||||
notifications[id] = label;
|
||||
return () => {
|
||||
delete notifications[id];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Here, the `notifications` variable is a reactive object. Notice how we didn't give `reactive` a
|
||||
callback: this is because in this case, all we care about is that adding or removing notifications
|
||||
in the `addNotification` function goes through the reactivity system. The `NotificationContainer`
|
||||
component reobserves this object with `useState`, and is updated whenever notifications are
|
||||
added or removed.
|
||||
|
||||
### Store
|
||||
|
||||
Centralizing application state is a pretty common want/need in web applications. Because of the way
|
||||
the reactivity system works, you can treat any reactive object as a store, and if you call `useState`
|
||||
on it, components automatically observe only the part of the store that they're interested in:
|
||||
|
||||
```js
|
||||
export const store = reactive({
|
||||
list: [],
|
||||
add(item) {
|
||||
this.list.push(item);
|
||||
},
|
||||
});
|
||||
|
||||
export function useStore() {
|
||||
return useState(store);
|
||||
}
|
||||
```
|
||||
|
||||
In any component:
|
||||
|
||||
```js
|
||||
import { useStore } from "./store";
|
||||
|
||||
class List extends Component {
|
||||
static template = xml`
|
||||
<t t-foreach="store.list" t-as="item" t-key="item" t-esc="item"/>
|
||||
`;
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Anywhere in the application:
|
||||
|
||||
```js
|
||||
import { store } from "./store";
|
||||
// Will cause any instance of the List component in the app to update
|
||||
store.add("New list item!");
|
||||
```
|
||||
|
||||
Notice how we can make objects with methods into reactive objects, and when these methods are used
|
||||
to mutate the store contents, it works as expected. And while stores are generally one-off objects,
|
||||
it is entirely possible to make class instances reactive:
|
||||
|
||||
```js
|
||||
class Store {
|
||||
list = [];
|
||||
add(item) {
|
||||
this.list.push(item);
|
||||
}
|
||||
}
|
||||
// Essentially equivalent to the previous code
|
||||
export const store = reactive(new Store());
|
||||
```
|
||||
|
||||
Which can be useful to unit test the class separately.
|
||||
|
||||
### Local storage synchronization
|
||||
|
||||
Sometimes, you want to persist some state accross reloads, you can do this by storing it in the
|
||||
`localStorage`, but what if you want to update the `localStorage` item every time the state changes,
|
||||
so that you don't have to manually synchronize the states? Well, you can use the reactivity system
|
||||
to write a custom hook that will do that for you:
|
||||
|
||||
```js
|
||||
function useStoredState(key, initialState) {
|
||||
const state = JSON.parse(localStorage.getItem(key)) || initialState;
|
||||
const store = (obj) => localStorage.setItem(key, JSON.stringify(obj));
|
||||
const reactiveState = reactive(state, () => store(reactiveState));
|
||||
store(reactiveState);
|
||||
return useState(state);
|
||||
}
|
||||
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.state = useStoredState("MyComponent.state", { value: 1 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
One important thing to notice is that both times we call `store`, we call it with `reactiveState`,
|
||||
not `state`: we need `store` to read the keys through a reactive object for it to correctly
|
||||
subscribe to state changes. Notice also that we call `store` the first time by hand, as otherwise it
|
||||
will not be subscribed to anything, and no amount of change in the object will cause the reactive
|
||||
callback to be invoked.
|
||||
@@ -1,37 +0,0 @@
|
||||
# 🦉 References 🦉
|
||||
|
||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||
targeted by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
|
||||
more detail.
|
||||
|
||||
As a short example, here is how we could set the focus on a given input:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-ref="input"/>
|
||||
<button t-on-click="focusInput">Click</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
import { useRef } from "owl/hooks";
|
||||
|
||||
class SomeComponent extends Component {
|
||||
inputRef = useRef("input");
|
||||
|
||||
focusInput() {
|
||||
this.inputRef.el.focus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Be aware that the `el` property will only be set when the target of the `t-ref`
|
||||
directive is mounted in the DOM. Otherwise, it will be set to `null`.
|
||||
|
||||
The `useRef` hook cannot be used to get a reference to an instance of a sub
|
||||
component.
|
||||
|
||||
Note that this example uses the suffix `ref` to name the reference. This
|
||||
is not mandatory, but it is a useful convention, so we do not forget that it is
|
||||
a reference object.
|
||||
@@ -3,92 +3,71 @@
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Named slots](#named-slots)
|
||||
- [Rendering Context](#rendering-context)
|
||||
- [Default Slot](#default-slot)
|
||||
- [Default Content](#default-content)
|
||||
- [Dynamic slots](#dynamic-slots)
|
||||
- [Slots and props](#slots-and-props)
|
||||
- [Slot params](#slot-params)
|
||||
- [Slot scopes](#slot-scopes)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
|
||||
## Overview
|
||||
|
||||
Owl is a template based component system. There is therefore a need to be able
|
||||
to make generic components. For example, imagine a generic `Navbar`
|
||||
component, which displays a navbar, but with some customizable content. Since
|
||||
the specific content is only known to the user of the `Navbar`, it would be nice
|
||||
to specify it in the template where `Navbar` is used:
|
||||
to make generic components. For example, imagine a generic `Dialog`
|
||||
component, which is able to display some arbitrary content.
|
||||
|
||||
Obviously, we want to use this component everywhere in our application, to
|
||||
display various different content. The `Dialog` component is technically the
|
||||
owner of its content, but is only a container. The user of the `Dialog` is
|
||||
the component that want to _inject_ something inside the `Dialog`. This is
|
||||
exactly what slots are for.
|
||||
|
||||
## Example
|
||||
|
||||
To make generic components, it is useful to be able for a parent component to _inject_
|
||||
some sub template, but still be the owner. For example, a generic dialog component
|
||||
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>
|
||||
<Navbar>
|
||||
<span>Hello Owl</span>
|
||||
</Navbar>
|
||||
</div>
|
||||
```
|
||||
|
||||
This is exactly the way slots work! In the example above, the user of the `Navbar`
|
||||
component specify some content (here, in the default slot). The `Navbar`
|
||||
component can insert that content in its own template at the appropriate location.
|
||||
An important information to notice is that the content of the slot is rendered in
|
||||
the parent context, not in the navbar. As such, it can access values and methods
|
||||
from the parent component.
|
||||
|
||||
Here is how the `Navbar` component could be defined, with the `t-slot` directive:
|
||||
|
||||
```xml
|
||||
<div class="navbar">
|
||||
<t t-slot="default"/>
|
||||
<ul>
|
||||
<!-- rest of the navbar here -->
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Named slots
|
||||
|
||||
Default slots are very useful, but sometimes, we may need more than one slot.
|
||||
This is what named slots are for! For example, suppose we implement a component
|
||||
`InfoBox` that display a title and some specific content. Its template could look
|
||||
like this:
|
||||
|
||||
```xml
|
||||
<div class="info-box">
|
||||
<div class="info-box-title">
|
||||
<t t-slot="title"/>
|
||||
<span class="info-box-close-button" t-on-click="close">X</span>
|
||||
</div>
|
||||
<div class="info-box-content">
|
||||
<div t-name="Dialog" class="modal">
|
||||
<div class="modal-title"><t t-esc="props.title"/></div>
|
||||
<div class="modal-content">
|
||||
<t t-slot="content"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<t t-slot="footer"/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
And one could use it with the `t-set-slot` directive:
|
||||
Slots are defined by the caller, with the `t-set-slot` directive:
|
||||
|
||||
```xml
|
||||
<InfoBox>
|
||||
<t t-set-slot="title">
|
||||
Specific Title. It could be html also.
|
||||
</t>
|
||||
<t t-set-slot="content">
|
||||
<!-- some template here, with html, events, whatever -->
|
||||
</t>
|
||||
</InfoBox>
|
||||
<div t-name="SomeComponent">
|
||||
<div>some component</div>
|
||||
<Dialog title="'Some Dialog'">
|
||||
<t t-set-slot="content">
|
||||
<div>hey</div>
|
||||
</t>
|
||||
<t t-set-slot="footer">
|
||||
<button t-on-click="doSomething">ok</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Rendering context
|
||||
In this example, the component `Dialog` will render the slots `content` and `footer`
|
||||
with its parent as rendering context. This means that clicking on the button
|
||||
will execute the `doSomething` method on the parent, not on the dialog.
|
||||
|
||||
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).
|
||||
Note: Owl previously used the `t-set` directive to define the content of a slot.
|
||||
This is deprecated and should no longer be used in new code.
|
||||
|
||||
## Default Slot
|
||||
## Reference
|
||||
|
||||
All elements inside the component which are not a named slot will be treated as
|
||||
part of the content of the `default` slot. For example:
|
||||
### Default Slot
|
||||
|
||||
The first element inside the component which is not a named slot will
|
||||
be considered the `default` slot. For example:
|
||||
|
||||
```xml
|
||||
<div t-name="Parent">
|
||||
@@ -102,20 +81,7 @@ part of the content of the `default` slot. For example:
|
||||
</div>
|
||||
```
|
||||
|
||||
One can mix default slot and named slots:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<Child>
|
||||
default content
|
||||
<t t-set-slot="footer">
|
||||
content for footer slot here
|
||||
</t>
|
||||
</Child>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Default content
|
||||
### Default content
|
||||
|
||||
Slots can define a default content, in case the parent did not define them:
|
||||
|
||||
@@ -130,132 +96,16 @@ Slots can define a default content, in case the parent did not define them:
|
||||
<!-- will be rendered as: <div><span>default content</span></div> -->
|
||||
```
|
||||
|
||||
## Dynamic Slots
|
||||
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
|
||||
interpolation:
|
||||
interplolation:
|
||||
|
||||
```xml
|
||||
<t t-slot="{{current}}" />
|
||||
```
|
||||
|
||||
This will evaluate the `current` expression, and insert the corresponding slot
|
||||
at the place of the `t-slot` directive.
|
||||
|
||||
## Slots and props
|
||||
|
||||
In a sense, slots are almost the same as a prop: they define some information
|
||||
to pass to the child component. To make it possible to use it, and to pass it
|
||||
down to sub component, Owl actually define a special prop `slots` that contains
|
||||
all slot information given to the component. It looks like this:
|
||||
|
||||
```js
|
||||
{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }
|
||||
```
|
||||
|
||||
So, a component can pass its slots to a subcomponent like this:
|
||||
|
||||
```xml
|
||||
<Child slots="props.slots"/>
|
||||
```
|
||||
|
||||
## Slot params
|
||||
|
||||
For advanced usecases, it may be necessary to pass additional information to a
|
||||
slot. This can be done by providing extra key/value pairs to the `t-set-slot`
|
||||
directive. Then, the generic component can read them in its prop `slots`.
|
||||
|
||||
For example, here is how a Notebook component could be implemented (a component
|
||||
with multiple page, and a tab bar, which only render the current active page,
|
||||
and each page has a title).
|
||||
|
||||
```js
|
||||
class Notebook extends Component {
|
||||
static template = xml`
|
||||
<div class="notebook">
|
||||
<div class="tabs">
|
||||
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
|
||||
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab_index">
|
||||
<t t-esc="props.slots[tab].title"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="page">
|
||||
<t t-slot="{{currentSlot}}"/>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ activeTab: 0 });
|
||||
this.tabNames = Object.keys(this.props.slots);
|
||||
}
|
||||
|
||||
get currentSlot() {
|
||||
return this.tabNames[this.state.activeTab];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice how one can read the `title` value for each slots. Here is how one could
|
||||
use this `Notebook` component:
|
||||
|
||||
```xml
|
||||
<Notebook>
|
||||
<t t-set-slot="page1" title.translate="Page 1">
|
||||
<div>this is in the page 1</div>
|
||||
</t>
|
||||
<t t-set-slot="page2" title.translate="Page 2" hidden="somevalue">
|
||||
<div>this is in the page 2</div>
|
||||
</t>
|
||||
</Notebook>
|
||||
```
|
||||
|
||||
Slot params works like normal props, so one can use suffixes like `.translate`
|
||||
when a prop is a user facing string and should be translated, or `.bind` to
|
||||
bind a function if needed.
|
||||
|
||||
## Slot scopes
|
||||
|
||||
For other kinds of advanced use cases, the content of a slot may depends on some
|
||||
information specific to the generic component. This is the opposite of the slot
|
||||
params.
|
||||
|
||||
To solve this kind of problems, one can use the `t-slot-scope` directive along
|
||||
with the `t-set-slot`. This defines the name of a variable that can access
|
||||
everything given by the child component:
|
||||
|
||||
```xml
|
||||
<MyComponent>
|
||||
<t t-set-slot="foo" t-slot-scope="scope">
|
||||
content
|
||||
<t t-esc="scope.bool"/>
|
||||
<t t-esc="scope.num"/>
|
||||
</t>
|
||||
</MyComponent>
|
||||
```
|
||||
|
||||
And the child component that includes the slot can provide values like this:
|
||||
|
||||
```xml
|
||||
<t t-slot="foo" bool="other_var" num="5">
|
||||
```
|
||||
|
||||
or this:
|
||||
|
||||
```xml
|
||||
<t t-slot="foo" t-props="someObject">
|
||||
```
|
||||
|
||||
In the case of the default slot, you may declare the slot scope directly on the
|
||||
component itself:
|
||||
|
||||
```xml
|
||||
<MyComponent t-slot-scope="scope">
|
||||
content
|
||||
<t t-esc="scope.bool"/>
|
||||
<t t-esc="scope.num"/>
|
||||
</MyComponent>
|
||||
```
|
||||
|
||||
Slot values works like normal props, so one can use the `.bind` suffix to
|
||||
bind a function if needed.
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# 🦉 Tags 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [`xml` tag](#xml-tag)
|
||||
- [`css` tag](#css-tag)
|
||||
|
||||
## Overview
|
||||
|
||||
Tags are very small helpers intended to make it easy to write inline templates
|
||||
or styles. There are currently two tags: `css` and `xml`. With these functions,
|
||||
it is possible to write [single file components](../learning/how_to_write_sfc.md).
|
||||
|
||||
## XML tag
|
||||
|
||||
The `xml` tag is certainly the most useful tag. It is used to define an inline
|
||||
QWeb template for a component. Without tags, creating a standalone component
|
||||
would look like this:
|
||||
|
||||
```js
|
||||
import { Component } from 'owl'
|
||||
|
||||
const name = 'some-unique-name';
|
||||
const template = `
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
QWeb.registerTemplate(name, template);
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = name;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
With tags, this process is slightly simplified. The name is uniquely generated,
|
||||
and the template is automatically registered:
|
||||
|
||||
```js
|
||||
const { Component } = owl;
|
||||
const { xml } = owl.tags;
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## CSS tag
|
||||
|
||||
The CSS tag is useful to define a css stylesheet in the javascript file:
|
||||
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div class="my-component">some template</div>
|
||||
`;
|
||||
static style = css`
|
||||
.my-component {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
The `css` tag registers internally the css information. Then, whenever the first
|
||||
instance of the component is created, will add a `<style>` tag to the document
|
||||
`<head>`.
|
||||
|
||||
Note that to make it more useful, like other css preprocessors, the `css` tag
|
||||
accepts a small extension of the css specification: css scopes can be nested,
|
||||
and the rules will then be expanded by the `css` helper:
|
||||
|
||||
```scss
|
||||
.my-component {
|
||||
display: block;
|
||||
.sub-component h {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
will be formatted as:
|
||||
|
||||
```css
|
||||
.my-component {
|
||||
display: block;
|
||||
}
|
||||
.my-component .sub-component h {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
This extension brings another useful feature: the `&` selector which refers to
|
||||
the parent selector. For example, we want our component to be red when hovered.
|
||||
We would like to write something like:
|
||||
|
||||
```scss
|
||||
.my-component {
|
||||
display: block;
|
||||
:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
but it will be formatted as:
|
||||
|
||||
```css
|
||||
.my-component {
|
||||
display: block;
|
||||
}
|
||||
.my-component :hover {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
The `&` selector can be used to solve this problem:
|
||||
|
||||
```scss
|
||||
.my-component {
|
||||
display: block;
|
||||
&:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
will be formatted as:
|
||||
|
||||
```css
|
||||
.my-component {
|
||||
display: block;
|
||||
}
|
||||
.my-component:hover {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
Now, there is no additional processing done by the `css` tag. However, since it
|
||||
is done in javascript at runtime, we actually have more power. For example:
|
||||
|
||||
1. sharing values between javascript and css:
|
||||
|
||||
```js
|
||||
import { theme } from "./theme";
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div class="my-component">...</div>`;
|
||||
static style = css`
|
||||
.my-component {
|
||||
color: ${theme.MAIN_COLOR};
|
||||
background-color: ${theme.SECONDARY_color};
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
2. scoping rules to the current component:
|
||||
|
||||
```js
|
||||
import { generateUUID } from "./utils";
|
||||
|
||||
const uuid = generateUUID();
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div data-o-${uuid}="">...</div>`;
|
||||
static style = css`
|
||||
[data-o-${uuid}] {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
# 🦉 Translations 🦉
|
||||
|
||||
If properly setup, Owl can translate all rendered templates. To do
|
||||
so, it needs a translate function, which takes a string and returns a string.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
const translations = {
|
||||
hello: "bonjour",
|
||||
yes: "oui",
|
||||
no: "non",
|
||||
};
|
||||
const translateFn = (str) => translations[str] || str;
|
||||
|
||||
const app = new App(Root, { templates, tranaslateFn });
|
||||
// ...
|
||||
```
|
||||
|
||||
See the [app configuration page](app.md#configuration) for more info on how to
|
||||
configure an Owl application.
|
||||
|
||||
Once setup, all rendered templates will be translated using `translateFn`:
|
||||
|
||||
- each text node will be replaced with its translation,
|
||||
- each of the following attribute values will be translated as well: `title`,
|
||||
`placeholder`, `label` and `alt`,
|
||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||
if its value is `off`.
|
||||
|
||||
So, with the above `translateFn`, the following templates:
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
<div t-translation="off">hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="hello" other="yes"/>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<div>bonjour</div>
|
||||
<div>hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="bonjour" other="yes"/>
|
||||
```
|
||||
|
||||
Note that the translation is done during the compilation of the template, not
|
||||
when it is rendered.
|
||||
|
||||
In some case, it is useful to be able to extend the list of translatable attributes.
|
||||
For example, one may want to also translate `data-title` attributes. To do that,
|
||||
we can define additional attributes with the `translatableAttributes` option:
|
||||
|
||||
```js
|
||||
const app = new App(Root, { templates, tranaslateFn, translatableAttributes: ["data-title"] });
|
||||
// ...
|
||||
```
|
||||
|
||||
It is also possible to remove an attribute from the default list by prefixing it with `-`:
|
||||
|
||||
```js
|
||||
const app = new App(Root, {
|
||||
templates,
|
||||
tranaslateFn,
|
||||
translatableAttributes: ["data-title", "-title"],
|
||||
});
|
||||
// data-title attribute will be translated, but not title attribute...
|
||||
```
|
||||
@@ -6,10 +6,11 @@ functions are all available in the `owl.utils` namespace.
|
||||
## Content
|
||||
|
||||
- [`whenReady`](#whenready): executing code when DOM is ready
|
||||
- [`loadJS`](#loadjs): loading script files
|
||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||
- [`EventBus`](#eventbus): a simple EventBus
|
||||
- [`validate`](#validate): a validation function
|
||||
- [`batched`](#batched): batch function calls
|
||||
- [`escape`](#escape): sanitizing strings
|
||||
- [`debounce`](#debounce): limiting rate of function calls
|
||||
- [`shallowEqual`](#shallowequal): shallow object comparison
|
||||
|
||||
## `whenReady`
|
||||
|
||||
@@ -18,20 +19,40 @@ not ready yet, resolved directly otherwise). If called with a callback as
|
||||
argument, it executes it as soon as the DOM ready (or directly).
|
||||
|
||||
```js
|
||||
const { whenReady } = owl;
|
||||
|
||||
await whenReady();
|
||||
// do something
|
||||
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
|
||||
const qweb = new owl.QWeb({ templates });
|
||||
const env = { qweb };
|
||||
await mount(App, { env, target: document.body });
|
||||
});
|
||||
```
|
||||
|
||||
or alternatively:
|
||||
|
||||
```js
|
||||
whenReady(function () {
|
||||
// do something
|
||||
owl.utils.whenReady(function () {
|
||||
const qweb = new owl.QWeb();
|
||||
const env = { qweb };
|
||||
await mount(App, { env, target: document.body });
|
||||
});
|
||||
```
|
||||
|
||||
## `loadJS`
|
||||
|
||||
`loadJS` takes a url (string) for a javascript resource, and loads it (by adding
|
||||
a script tag in the document head). It returns a promise, so the caller can
|
||||
properly reacts when it is ready. Also, it is smart: it maintains a list of urls
|
||||
previously loaded (or currently being loaded), and prevent doing twice the work.
|
||||
|
||||
For example, it is useful for lazy loading external libraries:
|
||||
|
||||
```js
|
||||
class MyComponent extends owl.Component {
|
||||
willStart() {
|
||||
return owl.utils.loadJS("/static/libs/someLib.js");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `loadFile`
|
||||
|
||||
`loadFile` is a helper function to fetch a file. It simply
|
||||
@@ -39,62 +60,89 @@ performs a `GET` request and returns the resulting string in a promise. The
|
||||
initial usecase for this function is to load a template file. For example:
|
||||
|
||||
```js
|
||||
const { loadFile } = owl;
|
||||
|
||||
async function makeEnv() {
|
||||
const templates = await loadFile("templates.xml");
|
||||
// do something
|
||||
const templates = await owl.utils.loadFile("templates.xml");
|
||||
const qweb = new owl.QWeb({ templates });
|
||||
return { qweb };
|
||||
}
|
||||
```
|
||||
|
||||
## `EventBus`
|
||||
Note that unlike `loadJS`, this function returns the content of the file as a
|
||||
string. It does not add a `script` tag or any other side effect.
|
||||
|
||||
It is a simple `EventBus`, with the same API as usual DOM elements, and an
|
||||
additional `trigger` method to dispatch events:
|
||||
## `escape`
|
||||
|
||||
```js
|
||||
const bus = new EventBus();
|
||||
bus.addEventListener("event", () => console.log("something happened"));
|
||||
Sometimes, we need to display dynamic data (for example user-generated data) in
|
||||
the user interface. If this is done by a `QWeb` template, it is not an issue:
|
||||
|
||||
bus.trigger("event"); // 'something happened' is logged
|
||||
```xml
|
||||
<div><t t-esc="user.data"/></div>
|
||||
```
|
||||
|
||||
## `validate`
|
||||
|
||||
The `validate` function is a function that validates if a given object satisfies a
|
||||
specified schema. It is actually used by Owl itself to perform
|
||||
[props validation](props.md#props-validation). For example:
|
||||
The `QWeb` engine will create a `div` node and add the content of the `user.data`
|
||||
string as a text node, so the web browser will not parse it as html. However,
|
||||
it may be a problem if this is done with some javascript code like this:
|
||||
|
||||
```js
|
||||
validate(
|
||||
{ a: "hey" },
|
||||
{
|
||||
id: Number,
|
||||
url: [Boolean, { type: Array, element: Number }],
|
||||
class BadComponent extends Component {
|
||||
// some template with a ref to a div
|
||||
// some code ...
|
||||
|
||||
mounted() {
|
||||
this.divRef.el.innerHTML = this.state.value;
|
||||
}
|
||||
);
|
||||
|
||||
// throws an error with the following information:
|
||||
// - unknown key 'a',
|
||||
// - 'id' is missing (should be a number),
|
||||
// - 'url' is missing (should be a boolean or list of numbers),
|
||||
}
|
||||
```
|
||||
|
||||
## `batched`
|
||||
|
||||
The `batched` function creates a batched version of a callback so that multiple calls to it within the same microtick will only result in a single invocation of the original callback.
|
||||
In this case, the content of the `div` will be parsed as html, which may inject
|
||||
unwanted behaviour. To fix this, the `escape` function will simply transform a
|
||||
string into an escaped version of the same string, which will be properly displayed
|
||||
by the browser, but which will not be parsed as html (for example, `"<ok>"` is
|
||||
escaped to the string: `"<ok>"`). So, the bad example above can be fixed
|
||||
with the following change:
|
||||
|
||||
```js
|
||||
function hello() {
|
||||
console.log("hello");
|
||||
}
|
||||
|
||||
const batchedHello = batched(hello);
|
||||
batchedHello();
|
||||
// Nothing is logged
|
||||
batchedHello();
|
||||
// Still not logged
|
||||
|
||||
await Promise.resolve(); // Await the next microtick
|
||||
// "hello" is logged only once
|
||||
this.divRef.el.innerHTML = owl.utils.escape(this.state.value);
|
||||
```
|
||||
|
||||
## `debounce`
|
||||
|
||||
The `debounce` function is useful when we want to limit the number of times some
|
||||
function/action is perfomed. For example, this may be useful to prevent issue
|
||||
with people double clicking on a button.
|
||||
|
||||
It takes three arguments:
|
||||
|
||||
- `func` (function): this is the function that will be rate limited
|
||||
- `wait` (number): this is the number of milliseconds that we want to use to
|
||||
rate limit the function `func`
|
||||
- `immediate` (optional, boolean, default=false): if `immediate` is true, the
|
||||
function will be triggered immediately (leading edge of the interval). If false,
|
||||
the function will be triggered at the end (trailing edge).
|
||||
|
||||
It returns a function. For example:
|
||||
|
||||
```js
|
||||
const debounce = owl.utils.debounce;
|
||||
window.addEventListener("mousemove", debounce(doSomething, 100));
|
||||
```
|
||||
|
||||
As this example shows, it is usualy useful for event handlers which are triggered
|
||||
very quickly, such as `scroll` or `mousemove` events.
|
||||
|
||||
## `shallowEqual`
|
||||
|
||||
This function checks if two objects have the same values assigned to each keys:
|
||||
|
||||
```js
|
||||
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true
|
||||
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 3 }); // false
|
||||
```
|
||||
|
||||
However, for performance reasons, it assumes that the two objects have the same
|
||||
keys. If we are in a situation where this is not guaranteed, the following code
|
||||
will work:
|
||||
|
||||
```js
|
||||
const completeShallowEqual = (a, b) => shallowEqual(a, b) && shallowEqual(b, a);
|
||||
```
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
# Owl Devtools Browser extension
|
||||
|
||||
The owl devtools browser extension is an extension available on chrome or firefox which adds an owl tab
|
||||
to the browser devtools in order to inspect all owl apps that are present on any web page, their components
|
||||
and allows to interract with their data to a certain extend. There is also a profiler available to visualize
|
||||
the components' lifecycle and be able to trace their origin.
|
||||
|
||||
See the [`devtools doc`](devtools_guide.md) for more information.
|
||||
|
||||
## Install the extension manually (for devs)
|
||||
|
||||
In the owl root folder:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
For chrome:
|
||||
|
||||
```bash
|
||||
npm run build:devtools-chrome
|
||||
```
|
||||
|
||||
For firefox:
|
||||
|
||||
```bash
|
||||
npm run build:devtools-firefox
|
||||
```
|
||||
|
||||
You can also run:
|
||||
|
||||
```bash
|
||||
npm run dev:devtools-chrome
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
npm run dev:devtools-firefox
|
||||
```
|
||||
|
||||
to avoid recompiling owl and gain time if it has already been done.
|
||||
|
||||
To run the extension:
|
||||
|
||||
In google chrome: go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
|
||||
Select the output folder (dist/devtools) and that's it, your extension is active!
|
||||
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
|
||||
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
|
||||
|
||||
In firefox: go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
|
||||
Select any file of the output folder (dist/devtools) and that's it, your extension is active!
|
||||
Here, you can use the reload button to refresh the extension.
|
||||
|
||||
Note that you may have to open another window or reload your tab to see the extension working.
|
||||
Also note that the extension will only be active on pages that have a sufficient version of owl.
|
||||
@@ -1,171 +0,0 @@
|
||||
# Owl Devtools Guide
|
||||
|
||||
## Information popup
|
||||
|
||||
After having installed the extension, a new icon will be added to your extension bar.
|
||||
If you don't see it, you can pin the extension using the extensions popup.
|
||||
|
||||
<img src="screenshots/extensions.png"/>
|
||||
|
||||
Clicking on the owl icon will open the information popup. This popup is useful
|
||||
to know in advance whether owl is loaded in the tab or not. This is also indicated
|
||||
by the icon itself: if it is flipped upside-down, it means that owl is not loaded in the
|
||||
active tab. Do note that old versions of owl are not supported by the extension and will
|
||||
therefore be indicated either as obsolete or absent by the extension popup.
|
||||
|
||||
<img src="screenshots/popup.png"/>
|
||||
|
||||
## First steps
|
||||
|
||||
When you are on a page where owl is detected, you can open your devtools either with
|
||||
right-click -> Inspect or using F12. In the devtools menu, you can search for the Owl
|
||||
tab which is added by the extension. It will be present by default at the end of the list but
|
||||
you can drag and drop it at the position you want for easier navigation in the future.
|
||||
|
||||
<img src="screenshots/find_owl_tab.png"/>
|
||||
|
||||
When you open the tab, you arrive on the Components view by default which is one of the
|
||||
two available tabs at the top. Here is an example of the devtools on the Odoo CRM app:
|
||||
|
||||
<img src="screenshots/crm.png"/>
|
||||
|
||||
## Components tab
|
||||
|
||||
The components tab is separated into two sub windows: the components tree in the left and
|
||||
the component details in the right. The components tree will display all the different
|
||||
components that are present in the tab in the form of a tree. The root of this tree is
|
||||
actually the app which is not a component but can still be inspected by the devtools like
|
||||
one. There can also be multiple apps loaded in the page like in website:
|
||||
|
||||
<img src="screenshots/multi_apps.png"/>
|
||||
|
||||
There is a convenient search bar at the top of the components tree which will help finding
|
||||
the components tou want in the tree and also, an element picker can be used to directly select
|
||||
the component you want to focus on in the page which is especially useful when 1trying to find
|
||||
what you want. Just click on the elements picker icon and click on the element you want to focus
|
||||
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
|
||||
page in this mode will highlight it and the same happens anytime in the components tree.
|
||||
|
||||
<img src="screenshots/picker.png"/>
|
||||
|
||||
In the tree itself, the navigation is quite simple and is similar to the one in the Elements tab
|
||||
of the browser's devtools. It is possible to navigate with the keyboard using the arrow keys and
|
||||
multiple shortcuts are available in a custom menu when right-clicking on a component. This menu
|
||||
allows to expand/fold all the children nodes of a component, fold its direct children only, inspect
|
||||
the source code of the component, send it as a global variable in the console, go to the Elements tab
|
||||
and focus on its content, force a rerender of the component, send its observed states to the console
|
||||
as a global variable, inspect its compiled template in the Sources tab or send its raw template
|
||||
to the console.
|
||||
|
||||
<img src="screenshots/menu.png"/>
|
||||
|
||||
The component details window in the right will show the component that is currently selected as well
|
||||
as its env, props, observed states and all the other variables that are present on its instance.
|
||||
While the props and the env are already present on the actual instance of the component and are
|
||||
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
|
||||
|
||||
The observed state is actually information about which variables are being observed by the component:
|
||||
when any property of a reactive object is being read by the component, the component will subscribe
|
||||
to this property which means it will listen to any change that can occur on the property and render
|
||||
when such a change occurs. This can be visualized easily within the devtools inside of the observed
|
||||
state section: observed properties of the reactive object(s) are displayed in bold while the others
|
||||
are greyed out. Do keep in mind that a greyed out property in the observed state of one component
|
||||
may be observed by another and the other way around is also possible. Here is an example for some
|
||||
user Field component:
|
||||
|
||||
<img src="screenshots/states.png"/>
|
||||
|
||||
Navigation inside the properties is also similar to the one in console variables: properties have
|
||||
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
|
||||
send any property to the console using the right-click context menu on it and functions can be inspected
|
||||
in the sources tab as well.
|
||||
|
||||
<img src="screenshots/function_menu.png"/>
|
||||
|
||||
Using the right-click context menu on a property also allows to observe variables. Observed variables will
|
||||
be sent to a dedicated section of the details window and their value will be refreshed every 200ms. These
|
||||
variables are only shown when they are found and their access path will be kept in memory inside the
|
||||
browser so that it will always persist until the user decides to stop observing the variable. As in the
|
||||
browser's devtools, observed objects are displayed in reduced form and cannot be interacted with. It is
|
||||
still possible to send them to the console or remove them from the list using right-click.
|
||||
|
||||
<img src="screenshots/observe_variables.png"/>
|
||||
|
||||
The last section of the details window is filled with the component's lifecycle hooks. Using right click on
|
||||
them allows to place breakpoints inside the hook (either on its instance or class, hooks like mounted and
|
||||
willStart cannot have instance-based breakpoints because they will never trigger). Conditions in conditional
|
||||
breakpoints will be evaluated in the context of the component's definition.
|
||||
|
||||
<img src="screenshots/hooks.png"/>
|
||||
|
||||
There are several icons available to perform several of the actions described before in the components
|
||||
tree context menu and all these actions are also available by opening the menu by right-clicking on the
|
||||
component's name. Using the left click on the component's name will focus it in the components tree.
|
||||
|
||||
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
|
||||
property's value and modify it using the freshly created input then press enter to apply the changes.
|
||||
Do note that the modified values should be written in JSON format in order to be valid (examples:
|
||||
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Editing any value will produce a
|
||||
manual render of the component (or the root component of the application in the case of env values).
|
||||
Whether the edition has an impact on the component or not and whether it produces an error is the
|
||||
responsability of the user.
|
||||
|
||||
<img src="screenshots/edit.png"/>
|
||||
|
||||
## Profiler
|
||||
|
||||
The profiler tab is the other tab of the owl devtools. It consists in an actions bar at the top and
|
||||
a tree/list of events related to the owl components' renders. Here is an example of the events launched
|
||||
when entering the Odoo Crm app.
|
||||
|
||||
<img src="screenshots/profiler.png"/>
|
||||
|
||||
In the initial state, no event is displayed. You need to activate the recording of events before they
|
||||
are intercepted by the devtools using the record button.
|
||||
|
||||
<img src="screenshots/record.png"/>
|
||||
|
||||
The second button is used to clear all the events that have been recorded. The select can be used to
|
||||
switch between the tree view (which shows the causality between renders) and the events log view which
|
||||
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
|
||||
update and destroy events which reveals the component that initiated the event. Also, a transition line will
|
||||
appear each time a new animation frame has been loaded between events.
|
||||
|
||||
<img src="screenshots/events_log.png"/>
|
||||
|
||||
The third button is only visible in tree view and allows to fold all the render events that were recorded.
|
||||
Some actions are also available when using the right-click on any event of the tree view for navigation
|
||||
purpose in a similar fashion as in the components tree.
|
||||
|
||||
<img src="screenshots/tree_actions.png"/>
|
||||
|
||||
There is also the Trace Renderings and Trace Subscriptions features. These features are independant of the
|
||||
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
|
||||
the console all the render events and allows to show their traceback information. Similarly, the Trace
|
||||
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
|
||||
of the modification.
|
||||
|
||||
<img src="screenshots/trace_rendering.png"/>
|
||||
<img src="screenshots/trace_subscriptions.png"/>
|
||||
|
||||
The Owl Devtools also allow to inspect iframes coded in Owl: when an Owl iframe is detected in the page,
|
||||
the iframe selector will appear next to the tabs. This allows to switch from an iframe to another easily.
|
||||
Be aware that switching iframes will clear all record events from the profiler tab. Iframes detection is
|
||||
currently not working in the firefox version, we are aware of this issue and will try to address it in the
|
||||
future.
|
||||
|
||||
<img src="screenshots/iframes.png"/>
|
||||
|
||||
## Options
|
||||
|
||||
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
|
||||
be toggled using the sun/moon icon at the top-right corner of the tab. There is also a refresh button to
|
||||
completely reset the owl devtools.
|
||||
|
||||
<img src="screenshots/darkmode.png"/>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the feedback from the page to the devtools seems to be cut, you can first try to use the refresh
|
||||
button mentioned above but if it still doesn't seem to work, just close the devtools and refresh the page.
|
||||
This will eventually happen any time a tab stays opened for too long without being refreshed.
|
||||
|
Before Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 182 KiB |
@@ -1,72 +0,0 @@
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
|
||||
/* Tomorrow Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #8e908c;
|
||||
}
|
||||
|
||||
/* Tomorrow Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-deletion {
|
||||
color: #c82829;
|
||||
}
|
||||
|
||||
/* Tomorrow Orange */
|
||||
.hljs-number,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params,
|
||||
.hljs-meta,
|
||||
.hljs-link {
|
||||
color: #f5871f;
|
||||
}
|
||||
|
||||
/* Tomorrow Yellow */
|
||||
.hljs-attribute {
|
||||
color: #eab700;
|
||||
}
|
||||
|
||||
/* Tomorrow Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
color: #718c00;
|
||||
}
|
||||
|
||||
/* Tomorrow Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #4271ae;
|
||||
}
|
||||
|
||||
/* Tomorrow Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #8959a8;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
background: white;
|
||||
color: #4d4d4c;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
html, body {
|
||||
background-color: #fefefe;
|
||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-gap: 3rem;
|
||||
grid-template-areas: "header" "nav" "main" "footer";
|
||||
grid-template-columns: 1fr;
|
||||
height: 100%;
|
||||
grid-template-rows: 1fr 3rem 1fr 3rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
body {
|
||||
grid-template-rows: 1fr 3rem 1fr 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00A09D;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:visited {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
header {
|
||||
grid-area: header;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
text-align: center;
|
||||
padding-top: 3rem !important;
|
||||
}
|
||||
|
||||
hgroup > p {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
nav {
|
||||
grid-area: nav;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-area: main;
|
||||
/*justify-self: center;*/
|
||||
grid-column: 1 / -1;
|
||||
/*max-width: 80rem !important;*/
|
||||
/*margin: 3rem 0;*/
|
||||
}
|
||||
|
||||
footer {
|
||||
grid-area: footer;
|
||||
padding-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
header, nav, footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
article h3 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
article.brand {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
article.design {
|
||||
background-color: #875A7B;
|
||||
color: #f0eeee;
|
||||
}
|
||||
|
||||
header .homepage-logo {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
margin-left: initial;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row .column {
|
||||
margin-bottom: initial;
|
||||
padding: initial;
|
||||
}
|
||||
}
|
||||
@media (min-width: 60rem) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
margin-left: -1.0rem;
|
||||
width: calc(100% + 2.0rem);
|
||||
}
|
||||
.row .column {
|
||||
margin-bottom: inherit;
|
||||
padding: 0 1.0rem;
|
||||
}
|
||||
}
|
||||
@@ -1,602 +0,0 @@
|
||||
/*!
|
||||
* Milligram v1.3.0
|
||||
* https://milligram.github.io
|
||||
*
|
||||
* Copyright (c) 2017 CJ Patoilo
|
||||
* Licensed under the MIT license
|
||||
*/
|
||||
|
||||
*,
|
||||
*:after,
|
||||
*:before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #606c76;
|
||||
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 1.6em;
|
||||
font-weight: 300;
|
||||
letter-spacing: .01em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 0.3rem solid #d1d1d1;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
blockquote *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.button,
|
||||
button,
|
||||
input[type='button'],
|
||||
input[type='reset'],
|
||||
input[type='submit'] {
|
||||
background-color: #9b4dca;
|
||||
border: 0.1rem solid #9b4dca;
|
||||
border-radius: .4rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
height: 3.8rem;
|
||||
letter-spacing: .1rem;
|
||||
line-height: 3.8rem;
|
||||
padding: 0 3.0rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button:focus, .button:hover,
|
||||
button:focus,
|
||||
button:hover,
|
||||
input[type='button']:focus,
|
||||
input[type='button']:hover,
|
||||
input[type='reset']:focus,
|
||||
input[type='reset']:hover,
|
||||
input[type='submit']:focus,
|
||||
input[type='submit']:hover {
|
||||
background-color: #606c76;
|
||||
border-color: #606c76;
|
||||
color: #fff;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.button[disabled],
|
||||
button[disabled],
|
||||
input[type='button'][disabled],
|
||||
input[type='reset'][disabled],
|
||||
input[type='submit'][disabled] {
|
||||
cursor: default;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.button[disabled]:focus, .button[disabled]:hover,
|
||||
button[disabled]:focus,
|
||||
button[disabled]:hover,
|
||||
input[type='button'][disabled]:focus,
|
||||
input[type='button'][disabled]:hover,
|
||||
input[type='reset'][disabled]:focus,
|
||||
input[type='reset'][disabled]:hover,
|
||||
input[type='submit'][disabled]:focus,
|
||||
input[type='submit'][disabled]:hover {
|
||||
background-color: #9b4dca;
|
||||
border-color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-outline,
|
||||
button.button-outline,
|
||||
input[type='button'].button-outline,
|
||||
input[type='reset'].button-outline,
|
||||
input[type='submit'].button-outline {
|
||||
background-color: transparent;
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-outline:focus, .button.button-outline:hover,
|
||||
button.button-outline:focus,
|
||||
button.button-outline:hover,
|
||||
input[type='button'].button-outline:focus,
|
||||
input[type='button'].button-outline:hover,
|
||||
input[type='reset'].button-outline:focus,
|
||||
input[type='reset'].button-outline:hover,
|
||||
input[type='submit'].button-outline:focus,
|
||||
input[type='submit'].button-outline:hover {
|
||||
background-color: transparent;
|
||||
border-color: #606c76;
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
|
||||
button.button-outline[disabled]:focus,
|
||||
button.button-outline[disabled]:hover,
|
||||
input[type='button'].button-outline[disabled]:focus,
|
||||
input[type='button'].button-outline[disabled]:hover,
|
||||
input[type='reset'].button-outline[disabled]:focus,
|
||||
input[type='reset'].button-outline[disabled]:hover,
|
||||
input[type='submit'].button-outline[disabled]:focus,
|
||||
input[type='submit'].button-outline[disabled]:hover {
|
||||
border-color: inherit;
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-clear,
|
||||
button.button-clear,
|
||||
input[type='button'].button-clear,
|
||||
input[type='reset'].button-clear,
|
||||
input[type='submit'].button-clear {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-clear:focus, .button.button-clear:hover,
|
||||
button.button-clear:focus,
|
||||
button.button-clear:hover,
|
||||
input[type='button'].button-clear:focus,
|
||||
input[type='button'].button-clear:hover,
|
||||
input[type='reset'].button-clear:focus,
|
||||
input[type='reset'].button-clear:hover,
|
||||
input[type='submit'].button-clear:focus,
|
||||
input[type='submit'].button-clear:hover {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
|
||||
button.button-clear[disabled]:focus,
|
||||
button.button-clear[disabled]:hover,
|
||||
input[type='button'].button-clear[disabled]:focus,
|
||||
input[type='button'].button-clear[disabled]:hover,
|
||||
input[type='reset'].button-clear[disabled]:focus,
|
||||
input[type='reset'].button-clear[disabled]:hover,
|
||||
input[type='submit'].button-clear[disabled]:focus,
|
||||
input[type='submit'].button-clear[disabled]:hover {
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f4f5f6;
|
||||
border-radius: .4rem;
|
||||
font-size: 86%;
|
||||
margin: 0 .2rem;
|
||||
padding: .2rem .5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f4f5f6;
|
||||
border-left: 0.3rem solid #9b4dca;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 0.1rem solid #f4f5f6;
|
||||
margin: 3.0rem 0;
|
||||
}
|
||||
|
||||
input[type='email'],
|
||||
input[type='number'],
|
||||
input[type='password'],
|
||||
input[type='search'],
|
||||
input[type='tel'],
|
||||
input[type='text'],
|
||||
input[type='url'],
|
||||
textarea,
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 0.1rem solid #d1d1d1;
|
||||
border-radius: .4rem;
|
||||
box-shadow: none;
|
||||
box-sizing: inherit;
|
||||
height: 3.8rem;
|
||||
padding: .6rem 1.0rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type='email']:focus,
|
||||
input[type='number']:focus,
|
||||
input[type='password']:focus,
|
||||
input[type='search']:focus,
|
||||
input[type='tel']:focus,
|
||||
input[type='text']:focus,
|
||||
input[type='url']:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: #9b4dca;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
|
||||
padding-right: 3.0rem;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 6.5rem;
|
||||
}
|
||||
|
||||
label,
|
||||
legend {
|
||||
display: block;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.label-inline {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 112.0rem;
|
||||
padding: 0 2.0rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row.row-no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.row.row-no-padding > .column {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.row.row-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row.row-top {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.row.row-bottom {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.row.row-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row.row-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.row.row-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.row .column {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
margin-left: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-10 {
|
||||
margin-left: 10%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-20 {
|
||||
margin-left: 20%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-25 {
|
||||
margin-left: 25%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-33, .row .column.column-offset-34 {
|
||||
margin-left: 33.3333%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-50 {
|
||||
margin-left: 50%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-66, .row .column.column-offset-67 {
|
||||
margin-left: 66.6666%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-75 {
|
||||
margin-left: 75%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-80 {
|
||||
margin-left: 80%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-90 {
|
||||
margin-left: 90%;
|
||||
}
|
||||
|
||||
.row .column.column-10 {
|
||||
flex: 0 0 10%;
|
||||
max-width: 10%;
|
||||
}
|
||||
|
||||
.row .column.column-20 {
|
||||
flex: 0 0 20%;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.row .column.column-25 {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.row .column.column-33, .row .column.column-34 {
|
||||
flex: 0 0 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
}
|
||||
|
||||
.row .column.column-40 {
|
||||
flex: 0 0 40%;
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
.row .column.column-50 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.row .column.column-60 {
|
||||
flex: 0 0 60%;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.row .column.column-66, .row .column.column-67 {
|
||||
flex: 0 0 66.6666%;
|
||||
max-width: 66.6666%;
|
||||
}
|
||||
|
||||
.row .column.column-75 {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.row .column.column-80 {
|
||||
flex: 0 0 80%;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.row .column.column-90 {
|
||||
flex: 0 0 90%;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.row .column .column-top {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.row .column .column-bottom {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.row .column .column-center {
|
||||
-ms-grid-row-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
margin-left: -1.0rem;
|
||||
width: calc(100% + 2.0rem);
|
||||
}
|
||||
.row .column {
|
||||
margin-bottom: inherit;
|
||||
padding: 0 1.0rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #9b4dca;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:focus, a:hover {
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
dl,
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
dl dl,
|
||||
dl ol,
|
||||
dl ul,
|
||||
ol dl,
|
||||
ol ol,
|
||||
ol ul,
|
||||
ul dl,
|
||||
ul ol,
|
||||
ul ul {
|
||||
font-size: 90%;
|
||||
margin: 1.5rem 0 1.5rem 3.0rem;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal inside;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: circle inside;
|
||||
}
|
||||
|
||||
.button,
|
||||
button,
|
||||
dd,
|
||||
dt,
|
||||
li {
|
||||
margin-bottom: 1.0rem;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
form,
|
||||
ol,
|
||||
p,
|
||||
pre,
|
||||
table,
|
||||
ul {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 0.1rem solid #e1e1e1;
|
||||
padding: 1.2rem 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td:first-child,
|
||||
th:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
td:last-child,
|
||||
th:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 300;
|
||||
letter-spacing: -.1rem;
|
||||
margin-bottom: 2.0rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4.6rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3.6rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2.8rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: -.08rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.8rem;
|
||||
letter-spacing: -.05rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=milligram.css.map */
|
||||
@@ -1,349 +0,0 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,15 +0,0 @@
|
||||
import { mount, Component, useState, loadFile } from "@odoo/owl";
|
||||
|
||||
class Counter extends Component {
|
||||
static template = "Counter";
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
const templates = await loadFile("./counter.xml");
|
||||
|
||||
mount(Counter, document.getElementById("app-container"), { templates });
|
||||
@@ -1,7 +0,0 @@
|
||||
<templates>
|
||||
<t t-name="Counter">
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,9 +0,0 @@
|
||||
import { loadFile } from "@odoo/owl";
|
||||
|
||||
for (const [className, type] of [["xml", "xml"], ["javascript", "js"]]) {
|
||||
loadFile(`./counter.${type}`).then(code => {
|
||||
const el = document.querySelector(`code.${className}`);
|
||||
el.textContent = code;
|
||||
hljs.highlightBlock(el);
|
||||
})
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OWL Framework</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
|
||||
<link rel="stylesheet" href="assets/normalize.css">
|
||||
<link rel="stylesheet" href="assets/milligram.css">
|
||||
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
|
||||
<link rel="stylesheet" href="assets/main.css">
|
||||
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="container">
|
||||
<img class="homepage-logo" src="assets/owl_1f989.png" alt="OWL Logo">
|
||||
<hgroup>
|
||||
<h1>OWL Framework</h1>
|
||||
<p>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
<nav class="container">
|
||||
<p>
|
||||
<a href="https://github.com/odoo/owl" title="https://github.com/odoo/owl">Github</a>
|
||||
•
|
||||
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Documentation</a>
|
||||
•
|
||||
<a href="playground/">Playground</a>
|
||||
</p>
|
||||
</nav>
|
||||
<main>
|
||||
<article class="design">
|
||||
<div class="container">
|
||||
<h2>Why OWL?</h2>
|
||||
<div class="row">
|
||||
<section class="column">
|
||||
<h3>XML based</h3>
|
||||
<p>Templates are based on the XML format, which allows interesting applications. For example, they could be stored in a database and modified dynamically with <tt>xpaths</tt>.</p>
|
||||
</section>
|
||||
<section class="column">
|
||||
<h3>Templates compiled in the browser</h3>
|
||||
<p>This may not be a good fit for all applications, but if you need to generate dynamically user interfaces in the browser, this is very powerful. For example, a generic form view component could generate a specific form user interface for each various models, from a XML view.</p>
|
||||
</section>
|
||||
<section class="column">
|
||||
<h3>No toolchain required</h3>
|
||||
<p>This is extremely useful for some applications, if, for various reasons (security/deployment/dynamic modules/specific assets tools), it is not ok to use standard web tools based on <tt>npm</tt>.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="example">
|
||||
<div class="container">
|
||||
<h2>Example</h2>
|
||||
<div class="row">
|
||||
<section class="column">
|
||||
<pre><code class="javascript"></code></pre>
|
||||
</section>
|
||||
<section class="column">
|
||||
<pre><code class="xml"></code></pre>
|
||||
<div id="app-container"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
<footer>
|
||||
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
|
||||
</footer>
|
||||
<script src="assets/highlight.pack.js"></script>
|
||||
<script type="module" src="display_code.js"></script>
|
||||
<script type="module" src="counter.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OWL Playground</title>
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
|
||||
|
||||
<!-- Application JS/CSS -->
|
||||
<link rel="stylesheet" href="playground.css">
|
||||
<script type="module" src="playground.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
tr.danger {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
.remove {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Component, mount, useState, onPatched} from "@odoo/owl";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Data generation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let idCounter = 1;
|
||||
const adjectives = [
|
||||
"pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
|
||||
"quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
|
||||
"unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
|
||||
const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
|
||||
const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
|
||||
|
||||
function _random (max) { return Math.round(Math.random() * 1000) % max; };
|
||||
|
||||
function buildData(count) {
|
||||
const data = new Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const label = `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`;
|
||||
data[i] = {
|
||||
id: idCounter++,
|
||||
label,
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Components
|
||||
// -----------------------------------------------------------------------------
|
||||
class Button extends Component {
|
||||
static template = "Button";
|
||||
}
|
||||
|
||||
|
||||
class Row extends Component {
|
||||
static template = "Row";
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = "Root";
|
||||
static components = { Button, Row };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
rows: [],
|
||||
selectedRowId: null
|
||||
});
|
||||
this.benchmarking = false;
|
||||
onPatched(() => {
|
||||
if (this.benchmarking) {
|
||||
this.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start(descr) {
|
||||
this.benchmarking = `[${descr}]`;
|
||||
console.time(this.benchmarking);
|
||||
}
|
||||
stop() {
|
||||
console.timeEnd(this.benchmarking);
|
||||
this.benchmarking = false;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.start('add1000');
|
||||
this.state.rows = buildData(1000);
|
||||
this.state.selectedRowId = null;
|
||||
}
|
||||
|
||||
runLots() {
|
||||
this.start('add10_000');
|
||||
this.state.rows = buildData(10_000);
|
||||
this.state.selectedRowId = null;
|
||||
}
|
||||
|
||||
add() {
|
||||
this.start('append1000');
|
||||
this.state.rows = this.state.rows.concat(buildData(1000));
|
||||
}
|
||||
|
||||
update() {
|
||||
this.start('update1/10th');
|
||||
let index = 0;
|
||||
const rows = this.state.rows;
|
||||
while (index < rows.length) {
|
||||
rows[index].label = rows[index].label + " !!!";
|
||||
index += 10;
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.start('clear');
|
||||
this.state.rows = [];
|
||||
this.state.selectedRowId = null;
|
||||
}
|
||||
|
||||
swapRows() {
|
||||
this.start('swap');
|
||||
const rows = this.state.rows;
|
||||
if (rows.length > 998) {
|
||||
let tmp = rows[1];
|
||||
rows[1] = rows[998];
|
||||
rows[998] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
selectRow(id) {
|
||||
this.start('select');
|
||||
this.state.selectedRowId = id;
|
||||
}
|
||||
|
||||
removeRow(id) {
|
||||
this.start('remove1');
|
||||
const rows = this.state.rows;
|
||||
rows.splice(rows.findIndex(row => row.id === id), 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// dev=false for benchmarking. we don't want to benchmark dev code!
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: false });
|
||||
@@ -1,52 +0,0 @@
|
||||
<templates>
|
||||
<t t-name="Button">
|
||||
<div class='col-sm-6 smallpad'>
|
||||
<button t-att-id="props.id" class='btn btn-primary btn-block' type='button' t-on-click="props.onClick">
|
||||
<t t-esc="props.text"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="Row">
|
||||
<tr t-att-class="props.isSelected ? 'danger' : ''">
|
||||
<td class="col-md-1" t-esc="props.row.id" />
|
||||
<td class="col-md-4">
|
||||
<a t-on-click="() => props.onSelect(props.row.id)" t-esc="props.row.label" />
|
||||
</td>
|
||||
<td class="col-md-1">
|
||||
<a t-on-click="() => props.onRemove(props.row.id)" class="remove">[x]
|
||||
<span class='glyphicon glyphicon-remove' aria-hidden="true" />
|
||||
</a>
|
||||
</td>
|
||||
<td class='col-md-6'/>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-name="Root">
|
||||
<div class='container'>
|
||||
<div class='jumbotron'>
|
||||
<div class='row'>
|
||||
<div class='col-md-6'>
|
||||
<h1>Owl Keyed</h1>
|
||||
</div>
|
||||
<div class='col-md-6'>
|
||||
<div class='row'>
|
||||
<Button id="'run'" onClick.bind="run" text="'Create 1,000 rows'" />
|
||||
<Button id="'runlots'" onClick.bind="runLots" text="'Create 10,000 rows'" />
|
||||
<Button id="'add'" onClick.bind="add" text="'Append 1,000 rows'" />
|
||||
<Button id="'update'" onClick.bind="update" text="'Update every 10th row'" />
|
||||
<Button id="'clear'" onClick.bind="clear" text="'Clear'" />
|
||||
<Button id="'swaprows'" onClick.bind="swapRows" text="'Swap Rows'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class='table table-hover table-striped test-data'>
|
||||
<tbody>
|
||||
<t t-foreach="state.rows" t-as="row" t-key="row.id">
|
||||
<Row row="row" isSelected="row.id === state.selectedRowId" onSelect.bind="selectRow" onRemove.bind="removeRow"/>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,10 +0,0 @@
|
||||
.greeter {
|
||||
font-size: 20px;
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
margin: 5px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
background-color: #eeeeee;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// In this example, we show how components can be defined and created.
|
||||
import { Component, useState, mount } from "@odoo/owl";
|
||||
|
||||
class Greeter extends Component {
|
||||
static template = "Greeter";
|
||||
|
||||
setup() {
|
||||
this.state = useState({ word: 'Hello' });
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
|
||||
}
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class Root extends Component {
|
||||
static components = { Greeter };
|
||||
static template = "Root"
|
||||
|
||||
setup() {
|
||||
this.state = useState({ name: 'World'});
|
||||
}
|
||||
}
|
||||
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||
@@ -1,9 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="Greeter" class="greeter" t-on-click="toggle">
|
||||
<t t-esc="state.word"/>, <t t-esc="props.name"/>
|
||||
</div>
|
||||
|
||||
<t t-name="Root">
|
||||
<Greeter name="state.name"/>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,6 +0,0 @@
|
||||
|
||||
button {
|
||||
width: 120px;
|
||||
height: 35px;
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// In this example, we show how hooks can be used or defined.
|
||||
import { Component, mount, useState, onWillDestroy } from "@odoo/owl";
|
||||
|
||||
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||
// position
|
||||
function useMouse() {
|
||||
const position = useState({x:0, y: 0});
|
||||
|
||||
function update(e) {
|
||||
position.x = e.clientX;
|
||||
position.y = e.clientY;
|
||||
}
|
||||
window.addEventListener('mousemove', update);
|
||||
onWillDestroy(() => {
|
||||
window.removeEventListener('mousemove', update);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
|
||||
// Main root component
|
||||
class Root extends Component {
|
||||
static template = "Root";
|
||||
|
||||
setup() {
|
||||
// simple state hook (reactive object)
|
||||
this.counter = useState({ value: 0 });
|
||||
|
||||
// this hooks is bound to the 'mouse' property.
|
||||
this.mouse = useMouse();
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.counter.value++;
|
||||
}
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
<templates>
|
||||
<div t-name="Root">
|
||||
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
|
||||
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -1,22 +0,0 @@
|
||||
// This example illustrate how the t-model directive can be used to synchronize
|
||||
// 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.
|
||||
import { Component, useState, mount } from "@odoo/owl";
|
||||
|
||||
class Form extends Component {
|
||||
static template = "Form";
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
text: "",
|
||||
othertext: "",
|
||||
number: 11,
|
||||
color: "",
|
||||
bool: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Form, document.body, { templates: TEMPLATES, dev: true });
|
||||
@@ -1,37 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="Form">
|
||||
<h1>Form</h1>
|
||||
<div>
|
||||
Text (immediate): <input t-model="state.text"/>
|
||||
</div>
|
||||
<div>
|
||||
Other text (lazy): <input t-model.lazy="state.othertext"/>
|
||||
</div>
|
||||
<div>
|
||||
Number: <input t-model.number="state.number"/>
|
||||
</div>
|
||||
<div>
|
||||
Boolean: <input type="checkbox" t-model="state.bool"/>
|
||||
</div>
|
||||
<div>
|
||||
Color, with a select:
|
||||
<select t-model="state.color">
|
||||
<option value="">Select a color</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
Color, with radio buttons:
|
||||
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
|
||||
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
|
||||
</div>
|
||||
<hr/>
|
||||
<h1>State</h1>
|
||||
<div>Text: <t t-esc="state.text"/></div>
|
||||
<div>Other Text: <t t-esc="state.othertext"/></div>
|
||||
<div>Number: <t t-esc="state.number"/></div>
|
||||
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
|
||||
<div>Color: <t t-esc="state.color"/></div>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"owl": ["../../../src/index.ts"],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
button {
|
||||
font-size: 18px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.demo {
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
background-color: #dddddd;
|
||||
width: 250px;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
// This example shows all the possible lifecycle hooks
|
||||
//
|
||||
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
|
||||
// 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.
|
||||
import {
|
||||
Component,
|
||||
useState,
|
||||
mount,
|
||||
useComponent,
|
||||
onWillStart,
|
||||
onMounted,
|
||||
onWillUnmount,
|
||||
onWillUpdateProps,
|
||||
onPatched,
|
||||
onWillPatch,
|
||||
onWillRender,
|
||||
onRendered,
|
||||
onWillDestroy,
|
||||
} from "@odoo/owl";
|
||||
|
||||
function useLogLifecycle() {
|
||||
const component = useComponent();
|
||||
const name = component.constructor.name;
|
||||
onWillStart(() => console.log(`${name}:willStart`));
|
||||
onMounted(() => console.log(`${name}:mounted`));
|
||||
onWillUpdateProps(() => console.log(`${name}:willUpdateProps`));
|
||||
onWillRender(() => console.log(`${name}:willRender`));
|
||||
onRendered(() => console.log(`${name}:rendered`));
|
||||
onWillPatch(() => console.log(`${name}:willPatch`));
|
||||
onPatched(() => console.log(`${name}:patched`));
|
||||
onWillUnmount(() => console.log(`${name}:willUnmount`));
|
||||
onWillDestroy(() => console.log(`${name}:willDestroy`));
|
||||
}
|
||||
|
||||
class DemoComponent extends Component {
|
||||
static template = "DemoComponent";
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
this.state = useState({ n: 0 });
|
||||
}
|
||||
increment() {
|
||||
this.state.n++;
|
||||
}
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = "Root";
|
||||
static components = { DemoComponent };
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
this.state = useState({ n: 0, flag: true });
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.state.n++;
|
||||
}
|
||||
|
||||
toggleSubComponent() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
}
|
||||
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="DemoComponent" t-on-click="increment" class="demo">
|
||||
<div>Demo Sub Component</div>
|
||||
<div>(click on me to update me)</div>
|
||||
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
|
||||
</div>
|
||||
|
||||
<div t-name="Root">
|
||||
<button t-on-click="increment">Increment Parent State</button>
|
||||
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
|
||||
<div t-if="state.flag">
|
||||
<DemoComponent n="state.n"/>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -1,56 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app {
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app.desktop {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
flex: 0 0 30px;
|
||||
height: 30px;
|
||||
background-color: cadetblue;
|
||||
color: white;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.controlpanel {
|
||||
flex: 0 0 100px;
|
||||
height: 100px;
|
||||
background-color: #dddddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.formview {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 60%;
|
||||
min-height: 200px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.chatter {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 40%;
|
||||
background-color: #eeeeee;
|
||||
color: #333333;
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
// In this example, we show how one can design an application that is responsive:
|
||||
// its UI is different in mobile mode or in desktop mode.
|
||||
//
|
||||
// The main idea is to have a "isMobile" key in the environment, then listen
|
||||
// to resize events and update the env if needed. Then, the whole interface
|
||||
// will be updated, creating and destroying components as needed.
|
||||
//
|
||||
// To see this in action, try resizing the window. The application will switch
|
||||
// to mobile mode whenever it has less than 768px.
|
||||
import { Component, useState, mount, reactive, useEnv } from "@odoo/owl";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function debounce(func, wait, immediate) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
function later() {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
}
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Responsive hook
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function createUI() {
|
||||
const getIsMobile = () => window.innerWidth <= 768;
|
||||
|
||||
const ui = reactive({ isMobile: getIsMobile() });
|
||||
|
||||
const updateEnv = debounce(() => {
|
||||
const isMobile = getIsMobile();
|
||||
if (ui.isMobile !== isMobile) {
|
||||
ui.isMobile = isMobile;
|
||||
}
|
||||
}, 15);
|
||||
window.addEventListener("resize", updateEnv);
|
||||
return ui;
|
||||
}
|
||||
|
||||
function useUI() {
|
||||
const env = useEnv();
|
||||
return useState(env.ui);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Components
|
||||
//------------------------------------------------------------------------------
|
||||
class Navbar extends Component {
|
||||
static template = "Navbar";
|
||||
}
|
||||
|
||||
class MobileSearchView extends Component {
|
||||
static template = "MobileSearchView";
|
||||
}
|
||||
|
||||
class ControlPanel extends Component {
|
||||
static template = "ControlPanel";
|
||||
static components = { MobileSearchView };
|
||||
setup() {
|
||||
this.ui = useUI();
|
||||
}
|
||||
}
|
||||
|
||||
class AdvancedComponent extends Component {
|
||||
static template = "AdvancedComponent";
|
||||
}
|
||||
|
||||
class FormView extends Component {
|
||||
static template = "FormView";
|
||||
static components = { AdvancedComponent };
|
||||
setup() {
|
||||
this.ui = useUI();
|
||||
}
|
||||
}
|
||||
|
||||
class Chatter extends Component {
|
||||
static template = "Chatter";
|
||||
|
||||
setup() {
|
||||
this.messages = Array.from(Array(100).keys());
|
||||
}
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = "Root";
|
||||
static components = { Navbar, ControlPanel, FormView, Chatter };
|
||||
|
||||
setup() {
|
||||
this.ui = useUI();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application Startup
|
||||
//------------------------------------------------------------------------------
|
||||
const env = {
|
||||
ui: createUI()
|
||||
};
|
||||
|
||||
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
|
||||
@@ -1,42 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="Navbar" class="navbar">Navbar</div>
|
||||
|
||||
<div t-name="ControlPanel" class="controlpanel">
|
||||
<h2>Control Panel</h2>
|
||||
<MobileSearchView t-if="ui.isMobile" />
|
||||
</div>
|
||||
|
||||
<div t-name="FormView" class="formview">
|
||||
<h2>Form View</h2>
|
||||
<AdvancedComponent t-if="!ui.isMobile" />
|
||||
</div>
|
||||
|
||||
<div t-name="Chatter" class="chatter">
|
||||
<h2>Chatter</h2>
|
||||
<t t-foreach="messages" t-as="item" t-key="item"><div>Message <t t-esc="item"/></div></t>
|
||||
</div>
|
||||
|
||||
<div t-name="MobileSearchView">Mobile searchview</div>
|
||||
|
||||
<div t-name="AdvancedComponent">
|
||||
This component is only created in desktop mode.
|
||||
<button>Button!</button>
|
||||
</div>
|
||||
|
||||
<t t-name="maincontent">
|
||||
<FormView />
|
||||
<Chatter />
|
||||
</t>
|
||||
<div t-name="Root" class="app" t-att-class="{mobile: ui.isMobile, desktop: !ui.isMobile}">
|
||||
<Navbar/>
|
||||
<ControlPanel/>
|
||||
<div class="content-wrapper" t-if="!ui.isMobile">
|
||||
<div class="content">
|
||||
<t t-call="maincontent"/>
|
||||
</div>
|
||||
</div>
|
||||
<t t-else="">
|
||||
<t t-call="maincontent"/>
|
||||
</t>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -1,28 +0,0 @@
|
||||
// This example illustrates how one can write Owl components with
|
||||
// inline templates.
|
||||
|
||||
import { Component, useState, xml, mount } from "@odoo/owl";
|
||||
|
||||
// Counter component
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="() => state.value++">
|
||||
Click! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 })
|
||||
}
|
||||
|
||||
// Root
|
||||
class Root extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Counter/>
|
||||
<Counter/>
|
||||
</div>`;
|
||||
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true});
|
||||
@@ -1,43 +0,0 @@
|
||||
.main {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #eeeeee;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
margin: 10px;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
.card.full {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.card.small {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
flex: 0 0 25px;
|
||||
font-weight: bold;
|
||||
background-color: darkcyan;
|
||||
color: white;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.card-title button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1 1 auto;
|
||||
padding: 5px;
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// We show here how slots can be used to create generic components.
|
||||
// In this example, the Card component is basically only a container. It is not
|
||||
// aware of its content. It just knows where it should be (with t-slot).
|
||||
// The parent component define the content with t-set-slot.
|
||||
//
|
||||
// Note that the t-on-click event, defined in the Root template, is executed in
|
||||
// the context of the Root component, even though it is inside the Card component
|
||||
import { Component, useState, mount } from "@odoo/owl";
|
||||
|
||||
class Card extends Component {
|
||||
static template = "Card";
|
||||
|
||||
setup() {
|
||||
this.state = useState({ showContent: true });
|
||||
}
|
||||
|
||||
toggleDisplay() {
|
||||
this.state.showContent = !this.state.showContent;
|
||||
}
|
||||
}
|
||||
|
||||
class Counter extends Component {
|
||||
static template = "Counter";
|
||||
|
||||
setup() {
|
||||
this.state = useState({val: 1});
|
||||
}
|
||||
|
||||
inc() {
|
||||
this.state.val++;
|
||||
}
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class Root extends Component {
|
||||
static template = "Root"
|
||||
static components = { Card, Counter };
|
||||
|
||||
setup() {
|
||||
this.state = useState({a: 1, b: 3});
|
||||
}
|
||||
|
||||
inc(key, delta) {
|
||||
this.state[key] += delta;
|
||||
}
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true});
|
||||
@@ -1,33 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
|
||||
<div class="card-title">
|
||||
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
|
||||
</div>
|
||||
<t t-if="state.showContent">
|
||||
<div class="card-content" >
|
||||
<t t-slot="content"/>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<t t-slot="footer"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<t t-esc="state.val"/><button t-on-click="inc">Inc</button>
|
||||
</div>
|
||||
|
||||
<div t-name="Root" class="main">
|
||||
<Card title="'Title card A'">
|
||||
<t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
|
||||
<t t-set-slot="footer"><button t-on-click="() => this.inc('a', 1)">Increment A</button></t>
|
||||
</Card>
|
||||
<Card title="'Title card B'">
|
||||
<t t-set-slot="content">
|
||||
<div>Card 2... [<t t-esc="state.b"/>]</div>
|
||||
<Counter />
|
||||
</t>
|
||||
<t t-set-slot="footer"><button t-on-click="() => this.inc('b', -1)">Decrement B</button></t>
|
||||
</Card>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -1,379 +0,0 @@
|
||||
html,body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #4d4d4d;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -155px;
|
||||
width: 100%;
|
||||
font-size: 100px;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
color: rgba(175, 47, 47, 0.15);
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none; /* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.toggle-all + label {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -52px;
|
||||
left: -13px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all + label:before {
|
||||
content: "❯";
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked + label:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: calc(100% - 43px);
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle + label {
|
||||
/*
|
||||
Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked + label {
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #d9d9d9;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: "×";
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: #777;
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #bfbfbf;
|
||||
font-size: 10px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.filters a {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// This example is an implementation of the TodoList application, from the
|
||||
// www.todomvc.com project. This is a non trivial application with some
|
||||
// interesting user interactions. It uses the local storage for persistence.
|
||||
//
|
||||
// In this implementation, we use the owl reactivity mechanism.
|
||||
import { Component, useState, mount, useRef, reactive, useEnv, useEffect } from "@odoo/owl";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants, helpers
|
||||
//------------------------------------------------------------------------------
|
||||
const ENTER_KEY = 13;
|
||||
const ESC_KEY = 27;
|
||||
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
useEffect(el => el && el.focus(), () => [ref.el]);
|
||||
}
|
||||
|
||||
function useStore() {
|
||||
const env = useEnv();
|
||||
return useState(env.store);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Task store
|
||||
//------------------------------------------------------------------------------
|
||||
class TaskList {
|
||||
constructor(tasks) {
|
||||
this.tasks = tasks || [];
|
||||
const taskIds = this.tasks.map((t) => t.id);
|
||||
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
|
||||
}
|
||||
|
||||
addTask(text) {
|
||||
text = text.trim();
|
||||
if (text) {
|
||||
const task = {
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
this.tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
toggleTask(id) {
|
||||
const task = this.tasks.find(t => t.id === id);
|
||||
task.isCompleted = !task.isCompleted;
|
||||
}
|
||||
|
||||
toggleAll(value) {
|
||||
for (let task of this.tasks) {
|
||||
task.isCompleted = value;
|
||||
}
|
||||
}
|
||||
|
||||
clearCompleted() {
|
||||
const tasks = this.tasks.filter(t => t.isCompleted);
|
||||
for (let task of tasks) {
|
||||
this.deleteTask(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
deleteTask(id) {
|
||||
const index = this.tasks.findIndex((t) => t.id === id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
|
||||
updateTask(id, text) {
|
||||
const value = text.trim();
|
||||
if (!value) {
|
||||
this.deleteTask(id);
|
||||
} else {
|
||||
const task = this.tasks.find(t => t.id === id);
|
||||
task.text = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
|
||||
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
|
||||
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
|
||||
saveTasks();
|
||||
return taskStore;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Todo
|
||||
//------------------------------------------------------------------------------
|
||||
class Todo extends Component {
|
||||
static template = "Todo";
|
||||
|
||||
setup() {
|
||||
useAutofocus("input");
|
||||
this.store = useStore();
|
||||
this.state = useState({
|
||||
isEditing: false
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyup(ev) {
|
||||
if (ev.keyCode === ENTER_KEY) {
|
||||
this.updateText(ev.target.value);
|
||||
}
|
||||
if (ev.keyCode === ESC_KEY) {
|
||||
ev.target.value = this.props.text;
|
||||
this.state.isEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur(ev) {
|
||||
this.updateText(ev.target.value);
|
||||
}
|
||||
|
||||
updateText(text) {
|
||||
this.store.updateTask(this.props.id, text);
|
||||
this.state.isEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TodoList
|
||||
//------------------------------------------------------------------------------
|
||||
class TodoList extends Component {
|
||||
static template = "TodoList";
|
||||
static components = { Todo };
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.state = useState({ filter: "all" });
|
||||
}
|
||||
|
||||
get displayedTasks() {
|
||||
const tasks = this.store.tasks;
|
||||
switch (this.state.filter) {
|
||||
case "active":
|
||||
return tasks.filter((t) => !t.isCompleted);
|
||||
case "completed":
|
||||
return tasks.filter((t) => t.isCompleted);
|
||||
case "all":
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
get allChecked() {
|
||||
return this.store.tasks.every(todo => todo.isCompleted);
|
||||
}
|
||||
|
||||
get remaining() {
|
||||
return this.store.tasks.filter(todo => !todo.isCompleted).length;
|
||||
}
|
||||
|
||||
get remainingText() {
|
||||
const items = this.remaining < 2 ? "item" : "items";
|
||||
return ` ${items} left`;
|
||||
}
|
||||
|
||||
addTodo(ev) {
|
||||
if (ev.keyCode === ENTER_KEY) {
|
||||
const text = ev.target.value;
|
||||
if (text.trim()) {
|
||||
this.store.addTask(text);
|
||||
}
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.state.filter = filter;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// App Initialization
|
||||
//------------------------------------------------------------------------------
|
||||
const env = { store: createTaskStore() };
|
||||
mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
|
||||