Compare commits
205 Commits
v2.0.0-alpha.2
...
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b9e6bad0b | |||
| 9a5ff7a619 | |||
| 5d40cc112d | |||
| aa19897031 | |||
| afd7f957df | |||
| cca4b4bb8e | |||
| b848a83777 | |||
| c0c40c0387 | |||
| 606421776b | |||
| 47009912df | |||
| fea8fcaf61 | |||
| e562f6a24f | |||
| 23acf203e9 | |||
| 94ae940e82 | |||
| 64de716b91 | |||
| 1b1597c49e | |||
| ef5e4a0637 | |||
| a9323c3fcd | |||
| ce61e90135 | |||
| 8893e026d3 | |||
| 0024f33fa1 | |||
| 532ab7fae0 | |||
| a51b286671 | |||
| aadc9eafa3 | |||
| 1f60bc79c5 | |||
| 975ed32f0b | |||
| 7ac81ed5fe | |||
| cdad48d3a6 | |||
| f892929c80 | |||
| f0fb3ab64e | |||
| a35b9814c0 | |||
| 276c8a0295 | |||
| 0717fe241d | |||
| 1291f1f175 | |||
| 6c0a3525c8 | |||
| 13241422e9 | |||
| 8f2c7f24d7 | |||
| fed9cc467f | |||
| 33174b301b | |||
| ea5d2be502 | |||
| 4a761a7403 | |||
| 8702db03fb | |||
| b2685b6709 | |||
| c30678f3ea | |||
| 6ca6717965 | |||
| ad4adb930e | |||
| cea82e945d | |||
| a69f8a39e7 | |||
| 316eb06279 | |||
| df59ec49ae | |||
| 2a008a8679 | |||
| 3d40533de1 | |||
| 39329f80b2 | |||
| 203ac7ac66 | |||
| 530c2f9e4c | |||
| ef8baa23d7 | |||
| acfcc5677a | |||
| f6e8aff725 | |||
| 4a5316ced5 | |||
| 9a5edb4590 | |||
| 3d49daedcd | |||
| 620e41daa1 | |||
| de84075c11 | |||
| 9fe8e93980 | |||
| bd199971bd | |||
| 6f23b18cab | |||
| 2c244aa31a | |||
| ba1a270c93 | |||
| d546244fc3 | |||
| a1f22829c1 | |||
| 64bad25762 | |||
| 7ab34c5ca5 | |||
| 669fd622ec | |||
| ab72cdddde | |||
| 17fb33475c | |||
| ab29b896eb | |||
| d5ed25cd19 | |||
| c4f0f17b9b | |||
| d27455e9f2 | |||
| 6ef38676c4 | |||
| b51756f356 | |||
| cfdf7caa50 | |||
| a5a6a592c1 | |||
| d0d7482b0f | |||
| 8fe4c0c76e | |||
| c1afaeb92a | |||
| 3883cec079 | |||
| 9cb74d619b | |||
| a93f015795 | |||
| 02a187d80b | |||
| d3b0d1971e | |||
| b90aa0e23a | |||
| 163366997c | |||
| 588b655c11 | |||
| f5d5273c25 | |||
| 9fd662fdce | |||
| 7786077921 | |||
| 30bc605c84 | |||
| d1118455aa | |||
| f8073cb153 | |||
| 6c72e0a143 | |||
| 382e3e4010 | |||
| 76c389a7a8 | |||
| b9ba0abf41 | |||
| 4ca37be7f3 | |||
| d6667ddf2e | |||
| 6f86beeaf3 | |||
| 7f580a4e1d | |||
| c7459ef87b | |||
| 5772b4e9e4 | |||
| e57e2ee378 | |||
| 2e03332acd | |||
| c16d7d52b1 | |||
| 9c4c3e3b83 | |||
| 55ac43c1db | |||
| d046913a01 | |||
| 0e6059467f | |||
| 51538c2fea | |||
| 83d4471048 | |||
| 8a967e5b2d | |||
| 385e118e58 | |||
| a3111eb9ca | |||
| 1fc88f626f | |||
| 5d5a530505 | |||
| c7bd0ab85c | |||
| 31b57cbb40 | |||
| 31fce0926c | |||
| b56a9c24cf | |||
| 6dcdb77eab | |||
| a7daef380a | |||
| 22a79cdedd | |||
| e4b810c027 | |||
| b10a700381 | |||
| 2b4d8874c7 | |||
| 98b58b505b | |||
| 586033fd95 | |||
| 1fe0bf08b1 | |||
| 4779707923 | |||
| d917af4614 | |||
| 4c77132ae2 | |||
| 114f21586e | |||
| 32d8b23b9d | |||
| a83731007a | |||
| 5c71744e19 | |||
| d09771b04b | |||
| 7137130c38 | |||
| 0cd66c8518 | |||
| 5d9cff0331 | |||
| 41f1262eb7 | |||
| 41344ef4ec | |||
| 7d14db7d31 | |||
| 1179e84971 | |||
| 24b1ea7604 | |||
| 3e4ebb6378 | |||
| 859748aed9 | |||
| fd13277e1d | |||
| 7fb166bd50 | |||
| decf42c742 | |||
| 989b0d6709 | |||
| 9b93521da4 | |||
| de240b1ebc | |||
| 55dbc01a1b | |||
| e3b1566943 | |||
| 828be28653 | |||
| d80fad760c | |||
| 7611ea6033 | |||
| c7d515a6b3 | |||
| d277039b14 | |||
| 77ff5ee895 | |||
| 47c6d6cc3c | |||
| 0625b5883a | |||
| 53ab54b1ec | |||
| 4770b91faa | |||
| c356351de2 | |||
| f04423da23 | |||
| b3062d29f1 | |||
| 56086242bb | |||
| 998ecbb337 | |||
| 50355e6a3d | |||
| 67f86a4ab8 | |||
| 14d2328c88 | |||
| e4fdd32f22 | |||
| 8d1d0a2244 | |||
| 0457e5d4ed | |||
| 8920b4b93a | |||
| 6908102a72 | |||
| d6348b8310 | |||
| 79738e00c7 | |||
| 2a1b99be2d | |||
| 8a472231cf | |||
| bb373e6a7a | |||
| d735213758 | |||
| 076b0d774e | |||
| f405fe9323 | |||
| 1ae9d514b9 | |||
| 592d9a458e | |||
| 0dbd2bd463 | |||
| 8ec7a6f9bf | |||
| 12b8ce963e | |||
| bb9d65e95b | |||
| 73c339fff1 | |||
| 406be446a5 | |||
| bd2aa8a72f | |||
| 3536f41f00 | |||
| 804ad3c35e |
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -22,7 +22,8 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm install
|
- run: npm ci
|
||||||
- run: npm run test
|
- run: npm run test
|
||||||
- run: npm run check-formatting
|
- run: npm run check-formatting
|
||||||
|
- run: npm run lint
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
|
|
||||||
#ide's
|
#ide's
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ All changes are documented here in no particular order.
|
|||||||
- 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))
|
- 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))
|
- 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: 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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -43,7 +44,6 @@ All changes are documented here in no particular order.
|
|||||||
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
|
- 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))
|
- 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))
|
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
|
||||||
- breaking: `t-on` does not work on components any more ([details](#12-t-on-does-not-work-on-components-any-more))
|
|
||||||
- new: an error is thrown if an handler defined in a `t-on-` directive is not a function (failed silently previously in some cases)
|
- 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: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
|
- 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
|
- new: the `this` variable in template expressions is now bound to the component
|
||||||
@@ -80,7 +80,6 @@ All changes are documented here in no particular order.
|
|||||||
- improved performance
|
- improved performance
|
||||||
- much simpler code
|
- much simpler code
|
||||||
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
|
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
|
||||||
- new `Memo` component
|
|
||||||
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
||||||
- breaking: `Context` is removed ([details](#15-context-is-removed))
|
- 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 totally empty ([details](#16-env-is-now-totally-empty))
|
||||||
@@ -248,35 +247,12 @@ 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),
|
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
|
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
|
subset of components that is subscribed to a piece of state. Now, Owl 2 features
|
||||||
a much more powerful reactivity system.
|
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.
|
||||||
|
|
||||||
Migration code: remove the `shouldUpdate` methods. Then, maybe the following
|
Migration code: remove the `shouldUpdate` methods, and it should work as well
|
||||||
ideas may help:
|
as before.
|
||||||
|
|
||||||
- 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:
|
|
||||||
|
|
||||||
```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>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. component.el is removed
|
### 9. component.el is removed
|
||||||
|
|
||||||
@@ -336,41 +312,6 @@ Documentation:
|
|||||||
- [Mounting a component](doc/reference/app.md#mount-helper)
|
- [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"/>
|
|
||||||
<!-- or alternatively: -->
|
|
||||||
<SomeComponent onSomeEvent.bind="doSomething"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that one of the example above uses the `.bind` suffix, to bind the function
|
|
||||||
prop to the component. Most of the time, binding the function is necessary, and
|
|
||||||
using the `.bind` suffix is very helpful in that case.
|
|
||||||
|
|
||||||
Documentation: [Binding function props](doc/reference/props.md#binding-function-props)
|
|
||||||
|
|
||||||
### 13. Portal does no longer transfer DOM events
|
### 13. Portal does no longer transfer DOM events
|
||||||
|
|
||||||
In Owl 1, a Portal component would listen to events emitted on its portalled
|
In Owl 1, a Portal component would listen to events emitted on its portalled
|
||||||
@@ -799,3 +740,27 @@ 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,
|
Migration: code should just be adapted to either use another browser object,
|
||||||
or to use native browser function (and then, just mock them directly).
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ Are you new to Owl? This is the place to start!
|
|||||||
- [Component](doc/reference/component.md)
|
- [Component](doc/reference/component.md)
|
||||||
- [Component Lifecycle](doc/reference/component.md#lifecycle)
|
- [Component Lifecycle](doc/reference/component.md#lifecycle)
|
||||||
- [Concurrency Model](doc/reference/concurrency_model.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)
|
- [Dynamic sub components](doc/reference/component.md#dynamic-sub-components)
|
||||||
- [Environment](doc/reference/environment.md)
|
- [Environment](doc/reference/environment.md)
|
||||||
- [Error Handling](doc/reference/error_handling.md)
|
- [Error Handling](doc/reference/error_handling.md)
|
||||||
@@ -92,6 +93,7 @@ Are you new to Owl? This is the place to start!
|
|||||||
- [Loading Templates](doc/reference/app.md#loading-templates)
|
- [Loading Templates](doc/reference/app.md#loading-templates)
|
||||||
- [Mounting a component](doc/reference/app.md#mount-helper)
|
- [Mounting a component](doc/reference/app.md#mount-helper)
|
||||||
- [Portal](doc/reference/portal.md)
|
- [Portal](doc/reference/portal.md)
|
||||||
|
- [Precompiling templates](doc/reference/precompiling_templates.md)
|
||||||
- [Props](doc/reference/props.md)
|
- [Props](doc/reference/props.md)
|
||||||
- [Props Validation](doc/reference/props.md#props-validation)
|
- [Props Validation](doc/reference/props.md#props-validation)
|
||||||
- [Reactivity](doc/reference/reactivity.md)
|
- [Reactivity](doc/reference/reactivity.md)
|
||||||
@@ -110,6 +112,8 @@ Are you new to Owl? This is the place to start!
|
|||||||
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
|
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
|
||||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.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)
|
||||||
|
|
||||||
## Installing Owl
|
## Installing Owl
|
||||||
|
|
||||||
@@ -118,8 +122,28 @@ Owl is available on `npm` and can be installed with the following command:
|
|||||||
```
|
```
|
||||||
npm install @odoo/owl
|
npm install @odoo/owl
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||||
|
|
||||||
- [owl-1.4.10](https://github.com/odoo/owl/releases/tag/v1.4.10)
|
- [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.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -174,9 +174,9 @@ class Root extends Component {
|
|||||||
```
|
```
|
||||||
|
|
||||||
The template contains a [`t-foreach`](../reference/templates.md#loops) loop to iterate
|
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
|
through the tasks. It can find the `tasks` list from the component, since the rendering
|
||||||
component is the rendering context. Note that we use the `id` of each task as a
|
context contains the properties of the component. Note that we use the `id` of each task
|
||||||
`t-key`, which is very common. There are two css classes: `task-list` and `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.
|
that we will use in the next section.
|
||||||
|
|
||||||
Finally, notice the use of the `t-att-checked` attribute:
|
Finally, notice the use of the `t-att-checked` attribute:
|
||||||
@@ -240,7 +240,7 @@ class Task extends Component {
|
|||||||
static template = xml /* xml */`
|
static template = xml /* xml */`
|
||||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||||
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
|
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
|
||||||
<span><t t-esc="props.task.title"/></span>
|
<span><t t-esc="props.task.text"/></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
static props = ["task"];
|
static props = ["task"];
|
||||||
}
|
}
|
||||||
@@ -502,6 +502,10 @@ deleteTask(task) {
|
|||||||
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
|
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.
|
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
|
## 10. Using a store
|
||||||
|
|
||||||
Looking at the code, it is apparent that all the code handling tasks is scattered
|
Looking at the code, it is apparent that all the code handling tasks is scattered
|
||||||
@@ -743,7 +747,7 @@ the user experience.
|
|||||||
```xml
|
```xml
|
||||||
<input type="checkbox" t-att-checked="props.task.isCompleted"
|
<input type="checkbox" t-att-checked="props.task.isCompleted"
|
||||||
t-att-id="props.task.id"
|
t-att-id="props.task.id"
|
||||||
t-on-click="dispatch('toggleTask', props.task.id)"/>
|
t-on-click="() => store.toggleTask(props.task)"/>
|
||||||
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
|
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -770,10 +774,11 @@ For reference, here is the final code:
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>OWL Todo App</title>
|
<title>OWL Todo App</title>
|
||||||
<link rel="stylesheet" href="app.css" />
|
<link rel="stylesheet" href="app.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
<script src="owl.js"></script>
|
<script src="owl.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</head>
|
</body>
|
||||||
<body></body>
|
|
||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 🦉 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.
|
||||||
@@ -45,4 +45,5 @@ Utility/helpers:
|
|||||||
- [`loadFile`](reference/utils.md#loadfile): an helper to load a file from the server
|
- [`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)
|
- [`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)
|
- [`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
|
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
||||||
|
|||||||
@@ -51,17 +51,18 @@ 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
|
- **`env (object)`**: if given, this will be the shared `env` given to each component
|
||||||
- **`props (object)`**: the props given to the root component
|
- **`props (object)`**: the props given to the root component
|
||||||
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in `dev`
|
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in
|
||||||
mode, which activates some additional checks (in particular, the props validation
|
[`dev` mode](#dev-mode);
|
||||||
code is only performed in dev mode)
|
|
||||||
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
|
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
|
||||||
that Owll will not log a message to warn that Owl is in `dev` mode.
|
that Owl will not log a message to warn that Owl is in `dev` mode.
|
||||||
- **`translatableAttributes (string[])`**: a list of additional attributes that should
|
- **`translatableAttributes (string[])`**: a list of additional attributes that should
|
||||||
be translated (see [translations](translations.md))
|
be translated (see [translations](translations.md))
|
||||||
- **`translateFn (function)`**: a function that will be called by owl to translate
|
- **`translateFn (function)`**: a function that will be called by owl to translate
|
||||||
templates (see [translations](translations.md))
|
templates (see [translations](translations.md))
|
||||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||||
the components created by the application.
|
the components created by the application.
|
||||||
|
- **`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).
|
||||||
|
|
||||||
## `mount` helper
|
## `mount` helper
|
||||||
|
|
||||||
@@ -110,3 +111,13 @@ const { loadFile, mount } = owl;
|
|||||||
mount(Root, document.body, { env });
|
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
|
||||||
|
|||||||
@@ -68,12 +68,17 @@ The `Component` class has a very small API.
|
|||||||
component will go through the following lifecycle methods: `willUpdateProps`,
|
component will go through the following lifecycle methods: `willUpdateProps`,
|
||||||
`willPatch` and `patched`.
|
`willPatch` and `patched`.
|
||||||
|
|
||||||
* **`render()`**: calling this method directly will cause a rerender. Note
|
* **`render(deep[=false])`**: calling this method directly will cause a rerender. Note
|
||||||
that with the reactivity system, this should be rare to have to do it manually.
|
that with the reactivity system, this should be rare to have to do it manually.
|
||||||
Also, the rendering operation is asynchronous, so the DOM will only be updated
|
Also, the rendering operation is asynchronous, so the DOM will only be updated
|
||||||
slightly later (at the next animation frame, if no component delays the
|
slightly later (at the next animation frame, if no component delays the
|
||||||
rendering)
|
rendering)
|
||||||
|
|
||||||
|
By default, the render initiated by this method will stop at each child
|
||||||
|
component if their props are (shallow) equal. To force a render to update
|
||||||
|
all child components, one can use the optional `deep` argument. Note that the
|
||||||
|
value of the `deep` argument needs to be a boolean, not a truthy value.
|
||||||
|
|
||||||
## Static Properties
|
## Static Properties
|
||||||
|
|
||||||
- **`template (string)`**: this is the name of the template that
|
- **`template (string)`**: this is the name of the template that
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
# 🦉 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.
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 🦉 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 |
|
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
- [Event Handling](#event-handling)
|
- [Event Handling](#event-handling)
|
||||||
- [Modifiers](#modifiers)
|
- [Modifiers](#modifiers)
|
||||||
- [Synthetic Events](#synthetic-events)
|
- [Synthetic Events](#synthetic-events)
|
||||||
|
- [On Components](#on-components)
|
||||||
|
|
||||||
## Event Handling
|
## Event Handling
|
||||||
|
|
||||||
@@ -100,3 +101,19 @@ To enable it, one can just use the `.synthetic` suffix:
|
|||||||
</t>
|
</t>
|
||||||
</div>
|
</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).
|
||||||
|
|||||||
@@ -112,17 +112,16 @@ of a component, rendered by Owl. It only work on a html element tagged by the
|
|||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div>
|
<div>
|
||||||
<input t-ref="someDiv"/>
|
<input t-ref="someInput"/>
|
||||||
<span>hello</span>
|
<span>hello</span>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the component will be able to access the `div` and the component
|
In this example, the component will be able to access the `input` with the `useRef` hook:
|
||||||
`SubComponent` with the `useRef` hook:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class Parent extends Component {
|
class Parent extends Component {
|
||||||
inputRef = useRef("someComponent");
|
inputRef = useRef("someInput");
|
||||||
|
|
||||||
someMethod() {
|
someMethod() {
|
||||||
// here, if component is mounted, refs are active:
|
// here, if component is mounted, refs are active:
|
||||||
@@ -139,18 +138,18 @@ The `t-ref` directive also accepts dynamic values with string interpolation
|
|||||||
`t-component` directives). For example,
|
`t-component` directives). For example,
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>
|
||||||
```
|
```
|
||||||
|
|
||||||
Here, the references need to be set like this:
|
Here, the references need to be set like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
this.ref1 = useRef("component_1");
|
this.ref1 = useRef("div_1");
|
||||||
this.ref2 = useRef("component_2");
|
this.ref2 = useRef("div_2");
|
||||||
```
|
```
|
||||||
|
|
||||||
References are only guaranteed to be active while the parent component is mounted.
|
References are only guaranteed to be active while the parent component is mounted.
|
||||||
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
If this is not the case, accessing `el` on it will return `null`.
|
||||||
|
|
||||||
### `useSubEnv` and `useChildSubEnv`
|
### `useSubEnv` and `useChildSubEnv`
|
||||||
|
|
||||||
@@ -234,7 +233,8 @@ are defined by a function instead of just the dependencies.
|
|||||||
|
|
||||||
The `useEffect` hook takes two function: the effect function and the dependency
|
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 effect function perform some task and return (optionally) a cleanup
|
||||||
function. The dependency function returns a list of dependencies. If any of these
|
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.
|
dependencies changes, then the current effect will be cleaned up and reexecuted.
|
||||||
|
|
||||||
Here is an example without any dependencies:
|
Here is an example without any dependencies:
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 🦉 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,6 +4,7 @@
|
|||||||
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Definition](#definition)
|
- [Definition](#definition)
|
||||||
|
- [Props comparison](#props-comparison)
|
||||||
- [Binding function props](#binding-function-props)
|
- [Binding function props](#binding-function-props)
|
||||||
- [Dynamic Props](#dynamic-props)
|
- [Dynamic Props](#dynamic-props)
|
||||||
- [Default Props](#default-props)
|
- [Default Props](#default-props)
|
||||||
@@ -56,6 +57,47 @@ the `props` object contains the following keys:
|
|||||||
- for `ComponentA`: `a` and `b`,
|
- for `ComponentA`: `a` and `b`,
|
||||||
- for `ComponentB`: `model`,
|
- 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
|
## Binding function props
|
||||||
|
|
||||||
It is common to have the need to pass a callback as a prop. Since Owl components
|
It is common to have the need to pass a callback as a prop. Since Owl components
|
||||||
@@ -95,6 +137,9 @@ class SomeComponent extends Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `.bind` suffix also implies `.alike`, so these props will not cause additional
|
||||||
|
renderings.
|
||||||
|
|
||||||
## Dynamic Props
|
## Dynamic Props
|
||||||
|
|
||||||
The `t-props` directive can be used to specify totally dynamic props:
|
The `t-props` directive can be used to specify totally dynamic props:
|
||||||
@@ -159,6 +204,7 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of
|
|||||||
- a boolean: indicate that the props exists, and is mandatory.
|
- a boolean: indicate that the props exists, and is mandatory.
|
||||||
- a constructor: this should describe the type, for example: `id: Number` describe
|
- a constructor: this should describe the type, for example: `id: Number` describe
|
||||||
the props `id` as a number
|
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
|
- 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
|
type. For example, `id: [Number, String]` means that `id` can be either a string
|
||||||
or a number.
|
or a number.
|
||||||
@@ -166,6 +212,7 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of
|
|||||||
- `type`: the main type of the prop being validated
|
- `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,
|
- `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,
|
- `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
|
- `validate`: this is a function which should return a boolean to determine if
|
||||||
the value is valid or not. Useful for custom validation logic.
|
the value is valid or not. Useful for custom validation logic.
|
||||||
- `optional`: if true, the prop is not mandatory
|
- `optional`: if true, the prop is not mandatory
|
||||||
@@ -230,6 +277,10 @@ class ComponentB extends owl.Component {
|
|||||||
name: {type: String, optional: true},
|
name: {type: String, optional: true},
|
||||||
url: String
|
url: String
|
||||||
]}, // object, with keys id (number), name (string, optional) and 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`)
|
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||||
someVal: [Boolean, Date], // either a boolean or a date
|
someVal: [Boolean, Date], // either a boolean or a date
|
||||||
otherValue: true, // indicates that it is a prop
|
otherValue: true, // indicates that it is a prop
|
||||||
@@ -240,9 +291,12 @@ class ComponentB extends owl.Component {
|
|||||||
size: {
|
size: {
|
||||||
validate: e => ["small", "medium", "large"].includes(e)
|
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).
|
||||||
|
|
||||||
## Good Practices
|
## Good Practices
|
||||||
|
|
||||||
A `props` object is a collection of values that come from the parent. As such,
|
A `props` object is a collection of values that come from the parent. As such,
|
||||||
|
|||||||
@@ -2,118 +2,394 @@
|
|||||||
|
|
||||||
## Content
|
## Content
|
||||||
|
|
||||||
- [Overview](#overview)
|
- [Introduction](#introduction)
|
||||||
- [`useState`](#usestate)
|
- [`useState`](#usestate)
|
||||||
- [`reactive`](#reactive)
|
- [`reactive`](#reactive)
|
||||||
- [`markRaw`](#markraw)
|
- [`Escape hatches`](#escape-hatches)
|
||||||
- [`toRaw`](#toraw)
|
- [`Advanced usage`](#advanced-usage)
|
||||||
|
|
||||||
## Overview
|
## Introduction
|
||||||
|
|
||||||
Reactivity is a big topic in javascript frameworks. The goal is to provide a
|
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 automatically
|
simple way to manipulate state, in such a way that the interface updates automatically
|
||||||
update accordingly to state changes. Also, we obviously want this to happen in
|
according to state changes, and to do so in a performant manner.
|
||||||
a performant way.
|
|
||||||
|
|
||||||
To solve this issue, Owl provides two reactivity primitives:
|
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
|
||||||
- `reactive`, which returns a proxy to its first argument, and tracks all read/update
|
argument, it returns a proxy of the object. This proxy tracks what properties are read
|
||||||
operation going through it,
|
through the proxy, and calls the provided callback whenever one of these properties is changed
|
||||||
- `useState`: a hook, that internally uses `reactive`, and is linked to its
|
through any reactive version of the same object. It does so in depth, by returning reactive versions
|
||||||
owner component: any read operation will be tracked (key by key), and any
|
of the subobjects when they are read.
|
||||||
updates to these tracked values will cause the component to be rerendered.
|
|
||||||
|
|
||||||
Most of the time, the `useState` hook is the best solution.
|
|
||||||
|
|
||||||
## `useState`
|
## `useState`
|
||||||
|
|
||||||
Let us start by an example of how `useState` could be used:
|
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
|
```js
|
||||||
class Counter extends Component {
|
class Counter extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<div t-on-click="increment">
|
<div t-on-click="() => this.state.value++">
|
||||||
<t t-esc="state.value"/>
|
<t t-esc="state.value"/>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.state = useState({ value: 0 });
|
this.state = useState({ value: 0 });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
increment() {
|
This component reads `state.value` when it renders, subscribing it to changes to that key. Whenever
|
||||||
this.state.value++;
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If one were to use a simple state object, Owl would not be aware that the value
|
When clicking on the counter button, only the Counter rerenders, because the Parent has never read
|
||||||
was changed and that the component should be rerendered. With the `useState`
|
the "value" key in the state. When clicking on the "Reset Counter" button, the same thing happens:
|
||||||
hook, `this.state` is now a reactive object, so this component works as expected.
|
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`
|
## `reactive`
|
||||||
|
|
||||||
The `reactive` function is the basic reactivity primitive. It takes an object
|
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.
|
or an array as first argument, and optionally, a function as the second argument.
|
||||||
The function will be called whenever any tracked value is updated.
|
The function is called whenever any tracked value is updated.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const obj = reactive({ a: 1 }, () => console.log("changed"));
|
const obj = reactive({ a: 1 }, () => console.log("changed"));
|
||||||
|
|
||||||
obj.a = 2; // does not log anything: the 'a' key was not read
|
obj.a = 2; // does not log anything: the 'a' key has not been read yet
|
||||||
console.log(obj.a); // log 2, and reads the 'a' key => it is now tracked
|
console.log(obj.a); // logs 2 and reads the 'a' key => it is now tracked
|
||||||
obj.a = 3; // log 'changed' because we updated a tracked value
|
obj.a = 3; // logs 'changed' because we updated a tracked value
|
||||||
```
|
```
|
||||||
|
|
||||||
An important property of reactive objects is that they can be reobserved: this
|
An important property of reactive objects is that they can be reobserved: this
|
||||||
will create an independant proxy that tracks another set of keys:
|
will create an independent proxy that tracks another set of keys:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const obj1 = reactive({ a: 1, b: 2 }, () => console.log("observer 1"));
|
const obj1 = reactive({ a: 1, b: 2 }, () => console.log("observer 1"));
|
||||||
const obj2 = reactive(obj1, () => console.log("observer 2"));
|
const obj2 = reactive(obj1, () => console.log("observer 2"));
|
||||||
|
|
||||||
console.log(obj1.a); // log 1, and reads the 'a' key => it is now tracked by observer 1
|
console.log(obj1.a); // logs 1, and reads the 'a' key => it is now tracked by observer 1
|
||||||
console.log(obj1.b); // log 2, and 'b' is now tracked by observer 1
|
console.log(obj2.b); // logs 2, and 'b' is now tracked by observer 2
|
||||||
console.log(obj2.b); // log 2, and 'b' is now tracked by observer 1
|
obj2.a = 3; // only logs 'observer1', because observer2 does not track a
|
||||||
obj2.a = 3; // log 'observer1', because observer2 does not track a
|
obj2.b = 3; // only logs 'observer2', because observer1 does not track b
|
||||||
obj2.b = 3; // log 'observer1' and 'observer2'
|
console.log(obj2.a, obj1.b); // logs 3 and 3, while the object is observed independently, it is still a single object
|
||||||
```
|
```
|
||||||
|
|
||||||
Obviously, one can use `reactive` on the result of a `useState` if wanted, this
|
Because `useState` returns a normal reactive object, it is possible to call `reactive` on the result
|
||||||
is the proper way to watch for some state changes.
|
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.
|
||||||
|
|
||||||
## `markRaw`
|
### Subscriptions are ephemereal
|
||||||
|
|
||||||
Marks an object so that it is ignored by the reactivity system. This function returns its argument.
|
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
|
```js
|
||||||
const someObject = markRaw(...);
|
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="state.selected + ': ' + state[state.selected].value"/>
|
||||||
|
<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 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({
|
const state = useState({
|
||||||
a: 1,
|
a: 1,
|
||||||
obj: someObject
|
obj: someObject,
|
||||||
});
|
});
|
||||||
// here, state.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. For example, some complex and large object such
|
This is useful in some rare cases. One such example would be if you want to use an array of objects
|
||||||
that going through the reactivity system may cause a non trivial performance slowdown.
|
that is potentially large to render a list, but those objects are known to be immutable:
|
||||||
|
|
||||||
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!
|
|
||||||
|
|
||||||
## `toRaw`
|
|
||||||
|
|
||||||
Given a reactive object, this function returns the underlying, non-reactive,
|
|
||||||
corresponding object.
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// in setup
|
this.items = useState([
|
||||||
const state = useState({ value: 1 });
|
{ label: "some text", value: 42 },
|
||||||
|
// ... 1000 total objects
|
||||||
// later:
|
]);
|
||||||
const rawState = toRaw(this.state);
|
|
||||||
rawState.value = 3; // will NOT be picked up by the reactivity system!!!
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Here again, this is useful in some situations where we want to explicitely bypass
|
in the template:
|
||||||
Owl, but using this function means that the responsability of coordinating
|
|
||||||
state update is given to the user code, instead of Owl. Subtle bugs may arise!
|
```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.
|
||||||
|
|||||||
@@ -239,6 +239,12 @@ And the child component that includes the slot can provide values like this:
|
|||||||
<t t-slot="foo" bool="other_var" num="5">
|
<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
|
In the case of the default slot, you may declare the slot scope directly on the
|
||||||
component itself:
|
component itself:
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
- [Fragments](#fragments)
|
- [Fragments](#fragments)
|
||||||
- [Inline templates](#inline-templates)
|
- [Inline templates](#inline-templates)
|
||||||
- [Rendering svg](#rendering-svg)
|
- [Rendering svg](#rendering-svg)
|
||||||
|
- [Restrictions](#restrictions)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -114,7 +115,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>
|
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. it can use anything in the rendering context (typically, the component):
|
2. it can use anything in the rendering context (which typically contains the properties of the component):
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<p t-if="user.birthday === today()">Happy bithday!</p>
|
<p t-if="user.birthday === today()">Happy bithday!</p>
|
||||||
@@ -289,10 +290,11 @@ 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
|
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
|
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.
|
literal and dynamic elements, such as css classes. The dynamic elements can be
|
||||||
|
specified with either `{{...}}` or `#{...}`:
|
||||||
|
|
||||||
```xml
|
```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> -->
|
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -475,7 +477,7 @@ not work with other iterables, such as `Set`. However, it is only a matter of
|
|||||||
using the `...` javascript operator. For example:
|
using the `...` javascript operator. For example:
|
||||||
|
|
||||||
```xml
|
```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,
|
The `...` operator will convert the `Set` (or any other iterables) into a list,
|
||||||
@@ -538,6 +540,14 @@ This can be used to define variables scoped to a sub template:
|
|||||||
<!-- "var" does not exist here -->
|
<!-- "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
|
### Dynamic sub templates
|
||||||
|
|
||||||
The `t-call` directive can also be used to dynamically call a sub template,
|
The `t-call` directive can also be used to dynamically call a sub template,
|
||||||
@@ -680,3 +690,13 @@ 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
|
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
|
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.
|
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>
|
||||||
|
```
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ functions are all available in the `owl.utils` namespace.
|
|||||||
- [`whenReady`](#whenready): executing code when DOM is ready
|
- [`whenReady`](#whenready): executing code when DOM is ready
|
||||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||||
- [`EventBus`](#eventbus): a simple EventBus
|
- [`EventBus`](#eventbus): a simple EventBus
|
||||||
|
- [`validate`](#validate): a validation function
|
||||||
|
|
||||||
## `whenReady`
|
## `whenReady`
|
||||||
|
|
||||||
@@ -56,3 +57,24 @@ bus.addEventListener("event", () => console.log("something happened"));
|
|||||||
|
|
||||||
bus.trigger("event"); // 'something happened' is logged
|
bus.trigger("event"); // 'something happened' is logged
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
validate(
|
||||||
|
{ a: "hey" },
|
||||||
|
{
|
||||||
|
id: Number,
|
||||||
|
url: [Boolean, { type: Array, element: Number }],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# 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 the following:
|
||||||
|
|
||||||
|
<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 trying 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 observed by the component
|
||||||
|
which will trigger a rerender of the component when it is modified. The keys represent which part of the
|
||||||
|
variable is actually observed and the target is the actual variable. For simplicity, the properties
|
||||||
|
that are not observed by the component are greyed out while the others are in bold. This means that
|
||||||
|
editing bold ones will trigger a rerender while the greyed out ones will not.
|
||||||
|
|
||||||
|
<img src="screenshots/states.png"/>
|
||||||
|
|
||||||
|
In the given example, we have two keys/target pairs for two different variables. The first one indicates
|
||||||
|
that adding or removing an element to the array will trigger a rerender since the length will have changed.
|
||||||
|
Replacing the element at index 0, 1 or 2 will also have the same effect as implied by the keys. It doesn't
|
||||||
|
mean that editing the properties of element at index 0, 1 or 2 will rerender the component though. It may
|
||||||
|
be the case for some but this will be described in another keys/target pair. The second keys/target pair
|
||||||
|
is actually the element at index 0 of the first pair. It only has id in the keys meaning that only the
|
||||||
|
id property will actually trigger a rerender the component when modified. Be aware however that the other
|
||||||
|
properties may be in the observed state of another component like a child one in this case. A greyed out
|
||||||
|
property only implies it is not reactive for the selected component and not for the others.
|
||||||
|
|
||||||
|
The 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"/>
|
||||||
|
|
||||||
|
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, ...). Whether it 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.
|
||||||
|
|
||||||
|
<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"/>
|
||||||
|
|
||||||
|
## 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. All the examples above were created
|
||||||
|
with the dark mode enabled. 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, just close the devtools and refresh the page.
|
||||||
|
This will eventually happen any time a tab stays opened for too long without being refreshed.
|
||||||
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 146 KiB |
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.0.0-alpha.2",
|
"version": "2.1.1",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"browser": "dist/owl.iife.js",
|
|
||||||
"module": "dist/owl.es.js",
|
"module": "dist/owl.es.js",
|
||||||
"types": "dist/types/index.d.ts",
|
"types": "dist/types/owl.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
@@ -14,7 +13,14 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:bundle": "rollup -c --failAfterWarnings",
|
"build:bundle": "rollup -c --failAfterWarnings",
|
||||||
|
"build:runtime": "rollup -c --failAfterWarnings runtime",
|
||||||
|
"build:compiler": "rollup -c --failAfterWarnings compiler",
|
||||||
"build": "npm run build:bundle",
|
"build": "npm run build:bundle",
|
||||||
|
"build:devtools": "rollup -c ./tools/devtools/rollup.config.js",
|
||||||
|
"dev:devtools-chrome": "npm run build:devtools -- --config-browser=chrome",
|
||||||
|
"dev:devtools-firefox": "npm run build:devtools -- --config-browser=firefox",
|
||||||
|
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
|
||||||
|
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
@@ -22,10 +28,12 @@
|
|||||||
"playground": "npm run build && npm run playground:serve",
|
"playground": "npm run build && npm run playground:serve",
|
||||||
"preplayground:watch": "npm run build",
|
"preplayground:watch": "npm run build",
|
||||||
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
|
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
|
||||||
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
|
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
|
||||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
|
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
||||||
|
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
||||||
"publish": "npm run build && npm publish",
|
"publish": "npm run build && npm publish",
|
||||||
"release": "node tools/release.js"
|
"release": "node tools/release.js",
|
||||||
|
"compile_templates": "node tools/compile_xml.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -40,25 +48,29 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
"@types/node": "^14.11.8",
|
"@types/node": "^14.11.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||||
|
"@typescript-eslint/parser": "5.48.1",
|
||||||
"chalk": "^3.0.0",
|
"chalk": "^3.0.0",
|
||||||
"cpx": "^1.5.0",
|
|
||||||
"current-git-branch": "^1.1.0",
|
"current-git-branch": "^1.1.0",
|
||||||
"git-rev-sync": "^1.12.0",
|
"eslint": "8.31.0",
|
||||||
|
"git-rev-sync": "^3.0.2",
|
||||||
"github-api": "^3.3.0",
|
"github-api": "^3.3.0",
|
||||||
"jest": "^27.1.0",
|
"jest": "^27.1.0",
|
||||||
"jest-diff": "^27.3.1",
|
"jest-diff": "^27.3.1",
|
||||||
"jest-environment-jsdom": "^27.1.0",
|
"jest-environment-jsdom": "^27.1.0",
|
||||||
"live-server": "^1.2.1",
|
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "2.4.1",
|
"prettier": "2.4.1",
|
||||||
"rollup": "^2.56.3",
|
"rollup": "^2.56.3",
|
||||||
|
"rollup-plugin-copy": "^3.3.0",
|
||||||
|
"rollup-plugin-delete": "^2.0.0",
|
||||||
|
"rollup-plugin-dts": "^4.2.2",
|
||||||
|
"rollup-plugin-execute": "^1.1.1",
|
||||||
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"rollup-plugin-typescript2": "^0.31.1",
|
"rollup-plugin-typescript2": "^0.31.1",
|
||||||
"sass": "^1.16.1",
|
|
||||||
"source-map-support": "^0.5.10",
|
"source-map-support": "^0.5.10",
|
||||||
"ts-jest": "^27.0.5",
|
"ts-jest": "^27.0.5",
|
||||||
"typescript": "4.5.2",
|
"typescript": "4.5.2"
|
||||||
"uglify-es": "^3.3.9"
|
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "jsdom",
|
"testEnvironment": "jsdom",
|
||||||
|
|||||||
@@ -1,28 +1,9 @@
|
|||||||
# 🦉 OWL Roadmap 🦉
|
# 🦉 OWL Roadmap 🦉
|
||||||
|
|
||||||
- Current version: 1.4.10
|
- Current version: 2.X
|
||||||
- Status: stable
|
- Status: stable
|
||||||
|
|
||||||
This roadmap is only an attempt at predicting Owl's future. Everything may
|
Owl is currently stable. No (large) improvements is expected in the near future.
|
||||||
change!
|
|
||||||
|
|
||||||
|
|
||||||
### 1.x
|
|
||||||
|
|
||||||
- add chrome and firefox devtools,
|
|
||||||
- fix every bugs,
|
|
||||||
- improve documentation,
|
|
||||||
- small backward compatible improvements.
|
|
||||||
|
|
||||||
### 2.x (2020? 2021? 2022?)
|
|
||||||
|
|
||||||
- stop support for `t-set` directive to define the content of a slot
|
|
||||||
|
|
||||||
Maybe:
|
|
||||||
|
|
||||||
- reimplement vdom to use *block* system, like Vue 3, which should make Owl
|
|
||||||
much faster
|
|
||||||
- refactor `QWeb` to use an intermediate representation (some kind of AST) to
|
|
||||||
allow additional optimisations.
|
|
||||||
|
|
||||||
|
|
||||||
|
Note that we intend to keep maintaining owl, and as such, improvements and/or
|
||||||
|
breaking changes may require a version bump in the future.
|
||||||
|
|||||||
@@ -2,28 +2,57 @@ import pkg from "./package.json";
|
|||||||
import git from "git-rev-sync";
|
import git from "git-rev-sync";
|
||||||
import typescript from 'rollup-plugin-typescript2';
|
import typescript from 'rollup-plugin-typescript2';
|
||||||
import { terser } from "rollup-plugin-terser";
|
import { terser } from "rollup-plugin-terser";
|
||||||
|
import dts from "rollup-plugin-dts";
|
||||||
|
|
||||||
const name = "owl";
|
let input, output;
|
||||||
const extend = true;
|
|
||||||
|
const IIFE_FILENAME = "dist/owl.iife.js";
|
||||||
|
const CJS_FILENAME = "dist/owl.cjs.js";
|
||||||
|
const ES_FILENAME = "dist/owl.es.js";
|
||||||
|
|
||||||
|
if (pkg.module !== ES_FILENAME || pkg.main !== CJS_FILENAME) {
|
||||||
|
throw new Error("package.json has been modified. Build script should be updated accordingly");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Meta data to be added on the __info__ object.
|
|
||||||
* Used to let external tools know the current owl version.
|
|
||||||
*/
|
|
||||||
const outro = `
|
const outro = `
|
||||||
__info__.version = '${pkg.version}';
|
|
||||||
__info__.date = '${new Date().toISOString()}';
|
__info__.date = '${new Date().toISOString()}';
|
||||||
__info__.hash = '${git.short()}';
|
__info__.hash = '${git.short()}';
|
||||||
__info__.url = 'https://github.com/odoo/owl';
|
__info__.url = 'https://github.com/odoo/owl';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
switch (process.argv[4]) {
|
||||||
|
case "compiler":
|
||||||
|
input = "src/compiler/index.ts",
|
||||||
|
output = [
|
||||||
|
getConfigForFormat('cjs', 'dist/compiler.js', ''),
|
||||||
|
]
|
||||||
|
break;
|
||||||
|
case "runtime":
|
||||||
|
input = "src/runtime/index.ts";
|
||||||
|
output = [
|
||||||
|
getConfigForFormat('esm', addSuffix(ES_FILENAME, 'runtime'), outro),
|
||||||
|
getConfigForFormat('cjs', addSuffix(CJS_FILENAME, 'runtime'), outro),
|
||||||
|
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro),
|
||||||
|
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro, true),
|
||||||
|
]
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
input = "src/index.ts",
|
||||||
|
output = [
|
||||||
|
getConfigForFormat('esm', ES_FILENAME, outro),
|
||||||
|
getConfigForFormat('cjs', CJS_FILENAME, outro),
|
||||||
|
getConfigForFormat('iife', IIFE_FILENAME, outro),
|
||||||
|
getConfigForFormat('iife', IIFE_FILENAME, outro, true),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate from a string depicting a path a new path for the minified version.
|
* Generate from a string depicting a path a new path for the minified version.
|
||||||
* @param {string} pkgFileName file name
|
* @param {string} pkgFileName file name
|
||||||
*/
|
*/
|
||||||
function generateMinifiedNameFromPkgName(pkgFileName) {
|
function addSuffix(pkgFileName, suffix) {
|
||||||
const parts = pkgFileName.split('.');
|
const parts = pkgFileName.split('.');
|
||||||
parts.splice(parts.length - 1, 0, "min");
|
parts.splice(parts.length - 1, 0, suffix);
|
||||||
return parts.join('.');
|
return parts.join('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,12 +62,12 @@ function generateMinifiedNameFromPkgName(pkgFileName) {
|
|||||||
* @param {string} generatedFileName generated file name
|
* @param {string} generatedFileName generated file name
|
||||||
* @param {boolean} minified should it be minified
|
* @param {boolean} minified should it be minified
|
||||||
*/
|
*/
|
||||||
function getConfigForFormat(format, generatedFileName, minified = false) {
|
function getConfigForFormat(format, generatedFileName, outro, minified = false) {
|
||||||
return {
|
return {
|
||||||
file: minified ? generateMinifiedNameFromPkgName(generatedFileName) : generatedFileName,
|
file: minified ? addSuffix(generatedFileName, "min") : generatedFileName,
|
||||||
format: format,
|
format: format,
|
||||||
name: name,
|
name: "owl",
|
||||||
extend: extend,
|
extend: true,
|
||||||
outro: outro,
|
outro: outro,
|
||||||
freeze: false,
|
freeze: false,
|
||||||
plugins: minified ? [terser()] : [],
|
plugins: minified ? [terser()] : [],
|
||||||
@@ -46,26 +75,19 @@ function getConfigForFormat(format, generatedFileName, minified = false) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default [
|
||||||
input: "src/index.ts",
|
{
|
||||||
output: [
|
input,
|
||||||
|
output,
|
||||||
/**
|
plugins: [
|
||||||
* Read about module formats:
|
typescript({
|
||||||
* https://auth0.com/blog/javascript-module-systems-showdown/
|
useTsconfigDeclarationDir: true
|
||||||
* https://medium.com/@kelin2025/so-you-wanna-use-es6-modules-714f48b3a953
|
}),
|
||||||
*/
|
]
|
||||||
|
},
|
||||||
getConfigForFormat('esm', pkg.module),
|
{
|
||||||
getConfigForFormat('esm', pkg.module, true),
|
input: "dist/types/index.d.ts",
|
||||||
getConfigForFormat('cjs', pkg.main),
|
output: [{ file: "dist/types/owl.d.ts", format: "es" }],
|
||||||
getConfigForFormat('cjs', pkg.main, true),
|
plugins: [dts()],
|
||||||
getConfigForFormat('iife', pkg.browser),
|
},
|
||||||
getConfigForFormat('iife', pkg.browser, true),
|
];
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
typescript({
|
|
||||||
useTsconfigDeclarationDir: true
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
import { Component, ComponentConstructor } from "../component/component";
|
|
||||||
import { ComponentNode } from "../component/component_node";
|
|
||||||
import { MountOptions } from "../component/fibers";
|
|
||||||
import { Scheduler } from "../component/scheduler";
|
|
||||||
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
|
||||||
import { nodeErrorHandlers } from "../component/error_handling";
|
|
||||||
import { validateTarget } from "../utils";
|
|
||||||
|
|
||||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
|
||||||
|
|
||||||
export interface Env {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppConfig<P, E> extends TemplateSetConfig {
|
|
||||||
props?: P;
|
|
||||||
env?: E;
|
|
||||||
test?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEV_MSG = `Owl is running in 'dev' mode.
|
|
||||||
|
|
||||||
This is not suitable for production use.
|
|
||||||
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
|
|
||||||
|
|
||||||
export class App<
|
|
||||||
T extends abstract new (...args: any) => any = any,
|
|
||||||
P = any,
|
|
||||||
E = any
|
|
||||||
> extends TemplateSet {
|
|
||||||
static validateTarget = validateTarget;
|
|
||||||
|
|
||||||
Root: ComponentConstructor<P, E>;
|
|
||||||
props: P;
|
|
||||||
env: E;
|
|
||||||
scheduler = new Scheduler();
|
|
||||||
root: ComponentNode<P, E> | null = null;
|
|
||||||
|
|
||||||
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
|
|
||||||
super(config);
|
|
||||||
this.Root = Root;
|
|
||||||
if (config.test) {
|
|
||||||
this.dev = true;
|
|
||||||
}
|
|
||||||
if (this.dev && !config.test) {
|
|
||||||
console.info(DEV_MSG);
|
|
||||||
}
|
|
||||||
const descrs = Object.getOwnPropertyDescriptors(config.env || {});
|
|
||||||
this.env = Object.freeze(Object.defineProperties({}, descrs)) as E;
|
|
||||||
this.props = config.props || ({} as P);
|
|
||||||
}
|
|
||||||
|
|
||||||
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
|
|
||||||
App.validateTarget(target);
|
|
||||||
const node = this.makeNode(this.Root, this.props);
|
|
||||||
const prom = this.mountNode(node, target, options);
|
|
||||||
this.root = node;
|
|
||||||
return prom;
|
|
||||||
}
|
|
||||||
|
|
||||||
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
|
||||||
return new ComponentNode(Component, props, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
|
|
||||||
const promise: any = new Promise((resolve, reject) => {
|
|
||||||
let isResolved = false;
|
|
||||||
// manually set a onMounted callback.
|
|
||||||
// that way, we are independant from the current node.
|
|
||||||
node.mounted.push(() => {
|
|
||||||
resolve(node.component);
|
|
||||||
isResolved = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manually add the last resort error handler on the node
|
|
||||||
let handlers = nodeErrorHandlers.get(node);
|
|
||||||
if (!handlers) {
|
|
||||||
handlers = [];
|
|
||||||
nodeErrorHandlers.set(node, handlers);
|
|
||||||
}
|
|
||||||
handlers.unshift((e) => {
|
|
||||||
if (isResolved) {
|
|
||||||
console.error(e);
|
|
||||||
} else {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
node.mountComponent(target, options);
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
if (this.root) {
|
|
||||||
this.root.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mount<T extends abstract new (...args: any) => any = any, P = any, E = any>(
|
|
||||||
C: T & ComponentConstructor<P, E>,
|
|
||||||
target: HTMLElement,
|
|
||||||
config: AppConfig<P, E> & MountOptions = {}
|
|
||||||
): Promise<Component<P, E> & InstanceType<T>> {
|
|
||||||
return new App(C, config).mount(target, config);
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import { BDom, multi, text, toggler } from "../blockdom";
|
|
||||||
import { validateProps } from "../component/props_validation";
|
|
||||||
import { Markup } from "../utils";
|
|
||||||
import { html } from "../blockdom/index";
|
|
||||||
/**
|
|
||||||
* This file contains utility functions that will be injected in each template,
|
|
||||||
* to perform various useful tasks in the compiled code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function withDefault(value: any, defaultValue: any): any {
|
|
||||||
return value === undefined || value === null || value === false ? defaultValue : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function callSlot(
|
|
||||||
ctx: any,
|
|
||||||
parent: any,
|
|
||||||
key: string,
|
|
||||||
name: string,
|
|
||||||
dynamic: boolean,
|
|
||||||
extra: any,
|
|
||||||
defaultContent?: (ctx: any, node: any, key: string) => BDom
|
|
||||||
): BDom {
|
|
||||||
key = key + "__slot_" + name;
|
|
||||||
const slots = (ctx.props && ctx.props.slots) || {};
|
|
||||||
const { __render, __ctx, __scope } = slots[name] || {};
|
|
||||||
const slotScope = Object.create(__ctx || {});
|
|
||||||
if (__scope) {
|
|
||||||
slotScope[__scope] = extra || {};
|
|
||||||
}
|
|
||||||
const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
|
|
||||||
if (defaultContent) {
|
|
||||||
let child1: BDom | undefined = undefined;
|
|
||||||
let child2: BDom | undefined = undefined;
|
|
||||||
if (slotBDom) {
|
|
||||||
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
|
|
||||||
} else {
|
|
||||||
child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
|
|
||||||
}
|
|
||||||
return multi([child1, child2]);
|
|
||||||
}
|
|
||||||
return slotBDom || text("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function capture(ctx: any): any {
|
|
||||||
const component = ctx.__owl__.component;
|
|
||||||
const result = Object.create(component);
|
|
||||||
for (let k in ctx) {
|
|
||||||
result[k] = ctx[k];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function withKey(elem: any, k: string) {
|
|
||||||
elem.key = k;
|
|
||||||
return elem;
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareList(collection: any): [any[], any[], number, any[]] {
|
|
||||||
let keys: any[];
|
|
||||||
let values: any[];
|
|
||||||
|
|
||||||
if (Array.isArray(collection)) {
|
|
||||||
keys = collection;
|
|
||||||
values = collection;
|
|
||||||
} else if (collection) {
|
|
||||||
values = Object.keys(collection);
|
|
||||||
keys = Object.values(collection);
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid loop expression");
|
|
||||||
}
|
|
||||||
const n = values.length;
|
|
||||||
return [keys, values, n, new Array(n)];
|
|
||||||
}
|
|
||||||
|
|
||||||
const isBoundary = Symbol("isBoundary");
|
|
||||||
|
|
||||||
function setContextValue(ctx: { [key: string]: any }, key: string, value: any): void {
|
|
||||||
const ctx0 = ctx;
|
|
||||||
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
|
|
||||||
const newCtx = ctx.__proto__;
|
|
||||||
if (!newCtx) {
|
|
||||||
ctx = ctx0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ctx = newCtx;
|
|
||||||
}
|
|
||||||
ctx[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toNumber(val: string): number | string {
|
|
||||||
const n = parseFloat(val);
|
|
||||||
return isNaN(n) ? val : n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shallowEqual(l1: any[], l2: any[]): boolean {
|
|
||||||
for (let i = 0, l = l1.length; i < l; i++) {
|
|
||||||
if (l1[i] !== l2[i]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class LazyValue {
|
|
||||||
fn: any;
|
|
||||||
ctx: any;
|
|
||||||
node: any;
|
|
||||||
constructor(fn: any, ctx: any, node: any) {
|
|
||||||
this.fn = fn;
|
|
||||||
this.ctx = capture(ctx);
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate(): any {
|
|
||||||
return this.fn(this.ctx, this.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return this.evaluate().toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Safely outputs `value` as a block depending on the nature of `value`
|
|
||||||
*/
|
|
||||||
export function safeOutput(value: any): ReturnType<typeof toggler> {
|
|
||||||
if (!value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
let safeKey;
|
|
||||||
let block;
|
|
||||||
if (value instanceof Markup) {
|
|
||||||
safeKey = `string_safe`;
|
|
||||||
block = html(value as string);
|
|
||||||
} else if (value instanceof LazyValue) {
|
|
||||||
safeKey = `lazy_value`;
|
|
||||||
block = value.evaluate();
|
|
||||||
} else if (value instanceof String || typeof value === "string") {
|
|
||||||
safeKey = "string_unsafe";
|
|
||||||
block = text(value);
|
|
||||||
} else {
|
|
||||||
// Assuming it is a block
|
|
||||||
safeKey = "block_safe";
|
|
||||||
block = value;
|
|
||||||
}
|
|
||||||
return toggler(safeKey, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
let boundFunctions = new WeakMap();
|
|
||||||
|
|
||||||
function bind(ctx: any, fn: Function): Function {
|
|
||||||
let component = ctx.__owl__.component;
|
|
||||||
let boundFnMap = boundFunctions.get(component);
|
|
||||||
if (!boundFnMap) {
|
|
||||||
boundFnMap = new WeakMap();
|
|
||||||
boundFunctions.set(component, boundFnMap);
|
|
||||||
}
|
|
||||||
let boundFn = boundFnMap.get(fn);
|
|
||||||
if (!boundFn) {
|
|
||||||
boundFn = fn.bind(component);
|
|
||||||
boundFnMap.set(fn, boundFn);
|
|
||||||
}
|
|
||||||
return boundFn;
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefMap = { [key: string]: HTMLElement | null };
|
|
||||||
type RefSetter = (el: HTMLElement | null) => void;
|
|
||||||
|
|
||||||
function multiRefSetter(refs: RefMap, name: string): RefSetter {
|
|
||||||
let count = 0;
|
|
||||||
return (el) => {
|
|
||||||
if (el) {
|
|
||||||
count++;
|
|
||||||
if (count > 1) {
|
|
||||||
throw new Error("Cannot have 2 elements with same ref name at the same time");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (count === 0 || el) {
|
|
||||||
refs[name] = el;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UTILS = {
|
|
||||||
withDefault,
|
|
||||||
zero: Symbol("zero"),
|
|
||||||
isBoundary,
|
|
||||||
callSlot,
|
|
||||||
capture,
|
|
||||||
withKey,
|
|
||||||
prepareList,
|
|
||||||
setContextValue,
|
|
||||||
multiRefSetter,
|
|
||||||
shallowEqual,
|
|
||||||
toNumber,
|
|
||||||
validateProps,
|
|
||||||
LazyValue,
|
|
||||||
safeOutput,
|
|
||||||
bind,
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { BDom } from "../blockdom";
|
import type { TemplateSet } from "../runtime/template_set";
|
||||||
|
import type { BDom } from "../runtime/blockdom";
|
||||||
import { CodeGenerator, Config } from "./code_generator";
|
import { CodeGenerator, Config } from "./code_generator";
|
||||||
import { parse } from "./parser";
|
import { parse } from "./parser";
|
||||||
|
|
||||||
export type Template = (context: any, vnode: any, key?: string) => BDom;
|
export type Template = (context: any, vnode: any, key?: string) => BDom;
|
||||||
|
|
||||||
export type TemplateFunction = (blocks: any, utils: any) => Template;
|
export type TemplateFunction = (app: TemplateSet, bdom: any, helpers: any) => Template;
|
||||||
|
|
||||||
interface CompileOptions extends Config {
|
interface CompileOptions extends Config {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -26,5 +27,5 @@ export function compile(
|
|||||||
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
|
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
|
||||||
const code = codeGenerator.generateCode();
|
const code = codeGenerator.generateCode();
|
||||||
// template function
|
// template function
|
||||||
return new Function("bdom, helpers", code) as TemplateFunction;
|
return new Function("app, bdom, helpers", code) as TemplateFunction;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { OwlError } from "../runtime/error_handling";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Expression Parser
|
* Owl QWeb Expression Parser
|
||||||
*
|
*
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
const RESERVED_WORDS =
|
const RESERVED_WORDS =
|
||||||
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
|
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date".split(
|
||||||
","
|
","
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,8 +87,9 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(n
|
|||||||
});
|
});
|
||||||
|
|
||||||
// note that the space after typeof is relevant. It makes sure that the formatted
|
// note that the space after typeof is relevant. It makes sure that the formatted
|
||||||
// expression has a space after typeof
|
// expression has a space after typeof. Currently we don't support delete and void
|
||||||
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
|
const OPERATORS =
|
||||||
|
"...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~".split(",");
|
||||||
|
|
||||||
type Tokenizer = (expr: string) => Token | false;
|
type Tokenizer = (expr: string) => Token | false;
|
||||||
|
|
||||||
@@ -105,14 +108,14 @@ let tokenizeString: Tokenizer = function (expr) {
|
|||||||
i++;
|
i++;
|
||||||
cur = expr[i];
|
cur = expr[i];
|
||||||
if (!cur) {
|
if (!cur) {
|
||||||
throw new Error("Invalid expression");
|
throw new OwlError("Invalid expression");
|
||||||
}
|
}
|
||||||
s += cur;
|
s += cur;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
if (expr[i] !== start) {
|
if (expr[i] !== start) {
|
||||||
throw new Error("Invalid expression");
|
throw new OwlError("Invalid expression");
|
||||||
}
|
}
|
||||||
s += start;
|
s += start;
|
||||||
if (start === "`") {
|
if (start === "`") {
|
||||||
@@ -199,24 +202,30 @@ const TOKENIZERS = [
|
|||||||
export function tokenize(expr: string): Token[] {
|
export function tokenize(expr: string): Token[] {
|
||||||
const result: Token[] = [];
|
const result: Token[] = [];
|
||||||
let token: boolean | Token = true;
|
let token: boolean | Token = true;
|
||||||
|
let error: any;
|
||||||
|
let current = expr;
|
||||||
|
|
||||||
while (token) {
|
try {
|
||||||
expr = expr.trim();
|
while (token) {
|
||||||
if (expr) {
|
current = current.trim();
|
||||||
for (let tokenizer of TOKENIZERS) {
|
if (current) {
|
||||||
token = tokenizer(expr);
|
for (let tokenizer of TOKENIZERS) {
|
||||||
if (token) {
|
token = tokenizer(current);
|
||||||
result.push(token);
|
if (token) {
|
||||||
expr = expr.slice(token.size || token.value.length);
|
result.push(token);
|
||||||
break;
|
current = current.slice(token.size || token.value.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
token = false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
token = false;
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = e; // Silence all errors and throw a generic error below
|
||||||
}
|
}
|
||||||
if (expr.length) {
|
if (current.length || error) {
|
||||||
throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
|
throw new OwlError(`Tokenizer error: could not tokenize \`${expr}\``);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -338,21 +347,29 @@ export function compileExprToArray(expr: string): Token[] {
|
|||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leading spaces are trimmed during tokenization, so they need to be added back for some values
|
||||||
|
const paddedValues = new Map([["in ", " in "]]);
|
||||||
|
|
||||||
export function compileExpr(expr: string): string {
|
export function compileExpr(expr: string): string {
|
||||||
return compileExprToArray(expr)
|
return compileExprToArray(expr)
|
||||||
.map((t) => t.value)
|
.map((t) => paddedValues.get(t.value) || t.value)
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
export const INTERP_REGEXP = /\{\{.*?\}\}|\#\{.*?\}/g;
|
||||||
const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g;
|
|
||||||
|
|
||||||
export function interpolate(s: string): string {
|
export function replaceDynamicParts(s: string, replacer: (s: string) => string) {
|
||||||
let matches = s.match(INTERP_REGEXP);
|
let matches = s.match(INTERP_REGEXP);
|
||||||
if (matches && matches[0].length === s.length) {
|
if (matches && matches[0].length === s.length) {
|
||||||
return `(${compileExpr(s.slice(2, -2))})`;
|
return `(${replacer(s.slice(2, matches[0][0] === "{" ? -2 : -1))})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}");
|
let r = s.replace(
|
||||||
|
INTERP_REGEXP,
|
||||||
|
(s) => "${" + replacer(s.slice(2, s[0] === "{" ? -2 : -1)) + "}"
|
||||||
|
);
|
||||||
return "`" + r + "`";
|
return "`" + r + "`";
|
||||||
}
|
}
|
||||||
|
export function interpolate(s: string): string {
|
||||||
|
return replaceDynamicParts(s, compileExpr);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import { OwlError } from "../runtime/error_handling";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// AST Type definition
|
// AST Type definition
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type EventHandlers = { [eventName: string]: string };
|
||||||
|
export type Attrs = { [attrs: string]: string };
|
||||||
|
|
||||||
export const enum ASTType {
|
export const enum ASTType {
|
||||||
Text,
|
Text,
|
||||||
Comment,
|
Comment,
|
||||||
@@ -34,25 +39,25 @@ export interface ASTComment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TModelInfo {
|
interface TModelInfo {
|
||||||
hasDynamicChildren?: boolean;
|
|
||||||
baseExpr: string;
|
baseExpr: string;
|
||||||
expr: string;
|
expr: string;
|
||||||
targetAttr: string;
|
targetAttr: string;
|
||||||
specialInitTargetAttr: string | null;
|
|
||||||
eventType: "change" | "click" | "input";
|
eventType: "change" | "click" | "input";
|
||||||
shouldTrim: boolean;
|
shouldTrim: boolean;
|
||||||
shouldNumberize: boolean;
|
shouldNumberize: boolean;
|
||||||
|
hasDynamicChildren: boolean;
|
||||||
|
specialInitTargetAttr: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTDomNode {
|
export interface ASTDomNode {
|
||||||
type: ASTType.DomNode;
|
type: ASTType.DomNode;
|
||||||
tag: string;
|
tag: string;
|
||||||
dynamicTag: string | null;
|
|
||||||
attrs: { [key: string]: string };
|
|
||||||
content: AST[];
|
content: AST[];
|
||||||
|
attrs: Attrs | null;
|
||||||
ref: string | null;
|
ref: string | null;
|
||||||
on: { [key: string]: string };
|
on: EventHandlers | null;
|
||||||
model?: TModelInfo | null;
|
model: TModelInfo | null;
|
||||||
|
dynamicTag: string | null;
|
||||||
ns: string | null;
|
ns: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,13 +98,13 @@ export interface ASTTForEach {
|
|||||||
type: ASTType.TForEach;
|
type: ASTType.TForEach;
|
||||||
collection: string;
|
collection: string;
|
||||||
elem: string;
|
elem: string;
|
||||||
key: string | null;
|
|
||||||
body: AST;
|
body: AST;
|
||||||
memo: string;
|
memo: string;
|
||||||
hasNoFirst: boolean;
|
hasNoFirst: boolean;
|
||||||
hasNoLast: boolean;
|
hasNoLast: boolean;
|
||||||
hasNoIndex: boolean;
|
hasNoIndex: boolean;
|
||||||
hasNoValue: boolean;
|
hasNoValue: boolean;
|
||||||
|
key: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTKey {
|
export interface ASTTKey {
|
||||||
@@ -112,6 +117,14 @@ export interface ASTTCall {
|
|||||||
type: ASTType.TCall;
|
type: ASTType.TCall;
|
||||||
name: string;
|
name: string;
|
||||||
body: AST[] | null;
|
body: AST[] | null;
|
||||||
|
context: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SlotDefinition {
|
||||||
|
content: AST | null;
|
||||||
|
scope: string | null;
|
||||||
|
on: EventHandlers | null;
|
||||||
|
attrs: Attrs | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTComponent {
|
export interface ASTComponent {
|
||||||
@@ -119,14 +132,16 @@ export interface ASTComponent {
|
|||||||
name: string;
|
name: string;
|
||||||
isDynamic: boolean;
|
isDynamic: boolean;
|
||||||
dynamicProps: string | null;
|
dynamicProps: string | null;
|
||||||
props: { [name: string]: string };
|
on: EventHandlers | null;
|
||||||
slots: { [name: string]: { content: AST; attrs?: { [key: string]: string }; scope?: string } };
|
props: { [name: string]: string } | null;
|
||||||
|
slots: { [name: string]: SlotDefinition } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTSlot {
|
export interface ASTSlot {
|
||||||
type: ASTType.TSlot;
|
type: ASTType.TSlot;
|
||||||
name: string;
|
name: string;
|
||||||
attrs: { [key: string]: string };
|
attrs: Attrs | null;
|
||||||
|
on: EventHandlers | null;
|
||||||
defaultContent: AST | null;
|
defaultContent: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,16 +261,12 @@ function parseTNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
// Text and Comment Nodes
|
// Text and Comment Nodes
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
const lineBreakRE = /[\r\n]/;
|
const lineBreakRE = /[\r\n]/;
|
||||||
const whitespaceRE = /\s+/g;
|
|
||||||
|
|
||||||
function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
|
function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
let value = node.textContent || "";
|
let value = node.textContent || "";
|
||||||
if (!ctx.inPreTag) {
|
if (!ctx.inPreTag && lineBreakRE.test(value) && !value.trim()) {
|
||||||
if (lineBreakRE.test(value) && !value.trim()) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
value = value.replace(whitespaceRE, " ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: ASTType.Text, value };
|
return { type: ASTType.Text, value };
|
||||||
@@ -305,6 +316,9 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
if (tagName === "t" && !dynamicTag) {
|
if (tagName === "t" && !dynamicTag) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (tagName.startsWith("block-")) {
|
||||||
|
throw new OwlError(`Invalid tag name: '${tagName}'`);
|
||||||
|
}
|
||||||
ctx = Object.assign({}, ctx);
|
ctx = Object.assign({}, ctx);
|
||||||
if (tagName === "pre") {
|
if (tagName === "pre") {
|
||||||
ctx.inPreTag = true;
|
ctx.inPreTag = true;
|
||||||
@@ -316,20 +330,23 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
node.removeAttribute("t-ref");
|
node.removeAttribute("t-ref");
|
||||||
|
|
||||||
const nodeAttrsNames = node.getAttributeNames();
|
const nodeAttrsNames = node.getAttributeNames();
|
||||||
const attrs: ASTDomNode["attrs"] = {};
|
let attrs: ASTDomNode["attrs"] = null;
|
||||||
const on: ASTDomNode["on"] = {};
|
let on: EventHandlers | null = null;
|
||||||
let model: TModelInfo | null = null;
|
let model: TModelInfo | null = null;
|
||||||
|
|
||||||
for (let attr of nodeAttrsNames) {
|
for (let attr of nodeAttrsNames) {
|
||||||
const value = node.getAttribute(attr)!;
|
const value = node.getAttribute(attr)!;
|
||||||
if (attr.startsWith("t-on")) {
|
if (attr.startsWith("t-on")) {
|
||||||
if (attr === "t-on") {
|
if (attr === "t-on") {
|
||||||
throw new Error("Missing event name with t-on directive");
|
throw new OwlError("Missing event name with t-on directive");
|
||||||
}
|
}
|
||||||
|
on = on || {};
|
||||||
on[attr.slice(5)] = value;
|
on[attr.slice(5)] = value;
|
||||||
} else if (attr.startsWith("t-model")) {
|
} else if (attr.startsWith("t-model")) {
|
||||||
if (!["input", "select", "textarea"].includes(tagName)) {
|
if (!["input", "select", "textarea"].includes(tagName)) {
|
||||||
throw new Error("The t-model directive only works with <input>, <textarea> and <select>");
|
throw new OwlError(
|
||||||
|
"The t-model directive only works with <input>, <textarea> and <select>"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let baseExpr, expr;
|
let baseExpr, expr;
|
||||||
@@ -342,7 +359,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
baseExpr = value.slice(0, index);
|
baseExpr = value.slice(0, index);
|
||||||
expr = value.slice(index + 1, -1);
|
expr = value.slice(index + 1, -1);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
|
throw new OwlError(`Invalid t-model expression: "${value}" (it should be assignable)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeAttr = node.getAttribute("type");
|
const typeAttr = node.getAttribute("type");
|
||||||
@@ -363,6 +380,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
targetAttr: isCheckboxInput ? "checked" : "value",
|
targetAttr: isCheckboxInput ? "checked" : "value",
|
||||||
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
||||||
eventType,
|
eventType,
|
||||||
|
hasDynamicChildren: false,
|
||||||
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
|
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
|
||||||
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
|
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
|
||||||
};
|
};
|
||||||
@@ -371,14 +389,17 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
ctx = Object.assign({}, ctx);
|
ctx = Object.assign({}, ctx);
|
||||||
ctx.tModelInfo = model;
|
ctx.tModelInfo = model;
|
||||||
}
|
}
|
||||||
|
} else if (attr.startsWith("block-")) {
|
||||||
|
throw new OwlError(`Invalid attribute: '${attr}'`);
|
||||||
} else if (attr !== "t-name") {
|
} else if (attr !== "t-name") {
|
||||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||||
throw new Error(`Unknown QWeb directive: '${attr}'`);
|
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
||||||
}
|
}
|
||||||
const tModel = ctx.tModelInfo;
|
const tModel = ctx.tModelInfo;
|
||||||
if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
|
if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
|
||||||
tModel.hasDynamicChildren = true;
|
tModel.hasDynamicChildren = true;
|
||||||
}
|
}
|
||||||
|
attrs = attrs || {};
|
||||||
attrs[attr] = value;
|
attrs[attr] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,7 +447,7 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (ast.type === ASTType.TComponent) {
|
if (ast.type === ASTType.TComponent) {
|
||||||
throw new Error("t-esc is not supported on Component nodes");
|
throw new OwlError("t-esc is not supported on Component nodes");
|
||||||
}
|
}
|
||||||
return tesc;
|
return tesc;
|
||||||
}
|
}
|
||||||
@@ -482,7 +503,7 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
node.removeAttribute("t-as");
|
node.removeAttribute("t-as");
|
||||||
const key = node.getAttribute("t-key");
|
const key = node.getAttribute("t-key");
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new Error(
|
throw new OwlError(
|
||||||
`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`
|
`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -537,11 +558,13 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const subTemplate = node.getAttribute("t-call")!;
|
const subTemplate = node.getAttribute("t-call")!;
|
||||||
|
const context = node.getAttribute("t-call-context");
|
||||||
node.removeAttribute("t-call");
|
node.removeAttribute("t-call");
|
||||||
|
node.removeAttribute("t-call-context");
|
||||||
|
|
||||||
if (node.tagName !== "t") {
|
if (node.tagName !== "t") {
|
||||||
const ast = parseNode(node, ctx);
|
const ast = parseNode(node, ctx);
|
||||||
const tcall: AST = { type: ASTType.TCall, name: subTemplate, body: null };
|
const tcall: AST = { type: ASTType.TCall, name: subTemplate, body: null, context };
|
||||||
if (ast && ast.type === ASTType.DomNode) {
|
if (ast && ast.type === ASTType.DomNode) {
|
||||||
ast.content = [tcall];
|
ast.content = [tcall];
|
||||||
return ast;
|
return ast;
|
||||||
@@ -549,7 +572,7 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
if (ast && ast.type === ASTType.TComponent) {
|
if (ast && ast.type === ASTType.TComponent) {
|
||||||
return {
|
return {
|
||||||
...ast,
|
...ast,
|
||||||
slots: { default: { content: tcall } },
|
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,6 +582,7 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
type: ASTType.TCall,
|
type: ASTType.TCall,
|
||||||
name: subTemplate,
|
name: subTemplate,
|
||||||
body: body.length ? body : null,
|
body: body.length ? body : null,
|
||||||
|
context,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,7 +669,6 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
|
|
||||||
// Error messages when trying to use an unsupported directive on a component
|
// Error messages when trying to use an unsupported directive on a component
|
||||||
const directiveErrorMap = new Map([
|
const directiveErrorMap = new Map([
|
||||||
["t-on", "t-on is no longer supported on components. Consider passing a callback in props."],
|
|
||||||
[
|
[
|
||||||
"t-ref",
|
"t-ref",
|
||||||
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
|
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
|
||||||
@@ -663,7 +686,9 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
let isDynamic = node.hasAttribute("t-component");
|
let isDynamic = node.hasAttribute("t-component");
|
||||||
|
|
||||||
if (isDynamic && name !== "t") {
|
if (isDynamic && name !== "t") {
|
||||||
throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
|
throw new OwlError(
|
||||||
|
`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
|
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
|
||||||
@@ -679,19 +704,26 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
|
|
||||||
const defaultSlotScope = node.getAttribute("t-slot-scope");
|
const defaultSlotScope = node.getAttribute("t-slot-scope");
|
||||||
node.removeAttribute("t-slot-scope");
|
node.removeAttribute("t-slot-scope");
|
||||||
|
let on: ASTComponent["on"] = null;
|
||||||
|
|
||||||
const props: ASTComponent["props"] = {};
|
let props: ASTComponent["props"] = null;
|
||||||
for (let name of node.getAttributeNames()) {
|
for (let name of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(name)!;
|
const value = node.getAttribute(name)!;
|
||||||
if (name.startsWith("t-")) {
|
if (name.startsWith("t-")) {
|
||||||
const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
|
if (name.startsWith("t-on-")) {
|
||||||
throw new Error(message || `unsupported directive on Component: ${name}`);
|
on = on || {};
|
||||||
|
on[name.slice(5)] = value;
|
||||||
|
} else {
|
||||||
|
const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
|
||||||
|
throw new OwlError(message || `unsupported directive on Component: ${name}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
props = props || {};
|
||||||
props[name] = value;
|
props[name] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const slots: ASTComponent["slots"] = {};
|
let slots: ASTComponent["slots"] | null = null;
|
||||||
if (node.hasChildNodes()) {
|
if (node.hasChildNodes()) {
|
||||||
const clone = <Element>node.cloneNode(true);
|
const clone = <Element>node.cloneNode(true);
|
||||||
|
|
||||||
@@ -699,7 +731,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
|
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
|
||||||
for (let slotNode of slotNodes) {
|
for (let slotNode of slotNodes) {
|
||||||
if (slotNode.tagName !== "t") {
|
if (slotNode.tagName !== "t") {
|
||||||
throw new Error(
|
throw new OwlError(
|
||||||
`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`
|
`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -723,34 +755,35 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
slotNode.removeAttribute("t-set-slot");
|
slotNode.removeAttribute("t-set-slot");
|
||||||
slotNode.remove();
|
slotNode.remove();
|
||||||
const slotAst = parseNode(slotNode, ctx);
|
const slotAst = parseNode(slotNode, ctx);
|
||||||
if (slotAst) {
|
let on: SlotDefinition["on"] = null;
|
||||||
const slotInfo: any = { content: slotAst };
|
let attrs: Attrs | null = null;
|
||||||
const attrs: { [key: string]: string } = {};
|
let scope: string | null = null;
|
||||||
for (let attributeName of slotNode.getAttributeNames()) {
|
for (let attributeName of slotNode.getAttributeNames()) {
|
||||||
const value = slotNode.getAttribute(attributeName)!;
|
const value = slotNode.getAttribute(attributeName)!;
|
||||||
if (attributeName === "t-slot-scope") {
|
if (attributeName === "t-slot-scope") {
|
||||||
slotInfo.scope = value;
|
scope = value;
|
||||||
continue;
|
continue;
|
||||||
}
|
} else if (attributeName.startsWith("t-on-")) {
|
||||||
|
on = on || {};
|
||||||
|
on[attributeName.slice(5)] = value;
|
||||||
|
} else {
|
||||||
|
attrs = attrs || {};
|
||||||
attrs[attributeName] = value;
|
attrs[attributeName] = value;
|
||||||
}
|
}
|
||||||
if (Object.keys(attrs).length) {
|
|
||||||
slotInfo.attrs = attrs;
|
|
||||||
}
|
|
||||||
slots[name] = slotInfo;
|
|
||||||
}
|
}
|
||||||
|
slots = slots || {};
|
||||||
|
slots[name] = { content: slotAst, on, attrs, scope };
|
||||||
}
|
}
|
||||||
|
|
||||||
// default slot
|
// default slot
|
||||||
const defaultContent = parseChildNodes(clone, ctx);
|
const defaultContent = parseChildNodes(clone, ctx);
|
||||||
if (defaultContent) {
|
slots = slots || {};
|
||||||
slots.default = { content: defaultContent };
|
// t-set-slot="default" has priority over content
|
||||||
if (defaultSlotScope) {
|
if (defaultContent && !slots.default) {
|
||||||
slots.default.scope = defaultSlotScope;
|
slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots };
|
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots, on };
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -763,15 +796,23 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
}
|
}
|
||||||
const name = node.getAttribute("t-slot")!;
|
const name = node.getAttribute("t-slot")!;
|
||||||
node.removeAttribute("t-slot");
|
node.removeAttribute("t-slot");
|
||||||
const attrs: { [key: string]: string } = {};
|
let attrs: Attrs | null = null;
|
||||||
|
let on: ASTComponent["on"] = null;
|
||||||
for (let attributeName of node.getAttributeNames()) {
|
for (let attributeName of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(attributeName)!;
|
const value = node.getAttribute(attributeName)!;
|
||||||
attrs[attributeName] = value;
|
if (attributeName.startsWith("t-on-")) {
|
||||||
|
on = on || {};
|
||||||
|
on[attributeName.slice(5)] = value;
|
||||||
|
} else {
|
||||||
|
attrs = attrs || {};
|
||||||
|
attrs[attributeName] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name,
|
name,
|
||||||
attrs,
|
attrs,
|
||||||
|
on,
|
||||||
defaultContent: parseChildNodes(node, ctx),
|
defaultContent: parseChildNodes(node, ctx),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -866,7 +907,7 @@ function normalizeTIf(el: Element) {
|
|||||||
let nattr = (name: string) => +!!node.getAttribute(name);
|
let nattr = (name: string) => +!!node.getAttribute(name);
|
||||||
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
||||||
if (pattr("t-foreach")) {
|
if (pattr("t-foreach")) {
|
||||||
throw new Error(
|
throw new OwlError(
|
||||||
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
|
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -875,19 +916,19 @@ function normalizeTIf(el: Element) {
|
|||||||
return a + b;
|
return a + b;
|
||||||
}) > 1
|
}) > 1
|
||||||
) {
|
) {
|
||||||
throw new Error("Only one conditional branching directive is allowed per node");
|
throw new OwlError("Only one conditional branching directive is allowed per node");
|
||||||
}
|
}
|
||||||
// All text (with only spaces) and comment nodes (nodeType 8) between
|
// All text (with only spaces) and comment nodes (nodeType 8) between
|
||||||
// branch nodes are removed
|
// branch nodes are removed
|
||||||
let textNode;
|
let textNode;
|
||||||
while ((textNode = node.previousSibling) !== prevElem) {
|
while ((textNode = node.previousSibling) !== prevElem) {
|
||||||
if (textNode!.nodeValue!.trim().length && textNode!.nodeType !== 8) {
|
if (textNode!.nodeValue!.trim().length && textNode!.nodeType !== 8) {
|
||||||
throw new Error("text is not allowed between branching directives");
|
throw new OwlError("text is not allowed between branching directives");
|
||||||
}
|
}
|
||||||
textNode!.remove();
|
textNode!.remove();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new OwlError(
|
||||||
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
|
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -908,7 +949,7 @@ function normalizeTEsc(el: Element) {
|
|||||||
);
|
);
|
||||||
for (const el of elements) {
|
for (const el of elements) {
|
||||||
if (el.childNodes.length) {
|
if (el.childNodes.length) {
|
||||||
throw new Error("Cannot have t-esc on a component that already has content");
|
throw new OwlError("Cannot have t-esc on a component that already has content");
|
||||||
}
|
}
|
||||||
const value = el.getAttribute("t-esc");
|
const value = el.getAttribute("t-esc");
|
||||||
el.removeAttribute("t-esc");
|
el.removeAttribute("t-esc");
|
||||||
@@ -962,7 +1003,7 @@ function parseXML(xml: string): XMLDocument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(msg);
|
throw new OwlError(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
|
|||||||
@@ -1,310 +0,0 @@
|
|||||||
import type { App, Env } from "../app/app";
|
|
||||||
import { BDom, VNode } from "../blockdom";
|
|
||||||
import { Component, ComponentConstructor } from "./component";
|
|
||||||
import {
|
|
||||||
Fiber,
|
|
||||||
makeChildFiber,
|
|
||||||
makeRootFiber,
|
|
||||||
MountFiber,
|
|
||||||
MountOptions,
|
|
||||||
RootFiber,
|
|
||||||
} from "./fibers";
|
|
||||||
import { handleError, fibersInError } from "./error_handling";
|
|
||||||
import { applyDefaultProps } from "./props_validation";
|
|
||||||
import { STATUS } from "./status";
|
|
||||||
|
|
||||||
let currentNode: ComponentNode | null = null;
|
|
||||||
|
|
||||||
export function getCurrent(): ComponentNode {
|
|
||||||
if (!currentNode) {
|
|
||||||
throw new Error("No active component (a hook function should only be called in 'setup')");
|
|
||||||
}
|
|
||||||
return currentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useComponent(): Component {
|
|
||||||
return currentNode!.component;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function component(
|
|
||||||
name: string | typeof Component,
|
|
||||||
props: any,
|
|
||||||
key: string,
|
|
||||||
ctx: ComponentNode,
|
|
||||||
parent: any
|
|
||||||
): ComponentNode {
|
|
||||||
let node: any = ctx.children[key];
|
|
||||||
let isDynamic = typeof name !== "string";
|
|
||||||
|
|
||||||
if (node) {
|
|
||||||
if (node.status < STATUS.MOUNTED) {
|
|
||||||
node.destroy();
|
|
||||||
node = undefined;
|
|
||||||
} else if (node.status === STATUS.DESTROYED) {
|
|
||||||
node = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isDynamic && node && node.component.constructor !== name) {
|
|
||||||
node = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentFiber = ctx.fiber!;
|
|
||||||
if (node) {
|
|
||||||
node.updateAndRender(props, parentFiber);
|
|
||||||
} else {
|
|
||||||
// new component
|
|
||||||
let C;
|
|
||||||
if (isDynamic) {
|
|
||||||
C = name;
|
|
||||||
} else {
|
|
||||||
C = parent.constructor.components[name as any];
|
|
||||||
if (!C) {
|
|
||||||
throw new Error(`Cannot find the definition of component "${name}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node = new ComponentNode(C, props, ctx.app, ctx);
|
|
||||||
ctx.children[key] = node;
|
|
||||||
|
|
||||||
const fiber = makeChildFiber(node, parentFiber);
|
|
||||||
node.initiateRender(fiber);
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// Component VNode
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type LifecycleHook = Function;
|
|
||||||
|
|
||||||
export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E>> {
|
|
||||||
el?: HTMLElement | Text | undefined;
|
|
||||||
app: App;
|
|
||||||
fiber: Fiber | null = null;
|
|
||||||
component: Component<P, E>;
|
|
||||||
bdom: BDom | null = null;
|
|
||||||
status: STATUS = STATUS.NEW;
|
|
||||||
|
|
||||||
renderFn: Function;
|
|
||||||
parent: ComponentNode | null;
|
|
||||||
level: number;
|
|
||||||
childEnv: Env;
|
|
||||||
children: { [key: string]: ComponentNode } = Object.create(null);
|
|
||||||
refs: any = {};
|
|
||||||
|
|
||||||
willStart: LifecycleHook[] = [];
|
|
||||||
willUpdateProps: LifecycleHook[] = [];
|
|
||||||
willUnmount: LifecycleHook[] = [];
|
|
||||||
mounted: LifecycleHook[] = [];
|
|
||||||
willPatch: LifecycleHook[] = [];
|
|
||||||
patched: LifecycleHook[] = [];
|
|
||||||
willDestroy: LifecycleHook[] = [];
|
|
||||||
|
|
||||||
constructor(C: ComponentConstructor<P, E>, props: P, app: App, parent?: ComponentNode) {
|
|
||||||
currentNode = this;
|
|
||||||
this.app = app;
|
|
||||||
this.parent = parent || null;
|
|
||||||
this.level = parent ? parent.level + 1 : 0;
|
|
||||||
applyDefaultProps(props, C);
|
|
||||||
const env = (parent && parent.childEnv) || app.env;
|
|
||||||
this.childEnv = env;
|
|
||||||
this.component = new C(props, env, this) as any;
|
|
||||||
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
|
|
||||||
this.component.setup();
|
|
||||||
currentNode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
mountComponent(target: any, options?: MountOptions) {
|
|
||||||
const fiber = new MountFiber(this, target, options);
|
|
||||||
this.app.scheduler.addFiber(fiber);
|
|
||||||
this.initiateRender(fiber);
|
|
||||||
}
|
|
||||||
|
|
||||||
async initiateRender(fiber: Fiber | MountFiber) {
|
|
||||||
this.fiber = fiber;
|
|
||||||
if (this.mounted.length) {
|
|
||||||
fiber.root!.mounted.push(fiber);
|
|
||||||
}
|
|
||||||
const component = this.component;
|
|
||||||
try {
|
|
||||||
await Promise.all(this.willStart.map((f) => f.call(component)));
|
|
||||||
} catch (e) {
|
|
||||||
handleError({ node: this, error: e });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.status === STATUS.NEW && this.fiber === fiber) {
|
|
||||||
this._render(fiber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async render() {
|
|
||||||
let current = this.fiber;
|
|
||||||
if (current && current.root!.locked) {
|
|
||||||
await Promise.resolve();
|
|
||||||
// situation may have changed after the microtask tick
|
|
||||||
current = this.fiber;
|
|
||||||
}
|
|
||||||
if (current && !current.bdom && !fibersInError.has(current)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.bdom && !current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fiber = makeRootFiber(this);
|
|
||||||
this.fiber = fiber;
|
|
||||||
this.app.scheduler.addFiber(fiber);
|
|
||||||
await Promise.resolve();
|
|
||||||
if (this.status === STATUS.DESTROYED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We only want to actually render the component if the following two
|
|
||||||
// conditions are true:
|
|
||||||
// * this.fiber: it could be null, in which case the render has been cancelled
|
|
||||||
// * (current || !fiber.parent): if current is not null, this means that the
|
|
||||||
// render function was called when a render was already occurring. In this
|
|
||||||
// case, the pending rendering was cancelled, and the fiber needs to be
|
|
||||||
// rendered to complete the work. If current is null, we check that the
|
|
||||||
// fiber has no parent. If that is the case, the fiber was downgraded from
|
|
||||||
// a root fiber to a child fiber in the previous microtick, because it was
|
|
||||||
// embedded in a rendering coming from above, so the fiber will be rendered
|
|
||||||
// in the next microtick anyway, so we should not render it again.
|
|
||||||
if (this.fiber === fiber && (current || !fiber.parent)) {
|
|
||||||
this._render(fiber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_render(fiber: Fiber | RootFiber) {
|
|
||||||
try {
|
|
||||||
fiber.bdom = this.renderFn();
|
|
||||||
fiber.root!.counter--;
|
|
||||||
} catch (e) {
|
|
||||||
handleError({ node: this, error: e });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
let shouldRemove = this.status === STATUS.MOUNTED;
|
|
||||||
this._destroy();
|
|
||||||
if (shouldRemove) {
|
|
||||||
this.bdom!.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_destroy() {
|
|
||||||
const component = this.component;
|
|
||||||
if (this.status === STATUS.MOUNTED) {
|
|
||||||
for (let cb of this.willUnmount) {
|
|
||||||
cb.call(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let child of Object.values(this.children)) {
|
|
||||||
child._destroy();
|
|
||||||
}
|
|
||||||
for (let cb of this.willDestroy) {
|
|
||||||
cb.call(component);
|
|
||||||
}
|
|
||||||
this.status = STATUS.DESTROYED;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateAndRender(props: any, parentFiber: Fiber) {
|
|
||||||
// update
|
|
||||||
const fiber = makeChildFiber(this, parentFiber);
|
|
||||||
this.fiber = fiber;
|
|
||||||
const component = this.component;
|
|
||||||
applyDefaultProps(props, component.constructor as any);
|
|
||||||
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
|
|
||||||
await prom;
|
|
||||||
if (fiber !== this.fiber) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
component.props = props;
|
|
||||||
this._render(fiber);
|
|
||||||
const parentRoot = parentFiber.root!;
|
|
||||||
if (this.willPatch.length) {
|
|
||||||
parentRoot.willPatch.push(fiber);
|
|
||||||
}
|
|
||||||
if (this.patched.length) {
|
|
||||||
parentRoot.patched.push(fiber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a child that has dom that is not yet updated, and update it. This
|
|
||||||
* method is meant to be used only in the context of repatching the dom after
|
|
||||||
* a mounted hook failed and was handled.
|
|
||||||
*/
|
|
||||||
updateDom() {
|
|
||||||
if (!this.fiber) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.bdom === this.fiber!.bdom) {
|
|
||||||
// If the error was handled by some child component, we need to find it to
|
|
||||||
// apply its change
|
|
||||||
for (let k in this.children) {
|
|
||||||
const child = this.children[k];
|
|
||||||
child.updateDom();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if we get here, this is the component that handled the error and rerendered
|
|
||||||
// itself, so we can simply patch the dom
|
|
||||||
this.bdom!.patch(this.fiber!.bdom, false);
|
|
||||||
this.fiber!.appliedToDom = true;
|
|
||||||
this.fiber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Block DOM methods
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
firstNode(): Node | undefined {
|
|
||||||
const bdom = this.bdom;
|
|
||||||
return bdom ? bdom.firstNode() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
mount(parent: HTMLElement, anchor: ChildNode) {
|
|
||||||
const bdom = this.fiber!.bdom!;
|
|
||||||
this.bdom = bdom;
|
|
||||||
bdom.mount(parent, anchor);
|
|
||||||
this.status = STATUS.MOUNTED;
|
|
||||||
this.fiber!.appliedToDom = true;
|
|
||||||
this.fiber = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
moveBefore(other: ComponentNode | null, afterNode: Node | null) {
|
|
||||||
this.bdom!.moveBefore(other ? other.bdom : null, afterNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
patch() {
|
|
||||||
const hasChildren = Object.keys(this.children).length > 0;
|
|
||||||
this.bdom!.patch(this!.fiber!.bdom!, hasChildren);
|
|
||||||
if (hasChildren) {
|
|
||||||
this.cleanOutdatedChildren();
|
|
||||||
}
|
|
||||||
this.fiber!.appliedToDom = true;
|
|
||||||
this.fiber = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeRemove() {
|
|
||||||
this._destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
this.bdom!.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanOutdatedChildren() {
|
|
||||||
const children = this.children;
|
|
||||||
for (const key in children) {
|
|
||||||
const node = children[key];
|
|
||||||
const status = node.status;
|
|
||||||
if (status !== STATUS.MOUNTED) {
|
|
||||||
delete children[key];
|
|
||||||
if (status !== STATUS.DESTROYED) {
|
|
||||||
node.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { getCurrent } from "./component_node";
|
|
||||||
import { nodeErrorHandlers } from "./error_handling";
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// hooks
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export function onWillStart(fn: () => Promise<void> | void | any) {
|
|
||||||
const node = getCurrent();
|
|
||||||
node.willStart.push(fn.bind(node.component));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
|
|
||||||
const node = getCurrent();
|
|
||||||
node.willUpdateProps.push(fn.bind(node.component));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onMounted(fn: () => void | any) {
|
|
||||||
const node = getCurrent();
|
|
||||||
node.mounted.push(fn.bind(node.component));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onWillPatch(fn: () => Promise<void> | any | void) {
|
|
||||||
const node = getCurrent();
|
|
||||||
node.willPatch.unshift(fn.bind(node.component));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onPatched(fn: () => void | any) {
|
|
||||||
const node = getCurrent();
|
|
||||||
node.patched.push(fn.bind(node.component));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onWillUnmount(fn: () => Promise<void> | void | any) {
|
|
||||||
const node = getCurrent();
|
|
||||||
node.willUnmount.unshift(fn.bind(node.component));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onWillDestroy(fn: () => Promise<void> | void | any) {
|
|
||||||
const node = getCurrent();
|
|
||||||
node.willDestroy.push(fn.bind(node.component));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onWillRender(fn: () => void | any) {
|
|
||||||
const node = getCurrent();
|
|
||||||
const renderFn = node.renderFn;
|
|
||||||
node.renderFn = () => {
|
|
||||||
fn.call(node.component);
|
|
||||||
return renderFn();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onRendered(fn: () => void | any) {
|
|
||||||
const node = getCurrent();
|
|
||||||
const renderFn = node.renderFn;
|
|
||||||
node.renderFn = () => {
|
|
||||||
const result = renderFn();
|
|
||||||
fn.call(node.component);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type OnErrorCallback = (error: any) => void | any;
|
|
||||||
export function onError(callback: OnErrorCallback) {
|
|
||||||
const node = getCurrent();
|
|
||||||
let handlers = nodeErrorHandlers.get(node);
|
|
||||||
if (!handlers) {
|
|
||||||
handlers = [];
|
|
||||||
nodeErrorHandlers.set(node, handlers);
|
|
||||||
}
|
|
||||||
handlers.push(callback.bind(node.component));
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { ComponentConstructor } from "./component";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply default props (only top level).
|
|
||||||
*
|
|
||||||
* Note that this method does modify in place the props
|
|
||||||
*/
|
|
||||||
export function applyDefaultProps<P>(props: P, ComponentClass: ComponentConstructor<P>) {
|
|
||||||
const defaultProps = ComponentClass.defaultProps;
|
|
||||||
if (defaultProps) {
|
|
||||||
for (let propName in defaultProps) {
|
|
||||||
if ((props as any)[propName] === undefined) {
|
|
||||||
(props as any)[propName] = defaultProps[propName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Prop validation helper
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
function getPropDescription(staticProps: any) {
|
|
||||||
if (staticProps instanceof Array) {
|
|
||||||
return Object.fromEntries(
|
|
||||||
staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return staticProps || { "*": true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the component props (or next props) against the (static) props
|
|
||||||
* description. This is potentially an expensive operation: it may needs to
|
|
||||||
* visit recursively the props and all the children to check if they are valid.
|
|
||||||
* This is why it is only done in 'dev' mode.
|
|
||||||
*/
|
|
||||||
export function validateProps<P>(name: string | ComponentConstructor<P>, props: P, parent?: any) {
|
|
||||||
const ComponentClass =
|
|
||||||
typeof name !== "string"
|
|
||||||
? name
|
|
||||||
: (parent.constructor.components[name] as ComponentConstructor<P> | undefined);
|
|
||||||
|
|
||||||
if (!ComponentClass) {
|
|
||||||
// this is an error, wrong component. We silently return here instead so the
|
|
||||||
// error is triggered by the usual path ('component' function)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyDefaultProps(props, ComponentClass);
|
|
||||||
|
|
||||||
const defaultProps = ComponentClass.defaultProps || {};
|
|
||||||
let propsDef = getPropDescription(ComponentClass.props);
|
|
||||||
const allowAdditionalProps = "*" in propsDef;
|
|
||||||
|
|
||||||
for (let propName in propsDef) {
|
|
||||||
if (propName === "*") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const propDef = propsDef[propName];
|
|
||||||
let isMandatory = !!propDef;
|
|
||||||
if (typeof propDef === "object" && "optional" in propDef) {
|
|
||||||
isMandatory = !propDef.optional;
|
|
||||||
}
|
|
||||||
if (isMandatory && propName in defaultProps) {
|
|
||||||
throw new Error(
|
|
||||||
`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ((props as any)[propName] === undefined) {
|
|
||||||
if (isMandatory) {
|
|
||||||
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let isValid;
|
|
||||||
try {
|
|
||||||
isValid = isValidProp((props as any)[propName], propDef);
|
|
||||||
} catch (e) {
|
|
||||||
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
|
|
||||||
(e as Error).message
|
|
||||||
})`;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
if (!isValid) {
|
|
||||||
throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!allowAdditionalProps) {
|
|
||||||
for (let propName in props) {
|
|
||||||
if (!(propName in propsDef)) {
|
|
||||||
throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an invidual prop value matches its (static) prop definition
|
|
||||||
*/
|
|
||||||
function isValidProp(prop: any, propDef: any): boolean {
|
|
||||||
if (propDef === true) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeof propDef === "function") {
|
|
||||||
// Check if a value is constructed by some Constructor. Note that there is a
|
|
||||||
// slight abuse of language: we want to consider primitive values as well.
|
|
||||||
//
|
|
||||||
// So, even though 1 is not an instance of Number, we want to consider that
|
|
||||||
// it is valid.
|
|
||||||
if (typeof prop === "object") {
|
|
||||||
return prop instanceof propDef;
|
|
||||||
}
|
|
||||||
return typeof prop === propDef.name.toLowerCase();
|
|
||||||
} else if (propDef instanceof Array) {
|
|
||||||
// If this code is executed, this means that we want to check if a prop
|
|
||||||
// matches at least one of its descriptor.
|
|
||||||
let result = false;
|
|
||||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
|
||||||
result = result || isValidProp(prop, propDef[i]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
// propsDef is an object
|
|
||||||
if (propDef.optional && prop === undefined) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
|
|
||||||
if (propDef.validate) {
|
|
||||||
result = result && propDef.validate(prop);
|
|
||||||
}
|
|
||||||
if (propDef.type === Array && propDef.element) {
|
|
||||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
|
||||||
result = result && isValidProp(prop[i], propDef.element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (propDef.type === Object && propDef.shape) {
|
|
||||||
const shape = propDef.shape;
|
|
||||||
for (let key in shape) {
|
|
||||||
result = result && isValidProp(prop[key], shape[key]);
|
|
||||||
}
|
|
||||||
if (result) {
|
|
||||||
for (let propName in prop) {
|
|
||||||
if (!(propName in shape)) {
|
|
||||||
throw new Error(`unknown prop '${propName}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { fibersInError } from "./error_handling";
|
|
||||||
import { Fiber, RootFiber } from "./fibers";
|
|
||||||
import { STATUS } from "./status";
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// Scheduler
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export class Scheduler {
|
|
||||||
// capture the value of requestAnimationFrame as soon as possible, to avoid
|
|
||||||
// interactions with other code, such as test frameworks that override them
|
|
||||||
static requestAnimationFrame = window.requestAnimationFrame.bind(window);
|
|
||||||
tasks: Set<RootFiber> = new Set();
|
|
||||||
isRunning: boolean = false;
|
|
||||||
requestAnimationFrame: Window["requestAnimationFrame"];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
this.isRunning = true;
|
|
||||||
this.scheduleTasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.isRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
addFiber(fiber: Fiber) {
|
|
||||||
this.tasks.add(fiber.root!);
|
|
||||||
if (!this.isRunning) {
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process all current tasks. This only applies to the fibers that are ready.
|
|
||||||
* Other tasks are left unchanged.
|
|
||||||
*/
|
|
||||||
flush() {
|
|
||||||
this.tasks.forEach((fiber) => {
|
|
||||||
if (fiber.root !== fiber) {
|
|
||||||
this.tasks.delete(fiber);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hasError = fibersInError.has(fiber);
|
|
||||||
if (hasError && fiber.counter !== 0) {
|
|
||||||
this.tasks.delete(fiber);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fiber.node.status === STATUS.DESTROYED) {
|
|
||||||
this.tasks.delete(fiber);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fiber.counter === 0) {
|
|
||||||
if (!hasError) {
|
|
||||||
fiber.complete();
|
|
||||||
}
|
|
||||||
this.tasks.delete(fiber);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (this.tasks.size === 0) {
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleTasks() {
|
|
||||||
this.requestAnimationFrame(() => {
|
|
||||||
this.flush();
|
|
||||||
if (this.isRunning) {
|
|
||||||
this.scheduleTasks();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +1,16 @@
|
|||||||
import { UTILS } from "./app/template_helpers";
|
import { TemplateSet } from "./runtime/template_set";
|
||||||
import {
|
import { compile } from "./compiler";
|
||||||
config,
|
|
||||||
createBlock,
|
|
||||||
html,
|
|
||||||
list,
|
|
||||||
mount as blockMount,
|
|
||||||
multi,
|
|
||||||
patch,
|
|
||||||
remove,
|
|
||||||
text,
|
|
||||||
toggler,
|
|
||||||
comment,
|
|
||||||
} from "./blockdom";
|
|
||||||
import { mainEventHandler } from "./component/handler";
|
|
||||||
import { Portal } from "./portal";
|
|
||||||
export type { Reactive } from "./reactivity";
|
|
||||||
|
|
||||||
config.shouldNormalizeDom = false;
|
export * from "./runtime";
|
||||||
config.mainEventHandler = mainEventHandler;
|
|
||||||
(UTILS as any).Portal = Portal;
|
|
||||||
|
|
||||||
export const blockDom = {
|
TemplateSet.prototype._compileTemplate = function _compileTemplate(
|
||||||
config,
|
name: string,
|
||||||
// bdom entry points
|
template: string | Element
|
||||||
mount: blockMount,
|
) {
|
||||||
patch,
|
return compile(template, {
|
||||||
remove,
|
name,
|
||||||
// bdom block types
|
dev: this.dev,
|
||||||
list,
|
translateFn: this.translateFn,
|
||||||
multi,
|
translatableAttributes: this.translatableAttributes,
|
||||||
text,
|
});
|
||||||
toggler,
|
|
||||||
createBlock,
|
|
||||||
html,
|
|
||||||
comment,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { App, mount } from "./app/app";
|
|
||||||
export { Component } from "./component/component";
|
|
||||||
export { useComponent } from "./component/component_node";
|
|
||||||
export { status } from "./component/status";
|
|
||||||
export { Memo } from "./memo";
|
|
||||||
export { xml } from "./app/template_set";
|
|
||||||
export { useState, reactive, markRaw, toRaw } from "./reactivity";
|
|
||||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
|
||||||
export { EventBus, whenReady, loadFile, markup } from "./utils";
|
|
||||||
export {
|
|
||||||
onWillStart,
|
|
||||||
onMounted,
|
|
||||||
onWillUnmount,
|
|
||||||
onWillUpdateProps,
|
|
||||||
onWillPatch,
|
|
||||||
onPatched,
|
|
||||||
onWillRender,
|
|
||||||
onRendered,
|
|
||||||
onWillDestroy,
|
|
||||||
onError,
|
|
||||||
} from "./component/lifecycle_hooks";
|
|
||||||
|
|
||||||
export const __info__ = {};
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { Component } from "./component/component";
|
|
||||||
import type { ComponentNode } from "./component/component_node";
|
|
||||||
import { xml } from "./app/template_set";
|
|
||||||
import { Fiber } from "./component/fibers";
|
|
||||||
|
|
||||||
export class Memo extends Component {
|
|
||||||
static template = xml`<t t-slot="default"/>`;
|
|
||||||
|
|
||||||
constructor(props: any, env: any, node: ComponentNode) {
|
|
||||||
super(props, env, node);
|
|
||||||
|
|
||||||
// prevent patching process conditionally
|
|
||||||
let applyPatch = false;
|
|
||||||
const patchFn = node.patch;
|
|
||||||
node.patch = () => {
|
|
||||||
if (applyPatch) {
|
|
||||||
patchFn.call(node);
|
|
||||||
applyPatch = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// check props change, and render/apply patch if it changed
|
|
||||||
let prevProps = props;
|
|
||||||
const updateAndRender = node.updateAndRender;
|
|
||||||
node.updateAndRender = function (props: any, parentFiber: Fiber) {
|
|
||||||
const shouldUpdate = !shallowEqual(prevProps, props);
|
|
||||||
if (shouldUpdate) {
|
|
||||||
prevProps = props;
|
|
||||||
updateAndRender.call(node, props, parentFiber);
|
|
||||||
applyPatch = true;
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* we assume that each object have the same set of keys
|
|
||||||
*/
|
|
||||||
function shallowEqual(p1: any, p2: any): boolean {
|
|
||||||
for (let k in p1) {
|
|
||||||
if (k !== "slots" && p1[k] !== p2[k]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { onWillUnmount } from "./component/lifecycle_hooks";
|
|
||||||
import { xml } from "./app/template_set";
|
|
||||||
import { BDom, text, VNode } from "./blockdom";
|
|
||||||
import { Component } from "./component/component";
|
|
||||||
|
|
||||||
const VText: any = text("").constructor;
|
|
||||||
|
|
||||||
class VPortal extends VText implements Partial<VNode<VPortal>> {
|
|
||||||
// selector: string;
|
|
||||||
realBDom: BDom | null;
|
|
||||||
target: HTMLElement | null = null;
|
|
||||||
|
|
||||||
constructor(selector: string, realBDom: BDom) {
|
|
||||||
super("");
|
|
||||||
this.selector = selector;
|
|
||||||
this.realBDom = realBDom;
|
|
||||||
}
|
|
||||||
mount(parent: HTMLElement, anchor: ChildNode) {
|
|
||||||
super.mount(parent, anchor);
|
|
||||||
this.target = document.querySelector(this.selector) as any;
|
|
||||||
if (!this.target) {
|
|
||||||
let el: any = this.el;
|
|
||||||
while (el && el.parentElement instanceof HTMLElement) {
|
|
||||||
el = el.parentElement;
|
|
||||||
}
|
|
||||||
this.target = el && el.querySelector(this.selector);
|
|
||||||
if (!this.target) {
|
|
||||||
throw new Error("invalid portal target");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.realBDom!.mount(this.target!, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeRemove() {
|
|
||||||
this.realBDom!.beforeRemove();
|
|
||||||
}
|
|
||||||
remove() {
|
|
||||||
if (this.realBDom) {
|
|
||||||
super.remove();
|
|
||||||
this.realBDom!.remove();
|
|
||||||
this.realBDom = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patch(other: VPortal) {
|
|
||||||
super.patch(other);
|
|
||||||
if (this.realBDom) {
|
|
||||||
this.realBDom.patch(other.realBDom!, true);
|
|
||||||
} else {
|
|
||||||
this.realBDom = other.realBDom;
|
|
||||||
this.realBDom!.mount(this.target!, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Portal extends Component {
|
|
||||||
static template = xml`<t t-slot="default"/>`;
|
|
||||||
static props = {
|
|
||||||
target: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
slots: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const node = this.__owl__;
|
|
||||||
const renderFn = node.renderFn;
|
|
||||||
node.renderFn = () => new VPortal(this.props.target, renderFn());
|
|
||||||
onWillUnmount(() => {
|
|
||||||
if (node.bdom) {
|
|
||||||
node.bdom.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
import { onWillDestroy } from "./component/lifecycle_hooks";
|
|
||||||
import { ComponentNode, getCurrent } from "./component/component_node";
|
|
||||||
import { batched, Callback } from "./utils";
|
|
||||||
|
|
||||||
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
|
|
||||||
const TARGET = Symbol("Target");
|
|
||||||
// Escape hatch to prevent reactivity system to turn something into a reactive
|
|
||||||
const SKIP = Symbol("Skip");
|
|
||||||
// Special key to subscribe to, to be notified of key creation/deletion
|
|
||||||
const KEYCHANGES = Symbol("Key changes");
|
|
||||||
|
|
||||||
type ObjectKey = string | number | symbol;
|
|
||||||
|
|
||||||
type Target = object;
|
|
||||||
|
|
||||||
export type Reactive<T extends Target = Target> = T & {
|
|
||||||
[TARGET]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NonReactive<T extends Target = Target> = T & {
|
|
||||||
[SKIP]: any;
|
|
||||||
};
|
|
||||||
const objectToString = Object.prototype.toString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given value can be made into a reactive object.
|
|
||||||
*
|
|
||||||
* @param value the value to check
|
|
||||||
* @returns whether the value can be made reactive
|
|
||||||
*/
|
|
||||||
function canBeMadeReactive(value: any): boolean {
|
|
||||||
if (typeof value !== "object") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// extract "RawType" from strings like "[object RawType]" => this lets us
|
|
||||||
// ignore many native objects such as Promise (whose toString is [object Promise])
|
|
||||||
// or Date ([object Date]).
|
|
||||||
const rawType = objectToString.call(value).slice(8, -1);
|
|
||||||
return rawType === "Object" || rawType === "Array";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark an object or array so that it is ignored by the reactivity system
|
|
||||||
*
|
|
||||||
* @param value the value to mark
|
|
||||||
* @returns the object itself
|
|
||||||
*/
|
|
||||||
export function markRaw<T extends Target>(value: T): NonReactive<T> {
|
|
||||||
(value as any)[SKIP] = true;
|
|
||||||
return value as NonReactive<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a reactive objet, return the raw (non reactive) underlying object
|
|
||||||
*
|
|
||||||
* @param value a reactive value
|
|
||||||
* @returns the underlying value
|
|
||||||
*/
|
|
||||||
export function toRaw<T extends object>(value: Reactive<T>): T {
|
|
||||||
return value[TARGET];
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
|
|
||||||
/**
|
|
||||||
* Observes a given key on a target with an callback. The callback will be
|
|
||||||
* called when the given key changes on the target.
|
|
||||||
*
|
|
||||||
* @param target the target whose key should be observed
|
|
||||||
* @param key the key to observe (or Symbol(KEYCHANGES) for key creation
|
|
||||||
* or deletion)
|
|
||||||
* @param callback the function to call when the key changes
|
|
||||||
*/
|
|
||||||
function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): void {
|
|
||||||
if (!targetToKeysToCallbacks.get(target)) {
|
|
||||||
targetToKeysToCallbacks.set(target, new Map());
|
|
||||||
}
|
|
||||||
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
|
|
||||||
if (!keyToCallbacks.get(key)) {
|
|
||||||
keyToCallbacks.set(key, new Set());
|
|
||||||
}
|
|
||||||
keyToCallbacks.get(key)!.add(callback);
|
|
||||||
if (!callbacksToTargets.has(callback)) {
|
|
||||||
callbacksToTargets.set(callback, new Set());
|
|
||||||
}
|
|
||||||
callbacksToTargets.get(callback)!.add(target);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Notify Reactives that are observing a given target that a key has changed on
|
|
||||||
* the target.
|
|
||||||
*
|
|
||||||
* @param target target whose Reactives should be notified that the target was
|
|
||||||
* changed.
|
|
||||||
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
|
||||||
* or deleted)
|
|
||||||
*/
|
|
||||||
function notifyReactives(target: Target, key: ObjectKey): void {
|
|
||||||
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
|
||||||
if (!keyToCallbacks) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const callbacks = keyToCallbacks.get(key);
|
|
||||||
if (!callbacks) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Loop on copy because clearReactivesForCallback will modify the set in place
|
|
||||||
for (const callback of [...callbacks]) {
|
|
||||||
clearReactivesForCallback(callback);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
|
|
||||||
/**
|
|
||||||
* Clears all subscriptions of the Reactives associated with a given callback.
|
|
||||||
*
|
|
||||||
* @param callback the callback for which the reactives need to be cleared
|
|
||||||
*/
|
|
||||||
function clearReactivesForCallback(callback: Callback): void {
|
|
||||||
const targetsToClear = callbacksToTargets.get(callback);
|
|
||||||
if (!targetsToClear) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const target of targetsToClear) {
|
|
||||||
const observedKeys = targetToKeysToCallbacks.get(target);
|
|
||||||
if (!observedKeys) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const callbacks of observedKeys.values()) {
|
|
||||||
callbacks.delete(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
targetsToClear.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive>>();
|
|
||||||
/**
|
|
||||||
* Creates a reactive proxy for an object. Reading data on the reactive object
|
|
||||||
* subscribes to changes to the data. Writing data on the object will cause the
|
|
||||||
* notify callback to be called if there are suscriptions to that data. Nested
|
|
||||||
* objects and arrays are automatically made reactive as well.
|
|
||||||
*
|
|
||||||
* Whenever you are notified of a change, all subscriptions are cleared, and if
|
|
||||||
* you would like to be notified of any further changes, you should go read
|
|
||||||
* the underlying data again. We assume that if you don't go read it again after
|
|
||||||
* being notified, it means that you are no longer interested in that data.
|
|
||||||
*
|
|
||||||
* Subscriptions:
|
|
||||||
* + Reading a property on an object will subscribe you to changes in the value
|
|
||||||
* of that property.
|
|
||||||
* + Accessing an object keys (eg with Object.keys or with `for..in`) will
|
|
||||||
* subscribe you to the creation/deletion of keys. Checking the presence of a
|
|
||||||
* key on the object with 'in' has the same effect.
|
|
||||||
* - getOwnPropertyDescriptor does not currently subscribe you to the property.
|
|
||||||
* This is a choice that was made because changing a key's value will trigger
|
|
||||||
* this trap and we do not want to subscribe by writes. This also means that
|
|
||||||
* Object.hasOwnProperty doesn't subscribe as it goes through this trap.
|
|
||||||
*
|
|
||||||
* @param target the object for which to create a reactive proxy
|
|
||||||
* @param callback the function to call when an observed property of the
|
|
||||||
* reactive has changed
|
|
||||||
* @returns a proxy that tracks changes to it
|
|
||||||
*/
|
|
||||||
export function reactive<T extends Target>(
|
|
||||||
target: T,
|
|
||||||
callback: Callback = () => {}
|
|
||||||
): Reactive<T> | NonReactive<T> {
|
|
||||||
if (!canBeMadeReactive(target)) {
|
|
||||||
throw new Error(`Cannot make the given value reactive`);
|
|
||||||
}
|
|
||||||
if (SKIP in target) {
|
|
||||||
return target as NonReactive<T>;
|
|
||||||
}
|
|
||||||
const originalTarget = (target as Reactive)[TARGET];
|
|
||||||
if (originalTarget) {
|
|
||||||
return reactive(originalTarget, callback);
|
|
||||||
}
|
|
||||||
if (!reactiveCache.has(target)) {
|
|
||||||
reactiveCache.set(target, new Map());
|
|
||||||
}
|
|
||||||
const reactivesForTarget = reactiveCache.get(target)!;
|
|
||||||
if (!reactivesForTarget.has(callback)) {
|
|
||||||
const proxy = new Proxy(target, {
|
|
||||||
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
|
|
||||||
if (key === TARGET) {
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
observeTargetKey(target, key, callback);
|
|
||||||
const value = Reflect.get(target, key, proxy);
|
|
||||||
if (!canBeMadeReactive(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return reactive(value, callback);
|
|
||||||
},
|
|
||||||
set(target, key, value, proxy) {
|
|
||||||
const isNewKey = !Object.hasOwnProperty.call(target, key);
|
|
||||||
const originalValue = Reflect.get(target, key, proxy);
|
|
||||||
const ret = Reflect.set(target, key, value, proxy);
|
|
||||||
if (isNewKey) {
|
|
||||||
notifyReactives(target, KEYCHANGES);
|
|
||||||
}
|
|
||||||
// While Array length may trigger the set trap, it's not actually set by this
|
|
||||||
// method but is updated behind the scenes, and the trap is not called with the
|
|
||||||
// new value. We disable the "same-value-optimization" for it because of that.
|
|
||||||
if (originalValue !== value || (Array.isArray(target) && key === "length")) {
|
|
||||||
notifyReactives(target, key);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
deleteProperty(target, key) {
|
|
||||||
const ret = Reflect.deleteProperty(target, key);
|
|
||||||
notifyReactives(target, KEYCHANGES);
|
|
||||||
notifyReactives(target, key);
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
ownKeys(target) {
|
|
||||||
observeTargetKey(target, KEYCHANGES, callback);
|
|
||||||
return Reflect.ownKeys(target);
|
|
||||||
},
|
|
||||||
has(target, key) {
|
|
||||||
// TODO: this observes all key changes instead of only the presence of the argument key
|
|
||||||
observeTargetKey(target, KEYCHANGES, callback);
|
|
||||||
return Reflect.has(target, key);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
reactivesForTarget.set(callback, proxy);
|
|
||||||
}
|
|
||||||
return reactivesForTarget.get(callback) as Reactive<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
|
||||||
/**
|
|
||||||
* Creates a reactive object that will be observed by the current component.
|
|
||||||
* Reading data from the returned object (eg during rendering) will cause the
|
|
||||||
* component to subscribe to that data and be rerendered when it changes.
|
|
||||||
*
|
|
||||||
* @param state the state to observe
|
|
||||||
* @returns a reactive object that will cause the component to re-render on
|
|
||||||
* relevant changes
|
|
||||||
* @see reactive
|
|
||||||
*/
|
|
||||||
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
|
|
||||||
const node = getCurrent();
|
|
||||||
if (!batchedRenderFunctions.has(node)) {
|
|
||||||
batchedRenderFunctions.set(
|
|
||||||
node,
|
|
||||||
batched(() => node.render())
|
|
||||||
);
|
|
||||||
onWillDestroy(() => clearReactivesForCallback(render));
|
|
||||||
}
|
|
||||||
const render = batchedRenderFunctions.get(node)!;
|
|
||||||
const reactiveState = reactive(state, render);
|
|
||||||
return reactiveState;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { version } from "../version";
|
||||||
|
import { Component, ComponentConstructor, Props } from "./component";
|
||||||
|
import { ComponentNode } from "./component_node";
|
||||||
|
import { nodeErrorHandlers, OwlError, handleError } from "./error_handling";
|
||||||
|
import { Fiber, RootFiber, MountOptions } from "./fibers";
|
||||||
|
import { Scheduler } from "./scheduler";
|
||||||
|
import { validateProps } from "./template_helpers";
|
||||||
|
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
||||||
|
import { validateTarget } from "./utils";
|
||||||
|
|
||||||
|
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||||
|
|
||||||
|
export interface Env {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppConfig<P, E> extends TemplateSetConfig {
|
||||||
|
name?: string;
|
||||||
|
props?: P;
|
||||||
|
env?: E;
|
||||||
|
test?: boolean;
|
||||||
|
warnIfNoStaticProps?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasBeenLogged = false;
|
||||||
|
|
||||||
|
export const DEV_MSG = () => {
|
||||||
|
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
||||||
|
|
||||||
|
return `Owl is running in 'dev' mode.
|
||||||
|
|
||||||
|
This is not suitable for production use.
|
||||||
|
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__OWL_DEVTOOLS__: {
|
||||||
|
apps: Set<App>;
|
||||||
|
Fiber: typeof Fiber;
|
||||||
|
RootFiber: typeof RootFiber;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__OWL_DEVTOOLS__ ||= {
|
||||||
|
apps: new Set<App>(),
|
||||||
|
Fiber: Fiber,
|
||||||
|
RootFiber: RootFiber,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class App<
|
||||||
|
T extends abstract new (...args: any) => any = any,
|
||||||
|
P extends object = any,
|
||||||
|
E = any
|
||||||
|
> extends TemplateSet {
|
||||||
|
static validateTarget = validateTarget;
|
||||||
|
static version = version;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
Root: ComponentConstructor<P, E>;
|
||||||
|
props: P;
|
||||||
|
env: E;
|
||||||
|
scheduler = new Scheduler();
|
||||||
|
root: ComponentNode<P, E> | null = null;
|
||||||
|
warnIfNoStaticProps: boolean;
|
||||||
|
|
||||||
|
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
|
||||||
|
super(config);
|
||||||
|
this.name = config.name || "";
|
||||||
|
this.Root = Root;
|
||||||
|
window.__OWL_DEVTOOLS__.apps.add(this);
|
||||||
|
if (config.test) {
|
||||||
|
this.dev = true;
|
||||||
|
}
|
||||||
|
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
||||||
|
if (this.dev && !config.test && !hasBeenLogged) {
|
||||||
|
console.info(DEV_MSG());
|
||||||
|
hasBeenLogged = true;
|
||||||
|
}
|
||||||
|
const env = config.env || {};
|
||||||
|
const descrs = Object.getOwnPropertyDescriptors(env);
|
||||||
|
this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
|
||||||
|
this.props = config.props || ({} as P);
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(
|
||||||
|
target: HTMLElement | ShadowRoot,
|
||||||
|
options?: MountOptions
|
||||||
|
): Promise<Component<P, E> & InstanceType<T>> {
|
||||||
|
App.validateTarget(target);
|
||||||
|
if (this.dev) {
|
||||||
|
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
||||||
|
}
|
||||||
|
const node = this.makeNode(this.Root, this.props);
|
||||||
|
const prom = this.mountNode(node, target, options);
|
||||||
|
this.root = node;
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
||||||
|
return new ComponentNode(Component, props, this, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
mountNode(node: ComponentNode, target: HTMLElement | ShadowRoot, options?: MountOptions) {
|
||||||
|
const promise: any = new Promise((resolve, reject) => {
|
||||||
|
let isResolved = false;
|
||||||
|
// manually set a onMounted callback.
|
||||||
|
// that way, we are independant from the current node.
|
||||||
|
node.mounted.push(() => {
|
||||||
|
resolve(node.component);
|
||||||
|
isResolved = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manually add the last resort error handler on the node
|
||||||
|
let handlers = nodeErrorHandlers.get(node);
|
||||||
|
if (!handlers) {
|
||||||
|
handlers = [];
|
||||||
|
nodeErrorHandlers.set(node, handlers);
|
||||||
|
}
|
||||||
|
handlers.unshift((e) => {
|
||||||
|
if (!isResolved) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
node.mountComponent(target, options);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.root) {
|
||||||
|
this.scheduler.flush();
|
||||||
|
this.root.destroy();
|
||||||
|
}
|
||||||
|
window.__OWL_DEVTOOLS__.apps.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
createComponent<P extends Props>(
|
||||||
|
name: string | null,
|
||||||
|
isStatic: boolean,
|
||||||
|
hasSlotsProp: boolean,
|
||||||
|
hasDynamicPropList: boolean,
|
||||||
|
propList: string[]
|
||||||
|
) {
|
||||||
|
const isDynamic = !isStatic;
|
||||||
|
let arePropsDifferent: (p1: Object, p2: Object) => boolean;
|
||||||
|
const hasNoProp = propList.length === 0;
|
||||||
|
if (hasSlotsProp) {
|
||||||
|
arePropsDifferent = (_1, _2) => true;
|
||||||
|
} else if (hasDynamicPropList) {
|
||||||
|
arePropsDifferent = function (props1: Props, props2: Props) {
|
||||||
|
for (let k in props1) {
|
||||||
|
if (props1[k] !== props2[k]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.keys(props1).length !== Object.keys(props2).length;
|
||||||
|
};
|
||||||
|
} else if (hasNoProp) {
|
||||||
|
arePropsDifferent = (_1: any, _2: any) => false;
|
||||||
|
} else {
|
||||||
|
arePropsDifferent = function (props1: Props, props2: Props) {
|
||||||
|
for (let p of propList) {
|
||||||
|
if (props1[p] !== props2[p]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAndRender = ComponentNode.prototype.updateAndRender;
|
||||||
|
const initiateRender = ComponentNode.prototype.initiateRender;
|
||||||
|
|
||||||
|
return (props: P, key: string, ctx: ComponentNode, parent: any, C: any) => {
|
||||||
|
let children = ctx.children;
|
||||||
|
let node: any = children[key];
|
||||||
|
if (isDynamic && node && node.component.constructor !== C) {
|
||||||
|
node = undefined;
|
||||||
|
}
|
||||||
|
const parentFiber = ctx.fiber!;
|
||||||
|
if (node) {
|
||||||
|
if (arePropsDifferent(node.props, props) || parentFiber.deep || node.forceNextRender) {
|
||||||
|
node.forceNextRender = false;
|
||||||
|
updateAndRender.call(node, props, parentFiber);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// new component
|
||||||
|
if (isStatic) {
|
||||||
|
const components = parent.constructor.components;
|
||||||
|
if (!components) {
|
||||||
|
throw new OwlError(
|
||||||
|
`Cannot find the definition of component "${name}", missing static components key in parent`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
C = components[name as any];
|
||||||
|
if (!C) {
|
||||||
|
throw new OwlError(`Cannot find the definition of component "${name}"`);
|
||||||
|
} else if (!(C.prototype instanceof Component)) {
|
||||||
|
throw new OwlError(
|
||||||
|
`"${name}" is not a Component. It must inherit from the Component class`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = new ComponentNode(C, props, this, ctx, key);
|
||||||
|
children[key] = node;
|
||||||
|
initiateRender.call(node, new Fiber(node, parentFiber));
|
||||||
|
}
|
||||||
|
parentFiber.childrenMap[key] = node;
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(...args: Parameters<typeof handleError>) {
|
||||||
|
return handleError(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function mount<
|
||||||
|
T extends abstract new (...args: any) => any = any,
|
||||||
|
P extends object = any,
|
||||||
|
E = any
|
||||||
|
>(
|
||||||
|
C: T & ComponentConstructor<P, E>,
|
||||||
|
target: HTMLElement,
|
||||||
|
config: AppConfig<P, E> & MountOptions = {}
|
||||||
|
): Promise<Component<P, E> & InstanceType<T>> {
|
||||||
|
return new App(C, config).mount(target, config);
|
||||||
|
}
|
||||||
@@ -93,6 +93,10 @@ function toClassObj(expr: string | number | { [c: string]: any }) {
|
|||||||
for (let key in expr as any) {
|
for (let key in expr as any) {
|
||||||
const value = (expr as any)[key];
|
const value = (expr as any)[key];
|
||||||
if (value) {
|
if (value) {
|
||||||
|
key = trim.call(key);
|
||||||
|
if (!key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const words = split.call(key, wordRegexp);
|
const words = split.call(key, wordRegexp);
|
||||||
for (let word of words) {
|
for (let word of words) {
|
||||||
result[word] = value;
|
result[word] = value;
|
||||||
@@ -136,32 +140,3 @@ export function updateClass(this: HTMLElement, val: any, oldVal: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makePropSetter(name: string): Setter<HTMLElement> {
|
|
||||||
return function setProp(this: HTMLElement, value: any) {
|
|
||||||
(this as any)[name] = value;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isProp(tag: string, key: string): boolean {
|
|
||||||
switch (tag) {
|
|
||||||
case "input":
|
|
||||||
return (
|
|
||||||
key === "checked" ||
|
|
||||||
key === "indeterminate" ||
|
|
||||||
key === "value" ||
|
|
||||||
key === "readonly" ||
|
|
||||||
key === "disabled"
|
|
||||||
);
|
|
||||||
case "option":
|
|
||||||
return key === "selected" || key === "disabled";
|
|
||||||
case "textarea":
|
|
||||||
return key === "value" || key === "readonly" || key === "disabled";
|
|
||||||
case "select":
|
|
||||||
return key === "value" || key === "disabled";
|
|
||||||
case "button":
|
|
||||||
case "optgroup":
|
|
||||||
return key === "disabled";
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
import {
|
import { OwlError } from "../error_handling";
|
||||||
attrsSetter,
|
import { attrsSetter, attrsUpdater, createAttrUpdater, setClass, updateClass } from "./attributes";
|
||||||
attrsUpdater,
|
|
||||||
createAttrUpdater,
|
|
||||||
isProp,
|
|
||||||
makePropSetter,
|
|
||||||
setClass,
|
|
||||||
updateClass,
|
|
||||||
} from "./attributes";
|
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
import { createEventHandler } from "./events";
|
import { createEventHandler } from "./events";
|
||||||
import type { VNode } from "./index";
|
import type { VNode } from "./index";
|
||||||
@@ -24,6 +17,13 @@ const nodeGetNextSibling = getDescriptor(nodeProto, "nextSibling").get!;
|
|||||||
|
|
||||||
const NO_OP = () => {};
|
const NO_OP = () => {};
|
||||||
|
|
||||||
|
function makePropSetter(name: string): Setter<HTMLElement> {
|
||||||
|
return function setProp(this: HTMLElement, value: any) {
|
||||||
|
// support 0, fallback to empty string for other falsy values
|
||||||
|
(this as any)[name] = value === 0 ? 0 : value ? value.valueOf() : "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Main compiler code
|
// Main compiler code
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -100,7 +100,7 @@ function normalizeNode(node: HTMLElement | Text) {
|
|||||||
interface DynamicInfo {
|
interface DynamicInfo {
|
||||||
idx: number;
|
idx: number;
|
||||||
refIdx?: number;
|
refIdx?: number;
|
||||||
type: "text" | "child" | "handler" | "attribute" | "attributes" | "ref";
|
type: "text" | "child" | "handler" | "attribute" | "attributes" | "property" | "ref";
|
||||||
isOnlyChild?: boolean;
|
isOnlyChild?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
@@ -156,6 +156,15 @@ function buildTree(
|
|||||||
: document.createElement(tagName);
|
: document.createElement(tagName);
|
||||||
}
|
}
|
||||||
if (el instanceof Element) {
|
if (el instanceof Element) {
|
||||||
|
if (!domParentTree) {
|
||||||
|
// some html elements may have side effects when setting their attributes.
|
||||||
|
// For example, setting the src attribute of an <img/> will trigger a
|
||||||
|
// request to get the corresponding image. This is something that we
|
||||||
|
// don't want at compile time. We avoid that by putting the content of
|
||||||
|
// the block in a <template/> element
|
||||||
|
const fragment = document.createElement("template").content;
|
||||||
|
fragment.appendChild(el);
|
||||||
|
}
|
||||||
for (let i = 0; i < attrs.length; i++) {
|
for (let i = 0; i < attrs.length; i++) {
|
||||||
const attrName = attrs[i].name;
|
const attrName = attrs[i].name;
|
||||||
const attrValue = attrs[i].value;
|
const attrValue = attrs[i].value;
|
||||||
@@ -174,6 +183,14 @@ function buildTree(
|
|||||||
name: attrValue,
|
name: attrValue,
|
||||||
tag: tagName,
|
tag: tagName,
|
||||||
});
|
});
|
||||||
|
} else if (attrName.startsWith("block-property-")) {
|
||||||
|
const idx = parseInt(attrName.slice(15), 10);
|
||||||
|
info.push({
|
||||||
|
type: "property",
|
||||||
|
idx,
|
||||||
|
name: attrValue,
|
||||||
|
tag: tagName,
|
||||||
|
});
|
||||||
} else if (attrName === "block-attributes") {
|
} else if (attrName === "block-attributes") {
|
||||||
info.push({
|
info.push({
|
||||||
type: "attributes",
|
type: "attributes",
|
||||||
@@ -245,7 +262,7 @@ function buildTree(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error("boom");
|
throw new OwlError("boom");
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRef(tree: IntermediateTree) {
|
function addRef(tree: IntermediateTree) {
|
||||||
@@ -367,15 +384,22 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "property": {
|
||||||
|
const refIdx = info.refIdx!;
|
||||||
|
const setProp = makePropSetter(info.name!);
|
||||||
|
ctx.locations.push({
|
||||||
|
idx: info.idx,
|
||||||
|
refIdx,
|
||||||
|
setData: setProp,
|
||||||
|
updateData: setProp,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "attribute": {
|
case "attribute": {
|
||||||
const refIdx = info.refIdx!;
|
const refIdx = info.refIdx!;
|
||||||
let updater: any;
|
let updater: any;
|
||||||
let setter: any;
|
let setter: any;
|
||||||
if (isProp(info.tag!, info.name!)) {
|
if (info.name === "class") {
|
||||||
const setProp = makePropSetter(info.name!);
|
|
||||||
setter = setProp;
|
|
||||||
updater = setProp;
|
|
||||||
} else if (info.name === "class") {
|
|
||||||
setter = setClass;
|
setter = setClass;
|
||||||
updater = updateClass;
|
updater = updateClass;
|
||||||
} else {
|
} else {
|
||||||
@@ -507,9 +531,13 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
|
|||||||
return this.el!;
|
return this.el!;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveBefore(other: Block | null, afterNode: Node | null) {
|
moveBeforeDOMNode(node: Node | null, parent = this.parentEl) {
|
||||||
const target = other ? other.el! : afterNode;
|
this.parentEl = parent;
|
||||||
nodeInsertBefore.call(this.parentEl, this.el!, target);
|
nodeInsertBefore.call(parent, this.el!, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBeforeVNode(other: Block | null, afterNode: Node | null) {
|
||||||
|
nodeInsertBefore.call(this.parentEl, this.el!, other ? other.el! : afterNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { createEventHandler } from "./events";
|
||||||
|
import type { VNode } from "./index";
|
||||||
|
|
||||||
|
type EventsSpec = { [name: string]: number };
|
||||||
|
|
||||||
|
type Catcher = (child: VNode, handlers: any[]) => VNode;
|
||||||
|
|
||||||
|
export function createCatcher(eventsSpec: EventsSpec): Catcher {
|
||||||
|
const n = Object.keys(eventsSpec).length;
|
||||||
|
|
||||||
|
class VCatcher {
|
||||||
|
child: VNode;
|
||||||
|
handlerData: any[];
|
||||||
|
handlerFns: any[] = [];
|
||||||
|
|
||||||
|
parentEl?: HTMLElement | undefined;
|
||||||
|
afterNode: Text | null = null;
|
||||||
|
|
||||||
|
constructor(child: VNode, handlers: any[]) {
|
||||||
|
this.child = child;
|
||||||
|
this.handlerData = handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||||
|
this.parentEl = parent;
|
||||||
|
this.child.mount(parent, afterNode);
|
||||||
|
this.afterNode = document.createTextNode("");
|
||||||
|
parent.insertBefore(this.afterNode, afterNode);
|
||||||
|
this.wrapHandlerData();
|
||||||
|
for (let name in eventsSpec) {
|
||||||
|
const index = eventsSpec[name];
|
||||||
|
const handler = createEventHandler(name);
|
||||||
|
this.handlerFns[index] = handler;
|
||||||
|
handler.setup.call(parent, this.handlerData[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapHandlerData() {
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
let handler = this.handlerData[i];
|
||||||
|
// handler = [...mods, fn, comp], so we need to replace second to last elem
|
||||||
|
let idx = handler.length - 2;
|
||||||
|
let origFn = handler[idx];
|
||||||
|
const self = this;
|
||||||
|
handler[idx] = function (ev: any) {
|
||||||
|
const target = ev.target;
|
||||||
|
let currentNode: any = self.child.firstNode();
|
||||||
|
const afterNode = self.afterNode;
|
||||||
|
while (currentNode && currentNode !== afterNode) {
|
||||||
|
if (currentNode.contains(target)) {
|
||||||
|
return origFn.call(this, ev);
|
||||||
|
}
|
||||||
|
currentNode = currentNode.nextSibling;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBeforeDOMNode(node: Node | null, parent = this.parentEl) {
|
||||||
|
this.parentEl = parent;
|
||||||
|
this.child.moveBeforeDOMNode(node, parent);
|
||||||
|
parent!.insertBefore(this.afterNode!, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBeforeVNode(other: VCatcher | null, afterNode: Node | null) {
|
||||||
|
if (other) {
|
||||||
|
// check this with @ged-odoo for use in foreach
|
||||||
|
afterNode = other.firstNode() || afterNode;
|
||||||
|
}
|
||||||
|
this.child.moveBeforeVNode(other ? other.child : null, afterNode);
|
||||||
|
this.parentEl!.insertBefore(this.afterNode!, afterNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(other: VCatcher, withBeforeRemove: boolean) {
|
||||||
|
if (this === other) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handlerData = other.handlerData;
|
||||||
|
this.wrapHandlerData();
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
this.handlerFns[i].update.call(this.parentEl!, this.handlerData[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.child.patch(other.child, withBeforeRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeRemove() {
|
||||||
|
this.child.beforeRemove();
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
this.handlerFns[i].remove.call(this.parentEl!);
|
||||||
|
}
|
||||||
|
this.child.remove();
|
||||||
|
this.afterNode!.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
firstNode(): Node | undefined {
|
||||||
|
return this.child.firstNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.child.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return function (child: VNode, handlers: any[]): VNode<VCatcher> {
|
||||||
|
return new VCatcher(child, handlers);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { inOwnerDocument } from "../utils";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
|
|
||||||
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
||||||
@@ -5,6 +6,7 @@ type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
|||||||
interface EventHandlerCreator {
|
interface EventHandlerCreator {
|
||||||
setup: EventHandlerSetter;
|
setup: EventHandlerSetter;
|
||||||
update: EventHandlerSetter;
|
update: EventHandlerSetter;
|
||||||
|
remove: (this: HTMLElement) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEventHandler(rawEvent: string): EventHandlerCreator {
|
export function createEventHandler(rawEvent: string): EventHandlerCreator {
|
||||||
@@ -26,8 +28,8 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listener(ev: Event) {
|
function listener(ev: Event) {
|
||||||
const currentTarget = ev.currentTarget;
|
const currentTarget = ev.currentTarget as HTMLElement;
|
||||||
if (!currentTarget || !document.contains(currentTarget as HTMLElement)) return;
|
if (!currentTarget || !inOwnerDocument(currentTarget)) return;
|
||||||
const data = (currentTarget as any)[eventKey];
|
const data = (currentTarget as any)[eventKey];
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
config.mainEventHandler(data, ev, currentTarget);
|
config.mainEventHandler(data, ev, currentTarget);
|
||||||
@@ -38,11 +40,15 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
|
|||||||
this.addEventListener(evName, listener, { capture });
|
this.addEventListener(evName, listener, { capture });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function remove(this: HTMLElement) {
|
||||||
|
delete (this as any)[eventKey];
|
||||||
|
this.removeEventListener(evName, listener, { capture });
|
||||||
|
}
|
||||||
function update(this: HTMLElement, data: any) {
|
function update(this: HTMLElement, data: any) {
|
||||||
(this as any)[eventKey] = data;
|
(this as any)[eventKey] = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { setup, update };
|
return { setup, update, remove };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synthetic handler: a form of event delegation that allows placing only one
|
// Synthetic handler: a form of event delegation that allows placing only one
|
||||||
@@ -60,7 +66,12 @@ function createSyntheticHandler(evName: string, capture: boolean = false): Event
|
|||||||
_data[currentId] = data;
|
_data[currentId] = data;
|
||||||
(this as any)[eventKey] = _data;
|
(this as any)[eventKey] = _data;
|
||||||
}
|
}
|
||||||
return { setup, update: setup };
|
|
||||||
|
function remove(this: HTMLElement) {
|
||||||
|
delete (this as any)[eventKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { setup, update: setup, remove };
|
||||||
}
|
}
|
||||||
|
|
||||||
function nativeToSyntheticEvent(eventKey: string, event: Event) {
|
function nativeToSyntheticEvent(eventKey: string, event: Event) {
|
||||||
@@ -29,14 +29,18 @@ class VHtml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveBefore(other: VHtml | null, afterNode: Node | null) {
|
moveBeforeDOMNode(node: Node | null, parent = this.parentEl) {
|
||||||
const target = other ? other.content[0] : afterNode;
|
this.parentEl = parent;
|
||||||
const parent = this.parentEl;
|
|
||||||
for (let elem of this.content) {
|
for (let elem of this.content) {
|
||||||
nodeInsertBefore.call(parent, elem, target);
|
nodeInsertBefore.call(parent, elem, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveBeforeVNode(other: VHtml | null, afterNode: Node | null) {
|
||||||
|
const target = other ? other.content[0] : afterNode;
|
||||||
|
this.moveBeforeDOMNode(target);
|
||||||
|
}
|
||||||
|
|
||||||
patch(other: VHtml) {
|
patch(other: VHtml) {
|
||||||
if (this === other) {
|
if (this === other) {
|
||||||
return;
|
return;
|
||||||
@@ -6,10 +6,12 @@ export { list } from "./list";
|
|||||||
export { multi } from "./multi";
|
export { multi } from "./multi";
|
||||||
export { text, comment } from "./text";
|
export { text, comment } from "./text";
|
||||||
export { html } from "./html";
|
export { html } from "./html";
|
||||||
|
export { createCatcher } from "./event_catcher";
|
||||||
|
|
||||||
export interface VNode<T = any> {
|
export interface VNode<T = any> {
|
||||||
mount(parent: HTMLElement, afterNode: Node | null): void;
|
mount(parent: HTMLElement, afterNode: Node | null): void;
|
||||||
moveBefore(other: T | null, afterNode: Node | null): void;
|
moveBeforeDOMNode(node: Node | null, parent?: HTMLElement): void;
|
||||||
|
moveBeforeVNode(other: T | null, afterNode: Node | null): void;
|
||||||
patch(other: T, withBeforeRemove: boolean): void;
|
patch(other: T, withBeforeRemove: boolean): void;
|
||||||
beforeRemove(): void;
|
beforeRemove(): void;
|
||||||
remove(): void;
|
remove(): void;
|
||||||
@@ -38,14 +38,23 @@ class VList {
|
|||||||
this.parentEl = parent;
|
this.parentEl = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveBefore(other: VList | null, afterNode: Node | null) {
|
moveBeforeDOMNode(node: Node | null, parent = this.parentEl) {
|
||||||
|
this.parentEl = parent;
|
||||||
|
const children = this.children;
|
||||||
|
for (let i = 0, l = children.length; i < l; i++) {
|
||||||
|
children[i].moveBeforeDOMNode(node, parent);
|
||||||
|
}
|
||||||
|
parent!.insertBefore(this.anchor!, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBeforeVNode(other: VList | null, afterNode: Node | null) {
|
||||||
if (other) {
|
if (other) {
|
||||||
const next = other!.children[0];
|
const next = other!.children[0];
|
||||||
afterNode = (next ? next.firstNode() : other!.anchor) || null;
|
afterNode = (next ? next.firstNode() : other!.anchor) || null;
|
||||||
}
|
}
|
||||||
const children = this.children;
|
const children = this.children;
|
||||||
for (let i = 0, l = children.length; i < l; i++) {
|
for (let i = 0, l = children.length; i < l; i++) {
|
||||||
children[i].moveBefore(null, afterNode);
|
children[i].moveBeforeVNode(null, afterNode);
|
||||||
}
|
}
|
||||||
this.parentEl!.insertBefore(this.anchor!, afterNode);
|
this.parentEl!.insertBefore(this.anchor!, afterNode);
|
||||||
}
|
}
|
||||||
@@ -66,7 +75,7 @@ class VList {
|
|||||||
patch: cPatch,
|
patch: cPatch,
|
||||||
remove: cRemove,
|
remove: cRemove,
|
||||||
beforeRemove,
|
beforeRemove,
|
||||||
moveBefore: cMoveBefore,
|
moveBeforeVNode: cMoveBefore,
|
||||||
firstNode: cFirstNode,
|
firstNode: cFirstNode,
|
||||||
} = proto;
|
} = proto;
|
||||||
|
|
||||||
@@ -38,7 +38,22 @@ export class VMulti {
|
|||||||
this.parentEl = parent;
|
this.parentEl = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveBefore(other: VMulti | null, afterNode: Node | null) {
|
moveBeforeDOMNode(node: Node | null, parent = this.parentEl) {
|
||||||
|
this.parentEl = parent;
|
||||||
|
const children = this.children;
|
||||||
|
const anchors = this.anchors;
|
||||||
|
for (let i = 0, l = children.length; i < l; i++) {
|
||||||
|
let child = children[i];
|
||||||
|
if (child) {
|
||||||
|
child.moveBeforeDOMNode(node, parent);
|
||||||
|
} else {
|
||||||
|
const anchor = anchors![i];
|
||||||
|
nodeInsertBefore.call(parent, anchor, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBeforeVNode(other: VMulti | null, afterNode: Node | null) {
|
||||||
if (other) {
|
if (other) {
|
||||||
const next = other!.children[0];
|
const next = other!.children[0];
|
||||||
afterNode = (next ? next.firstNode() : other!.anchors![0]) || null;
|
afterNode = (next ? next.firstNode() : other!.anchors![0]) || null;
|
||||||
@@ -49,7 +64,7 @@ export class VMulti {
|
|||||||
for (let i = 0, l = children.length; i < l; i++) {
|
for (let i = 0, l = children.length; i < l; i++) {
|
||||||
let child = children[i];
|
let child = children[i];
|
||||||
if (child) {
|
if (child) {
|
||||||
child.moveBefore(null, afterNode);
|
child.moveBeforeVNode(null, afterNode);
|
||||||
} else {
|
} else {
|
||||||
const anchor = anchors![i];
|
const anchor = anchors![i];
|
||||||
nodeInsertBefore.call(parent, anchor, afterNode);
|
nodeInsertBefore.call(parent, anchor, afterNode);
|
||||||
@@ -23,9 +23,13 @@ abstract class VSimpleNode {
|
|||||||
this.el = node;
|
this.el = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveBefore(other: VText | null, afterNode: Node | null) {
|
moveBeforeDOMNode(node: Node | null, parent = this.parentEl) {
|
||||||
const target = other ? other.el! : afterNode;
|
this.parentEl = parent;
|
||||||
nodeInsertBefore.call(this.parentEl, this.el!, target);
|
nodeInsertBefore.call(parent, this.el!, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBeforeVNode(other: VText | null, afterNode: Node | null) {
|
||||||
|
nodeInsertBefore.call(this.parentEl, this.el!, other ? other.el! : afterNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeRemove() {}
|
beforeRemove() {}
|
||||||
@@ -20,8 +20,12 @@ class VToggler {
|
|||||||
this.child.mount(parent, afterNode);
|
this.child.mount(parent, afterNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
moveBefore(other: VToggler | null, afterNode: Node | null) {
|
moveBeforeDOMNode(node: Node | null, parent?: HTMLElement) {
|
||||||
this.child.moveBefore(other ? other.child : null, afterNode);
|
this.child.moveBeforeDOMNode(node, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBeforeVNode(other: VToggler | null, afterNode: Node | null) {
|
||||||
|
this.moveBeforeDOMNode((other && other.firstNode()) || afterNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
patch(other: VToggler, withBeforeRemove: boolean) {
|
patch(other: VToggler, withBeforeRemove: boolean) {
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import { Schema } from "./validation";
|
||||||
import type { ComponentNode } from "./component_node";
|
import type { ComponentNode } from "./component_node";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Component Class
|
// Component Class
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
type Props = { [key: string]: any };
|
export type Props = { [key: string]: any };
|
||||||
|
|
||||||
interface StaticComponentProperties {
|
interface StaticComponentProperties {
|
||||||
template: string;
|
template: string;
|
||||||
defaultProps?: any;
|
defaultProps?: any;
|
||||||
props?: any;
|
props?: Schema;
|
||||||
|
components?: { [componentName: string]: ComponentConstructor };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComponentConstructor<P extends Props = any, E = any> = (new (
|
export type ComponentConstructor<P extends Props = any, E = any> = (new (
|
||||||
@@ -36,7 +38,7 @@ export class Component<Props = any, Env = any> {
|
|||||||
|
|
||||||
setup() {}
|
setup() {}
|
||||||
|
|
||||||
render() {
|
render(deep: boolean = false) {
|
||||||
this.__owl__.render();
|
this.__owl__.render(deep === true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
import type { App, Env } from "./app";
|
||||||
|
import { BDom, VNode } from "./blockdom";
|
||||||
|
import { Component, ComponentConstructor, Props } from "./component";
|
||||||
|
import { fibersInError, OwlError } from "./error_handling";
|
||||||
|
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
|
||||||
|
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
|
||||||
|
import { STATUS } from "./status";
|
||||||
|
import { batched, Callback } from "./utils";
|
||||||
|
|
||||||
|
let currentNode: ComponentNode | null = null;
|
||||||
|
|
||||||
|
export function getCurrent(): ComponentNode {
|
||||||
|
if (!currentNode) {
|
||||||
|
throw new OwlError("No active component (a hook function should only be called in 'setup')");
|
||||||
|
}
|
||||||
|
return currentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useComponent(): Component {
|
||||||
|
return currentNode!.component;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply default props (only top level).
|
||||||
|
*/
|
||||||
|
function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>) {
|
||||||
|
for (let propName in defaultProps) {
|
||||||
|
if (props[propName] === undefined) {
|
||||||
|
(props as any)[propName] = defaultProps[propName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Integration with reactivity system (useState)
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
||||||
|
/**
|
||||||
|
* Creates a reactive object that will be observed by the current component.
|
||||||
|
* Reading data from the returned object (eg during rendering) will cause the
|
||||||
|
* component to subscribe to that data and be rerendered when it changes.
|
||||||
|
*
|
||||||
|
* @param state the state to observe
|
||||||
|
* @returns a reactive object that will cause the component to re-render on
|
||||||
|
* relevant changes
|
||||||
|
* @see reactive
|
||||||
|
*/
|
||||||
|
export function useState<T extends object>(state: T): T {
|
||||||
|
const node = getCurrent();
|
||||||
|
let render = batchedRenderFunctions.get(node)!;
|
||||||
|
if (!render) {
|
||||||
|
render = batched(node.render.bind(node, false));
|
||||||
|
batchedRenderFunctions.set(node, render);
|
||||||
|
// manual implementation of onWillDestroy to break cyclic dependency
|
||||||
|
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
||||||
|
}
|
||||||
|
return reactive(state, render);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Component VNode class
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type LifecycleHook = Function;
|
||||||
|
|
||||||
|
export class ComponentNode<P extends Props = any, E = any> implements VNode<ComponentNode<P, E>> {
|
||||||
|
el?: HTMLElement | Text | undefined;
|
||||||
|
app: App;
|
||||||
|
fiber: Fiber | null = null;
|
||||||
|
component: Component<P, E>;
|
||||||
|
bdom: BDom | null = null;
|
||||||
|
status: STATUS = STATUS.NEW;
|
||||||
|
forceNextRender: boolean = false;
|
||||||
|
parentKey: string | null;
|
||||||
|
props: P;
|
||||||
|
nextProps: P | null = null;
|
||||||
|
|
||||||
|
renderFn: Function;
|
||||||
|
parent: ComponentNode | null;
|
||||||
|
childEnv: Env;
|
||||||
|
children: { [key: string]: ComponentNode } = Object.create(null);
|
||||||
|
refs: any = {};
|
||||||
|
|
||||||
|
willStart: LifecycleHook[] = [];
|
||||||
|
willUpdateProps: LifecycleHook[] = [];
|
||||||
|
willUnmount: LifecycleHook[] = [];
|
||||||
|
mounted: LifecycleHook[] = [];
|
||||||
|
willPatch: LifecycleHook[] = [];
|
||||||
|
patched: LifecycleHook[] = [];
|
||||||
|
willDestroy: LifecycleHook[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
C: ComponentConstructor<P, E>,
|
||||||
|
props: P,
|
||||||
|
app: App,
|
||||||
|
parent: ComponentNode | null,
|
||||||
|
parentKey: string | null
|
||||||
|
) {
|
||||||
|
currentNode = this;
|
||||||
|
this.app = app;
|
||||||
|
this.parent = parent;
|
||||||
|
this.props = props;
|
||||||
|
this.parentKey = parentKey;
|
||||||
|
const defaultProps = C.defaultProps;
|
||||||
|
props = Object.assign({}, props);
|
||||||
|
if (defaultProps) {
|
||||||
|
applyDefaultProps(props, defaultProps);
|
||||||
|
}
|
||||||
|
const env = (parent && parent.childEnv) || app.env;
|
||||||
|
this.childEnv = env;
|
||||||
|
for (const key in props) {
|
||||||
|
const prop = props[key];
|
||||||
|
if (prop && typeof prop === "object" && targets.has(prop)) {
|
||||||
|
props[key] = useState(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.component = new C(props, env, this);
|
||||||
|
const ctx = Object.assign(Object.create(this.component), { this: this.component });
|
||||||
|
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
|
||||||
|
this.component.setup();
|
||||||
|
currentNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mountComponent(target: any, options?: MountOptions) {
|
||||||
|
const fiber = new MountFiber(this, target, options);
|
||||||
|
this.app.scheduler.addFiber(fiber);
|
||||||
|
this.initiateRender(fiber);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initiateRender(fiber: Fiber | MountFiber) {
|
||||||
|
this.fiber = fiber;
|
||||||
|
if (this.mounted.length) {
|
||||||
|
fiber.root!.mounted.push(fiber);
|
||||||
|
}
|
||||||
|
const component = this.component;
|
||||||
|
try {
|
||||||
|
await Promise.all(this.willStart.map((f) => f.call(component)));
|
||||||
|
} catch (e) {
|
||||||
|
this.app.handleError({ node: this, error: e });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.status === STATUS.NEW && this.fiber === fiber) {
|
||||||
|
fiber.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(deep: boolean) {
|
||||||
|
let current = this.fiber;
|
||||||
|
if (current && (current.root!.locked || (current as any).bdom === true)) {
|
||||||
|
await Promise.resolve();
|
||||||
|
// situation may have changed after the microtask tick
|
||||||
|
current = this.fiber;
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
if (!current.bdom && !fibersInError.has(current)) {
|
||||||
|
if (deep) {
|
||||||
|
// we want the render from this point on to be with deep=true
|
||||||
|
current.deep = deep;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// if current rendering was with deep=true, we want this one to be the same
|
||||||
|
deep = deep || current.deep;
|
||||||
|
} else if (!this.bdom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fiber = makeRootFiber(this);
|
||||||
|
fiber.deep = deep;
|
||||||
|
this.fiber = fiber;
|
||||||
|
|
||||||
|
this.app.scheduler.addFiber(fiber);
|
||||||
|
await Promise.resolve();
|
||||||
|
if (this.status === STATUS.DESTROYED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// We only want to actually render the component if the following two
|
||||||
|
// conditions are true:
|
||||||
|
// * this.fiber: it could be null, in which case the render has been cancelled
|
||||||
|
// * (current || !fiber.parent): if current is not null, this means that the
|
||||||
|
// render function was called when a render was already occurring. In this
|
||||||
|
// case, the pending rendering was cancelled, and the fiber needs to be
|
||||||
|
// rendered to complete the work. If current is null, we check that the
|
||||||
|
// fiber has no parent. If that is the case, the fiber was downgraded from
|
||||||
|
// a root fiber to a child fiber in the previous microtick, because it was
|
||||||
|
// embedded in a rendering coming from above, so the fiber will be rendered
|
||||||
|
// in the next microtick anyway, so we should not render it again.
|
||||||
|
if (this.fiber === fiber && (current || !fiber.parent)) {
|
||||||
|
fiber.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
let shouldRemove = this.status === STATUS.MOUNTED;
|
||||||
|
this._destroy();
|
||||||
|
if (shouldRemove) {
|
||||||
|
this.bdom!.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_destroy() {
|
||||||
|
const component = this.component;
|
||||||
|
if (this.status === STATUS.MOUNTED) {
|
||||||
|
for (let cb of this.willUnmount) {
|
||||||
|
cb.call(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let child of Object.values(this.children)) {
|
||||||
|
child._destroy();
|
||||||
|
}
|
||||||
|
if (this.willDestroy.length) {
|
||||||
|
try {
|
||||||
|
for (let cb of this.willDestroy) {
|
||||||
|
cb.call(component);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.app.handleError({ error: e, node: this });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.status = STATUS.DESTROYED;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAndRender(props: P, parentFiber: Fiber) {
|
||||||
|
this.nextProps = props;
|
||||||
|
props = Object.assign({}, props);
|
||||||
|
// update
|
||||||
|
const fiber = makeChildFiber(this, parentFiber);
|
||||||
|
this.fiber = fiber;
|
||||||
|
const component = this.component;
|
||||||
|
const defaultProps = (component.constructor as any).defaultProps;
|
||||||
|
if (defaultProps) {
|
||||||
|
applyDefaultProps(props, defaultProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = this;
|
||||||
|
for (const key in props) {
|
||||||
|
const prop = props[key];
|
||||||
|
if (prop && typeof prop === "object" && targets.has(prop)) {
|
||||||
|
props[key] = useState(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentNode = null;
|
||||||
|
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
|
||||||
|
await prom;
|
||||||
|
if (fiber !== this.fiber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
component.props = props;
|
||||||
|
fiber.render();
|
||||||
|
const parentRoot = parentFiber.root!;
|
||||||
|
if (this.willPatch.length) {
|
||||||
|
parentRoot.willPatch.push(fiber);
|
||||||
|
}
|
||||||
|
if (this.patched.length) {
|
||||||
|
parentRoot.patched.push(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a child that has dom that is not yet updated, and update it. This
|
||||||
|
* method is meant to be used only in the context of repatching the dom after
|
||||||
|
* a mounted hook failed and was handled.
|
||||||
|
*/
|
||||||
|
updateDom() {
|
||||||
|
if (!this.fiber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.bdom === this.fiber!.bdom) {
|
||||||
|
// If the error was handled by some child component, we need to find it to
|
||||||
|
// apply its change
|
||||||
|
for (let k in this.children) {
|
||||||
|
const child = this.children[k];
|
||||||
|
child.updateDom();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if we get here, this is the component that handled the error and rerendered
|
||||||
|
// itself, so we can simply patch the dom
|
||||||
|
this.bdom!.patch(this.fiber!.bdom, false);
|
||||||
|
this.fiber!.appliedToDom = true;
|
||||||
|
this.fiber = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a ref to a given HTMLElement.
|
||||||
|
*
|
||||||
|
* @param name the name of the ref to set
|
||||||
|
* @param el the HTMLElement to set the ref to. The ref is not set if the el
|
||||||
|
* is null, but useRef will not return elements that are not in the DOM
|
||||||
|
*/
|
||||||
|
setRef(name: string, el: HTMLElement | null) {
|
||||||
|
if (el) {
|
||||||
|
this.refs[name] = el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Block DOM methods
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
firstNode(): Node | undefined {
|
||||||
|
const bdom = this.bdom;
|
||||||
|
return bdom ? bdom.firstNode() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(parent: HTMLElement, anchor: ChildNode) {
|
||||||
|
const bdom = this.fiber!.bdom!;
|
||||||
|
this.bdom = bdom;
|
||||||
|
bdom.mount(parent, anchor);
|
||||||
|
this.status = STATUS.MOUNTED;
|
||||||
|
this.fiber!.appliedToDom = true;
|
||||||
|
this.children = this.fiber!.childrenMap;
|
||||||
|
this.fiber = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBeforeDOMNode(node: Node | null, parent?: HTMLElement): void {
|
||||||
|
this.bdom!.moveBeforeDOMNode(node, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBeforeVNode(other: ComponentNode<P, E> | null, afterNode: Node | null) {
|
||||||
|
this.bdom!.moveBeforeVNode(other ? other.bdom : null, afterNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
patch() {
|
||||||
|
if (this.fiber && this.fiber.parent) {
|
||||||
|
// we only patch here renderings coming from above. renderings initiated
|
||||||
|
// by the component will be patched independently in the appropriate
|
||||||
|
// fiber.complete
|
||||||
|
this._patch();
|
||||||
|
this.props = this.nextProps!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_patch() {
|
||||||
|
let hasChildren = false;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
for (let _k in this.children) {
|
||||||
|
hasChildren = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const fiber = this.fiber!;
|
||||||
|
this.children = fiber.childrenMap;
|
||||||
|
this.bdom!.patch(fiber.bdom!, hasChildren);
|
||||||
|
fiber.appliedToDom = true;
|
||||||
|
this.fiber = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeRemove() {
|
||||||
|
this._destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.bdom!.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Some debug helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
get name(): string {
|
||||||
|
return this.component.constructor.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get subscriptions(): ReturnType<typeof getSubscriptions> {
|
||||||
|
const render = batchedRenderFunctions.get(this);
|
||||||
|
return render ? getSubscriptions(render) : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import type { ComponentNode } from "./component_node";
|
import type { ComponentNode } from "./component_node";
|
||||||
import type { Fiber } from "./fibers";
|
import type { Fiber } from "./fibers";
|
||||||
|
|
||||||
|
// Custom error class that wraps error that happen in the owl lifecycle
|
||||||
|
export class OwlError extends Error {
|
||||||
|
cause?: any;
|
||||||
|
}
|
||||||
|
|
||||||
// Maps fibers to thrown errors
|
// Maps fibers to thrown errors
|
||||||
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
|
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
|
||||||
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
|
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
|
||||||
|
|
||||||
function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean {
|
function _handleError(node: ComponentNode | null, error: any): boolean {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -16,22 +21,19 @@ function _handleError(node: ComponentNode | null, error: any, isFirstRound = fal
|
|||||||
|
|
||||||
const errorHandlers = nodeErrorHandlers.get(node);
|
const errorHandlers = nodeErrorHandlers.get(node);
|
||||||
if (errorHandlers) {
|
if (errorHandlers) {
|
||||||
let stopped = false;
|
let handled = false;
|
||||||
// execute in the opposite order
|
// execute in the opposite order
|
||||||
for (let i = errorHandlers.length - 1; i >= 0; i--) {
|
for (let i = errorHandlers.length - 1; i >= 0; i--) {
|
||||||
try {
|
try {
|
||||||
errorHandlers[i](error);
|
errorHandlers[i](error);
|
||||||
stopped = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stopped) {
|
if (handled) {
|
||||||
if (isFirstRound && fiber && fiber.node.fiber) {
|
|
||||||
fiber.root!.counter--;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +42,14 @@ function _handleError(node: ComponentNode | null, error: any, isFirstRound = fal
|
|||||||
|
|
||||||
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
|
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
|
||||||
export function handleError(params: ErrorParams) {
|
export function handleError(params: ErrorParams) {
|
||||||
const error = params.error;
|
let { error } = params;
|
||||||
|
// Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)
|
||||||
|
if (!(error instanceof OwlError)) {
|
||||||
|
error = Object.assign(
|
||||||
|
new OwlError(`An error occured in the owl lifecycle (see this Error's "cause" property)`),
|
||||||
|
{ cause: error }
|
||||||
|
);
|
||||||
|
}
|
||||||
const node = "node" in params ? params.node : params.fiber.node;
|
const node = "node" in params ? params.node : params.fiber.node;
|
||||||
const fiber = "fiber" in params ? params.fiber : node.fiber!;
|
const fiber = "fiber" in params ? params.fiber : node.fiber!;
|
||||||
|
|
||||||
@@ -54,7 +63,7 @@ export function handleError(params: ErrorParams) {
|
|||||||
|
|
||||||
fibersInError.set(fiber.root!, error);
|
fibersInError.set(fiber.root!, error);
|
||||||
|
|
||||||
const handled = _handleError(node, error, true);
|
const handled = _handleError(node, error);
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
console.warn(`[Owl] Unhandled error. Destroying the root component`);
|
console.warn(`[Owl] Unhandled error. Destroying the root component`);
|
||||||
try {
|
try {
|
||||||
@@ -62,5 +71,6 @@ export function handleError(params: ErrorParams) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { filterOutModifiersFromData } from "../blockdom/config";
|
import { filterOutModifiersFromData } from "./blockdom/config";
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
|
import { OwlError } from "./error_handling";
|
||||||
|
|
||||||
export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => {
|
export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => {
|
||||||
const { data: _data, modifiers } = filterOutModifiersFromData(data);
|
const { data: _data, modifiers } = filterOutModifiersFromData(data);
|
||||||
@@ -33,7 +34,7 @@ export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarg
|
|||||||
if (Object.hasOwnProperty.call(data, 0)) {
|
if (Object.hasOwnProperty.call(data, 0)) {
|
||||||
const handler = data[0];
|
const handler = data[0];
|
||||||
if (typeof handler !== "function") {
|
if (typeof handler !== "function") {
|
||||||
throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
|
throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);
|
||||||
}
|
}
|
||||||
let node = data[1] ? data[1].__owl__ : null;
|
let node = data[1] ? data[1].__owl__ : null;
|
||||||
if (node ? node.status === STATUS.MOUNTED : true) {
|
if (node ? node.status === STATUS.MOUNTED : true) {
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import { BDom, mount } from "../blockdom";
|
import { BDom, mount } from "./blockdom";
|
||||||
import type { ComponentNode } from "./component_node";
|
import type { ComponentNode } from "./component_node";
|
||||||
import { fibersInError, handleError } from "./error_handling";
|
import { fibersInError, OwlError } from "./error_handling";
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
|
|
||||||
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
||||||
let current = node.fiber;
|
let current = node.fiber;
|
||||||
if (current) {
|
if (current) {
|
||||||
let root = parent.root;
|
cancelFibers(current.children);
|
||||||
cancelFibers(root, current.children);
|
|
||||||
current.root = null;
|
current.root = null;
|
||||||
}
|
}
|
||||||
return new Fiber(node, parent);
|
return new Fiber(node, parent);
|
||||||
@@ -17,9 +16,14 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
|||||||
let current = node.fiber;
|
let current = node.fiber;
|
||||||
if (current) {
|
if (current) {
|
||||||
let root = current.root!;
|
let root = current.root!;
|
||||||
root.counter -= cancelFibers(root, current.children);
|
// lock root fiber because canceling children fibers may destroy components,
|
||||||
|
// which means any arbitrary code can be run in onWillDestroy, which may
|
||||||
|
// trigger new renderings
|
||||||
|
root.locked = true;
|
||||||
|
root.setCounter(root.counter + 1 - cancelFibers(current.children));
|
||||||
|
root.locked = false;
|
||||||
current.children = [];
|
current.children = [];
|
||||||
root.counter++;
|
current.childrenMap = {};
|
||||||
current.bdom = null;
|
current.bdom = null;
|
||||||
if (fibersInError.has(current)) {
|
if (fibersInError.has(current)) {
|
||||||
fibersInError.delete(current);
|
fibersInError.delete(current);
|
||||||
@@ -35,22 +39,38 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
|||||||
if (node.patched.length) {
|
if (node.patched.length) {
|
||||||
fiber.patched.push(fiber);
|
fiber.patched.push(fiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fiber;
|
return fiber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function throwOnRender() {
|
||||||
|
throw new OwlError("Attempted to render cancelled fiber");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns number of not-yet rendered fibers cancelled
|
* @returns number of not-yet rendered fibers cancelled
|
||||||
*/
|
*/
|
||||||
function cancelFibers(root: any, fibers: Fiber[]): number {
|
function cancelFibers(fibers: Fiber[]): number {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
for (let fiber of fibers) {
|
for (let fiber of fibers) {
|
||||||
fiber.node.fiber = null;
|
let node = fiber.node;
|
||||||
fiber.root = root;
|
fiber.render = throwOnRender;
|
||||||
if (!fiber.bdom) {
|
if (node.status === STATUS.NEW) {
|
||||||
|
node.destroy();
|
||||||
|
delete node.parent!.children[node.parentKey!];
|
||||||
|
}
|
||||||
|
node.fiber = null;
|
||||||
|
if (fiber.bdom) {
|
||||||
|
// if fiber has been rendered, this means that the component props have
|
||||||
|
// been updated. however, this fiber will not be patched to the dom, so
|
||||||
|
// it could happen that the next render compare the current props with
|
||||||
|
// the same props, and skip the render completely. With the next line,
|
||||||
|
// we kindly request the component code to force a render, so it works as
|
||||||
|
// expected.
|
||||||
|
node.forceNextRender = true;
|
||||||
|
} else {
|
||||||
result++;
|
result++;
|
||||||
}
|
}
|
||||||
result += cancelFibers(root, fiber.children);
|
result += cancelFibers(fiber.children);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -62,19 +82,59 @@ export class Fiber {
|
|||||||
parent: Fiber | null;
|
parent: Fiber | null;
|
||||||
children: Fiber[] = [];
|
children: Fiber[] = [];
|
||||||
appliedToDom = false;
|
appliedToDom = false;
|
||||||
|
deep: boolean = false;
|
||||||
|
childrenMap: ComponentNode["children"] = {};
|
||||||
|
|
||||||
constructor(node: ComponentNode, parent: Fiber | null) {
|
constructor(node: ComponentNode, parent: Fiber | null) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
if (parent) {
|
if (parent) {
|
||||||
|
this.deep = parent.deep;
|
||||||
const root = parent.root!;
|
const root = parent.root!;
|
||||||
root.counter++;
|
root.setCounter(root.counter + 1);
|
||||||
this.root = root;
|
this.root = root;
|
||||||
parent.children.push(this);
|
parent.children.push(this);
|
||||||
} else {
|
} else {
|
||||||
this.root = this as any;
|
this.root = this as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// if some parent has a fiber => register in followup
|
||||||
|
let prev = this.root!.node;
|
||||||
|
let scheduler = prev.app.scheduler;
|
||||||
|
let current = prev.parent;
|
||||||
|
while (current) {
|
||||||
|
if (current.fiber) {
|
||||||
|
let root = current.fiber.root!;
|
||||||
|
if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) {
|
||||||
|
current = root.node;
|
||||||
|
} else {
|
||||||
|
scheduler.delayedRenders.push(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev = current;
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// there are no current rendering from above => we can render
|
||||||
|
this._render();
|
||||||
|
}
|
||||||
|
|
||||||
|
_render() {
|
||||||
|
const node = this.node;
|
||||||
|
const root = this.root;
|
||||||
|
if (root) {
|
||||||
|
try {
|
||||||
|
(this.bdom as any) = true;
|
||||||
|
this.bdom = node.renderFn();
|
||||||
|
} catch (e) {
|
||||||
|
node.app.handleError({ node, error: e });
|
||||||
|
}
|
||||||
|
root.setCounter(root.counter - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RootFiber extends Fiber {
|
export class RootFiber extends Fiber {
|
||||||
@@ -109,7 +169,7 @@ export class RootFiber extends Fiber {
|
|||||||
current = undefined;
|
current = undefined;
|
||||||
|
|
||||||
// Step 2: patching the dom
|
// Step 2: patching the dom
|
||||||
node.patch();
|
node._patch();
|
||||||
this.locked = false;
|
this.locked = false;
|
||||||
|
|
||||||
// Step 4: calling all mounted lifecycle hooks
|
// Step 4: calling all mounted lifecycle hooks
|
||||||
@@ -135,7 +195,14 @@ export class RootFiber extends Fiber {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.locked = false;
|
this.locked = false;
|
||||||
handleError({ fiber: current || this, error: e });
|
node.app.handleError({ fiber: current || this, error: e });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCounter(newValue: number) {
|
||||||
|
this.counter = newValue;
|
||||||
|
if (newValue === 0) {
|
||||||
|
this.node.app.scheduler.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,6 +226,7 @@ export class MountFiber extends RootFiber {
|
|||||||
let current: Fiber | undefined = this;
|
let current: Fiber | undefined = this;
|
||||||
try {
|
try {
|
||||||
const node = this.node;
|
const node = this.node;
|
||||||
|
node.children = this.childrenMap;
|
||||||
(node.app.constructor as any).validateTarget(this.target);
|
(node.app.constructor as any).validateTarget(this.target);
|
||||||
if (node.bdom) {
|
if (node.bdom) {
|
||||||
// this is a complicated situation: if we mount a fiber with an existing
|
// this is a complicated situation: if we mount a fiber with an existing
|
||||||
@@ -191,7 +259,7 @@ export class MountFiber extends RootFiber {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError({ fiber: current as Fiber, error: e });
|
this.node.app.handleError({ fiber: current as Fiber, error: e });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Env } from "./app/app";
|
import type { Env } from "./app";
|
||||||
import { getCurrent } from "./component/component_node";
|
import { getCurrent } from "./component_node";
|
||||||
import { onMounted, onPatched, onWillUnmount } from "./component/lifecycle_hooks";
|
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
|
||||||
|
import { inOwnerDocument } from "./utils";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// useRef
|
// useRef
|
||||||
@@ -15,7 +16,8 @@ export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el:
|
|||||||
const refs = node.refs;
|
const refs = node.refs;
|
||||||
return {
|
return {
|
||||||
get el(): T | null {
|
get el(): T | null {
|
||||||
return refs[name] || null;
|
const el = refs[name];
|
||||||
|
return inOwnerDocument(el) ? el : null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -57,7 +59,6 @@ export function useChildSubEnv(envExtension: Env) {
|
|||||||
// useEffect
|
// useEffect
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
const NO_OP = () => {};
|
|
||||||
/**
|
/**
|
||||||
* @param {...any} dependencies the dependencies computed by computeDependencies
|
* @param {...any} dependencies the dependencies computed by computeDependencies
|
||||||
* @returns {void|(()=>void)} a cleanup function that reverses the side
|
* @returns {void|(()=>void)} a cleanup function that reverses the side
|
||||||
@@ -78,11 +79,11 @@ type Effect = (...dependencies: any[]) => void | (() => void);
|
|||||||
* NaN !== NaN, which will cause the effect to rerun on every patch.
|
* NaN !== NaN, which will cause the effect to rerun on every patch.
|
||||||
*/
|
*/
|
||||||
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
|
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
|
||||||
let cleanup: () => void;
|
let cleanup: (() => void) | void;
|
||||||
let dependencies: any[];
|
let dependencies: any[];
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
dependencies = computeDependencies();
|
dependencies = computeDependencies();
|
||||||
cleanup = effect(...dependencies) || NO_OP;
|
cleanup = effect(...dependencies);
|
||||||
});
|
});
|
||||||
|
|
||||||
onPatched(() => {
|
onPatched(() => {
|
||||||
@@ -90,12 +91,14 @@ export function useEffect(effect: Effect, computeDependencies: () => any[] = ()
|
|||||||
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
|
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
|
||||||
if (shouldReapply) {
|
if (shouldReapply) {
|
||||||
dependencies = newDeps;
|
dependencies = newDeps;
|
||||||
cleanup();
|
if (cleanup) {
|
||||||
cleanup = effect(...dependencies) || NO_OP;
|
cleanup();
|
||||||
|
}
|
||||||
|
cleanup = effect(...dependencies);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onWillUnmount(() => cleanup());
|
onWillUnmount(() => cleanup && cleanup());
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -116,7 +119,7 @@ export function useEffect(effect: Effect, computeDependencies: () => any[] = ()
|
|||||||
* `useExternalListener(window, 'click', this._doSomething);`
|
* `useExternalListener(window, 'click', this._doSomething);`
|
||||||
* */
|
* */
|
||||||
export function useExternalListener(
|
export function useExternalListener(
|
||||||
target: HTMLElement | typeof window,
|
target: EventTarget,
|
||||||
eventName: string,
|
eventName: string,
|
||||||
handler: EventListener,
|
handler: EventListener,
|
||||||
eventParams?: AddEventListenerOptions
|
eventParams?: AddEventListenerOptions
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { App } from "./app";
|
||||||
|
import {
|
||||||
|
config,
|
||||||
|
createBlock,
|
||||||
|
html,
|
||||||
|
list,
|
||||||
|
mount as blockMount,
|
||||||
|
multi,
|
||||||
|
patch,
|
||||||
|
remove,
|
||||||
|
text,
|
||||||
|
toggler,
|
||||||
|
comment,
|
||||||
|
} from "./blockdom";
|
||||||
|
import { mainEventHandler } from "./event_handling";
|
||||||
|
|
||||||
|
config.shouldNormalizeDom = false;
|
||||||
|
config.mainEventHandler = mainEventHandler;
|
||||||
|
|
||||||
|
export const blockDom = {
|
||||||
|
config,
|
||||||
|
// bdom entry points
|
||||||
|
mount: blockMount,
|
||||||
|
patch,
|
||||||
|
remove,
|
||||||
|
// bdom block types
|
||||||
|
list,
|
||||||
|
multi,
|
||||||
|
text,
|
||||||
|
toggler,
|
||||||
|
createBlock,
|
||||||
|
html,
|
||||||
|
comment,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { App, mount } from "./app";
|
||||||
|
export { xml } from "./template_set";
|
||||||
|
export { Component } from "./component";
|
||||||
|
export type { ComponentConstructor } from "./component";
|
||||||
|
export { useComponent, useState } from "./component_node";
|
||||||
|
export { status } from "./status";
|
||||||
|
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||||
|
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||||
|
export { EventBus, whenReady, loadFile, markup } from "./utils";
|
||||||
|
export {
|
||||||
|
onWillStart,
|
||||||
|
onMounted,
|
||||||
|
onWillUnmount,
|
||||||
|
onWillUpdateProps,
|
||||||
|
onWillPatch,
|
||||||
|
onPatched,
|
||||||
|
onWillRender,
|
||||||
|
onRendered,
|
||||||
|
onWillDestroy,
|
||||||
|
onError,
|
||||||
|
} from "./lifecycle_hooks";
|
||||||
|
export { validate, validateType } from "./validation";
|
||||||
|
export { OwlError } from "./error_handling";
|
||||||
|
|
||||||
|
export const __info__ = {
|
||||||
|
version: App.version,
|
||||||
|
};
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { getCurrent } from "./component_node";
|
||||||
|
import { nodeErrorHandlers, OwlError } from "./error_handling";
|
||||||
|
|
||||||
|
const TIMEOUT = Symbol("timeout");
|
||||||
|
function wrapError(fn: (...args: any[]) => any, hookName: string) {
|
||||||
|
const error = new OwlError(`The following error occurred in ${hookName}: `) as Error & {
|
||||||
|
cause: any;
|
||||||
|
};
|
||||||
|
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
|
||||||
|
const node = getCurrent();
|
||||||
|
return (...args: any[]) => {
|
||||||
|
const onError = (cause: any) => {
|
||||||
|
error.cause = cause;
|
||||||
|
if (cause instanceof Error) {
|
||||||
|
error.message += `"${cause.message}"`;
|
||||||
|
} else {
|
||||||
|
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const result = fn(...args);
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
|
||||||
|
const fiber = node.fiber;
|
||||||
|
Promise.race([
|
||||||
|
result.catch(() => {}),
|
||||||
|
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||||
|
]).then((res) => {
|
||||||
|
if (res === TIMEOUT && node.fiber === fiber) {
|
||||||
|
console.warn(timeoutError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result.catch(onError);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (cause) {
|
||||||
|
onError(cause);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// hooks
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function onWillStart(fn: () => Promise<void> | void | any) {
|
||||||
|
const node = getCurrent();
|
||||||
|
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||||
|
node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
|
||||||
|
const node = getCurrent();
|
||||||
|
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||||
|
node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onMounted(fn: () => void | any) {
|
||||||
|
const node = getCurrent();
|
||||||
|
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||||
|
node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onWillPatch(fn: () => any | void) {
|
||||||
|
const node = getCurrent();
|
||||||
|
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||||
|
node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onPatched(fn: () => void | any) {
|
||||||
|
const node = getCurrent();
|
||||||
|
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||||
|
node.patched.push(decorate(fn.bind(node.component), "onPatched"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onWillUnmount(fn: () => void | any) {
|
||||||
|
const node = getCurrent();
|
||||||
|
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||||
|
node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onWillDestroy(fn: () => void | any) {
|
||||||
|
const node = getCurrent();
|
||||||
|
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||||
|
node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onWillRender(fn: () => void | any) {
|
||||||
|
const node = getCurrent();
|
||||||
|
const renderFn = node.renderFn;
|
||||||
|
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||||
|
fn = decorate(fn.bind(node.component), "onWillRender");
|
||||||
|
node.renderFn = () => {
|
||||||
|
fn();
|
||||||
|
return renderFn();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onRendered(fn: () => void | any) {
|
||||||
|
const node = getCurrent();
|
||||||
|
const renderFn = node.renderFn;
|
||||||
|
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||||
|
fn = decorate(fn.bind(node.component), "onRendered");
|
||||||
|
node.renderFn = () => {
|
||||||
|
const result = renderFn();
|
||||||
|
fn();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnErrorCallback = (error: any) => void | any;
|
||||||
|
export function onError(callback: OnErrorCallback) {
|
||||||
|
const node = getCurrent();
|
||||||
|
let handlers = nodeErrorHandlers.get(node);
|
||||||
|
if (!handlers) {
|
||||||
|
handlers = [];
|
||||||
|
nodeErrorHandlers.set(node, handlers);
|
||||||
|
}
|
||||||
|
handlers.push(callback.bind(node.component));
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { onMounted, onWillUnmount } from "./lifecycle_hooks";
|
||||||
|
import { BDom, text, VNode } from "./blockdom";
|
||||||
|
import { Component } from "./component";
|
||||||
|
import { OwlError } from "./error_handling";
|
||||||
|
|
||||||
|
const VText: any = text("").constructor;
|
||||||
|
|
||||||
|
class VPortal extends VText implements Partial<VNode<VPortal>> {
|
||||||
|
content: BDom | null;
|
||||||
|
selector: string;
|
||||||
|
target: HTMLElement | null = null;
|
||||||
|
|
||||||
|
constructor(selector: string, content: BDom) {
|
||||||
|
super("");
|
||||||
|
this.selector = selector;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(parent: HTMLElement, anchor: ChildNode) {
|
||||||
|
super.mount(parent, anchor);
|
||||||
|
this.target = document.querySelector(this.selector) as any;
|
||||||
|
if (this.target) {
|
||||||
|
this.content!.mount(this.target!, null);
|
||||||
|
} else {
|
||||||
|
this.content!.mount(parent, anchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeRemove() {
|
||||||
|
this.content!.beforeRemove();
|
||||||
|
}
|
||||||
|
remove() {
|
||||||
|
if (this.content) {
|
||||||
|
super.remove();
|
||||||
|
this.content!.remove();
|
||||||
|
this.content = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(other: VPortal) {
|
||||||
|
super.patch(other);
|
||||||
|
if (this.content) {
|
||||||
|
this.content.patch(other.content!, true);
|
||||||
|
} else {
|
||||||
|
this.content = other.content;
|
||||||
|
this.content!.mount(this.target!, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kind of similar to <t t-slot="default"/>, but it wraps it around a VPortal
|
||||||
|
*/
|
||||||
|
export function portalTemplate(app: any, bdom: any, helpers: any) {
|
||||||
|
let { callSlot } = helpers;
|
||||||
|
return function template(ctx: any, node: any, key = ""): any {
|
||||||
|
return new VPortal(ctx.props.target, callSlot(ctx, node, key, "default", false, null));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Portal extends Component {
|
||||||
|
static template = "__portal__";
|
||||||
|
static props = {
|
||||||
|
target: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
slots: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const node: any = this.__owl__;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const portal: VPortal = node.bdom;
|
||||||
|
if (!portal.target) {
|
||||||
|
const target: HTMLElement = document.querySelector(this.props.target);
|
||||||
|
if (target) {
|
||||||
|
portal.content!.moveBeforeDOMNode(target.firstChild, target);
|
||||||
|
} else {
|
||||||
|
throw new OwlError("invalid portal target");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onWillUnmount(() => {
|
||||||
|
const portal: VPortal = node.bdom;
|
||||||
|
portal.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
import type { Callback } from "./utils";
|
||||||
|
import { OwlError } from "./error_handling";
|
||||||
|
|
||||||
|
// Special key to subscribe to, to be notified of key creation/deletion
|
||||||
|
const KEYCHANGES = Symbol("Key changes");
|
||||||
|
// Used to specify the absence of a callback, can be used as WeakMap key but
|
||||||
|
// should only be used as a sentinel value and never called.
|
||||||
|
const NO_CALLBACK = () => {
|
||||||
|
throw new Error("Called NO_CALLBACK. Owl is broken, please report this to the maintainers.");
|
||||||
|
};
|
||||||
|
|
||||||
|
// The following types only exist to signify places where objects are expected
|
||||||
|
// to be reactive or not, they provide no type checking benefit over "object"
|
||||||
|
type Target = object;
|
||||||
|
type Reactive<T extends Target> = T;
|
||||||
|
|
||||||
|
type Collection = Set<any> | Map<any, any> | WeakMap<any, any>;
|
||||||
|
type CollectionRawType = "Set" | "Map" | "WeakMap";
|
||||||
|
|
||||||
|
const objectToString = Object.prototype.toString;
|
||||||
|
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
|
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
||||||
|
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
||||||
|
* many native objects such as Promise (whose toString is [object Promise])
|
||||||
|
* or Date ([object Date]), while also supporting collections without using
|
||||||
|
* instanceof in a loop
|
||||||
|
*
|
||||||
|
* @param obj the object to check
|
||||||
|
* @returns the raw type of the object
|
||||||
|
*/
|
||||||
|
function rawType(obj: any) {
|
||||||
|
return objectToString.call(toRaw(obj)).slice(8, -1);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks whether a given value can be made into a reactive object.
|
||||||
|
*
|
||||||
|
* @param value the value to check
|
||||||
|
* @returns whether the value can be made reactive
|
||||||
|
*/
|
||||||
|
function canBeMadeReactive(value: any): boolean {
|
||||||
|
if (typeof value !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a reactive from the given object/callback if possible and returns it,
|
||||||
|
* returns the original object otherwise.
|
||||||
|
*
|
||||||
|
* @param value the value make reactive
|
||||||
|
* @returns a reactive for the given object when possible, the original otherwise
|
||||||
|
*/
|
||||||
|
function possiblyReactive(val: any, cb: Callback) {
|
||||||
|
return canBeMadeReactive(val) ? reactive(val, cb) : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skipped = new WeakSet<Target>();
|
||||||
|
/**
|
||||||
|
* Mark an object or array so that it is ignored by the reactivity system
|
||||||
|
*
|
||||||
|
* @param value the value to mark
|
||||||
|
* @returns the object itself
|
||||||
|
*/
|
||||||
|
export function markRaw<T extends Target>(value: T): T {
|
||||||
|
skipped.add(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a reactive objet, return the raw (non reactive) underlying object
|
||||||
|
*
|
||||||
|
* @param value a reactive value
|
||||||
|
* @returns the underlying value
|
||||||
|
*/
|
||||||
|
export function toRaw<T extends Target, U extends Reactive<T>>(value: U | T): T {
|
||||||
|
return targets.has(value) ? (targets.get(value) as T) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>();
|
||||||
|
/**
|
||||||
|
* Observes a given key on a target with an callback. The callback will be
|
||||||
|
* called when the given key changes on the target.
|
||||||
|
*
|
||||||
|
* @param target the target whose key should be observed
|
||||||
|
* @param key the key to observe (or Symbol(KEYCHANGES) for key creation
|
||||||
|
* or deletion)
|
||||||
|
* @param callback the function to call when the key changes
|
||||||
|
*/
|
||||||
|
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
|
||||||
|
if (callback === NO_CALLBACK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!targetToKeysToCallbacks.get(target)) {
|
||||||
|
targetToKeysToCallbacks.set(target, new Map());
|
||||||
|
}
|
||||||
|
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
|
||||||
|
if (!keyToCallbacks.get(key)) {
|
||||||
|
keyToCallbacks.set(key, new Set());
|
||||||
|
}
|
||||||
|
keyToCallbacks.get(key)!.add(callback);
|
||||||
|
if (!callbacksToTargets.has(callback)) {
|
||||||
|
callbacksToTargets.set(callback, new Set());
|
||||||
|
}
|
||||||
|
callbacksToTargets.get(callback)!.add(target);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Notify Reactives that are observing a given target that a key has changed on
|
||||||
|
* the target.
|
||||||
|
*
|
||||||
|
* @param target target whose Reactives should be notified that the target was
|
||||||
|
* changed.
|
||||||
|
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
||||||
|
* or deleted)
|
||||||
|
*/
|
||||||
|
function notifyReactives(target: Target, key: PropertyKey): void {
|
||||||
|
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
||||||
|
if (!keyToCallbacks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callbacks = keyToCallbacks.get(key);
|
||||||
|
if (!callbacks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Loop on copy because clearReactivesForCallback will modify the set in place
|
||||||
|
for (const callback of [...callbacks]) {
|
||||||
|
clearReactivesForCallback(callback);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
|
||||||
|
/**
|
||||||
|
* Clears all subscriptions of the Reactives associated with a given callback.
|
||||||
|
*
|
||||||
|
* @param callback the callback for which the reactives need to be cleared
|
||||||
|
*/
|
||||||
|
export function clearReactivesForCallback(callback: Callback): void {
|
||||||
|
const targetsToClear = callbacksToTargets.get(callback);
|
||||||
|
if (!targetsToClear) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const target of targetsToClear) {
|
||||||
|
const observedKeys = targetToKeysToCallbacks.get(target);
|
||||||
|
if (!observedKeys) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const [key, callbacks] of observedKeys.entries()) {
|
||||||
|
callbacks.delete(callback);
|
||||||
|
if (!callbacks.size) {
|
||||||
|
observedKeys.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetsToClear.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSubscriptions(callback: Callback) {
|
||||||
|
const targets = callbacksToTargets.get(callback) || [];
|
||||||
|
return [...targets].map((target) => {
|
||||||
|
const keysToCallbacks = targetToKeysToCallbacks.get(target);
|
||||||
|
let keys = [];
|
||||||
|
if (keysToCallbacks) {
|
||||||
|
for (const [key, cbs] of keysToCallbacks) {
|
||||||
|
if (cbs.has(callback)) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { target, keys };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Maps reactive objects to the underlying target
|
||||||
|
export const targets = new WeakMap<Reactive<Target>, Target>();
|
||||||
|
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>();
|
||||||
|
/**
|
||||||
|
* Creates a reactive proxy for an object. Reading data on the reactive object
|
||||||
|
* subscribes to changes to the data. Writing data on the object will cause the
|
||||||
|
* notify callback to be called if there are suscriptions to that data. Nested
|
||||||
|
* objects and arrays are automatically made reactive as well.
|
||||||
|
*
|
||||||
|
* Whenever you are notified of a change, all subscriptions are cleared, and if
|
||||||
|
* you would like to be notified of any further changes, you should go read
|
||||||
|
* the underlying data again. We assume that if you don't go read it again after
|
||||||
|
* being notified, it means that you are no longer interested in that data.
|
||||||
|
*
|
||||||
|
* Subscriptions:
|
||||||
|
* + Reading a property on an object will subscribe you to changes in the value
|
||||||
|
* of that property.
|
||||||
|
* + Accessing an object's keys (eg with Object.keys or with `for..in`) will
|
||||||
|
* subscribe you to the creation/deletion of keys. Checking the presence of a
|
||||||
|
* key on the object with 'in' has the same effect.
|
||||||
|
* - getOwnPropertyDescriptor does not currently subscribe you to the property.
|
||||||
|
* This is a choice that was made because changing a key's value will trigger
|
||||||
|
* this trap and we do not want to subscribe by writes. This also means that
|
||||||
|
* Object.hasOwnProperty doesn't subscribe as it goes through this trap.
|
||||||
|
*
|
||||||
|
* @param target the object for which to create a reactive proxy
|
||||||
|
* @param callback the function to call when an observed property of the
|
||||||
|
* reactive has changed
|
||||||
|
* @returns a proxy that tracks changes to it
|
||||||
|
*/
|
||||||
|
export function reactive<T extends Target>(target: T, callback: Callback = NO_CALLBACK): T {
|
||||||
|
if (!canBeMadeReactive(target)) {
|
||||||
|
throw new OwlError(`Cannot make the given value reactive`);
|
||||||
|
}
|
||||||
|
if (skipped.has(target)) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
if (targets.has(target)) {
|
||||||
|
// target is reactive, create a reactive on the underlying object instead
|
||||||
|
return reactive(targets.get(target) as T, callback);
|
||||||
|
}
|
||||||
|
if (!reactiveCache.has(target)) {
|
||||||
|
reactiveCache.set(target, new WeakMap());
|
||||||
|
}
|
||||||
|
const reactivesForTarget = reactiveCache.get(target)!;
|
||||||
|
if (!reactivesForTarget.has(callback)) {
|
||||||
|
const targetRawType = rawType(target);
|
||||||
|
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
||||||
|
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
||||||
|
: basicProxyHandler<T>(callback);
|
||||||
|
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||||
|
reactivesForTarget.set(callback, proxy);
|
||||||
|
targets.set(proxy, target);
|
||||||
|
}
|
||||||
|
return reactivesForTarget.get(callback) as Reactive<T>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a basic proxy handler for regular objects and arrays.
|
||||||
|
*
|
||||||
|
* @param callback @see reactive
|
||||||
|
* @returns a proxy handler object
|
||||||
|
*/
|
||||||
|
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> {
|
||||||
|
return {
|
||||||
|
get(target, key, receiver) {
|
||||||
|
// non-writable non-configurable properties cannot be made reactive
|
||||||
|
const desc = Object.getOwnPropertyDescriptor(target, key);
|
||||||
|
if (desc && !desc.writable && !desc.configurable) {
|
||||||
|
return Reflect.get(target, key, receiver);
|
||||||
|
}
|
||||||
|
observeTargetKey(target, key, callback);
|
||||||
|
return possiblyReactive(Reflect.get(target, key, receiver), callback);
|
||||||
|
},
|
||||||
|
set(target, key, value, receiver) {
|
||||||
|
const hadKey = objectHasOwnProperty.call(target, key);
|
||||||
|
const originalValue = Reflect.get(target, key, receiver);
|
||||||
|
const ret = Reflect.set(target, key, value, receiver);
|
||||||
|
if (!hadKey && objectHasOwnProperty.call(target, key)) {
|
||||||
|
notifyReactives(target, KEYCHANGES);
|
||||||
|
}
|
||||||
|
// While Array length may trigger the set trap, it's not actually set by this
|
||||||
|
// method but is updated behind the scenes, and the trap is not called with the
|
||||||
|
// new value. We disable the "same-value-optimization" for it because of that.
|
||||||
|
if (
|
||||||
|
originalValue !== Reflect.get(target, key, receiver) ||
|
||||||
|
(key === "length" && Array.isArray(target))
|
||||||
|
) {
|
||||||
|
notifyReactives(target, key);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
deleteProperty(target, key) {
|
||||||
|
const ret = Reflect.deleteProperty(target, key);
|
||||||
|
// TODO: only notify when something was actually deleted
|
||||||
|
notifyReactives(target, KEYCHANGES);
|
||||||
|
notifyReactives(target, key);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
ownKeys(target) {
|
||||||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||||||
|
return Reflect.ownKeys(target);
|
||||||
|
},
|
||||||
|
has(target, key) {
|
||||||
|
// TODO: this observes all key changes instead of only the presence of the argument key
|
||||||
|
// observing the key itself would observe value changes instead of presence changes
|
||||||
|
// so we may need a finer grained system to distinguish observing value vs presence.
|
||||||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||||||
|
return Reflect.has(target, key);
|
||||||
|
},
|
||||||
|
} as ProxyHandler<T>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a function that will observe the key that is passed to it when called
|
||||||
|
* and delegates to the underlying method.
|
||||||
|
*
|
||||||
|
* @param methodName name of the method to delegate to
|
||||||
|
* @param target @see reactive
|
||||||
|
* @param callback @see reactive
|
||||||
|
*/
|
||||||
|
function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callback) {
|
||||||
|
return (key: any) => {
|
||||||
|
key = toRaw(key);
|
||||||
|
observeTargetKey(target, key, callback);
|
||||||
|
return possiblyReactive(target[methodName](key), callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates an iterable that will delegate to the underlying iteration method and
|
||||||
|
* observe keys as necessary.
|
||||||
|
*
|
||||||
|
* @param methodName name of the method to delegate to
|
||||||
|
* @param target @see reactive
|
||||||
|
* @param callback @see reactive
|
||||||
|
*/
|
||||||
|
function makeIteratorObserver(
|
||||||
|
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
|
||||||
|
target: any,
|
||||||
|
callback: Callback
|
||||||
|
) {
|
||||||
|
return function* () {
|
||||||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||||||
|
const keys = target.keys();
|
||||||
|
for (const item of target[methodName]()) {
|
||||||
|
const key = keys.next().value;
|
||||||
|
observeTargetKey(target, key, callback);
|
||||||
|
yield possiblyReactive(item, callback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a forEach function that will delegate to forEach on the underlying
|
||||||
|
* collection while observing key changes, and keys as they're iterated over,
|
||||||
|
* and making the passed keys/values reactive.
|
||||||
|
*
|
||||||
|
* @param target @see reactive
|
||||||
|
* @param callback @see reactive
|
||||||
|
*/
|
||||||
|
function makeForEachObserver(target: any, callback: Callback) {
|
||||||
|
return function forEach(forEachCb: (val: any, key: any, target: any) => void, thisArg: any) {
|
||||||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||||||
|
target.forEach(function (val: any, key: any, targetObj: any) {
|
||||||
|
observeTargetKey(target, key, callback);
|
||||||
|
forEachCb.call(
|
||||||
|
thisArg,
|
||||||
|
possiblyReactive(val, callback),
|
||||||
|
possiblyReactive(key, callback),
|
||||||
|
possiblyReactive(targetObj, callback)
|
||||||
|
);
|
||||||
|
}, thisArg);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a function that will delegate to an underlying method, and check if
|
||||||
|
* that method has modified the presence or value of a key, and notify the
|
||||||
|
* reactives appropriately.
|
||||||
|
*
|
||||||
|
* @param setterName name of the method to delegate to
|
||||||
|
* @param getterName name of the method which should be used to retrieve the
|
||||||
|
* value before calling the delegate method for comparison purposes
|
||||||
|
* @param target @see reactive
|
||||||
|
*/
|
||||||
|
function delegateAndNotify(
|
||||||
|
setterName: "set" | "add" | "delete",
|
||||||
|
getterName: "has" | "get",
|
||||||
|
target: any
|
||||||
|
) {
|
||||||
|
return (key: any, value: any) => {
|
||||||
|
key = toRaw(key);
|
||||||
|
const hadKey = target.has(key);
|
||||||
|
const originalValue = target[getterName](key);
|
||||||
|
const ret = target[setterName](key, value);
|
||||||
|
const hasKey = target.has(key);
|
||||||
|
if (hadKey !== hasKey) {
|
||||||
|
notifyReactives(target, KEYCHANGES);
|
||||||
|
}
|
||||||
|
if (originalValue !== value) {
|
||||||
|
notifyReactives(target, key);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a function that will clear the underlying collection and notify that
|
||||||
|
* the keys of the collection have changed.
|
||||||
|
*
|
||||||
|
* @param target @see reactive
|
||||||
|
*/
|
||||||
|
function makeClearNotifier(target: Map<any, any> | Set<any>) {
|
||||||
|
return () => {
|
||||||
|
const allKeys = [...target.keys()];
|
||||||
|
target.clear();
|
||||||
|
notifyReactives(target, KEYCHANGES);
|
||||||
|
for (const key of allKeys) {
|
||||||
|
notifyReactives(target, key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Maps raw type of an object to an object containing functions that can be used
|
||||||
|
* to build an appropritate proxy handler for that raw type. Eg: when making a
|
||||||
|
* reactive set, calling the has method should mark the key that is being
|
||||||
|
* retrieved as observed, and calling the add or delete method should notify the
|
||||||
|
* reactives that the key which is being added or deleted has been modified.
|
||||||
|
*/
|
||||||
|
const rawTypeToFuncHandlers = {
|
||||||
|
Set: (target: any, callback: Callback) => ({
|
||||||
|
has: makeKeyObserver("has", target, callback),
|
||||||
|
add: delegateAndNotify("add", "has", target),
|
||||||
|
delete: delegateAndNotify("delete", "has", target),
|
||||||
|
keys: makeIteratorObserver("keys", target, callback),
|
||||||
|
values: makeIteratorObserver("values", target, callback),
|
||||||
|
entries: makeIteratorObserver("entries", target, callback),
|
||||||
|
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||||||
|
forEach: makeForEachObserver(target, callback),
|
||||||
|
clear: makeClearNotifier(target),
|
||||||
|
get size() {
|
||||||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||||||
|
return target.size;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Map: (target: any, callback: Callback) => ({
|
||||||
|
has: makeKeyObserver("has", target, callback),
|
||||||
|
get: makeKeyObserver("get", target, callback),
|
||||||
|
set: delegateAndNotify("set", "get", target),
|
||||||
|
delete: delegateAndNotify("delete", "has", target),
|
||||||
|
keys: makeIteratorObserver("keys", target, callback),
|
||||||
|
values: makeIteratorObserver("values", target, callback),
|
||||||
|
entries: makeIteratorObserver("entries", target, callback),
|
||||||
|
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||||||
|
forEach: makeForEachObserver(target, callback),
|
||||||
|
clear: makeClearNotifier(target),
|
||||||
|
get size() {
|
||||||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||||||
|
return target.size;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
WeakMap: (target: any, callback: Callback) => ({
|
||||||
|
has: makeKeyObserver("has", target, callback),
|
||||||
|
get: makeKeyObserver("get", target, callback),
|
||||||
|
set: delegateAndNotify("set", "get", target),
|
||||||
|
delete: delegateAndNotify("delete", "has", target),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Creates a proxy handler for collections (Set/Map/WeakMap)
|
||||||
|
*
|
||||||
|
* @param callback @see reactive
|
||||||
|
* @param target @see reactive
|
||||||
|
* @returns a proxy handler object
|
||||||
|
*/
|
||||||
|
function collectionsProxyHandler<T extends Collection>(
|
||||||
|
target: T,
|
||||||
|
callback: Callback,
|
||||||
|
targetRawType: CollectionRawType
|
||||||
|
): ProxyHandler<T> {
|
||||||
|
// TODO: if performance is an issue we can create the special handlers lazily when each
|
||||||
|
// property is read.
|
||||||
|
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
|
||||||
|
return Object.assign(basicProxyHandler(callback), {
|
||||||
|
// FIXME: probably broken when part of prototype chain since we ignore the receiver
|
||||||
|
get(target: any, key: PropertyKey) {
|
||||||
|
if (objectHasOwnProperty.call(specialHandlers, key)) {
|
||||||
|
return (specialHandlers as any)[key];
|
||||||
|
}
|
||||||
|
observeTargetKey(target, key, callback);
|
||||||
|
return possiblyReactive(target[key], callback);
|
||||||
|
},
|
||||||
|
}) as ProxyHandler<T>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { fibersInError } from "./error_handling";
|
||||||
|
import { Fiber, RootFiber } from "./fibers";
|
||||||
|
import { STATUS } from "./status";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Scheduler
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export class Scheduler {
|
||||||
|
// capture the value of requestAnimationFrame as soon as possible, to avoid
|
||||||
|
// interactions with other code, such as test frameworks that override them
|
||||||
|
static requestAnimationFrame = window.requestAnimationFrame.bind(window);
|
||||||
|
tasks: Set<RootFiber> = new Set();
|
||||||
|
requestAnimationFrame: Window["requestAnimationFrame"];
|
||||||
|
frame: number = 0;
|
||||||
|
delayedRenders: Fiber[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFiber(fiber: Fiber) {
|
||||||
|
this.tasks.add(fiber.root!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process all current tasks. This only applies to the fibers that are ready.
|
||||||
|
* Other tasks are left unchanged.
|
||||||
|
*/
|
||||||
|
flush() {
|
||||||
|
if (this.delayedRenders.length) {
|
||||||
|
let renders = this.delayedRenders;
|
||||||
|
this.delayedRenders = [];
|
||||||
|
for (let f of renders) {
|
||||||
|
if (f.root && f.node.status !== STATUS.DESTROYED && f.node.fiber === f) {
|
||||||
|
f.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.frame === 0) {
|
||||||
|
this.frame = this.requestAnimationFrame(() => {
|
||||||
|
this.frame = 0;
|
||||||
|
this.tasks.forEach((fiber) => this.processFiber(fiber));
|
||||||
|
for (let task of this.tasks) {
|
||||||
|
if (task.node.status === STATUS.DESTROYED) {
|
||||||
|
this.tasks.delete(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processFiber(fiber: RootFiber) {
|
||||||
|
if (fiber.root !== fiber) {
|
||||||
|
this.tasks.delete(fiber);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasError = fibersInError.has(fiber);
|
||||||
|
if (hasError && fiber.counter !== 0) {
|
||||||
|
this.tasks.delete(fiber);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fiber.node.status === STATUS.DESTROYED) {
|
||||||
|
this.tasks.delete(fiber);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fiber.counter === 0) {
|
||||||
|
if (!hasError) {
|
||||||
|
fiber.complete();
|
||||||
|
}
|
||||||
|
this.tasks.delete(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { BDom, multi, text, toggler, createCatcher } from "./blockdom";
|
||||||
|
import { Markup } from "./utils";
|
||||||
|
import { html } from "./blockdom/index";
|
||||||
|
import { isOptional, validateSchema } from "./validation";
|
||||||
|
import type { ComponentConstructor } from "./component";
|
||||||
|
import { markRaw } from "./reactivity";
|
||||||
|
import { OwlError } from "./error_handling";
|
||||||
|
import type { ComponentNode } from "./component_node";
|
||||||
|
|
||||||
|
const ObjectCreate = Object.create;
|
||||||
|
/**
|
||||||
|
* This file contains utility functions that will be injected in each template,
|
||||||
|
* to perform various useful tasks in the compiled code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function withDefault(value: any, defaultValue: any): any {
|
||||||
|
return value === undefined || value === null || value === false ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function callSlot(
|
||||||
|
ctx: any,
|
||||||
|
parent: any,
|
||||||
|
key: string,
|
||||||
|
name: string,
|
||||||
|
dynamic: boolean,
|
||||||
|
extra: any,
|
||||||
|
defaultContent?: (ctx: any, node: any, key: string) => BDom
|
||||||
|
): BDom {
|
||||||
|
key = key + "__slot_" + name;
|
||||||
|
const slots = ctx.props.slots || {};
|
||||||
|
const { __render, __ctx, __scope } = slots[name] || {};
|
||||||
|
const slotScope = ObjectCreate(__ctx || {});
|
||||||
|
if (__scope) {
|
||||||
|
slotScope[__scope] = extra;
|
||||||
|
}
|
||||||
|
const slotBDom = __render ? __render(slotScope, parent, key) : null;
|
||||||
|
if (defaultContent) {
|
||||||
|
let child1: BDom | undefined = undefined;
|
||||||
|
let child2: BDom | undefined = undefined;
|
||||||
|
if (slotBDom) {
|
||||||
|
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
|
||||||
|
} else {
|
||||||
|
child2 = defaultContent(ctx, parent, key);
|
||||||
|
}
|
||||||
|
return multi([child1, child2]);
|
||||||
|
}
|
||||||
|
return slotBDom || text("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function capture(ctx: any): any {
|
||||||
|
const result = ObjectCreate(ctx);
|
||||||
|
for (let k in ctx) {
|
||||||
|
result[k] = ctx[k];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withKey(elem: any, k: string) {
|
||||||
|
elem.key = k;
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareList(collection: any): [any[], any[], number, any[]] {
|
||||||
|
let keys: any[];
|
||||||
|
let values: any[];
|
||||||
|
|
||||||
|
if (Array.isArray(collection)) {
|
||||||
|
keys = collection;
|
||||||
|
values = collection;
|
||||||
|
} else if (collection) {
|
||||||
|
values = Object.keys(collection);
|
||||||
|
keys = Object.values(collection);
|
||||||
|
} else {
|
||||||
|
throw new OwlError("Invalid loop expression");
|
||||||
|
}
|
||||||
|
const n = values.length;
|
||||||
|
return [keys, values, n, new Array(n)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBoundary = Symbol("isBoundary");
|
||||||
|
|
||||||
|
function setContextValue(ctx: { [key: string]: any }, key: string, value: any): void {
|
||||||
|
const ctx0 = ctx;
|
||||||
|
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
|
||||||
|
const newCtx = ctx.__proto__;
|
||||||
|
if (!newCtx) {
|
||||||
|
ctx = ctx0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ctx = newCtx;
|
||||||
|
}
|
||||||
|
ctx[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber(val: string): number | string {
|
||||||
|
const n = parseFloat(val);
|
||||||
|
return isNaN(n) ? val : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shallowEqual(l1: any[], l2: any[]): boolean {
|
||||||
|
for (let i = 0, l = l1.length; i < l; i++) {
|
||||||
|
if (l1[i] !== l2[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LazyValue {
|
||||||
|
fn: any;
|
||||||
|
ctx: any;
|
||||||
|
component: any;
|
||||||
|
node: any;
|
||||||
|
key: any;
|
||||||
|
|
||||||
|
constructor(fn: any, ctx: any, component: any, node: any, key: any) {
|
||||||
|
this.fn = fn;
|
||||||
|
this.ctx = capture(ctx);
|
||||||
|
this.component = component;
|
||||||
|
this.node = node;
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate(): any {
|
||||||
|
return this.fn.call(this.component, this.ctx, this.node, this.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.evaluate().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Safely outputs `value` as a block depending on the nature of `value`
|
||||||
|
*/
|
||||||
|
export function safeOutput(value: any, defaultValue?: any): ReturnType<typeof toggler> {
|
||||||
|
if (value === undefined) {
|
||||||
|
return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
|
||||||
|
}
|
||||||
|
let safeKey;
|
||||||
|
let block;
|
||||||
|
switch (typeof value) {
|
||||||
|
case "object":
|
||||||
|
if (value instanceof Markup) {
|
||||||
|
safeKey = `string_safe`;
|
||||||
|
block = html(value as string);
|
||||||
|
} else if (value instanceof LazyValue) {
|
||||||
|
safeKey = `lazy_value`;
|
||||||
|
block = value.evaluate();
|
||||||
|
} else if (value instanceof String) {
|
||||||
|
safeKey = "string_unsafe";
|
||||||
|
block = text(value);
|
||||||
|
} else {
|
||||||
|
// Assuming it is a block
|
||||||
|
safeKey = "block_safe";
|
||||||
|
block = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
safeKey = "string_unsafe";
|
||||||
|
block = text(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
safeKey = "string_unsafe";
|
||||||
|
block = text(String(value));
|
||||||
|
}
|
||||||
|
return toggler(safeKey, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the component props (or next props) against the (static) props
|
||||||
|
* description. This is potentially an expensive operation: it may needs to
|
||||||
|
* visit recursively the props and all the children to check if they are valid.
|
||||||
|
* This is why it is only done in 'dev' mode.
|
||||||
|
*/
|
||||||
|
export function validateProps<P>(name: string | ComponentConstructor<P>, props: P, comp?: any) {
|
||||||
|
const ComponentClass =
|
||||||
|
typeof name !== "string"
|
||||||
|
? name
|
||||||
|
: (comp.constructor.components[name] as ComponentConstructor<P> | undefined);
|
||||||
|
|
||||||
|
if (!ComponentClass) {
|
||||||
|
// this is an error, wrong component. We silently return here instead so the
|
||||||
|
// error is triggered by the usual path ('component' function)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = ComponentClass.props;
|
||||||
|
if (!schema) {
|
||||||
|
if (comp.__owl__.app.warnIfNoStaticProps) {
|
||||||
|
console.warn(`Component '${ComponentClass.name}' does not have a static props description`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const defaultProps = ComponentClass.defaultProps;
|
||||||
|
if (defaultProps) {
|
||||||
|
let isMandatory = (name: string) =>
|
||||||
|
Array.isArray(schema)
|
||||||
|
? schema.includes(name)
|
||||||
|
: name in schema && !("*" in schema) && !isOptional(schema[name]);
|
||||||
|
for (let p in defaultProps) {
|
||||||
|
if (isMandatory(p)) {
|
||||||
|
throw new OwlError(
|
||||||
|
`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = validateSchema(props, schema);
|
||||||
|
if (errors.length) {
|
||||||
|
throw new OwlError(
|
||||||
|
`Invalid props for component '${ComponentClass.name}': ` + errors.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRefWrapper(node: ComponentNode) {
|
||||||
|
let refNames: Set<String> = new Set();
|
||||||
|
return (name: string, fn: Function) => {
|
||||||
|
if (refNames.has(name)) {
|
||||||
|
throw new OwlError(
|
||||||
|
`Cannot set the same ref more than once in the same component, ref "${name}" was set multiple times in ${node.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
refNames.add(name);
|
||||||
|
return fn;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const helpers = {
|
||||||
|
withDefault,
|
||||||
|
zero: Symbol("zero"),
|
||||||
|
isBoundary,
|
||||||
|
callSlot,
|
||||||
|
capture,
|
||||||
|
withKey,
|
||||||
|
prepareList,
|
||||||
|
setContextValue,
|
||||||
|
shallowEqual,
|
||||||
|
toNumber,
|
||||||
|
validateProps,
|
||||||
|
LazyValue,
|
||||||
|
safeOutput,
|
||||||
|
createCatcher,
|
||||||
|
markRaw,
|
||||||
|
OwlError,
|
||||||
|
makeRefWrapper,
|
||||||
|
};
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
|
import { compile, Template, TemplateFunction } from "../compiler";
|
||||||
import { compile, Template } from "../compiler";
|
import { comment, createBlock, html, list, multi, text, toggler } from "./blockdom";
|
||||||
import { component } from "../component/component_node";
|
import { getCurrent } from "./component_node";
|
||||||
import { UTILS } from "./template_helpers";
|
import { Portal, portalTemplate } from "./portal";
|
||||||
|
import { helpers } from "./template_helpers";
|
||||||
|
import { OwlError } from "./error_handling";
|
||||||
|
|
||||||
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
|
const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
||||||
|
|
||||||
export const globalTemplates: { [key: string]: string | Element } = {};
|
|
||||||
|
|
||||||
function parseXML(xml: string): Document {
|
function parseXML(xml: string): Document {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
@@ -32,7 +32,7 @@ function parseXML(xml: string): Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(msg);
|
throw new OwlError(msg);
|
||||||
}
|
}
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
@@ -45,18 +45,15 @@ export interface TemplateSetConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TemplateSet {
|
export class TemplateSet {
|
||||||
|
static registerTemplate(name: string, fn: TemplateFunction) {
|
||||||
|
globalTemplates[name] = fn;
|
||||||
|
}
|
||||||
dev: boolean;
|
dev: boolean;
|
||||||
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
||||||
templates: { [name: string]: Template } = {};
|
templates: { [name: string]: Template } = {};
|
||||||
translateFn?: (s: string) => string;
|
translateFn?: (s: string) => string;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
utils: typeof UTILS = Object.assign({}, UTILS, {
|
Portal = Portal;
|
||||||
call: (owner: any, subTemplate: string, ctx: any, parent: any, key: any) => {
|
|
||||||
const template = this.getTemplate(subTemplate);
|
|
||||||
return toggler(subTemplate, template.call(owner, ctx, parent, key));
|
|
||||||
},
|
|
||||||
getTemplate: (name: string) => this.getTemplate(name),
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(config: TemplateSetConfig = {}) {
|
constructor(config: TemplateSetConfig = {}) {
|
||||||
this.dev = config.dev || false;
|
this.dev = config.dev || false;
|
||||||
@@ -67,18 +64,25 @@ export class TemplateSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addTemplate(
|
addTemplate(name: string, template: string | Element) {
|
||||||
name: string,
|
if (name in this.rawTemplates) {
|
||||||
template: string | Element,
|
const rawTemplate = this.rawTemplates[name];
|
||||||
options: { allowDuplicate?: boolean } = {}
|
const currentAsString =
|
||||||
) {
|
typeof rawTemplate === "string"
|
||||||
if (name in this.rawTemplates && !options.allowDuplicate) {
|
? rawTemplate
|
||||||
throw new Error(`Template ${name} already defined`);
|
: rawTemplate instanceof Element
|
||||||
|
? rawTemplate.outerHTML
|
||||||
|
: rawTemplate.toString();
|
||||||
|
const newAsString = typeof template === "string" ? template : template.outerHTML;
|
||||||
|
if (currentAsString === newAsString) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new OwlError(`Template ${name} already defined with different content`);
|
||||||
}
|
}
|
||||||
this.rawTemplates[name] = template;
|
this.rawTemplates[name] = template;
|
||||||
}
|
}
|
||||||
|
|
||||||
addTemplates(xml: string | Document, options: { allowDuplicate?: boolean } = {}) {
|
addTemplates(xml: string | Document) {
|
||||||
if (!xml) {
|
if (!xml) {
|
||||||
// empty string
|
// empty string
|
||||||
return;
|
return;
|
||||||
@@ -86,7 +90,7 @@ export class TemplateSet {
|
|||||||
xml = xml instanceof Document ? xml : parseXML(xml);
|
xml = xml instanceof Document ? xml : parseXML(xml);
|
||||||
for (const template of xml.querySelectorAll("[t-name]")) {
|
for (const template of xml.querySelectorAll("[t-name]")) {
|
||||||
const name = template.getAttribute("t-name")!;
|
const name = template.getAttribute("t-name")!;
|
||||||
this.addTemplate(name, template, options);
|
this.addTemplate(name, template);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,34 +98,42 @@ export class TemplateSet {
|
|||||||
if (!(name in this.templates)) {
|
if (!(name in this.templates)) {
|
||||||
const rawTemplate = this.rawTemplates[name];
|
const rawTemplate = this.rawTemplates[name];
|
||||||
if (rawTemplate === undefined) {
|
if (rawTemplate === undefined) {
|
||||||
throw new Error(`Missing template: "${name}"`);
|
let extraInfo = "";
|
||||||
|
try {
|
||||||
|
const componentName = getCurrent().component.constructor.name;
|
||||||
|
extraInfo = ` (for component "${componentName}")`;
|
||||||
|
} catch {}
|
||||||
|
throw new OwlError(`Missing template: "${name}"${extraInfo}`);
|
||||||
}
|
}
|
||||||
const templateFn = this._compileTemplate(name, rawTemplate);
|
const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
|
||||||
|
const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
|
||||||
// first add a function to lazily get the template, in case there is a
|
// first add a function to lazily get the template, in case there is a
|
||||||
// recursive call to the template name
|
// recursive call to the template name
|
||||||
const templates = this.templates;
|
const templates = this.templates;
|
||||||
this.templates[name] = function (context, parent) {
|
this.templates[name] = function (context, parent) {
|
||||||
return templates[name].call(this, context, parent);
|
return templates[name].call(this, context, parent);
|
||||||
};
|
};
|
||||||
const template = templateFn(bdom, this.utils);
|
const template = templateFn(this, bdom, helpers);
|
||||||
this.templates[name] = template;
|
this.templates[name] = template;
|
||||||
}
|
}
|
||||||
return this.templates[name];
|
return this.templates[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
_compileTemplate(name: string, template: string | Element) {
|
_compileTemplate(name: string, template: string | Element): ReturnType<typeof compile> {
|
||||||
return compile(template, {
|
throw new OwlError(`Unable to compile a template. Please use owl full build instead`);
|
||||||
name,
|
}
|
||||||
dev: this.dev,
|
|
||||||
translateFn: this.translateFn,
|
callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any {
|
||||||
translatableAttributes: this.translatableAttributes,
|
const template = this.getTemplate(subTemplate);
|
||||||
});
|
return toggler(subTemplate, template.call(owner, ctx, parent, key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// xml tag helper
|
// xml tag helper
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
export const globalTemplates: { [key: string]: string | Element | TemplateFunction } = {};
|
||||||
|
|
||||||
export function xml(...args: Parameters<typeof String.raw>) {
|
export function xml(...args: Parameters<typeof String.raw>) {
|
||||||
const name = `__template__${xml.nextId++}`;
|
const name = `__template__${xml.nextId++}`;
|
||||||
const value = String.raw(...args);
|
const value = String.raw(...args);
|
||||||
@@ -130,3 +142,5 @@ export function xml(...args: Parameters<typeof String.raw>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
xml.nextId = 1;
|
xml.nextId = 1;
|
||||||
|
|
||||||
|
TemplateSet.registerTemplate("__portal__", portalTemplate);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OwlError } from "./error_handling";
|
||||||
export type Callback = () => void;
|
export type Callback = () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,22 +16,45 @@ export function batched(callback: Callback): Callback {
|
|||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
if (!called) {
|
if (!called) {
|
||||||
called = true;
|
called = true;
|
||||||
callback();
|
|
||||||
// wait for all calls in this microtick to fall through before resetting "called"
|
// wait for all calls in this microtick to fall through before resetting "called"
|
||||||
// so that only the first call to the batched function calls the original callback
|
// so that only the first call to the batched function calls the original callback.
|
||||||
await Promise.resolve();
|
// Schedule this before calling the callback so that calls to the batched function
|
||||||
called = false;
|
// within the callback will proceed only after resetting called to false, and have
|
||||||
|
// a chance to execute the callback again
|
||||||
|
Promise.resolve().then(() => (called = false));
|
||||||
|
callback();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateTarget(target: HTMLElement) {
|
/**
|
||||||
if (!(target instanceof HTMLElement)) {
|
* Determine whether the given element is contained in its ownerDocument:
|
||||||
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
* either directly or with a shadow root in between.
|
||||||
|
*/
|
||||||
|
export function inOwnerDocument(el?: HTMLElement) {
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (!document.body.contains(target)) {
|
if (el.ownerDocument.contains(el)) {
|
||||||
throw new Error("Cannot mount a component on a detached dom node");
|
return true;
|
||||||
}
|
}
|
||||||
|
const rootNode = el.getRootNode();
|
||||||
|
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
||||||
|
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||||
|
const document = target && target.ownerDocument;
|
||||||
|
if (document) {
|
||||||
|
const HTMLElement = document.defaultView!.HTMLElement;
|
||||||
|
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||||
|
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
||||||
|
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new OwlError("Cannot mount component: the target is not a valid DOM element");
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventBus extends EventTarget {
|
export class EventBus extends EventTarget {
|
||||||
@@ -52,7 +76,7 @@ export function whenReady(fn?: any): Promise<void> {
|
|||||||
export async function loadFile(url: string): Promise<string> {
|
export async function loadFile(url: string): Promise<string> {
|
||||||
const result = await fetch(url);
|
const result = await fetch(url);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new Error("Error while fetching xml templates");
|
throw new OwlError("Error while fetching xml templates");
|
||||||
}
|
}
|
||||||
return await result.text();
|
return await result.text();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { OwlError } from "./error_handling";
|
||||||
|
import { toRaw } from "./reactivity";
|
||||||
|
|
||||||
|
type BaseType =
|
||||||
|
| typeof String
|
||||||
|
| typeof Boolean
|
||||||
|
| typeof Number
|
||||||
|
| typeof Date
|
||||||
|
| typeof Object
|
||||||
|
| typeof Array
|
||||||
|
| true
|
||||||
|
| "*";
|
||||||
|
|
||||||
|
interface TypeInfo {
|
||||||
|
type?: TypeDescription;
|
||||||
|
optional?: boolean;
|
||||||
|
validate?: Function;
|
||||||
|
shape?: Schema;
|
||||||
|
element?: TypeDescription;
|
||||||
|
values?: TypeDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueType = { value: any };
|
||||||
|
|
||||||
|
type TypeDescription = BaseType | TypeInfo | ValueType | TypeDescription[];
|
||||||
|
type SimplifiedSchema = string[];
|
||||||
|
type NormalizedSchema = { [key: string]: TypeDescription };
|
||||||
|
export type Schema = SimplifiedSchema | NormalizedSchema;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
const isUnionType = (t: TypeDescription): t is TypeDescription[] => Array.isArray(t);
|
||||||
|
const isBaseType = (t: TypeDescription): t is BaseType => typeof t !== "object";
|
||||||
|
const isValueType = (t: TypeDescription): t is ValueType =>
|
||||||
|
typeof t === "object" && t && "value" in t;
|
||||||
|
|
||||||
|
export function isOptional(t: TypeDescription): Boolean {
|
||||||
|
return typeof t === "object" && "optional" in t ? t.optional || false : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeType(type: BaseType): string {
|
||||||
|
return type === "*" || type === true ? "value" : type.name.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function describe(info: TypeDescription): string {
|
||||||
|
if (isBaseType(info)) {
|
||||||
|
return describeType(info);
|
||||||
|
} else if (isUnionType(info)) {
|
||||||
|
return info.map(describe).join(" or ");
|
||||||
|
} else if (isValueType(info)) {
|
||||||
|
return String(info.value);
|
||||||
|
}
|
||||||
|
if ("element" in info) {
|
||||||
|
return `list of ${describe({ type: info.element, optional: false })}s`;
|
||||||
|
}
|
||||||
|
if ("shape" in (info as TypeInfo)) {
|
||||||
|
return `object`;
|
||||||
|
}
|
||||||
|
return describe(info.type || "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSchema(spec: SimplifiedSchema): NormalizedSchema {
|
||||||
|
return Object.fromEntries(
|
||||||
|
spec.map((e) =>
|
||||||
|
e.endsWith("?") ? [e.slice(0, -1), { optional: true }] : [e, { type: "*", optional: false }]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main validate function
|
||||||
|
*/
|
||||||
|
export function validate(obj: { [key: string]: any }, spec: Schema) {
|
||||||
|
let errors = validateSchema(obj, spec);
|
||||||
|
if (errors.length) {
|
||||||
|
throw new OwlError("Invalid object: " + errors.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper validate function, to get the list of errors. useful if one want to
|
||||||
|
* manipulate the errors without parsing an error object
|
||||||
|
*/
|
||||||
|
export function validateSchema(obj: { [key: string]: any }, schema: Schema): string[] {
|
||||||
|
if (Array.isArray(schema)) {
|
||||||
|
schema = toSchema(schema);
|
||||||
|
}
|
||||||
|
obj = toRaw(obj);
|
||||||
|
let errors = [];
|
||||||
|
// check if each value in obj has correct shape
|
||||||
|
for (let key in obj) {
|
||||||
|
if (key in schema) {
|
||||||
|
let result = validateType(key, obj[key], schema[key]);
|
||||||
|
if (result) {
|
||||||
|
errors.push(result);
|
||||||
|
}
|
||||||
|
} else if (!("*" in schema)) {
|
||||||
|
errors.push(`unknown key '${key}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check that all specified keys are defined in obj
|
||||||
|
for (let key in schema) {
|
||||||
|
const spec = schema[key];
|
||||||
|
if (key !== "*" && !isOptional(spec) && !(key in obj)) {
|
||||||
|
const isObj = typeof spec === "object" && !Array.isArray(spec);
|
||||||
|
const isAny = spec === "*" || (isObj && "type" in spec ? spec.type === "*" : isObj);
|
||||||
|
let detail = isAny ? "" : ` (should be a ${describe(spec)})`;
|
||||||
|
errors.push(`'${key}' is missing${detail}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBaseType(key: string, value: any, type: BaseType): string | null {
|
||||||
|
if (typeof type === "function") {
|
||||||
|
if (typeof value === "object") {
|
||||||
|
if (!(value instanceof type)) {
|
||||||
|
return `'${key}' is not a ${describeType(type)}`;
|
||||||
|
}
|
||||||
|
} else if (typeof value !== type.name.toLowerCase()) {
|
||||||
|
return `'${key}' is not a ${describeType(type)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateArrayType(key: string, value: any, descr: TypeDescription): string | null {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return `'${key}' is not a list of ${describe(descr)}s`;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const error = validateType(`${key}[${i}]`, value[i], descr);
|
||||||
|
if (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateType(key: string, value: any, descr: TypeDescription): string | null {
|
||||||
|
if (value === undefined) {
|
||||||
|
return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
|
||||||
|
} else if (isBaseType(descr)) {
|
||||||
|
return validateBaseType(key, value, descr);
|
||||||
|
} else if (isValueType(descr)) {
|
||||||
|
return value === descr.value ? null : `'${key}' is not equal to '${descr.value}'`;
|
||||||
|
} else if (isUnionType(descr)) {
|
||||||
|
let validDescr = descr.find((p) => !validateType(key, value, p));
|
||||||
|
return validDescr ? null : `'${key}' is not a ${describe(descr)}`;
|
||||||
|
}
|
||||||
|
let result: string | null = null;
|
||||||
|
if ("element" in descr) {
|
||||||
|
result = validateArrayType(key, value, descr.element!);
|
||||||
|
} else if ("shape" in descr) {
|
||||||
|
if (typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
result = `'${key}' is not an object`;
|
||||||
|
} else {
|
||||||
|
const errors = validateSchema(value, descr.shape!);
|
||||||
|
if (errors.length) {
|
||||||
|
result = `'${key}' doesn't have the correct shape (${errors.join(", ")})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("values" in descr) {
|
||||||
|
if (typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
result = `'${key}' is not an object`;
|
||||||
|
} else {
|
||||||
|
const errors = Object.entries(value)
|
||||||
|
.map(([key, value]) => validateType(key, value, descr.values!))
|
||||||
|
.filter(Boolean);
|
||||||
|
if (errors.length) {
|
||||||
|
result = `some of the values in '${key}' are invalid (${errors.join(", ")})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("type" in descr && !result) {
|
||||||
|
result = validateType(key, value, descr.type!);
|
||||||
|
}
|
||||||
|
if ("validate" in descr && !result) {
|
||||||
|
result = !descr.validate!(value) ? `'${key}' is not valid` : null;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// do not modify manually. This file is generated by the release script.
|
||||||
|
export const version = "2.1.1";
|
||||||
@@ -47,16 +47,17 @@ exports[`Reactivity: useState concurrent renderings 3`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
|
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let b2;
|
let b2;
|
||||||
if (ctx['state'].flag) {
|
if (ctx['state'].flag) {
|
||||||
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
}
|
}
|
||||||
return block1([], [b2]);
|
return block1([], [b2]);
|
||||||
}
|
}
|
||||||
@@ -64,9 +65,9 @@ exports[`Reactivity: useState destroyed component before being mounted is inacti
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState destroyed component before being mounted is inactive 2`] = `
|
exports[`Reactivity: useState destroyed component before being mounted is inactive 2`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||||
|
|
||||||
@@ -78,16 +79,17 @@ exports[`Reactivity: useState destroyed component before being mounted is inacti
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState destroyed component is inactive 1`] = `
|
exports[`Reactivity: useState destroyed component is inactive 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let b2;
|
let b2;
|
||||||
if (ctx['state'].flag) {
|
if (ctx['state'].flag) {
|
||||||
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
}
|
}
|
||||||
return block1([], [b2]);
|
return block1([], [b2]);
|
||||||
}
|
}
|
||||||
@@ -95,9 +97,9 @@ exports[`Reactivity: useState destroyed component is inactive 1`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState destroyed component is inactive 2`] = `
|
exports[`Reactivity: useState destroyed component is inactive 2`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||||
|
|
||||||
@@ -109,9 +111,9 @@ exports[`Reactivity: useState destroyed component is inactive 2`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState one components can subscribe twice to same context 1`] = `
|
exports[`Reactivity: useState one components can subscribe twice to same context 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
|
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
|
||||||
|
|
||||||
@@ -124,14 +126,15 @@ exports[`Reactivity: useState one components can subscribe twice to same context
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState parent and children subscribed to same context 1`] = `
|
exports[`Reactivity: useState parent and children subscribed to same context 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
let txt1 = ctx['contextObj'].b;
|
let txt1 = ctx['contextObj'].b;
|
||||||
return block1([txt1], [b2]);
|
return block1([txt1], [b2]);
|
||||||
}
|
}
|
||||||
@@ -139,9 +142,9 @@ exports[`Reactivity: useState parent and children subscribed to same context 1`]
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState parent and children subscribed to same context 2`] = `
|
exports[`Reactivity: useState parent and children subscribed to same context 2`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||||
|
|
||||||
@@ -216,24 +219,26 @@ exports[`Reactivity: useState several nodes on different level use same context
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState two components are updated in parallel 1`] = `
|
exports[`Reactivity: useState two components are updated in parallel 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
const comp2 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
let b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
|
const b3 = comp2({}, key + \`__2\`, node, this, null);
|
||||||
return block1([], [b2, b3]);
|
return block1([], [b2, b3]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState two components are updated in parallel 2`] = `
|
exports[`Reactivity: useState two components are updated in parallel 2`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||||
|
|
||||||
@@ -245,24 +250,26 @@ exports[`Reactivity: useState two components are updated in parallel 2`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState two components can subscribe to same context 1`] = `
|
exports[`Reactivity: useState two components can subscribe to same context 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
const comp2 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
let b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
|
const b3 = comp2({}, key + \`__2\`, node, this, null);
|
||||||
return block1([], [b2, b3]);
|
return block1([], [b2, b3]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState two components can subscribe to same context 2`] = `
|
exports[`Reactivity: useState two components can subscribe to same context 2`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||||
|
|
||||||
@@ -274,24 +281,26 @@ exports[`Reactivity: useState two components can subscribe to same context 2`] =
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState two independent components on different levels are updated in parallel 1`] = `
|
exports[`Reactivity: useState two independent components on different levels are updated in parallel 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
const comp2 = app.createComponent(\`Parent\`, true, false, false, []);
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
let b3 = component(\`Parent\`, {}, key + \`__2\`, node, ctx);
|
const b3 = comp2({}, key + \`__2\`, node, this, null);
|
||||||
return block1([], [b2, b3]);
|
return block1([], [b2, b3]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState two independent components on different levels are updated in parallel 2`] = `
|
exports[`Reactivity: useState two independent components on different levels are updated in parallel 2`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||||
|
|
||||||
@@ -303,23 +312,24 @@ exports[`Reactivity: useState two independent components on different levels are
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState two independent components on different levels are updated in parallel 3`] = `
|
exports[`Reactivity: useState two independent components on different levels are updated in parallel 3`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
return block1([], [b2]);
|
return block1([], [b2]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState useContext=useState hook is reactive, for one component 1`] = `
|
exports[`Reactivity: useState useContext=useState hook is reactive, for one component 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||||
|
|
||||||
@@ -331,23 +341,24 @@ exports[`Reactivity: useState useContext=useState hook is reactive, for one comp
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState useless atoms should be deleted 1`] = `
|
exports[`Reactivity: useState useless atoms should be deleted 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
let { prepareList, withKey } = helpers;
|
let { prepareList, withKey } = helpers;
|
||||||
|
const comp1 = app.createComponent(\`Quantity\`, true, false, false, [\\"id\\"]);
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/> Total: <block-text-0/> Count: <block-text-1/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/> Total: <block-text-0/> Count: <block-text-1/></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList(Object.keys(ctx['state']));
|
const [k_block2, v_block2, l_block2, c_block2] = prepareList(Object.keys(ctx['state']));;
|
||||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||||
ctx[\`id\`] = v_block2[i1];
|
ctx[\`id\`] = v_block2[i1];
|
||||||
let key1 = ctx['id'];
|
const key1 = ctx['id'];
|
||||||
c_block2[i1] = withKey(component(\`Quantity\`, {id: ctx['id']}, key + \`__1__\${key1}\`, node, ctx), key1);
|
c_block2[i1] = withKey(comp1({id: ctx['id']}, key + \`__1__\${key1}\`, node, this, null), key1);
|
||||||
}
|
}
|
||||||
ctx = ctx.__proto__;
|
ctx = ctx.__proto__;
|
||||||
let b2 = list(c_block2);
|
const b2 = list(c_block2);
|
||||||
let txt1 = ctx['total'];
|
let txt1 = ctx['total'];
|
||||||
let txt2 = Object.keys(ctx['state']).length;
|
let txt2 = Object.keys(ctx['state']).length;
|
||||||
return block1([txt1, txt2], [b2]);
|
return block1([txt1, txt2], [b2]);
|
||||||
@@ -356,9 +367,9 @@ exports[`Reactivity: useState useless atoms should be deleted 1`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState useless atoms should be deleted 2`] = `
|
exports[`Reactivity: useState useless atoms should be deleted 2`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||||
|
|
||||||
@@ -370,9 +381,9 @@ exports[`Reactivity: useState useless atoms should be deleted 2`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState very simple use, with initial value 1`] = `
|
exports[`Reactivity: useState very simple use, with initial value 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`app App supports env with getters/setters 1`] = `
|
exports[`app App supports env with getters/setters 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
|
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@ exports[`app App supports env with getters/setters 1`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`app can configure an app with props 1`] = `
|
exports[`app can configure an app with props 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||||
|
|
||||||
@@ -29,10 +29,23 @@ exports[`app can configure an app with props 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`app destroy remove the widget from the DOM 1`] = `
|
exports[`app can mount app in an iframe 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`app destroy remove the widget from the DOM 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
let block1 = createBlock(\`<div/>\`);
|
let block1 = createBlock(\`<div/>\`);
|
||||||
|
|
||||||
@@ -41,3 +54,17 @@ exports[`app destroy remove the widget from the DOM 1`] = `
|
|||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`app warnIfNoStaticProps works as expected 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let txt1 = ctx['message'];
|
||||||
|
return block1([txt1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { App, Component, xml } from "../../src";
|
import { App, Component, mount, xml } from "../../src";
|
||||||
import { status } from "../../src/component/status";
|
import { status } from "../../src/runtime/status";
|
||||||
import { makeTestFixture, snapshotEverything, nextTick, elem } from "../helpers";
|
import { makeTestFixture, snapshotEverything, nextTick, elem } from "../helpers";
|
||||||
|
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -59,4 +59,39 @@ describe("app", () => {
|
|||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div>333</div>");
|
expect(fixture.innerHTML).toBe("<div>333</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("warnIfNoStaticProps works as expected", async () => {
|
||||||
|
let originalconsoleWarn = console.warn;
|
||||||
|
let mockConsoleWarn = jest.fn(() => {});
|
||||||
|
console.warn = mockConsoleWarn;
|
||||||
|
|
||||||
|
class Root extends Component {
|
||||||
|
static template = xml`<div t-esc="message"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Root, fixture, { test: true, props: { messge: "hey" }, warnIfNoStaticProps: true });
|
||||||
|
|
||||||
|
console.warn = originalconsoleWarn;
|
||||||
|
expect(mockConsoleWarn).toBeCalledWith(
|
||||||
|
"Component 'Root' does not have a static props description"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can mount app in an iframe", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
fixture.appendChild(iframe);
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const iframeDoc = iframe.contentDocument!;
|
||||||
|
const comp = await app.mount(iframeDoc.body);
|
||||||
|
const div = iframeDoc.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(iframeDoc.contains(div)).toBe(true);
|
||||||
|
app.destroy();
|
||||||
|
expect(iframeDoc.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createBlock, mount, patch, remove, text } from "../../src/blockdom";
|
import { createBlock, mount, patch, remove, text } from "../../src/runtime/blockdom";
|
||||||
import { makeTestFixture } from "./helpers";
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mount, patch, createBlock } from "../../src/blockdom";
|
import { mount, patch, createBlock } from "../../src/runtime/blockdom";
|
||||||
import { makeTestFixture } from "./helpers";
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -145,38 +145,3 @@ test("class attribute (with a preexisting value", async () => {
|
|||||||
patch(tree, block([""]));
|
patch(tree, block([""]));
|
||||||
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
|
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("properties", () => {
|
|
||||||
test("input with value attribute", () => {
|
|
||||||
// render input with initial value
|
|
||||||
const block = createBlock(`<input block-attribute-0="value"/>`);
|
|
||||||
|
|
||||||
const tree = block(["zucchini"]);
|
|
||||||
mount(tree, fixture);
|
|
||||||
// const bnode1 = renderToBdom(template, { v: "zucchini" });
|
|
||||||
// const fixture = makeTestFixture();
|
|
||||||
// mount(bnode1, fixture);
|
|
||||||
const input = fixture.querySelector("input")!;
|
|
||||||
expect(input.value).toBe("zucchini");
|
|
||||||
|
|
||||||
// change value manually in input, to simulate user input
|
|
||||||
input.value = "tomato";
|
|
||||||
expect(input.value).toBe("tomato");
|
|
||||||
|
|
||||||
// rerender with a different value, and patch actual dom, to check that
|
|
||||||
// input value was properly reset by owl
|
|
||||||
patch(tree, block(["potato"]));
|
|
||||||
expect(input.value).toBe("potato");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("input type=checkbox with checked attribute", () => {
|
|
||||||
// render input with initial value
|
|
||||||
const block = createBlock(`<input type="checkbox" block-attribute-0="checked"/>`);
|
|
||||||
|
|
||||||
const tree = block([true]);
|
|
||||||
mount(tree, fixture);
|
|
||||||
expect(fixture.innerHTML).toBe(`<input type="checkbox">`);
|
|
||||||
const input = fixture.querySelector("input")!;
|
|
||||||
expect(input.checked).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mount, createBlock, multi, config, patch } from "../../src/blockdom";
|
import { mount, createBlock, multi, config, patch } from "../../src/runtime/blockdom";
|
||||||
// import { defaultHandler, setupMainHandler } from "../../src/bdom/block";
|
// import { defaultHandler, setupMainHandler } from "../../src/bdom/block";
|
||||||
import { makeTestFixture } from "./helpers";
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { mount, patch, createBlock } from "../../src/runtime/blockdom";
|
||||||
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("input with value property", () => {
|
||||||
|
// render input with initial value
|
||||||
|
const block = createBlock(`<input block-property-0="value"/>`);
|
||||||
|
|
||||||
|
const tree = block(["zucchini"]);
|
||||||
|
mount(tree, fixture);
|
||||||
|
// const bnode1 = renderToBdom(template, { v: "zucchini" });
|
||||||
|
// const fixture = makeTestFixture();
|
||||||
|
// mount(bnode1, fixture);
|
||||||
|
const input = fixture.querySelector("input")!;
|
||||||
|
expect(input.value).toBe("zucchini");
|
||||||
|
|
||||||
|
// change value manually in input, to simulate user input
|
||||||
|
input.value = "tomato";
|
||||||
|
expect(input.value).toBe("tomato");
|
||||||
|
|
||||||
|
// rerender with a different value, and patch actual dom, to check that
|
||||||
|
// input value was properly reset by owl
|
||||||
|
patch(tree, block(["potato"]));
|
||||||
|
expect(input.value).toBe("potato");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("input with value property, and falsy value given", () => {
|
||||||
|
const block = createBlock(`<input block-property-0="value"/>`);
|
||||||
|
|
||||||
|
const tree = block([undefined]);
|
||||||
|
mount(tree, fixture);
|
||||||
|
const input = fixture.querySelector("input")!;
|
||||||
|
expect(input.value).toBe("");
|
||||||
|
|
||||||
|
patch(tree, block([null]));
|
||||||
|
expect(input.value).toBe("");
|
||||||
|
|
||||||
|
patch(tree, block([0]));
|
||||||
|
expect(input.value).toBe("0");
|
||||||
|
|
||||||
|
patch(tree, block([""]));
|
||||||
|
expect(input.value).toBe("");
|
||||||
|
|
||||||
|
patch(tree, block([false]));
|
||||||
|
expect(input.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("input type=checkbox with checked property", () => {
|
||||||
|
// render input with initial value
|
||||||
|
const block = createBlock(`<input type="checkbox" block-property-0="checked"/>`);
|
||||||
|
|
||||||
|
const tree = block([true]);
|
||||||
|
mount(tree, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(`<input type="checkbox">`);
|
||||||
|
const input = fixture.querySelector("input")!;
|
||||||
|
expect(input.checked).toBe(true);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createBlock, mount, patch, remove } from "../../src/blockdom";
|
import { createBlock, mount, patch, remove } from "../../src/runtime/blockdom";
|
||||||
import { logStep } from "../helpers";
|
import { logStep } from "../helpers";
|
||||||
import { makeTestFixture } from "./helpers";
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { comment, mount } from "../../src/blockdom";
|
import { comment, mount } from "../../src/runtime/blockdom";
|
||||||
import { makeTestFixture } from "./helpers";
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { config, createBlock, createCatcher, mount } from "../../src/runtime/blockdom";
|
||||||
|
import { makeTestFixture } from "./helpers";
|
||||||
|
import { mainEventHandler } from "../../src/runtime/event_handling";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
config.mainEventHandler = mainEventHandler;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple event catcher", async () => {
|
||||||
|
const catcher = createCatcher({ click: 0 });
|
||||||
|
const block = createBlock("<div></div>");
|
||||||
|
let n = 0;
|
||||||
|
let ctx = {};
|
||||||
|
let handler = [() => n++, ctx];
|
||||||
|
const tree = catcher(block(), [handler]);
|
||||||
|
|
||||||
|
mount(tree, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
|
|
||||||
|
expect(fixture.firstChild).toBeInstanceOf(HTMLDivElement);
|
||||||
|
expect(n).toBe(0);
|
||||||
|
(fixture.firstChild as HTMLDivElement).click();
|
||||||
|
expect(n).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("do not catch events outside of itself", async () => {
|
||||||
|
const catcher = createCatcher({ click: 0 });
|
||||||
|
const childBlock = createBlock("<div></div>");
|
||||||
|
const parentBlock = createBlock("<button><block-child-0/></button>");
|
||||||
|
let n = 0;
|
||||||
|
let ctx = {};
|
||||||
|
let handler = [() => n++, ctx];
|
||||||
|
const tree = parentBlock([], [catcher(childBlock(), [handler])]);
|
||||||
|
|
||||||
|
mount(tree, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<button><div></div></button>");
|
||||||
|
|
||||||
|
expect(n).toBe(0);
|
||||||
|
fixture.querySelector("div")!.click();
|
||||||
|
expect(n).toBe(1);
|
||||||
|
fixture.querySelector("button")!.click();
|
||||||
|
expect(n).toBe(1);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { html, mount, patch, text } from "../../src/blockdom";
|
import { html, mount, patch, text } from "../../src/runtime/blockdom";
|
||||||
import { makeTestFixture } from "./helpers";
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { list, mount, multi, patch, text, createBlock, VNode, withKey } from "../../src/blockdom";
|
import {
|
||||||
|
list,
|
||||||
|
mount,
|
||||||
|
multi,
|
||||||
|
patch,
|
||||||
|
text,
|
||||||
|
createBlock,
|
||||||
|
VNode,
|
||||||
|
withKey,
|
||||||
|
} from "../../src/runtime/blockdom";
|
||||||
import { makeTestFixture } from "./helpers";
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mount, multi, patch, text, createBlock } from "../../src/blockdom";
|
import { mount, multi, patch, text, createBlock } from "../../src/runtime/blockdom";
|
||||||
import { makeTestFixture } from "./helpers";
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createBlock, mount } from "../../src/blockdom";
|
import { createBlock, mount } from "../../src/runtime/blockdom";
|
||||||
import { makeTestFixture } from "./helpers";
|
import { makeTestFixture } from "./helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||