Compare commits

..

162 Commits

Author SHA1 Message Date
Géry Debongnie 96de801fa5 [IMP] playground: update code to last version of master
The release script only update the owl code itself, not the playground
app code.
2023-04-17 12:59:34 +02:00
Géry Debongnie 930f8e3343 [IMP] update owl to v2.1.1 2023-04-17 11:09:50 +02:00
Géry Debongnie e5a08024cc [IMP] update owl to v2.0.9 2023-03-13 10:55:36 +01:00
Géry Debongnie 29fe04e645 [IMP] update owl to v2.0.8 2023-03-09 13:11:52 +01:00
Géry Debongnie c63cda8ec1 [IMP] update owl to v2.0.7 2023-02-20 09:45:04 +01:00
Géry Debongnie 32849c98dd [IMP] update owl to v2.0.6 2023-02-17 14:31:42 +01:00
Samuel Degueldre abd704f6fd [IMP] update owl to v2.0.5 2023-01-27 15:29:41 +01:00
Samuel Degueldre 3925e81b05 [IMP] update owl to v2.0.4 2023-01-23 12:26:16 +01:00
Géry Debongnie 2285e0d60c [IMP] update owl to v2.0.3 2023-01-12 16:29:42 +01:00
Géry Debongnie f8cc9afd69 [IMP] update owl to v2.0.0 2022-10-07 15:28:25 +02:00
Géry Debongnie 328f320bc5 [IMP] update owl to v2.0.0-beta-22 2022-09-29 09:17:32 +02:00
Géry Debongnie 4ada971c51 [IMP] update owl to v2.0.0-beta-21 2022-09-26 15:49:38 +02:00
Samuel Degueldre d7552f1900 [IMP] update example to owl 2 and mount it in the page 2022-09-09 09:51:28 +02:00
Géry Debongnie 60b01afee3 [IMP] update owl to v2.0.0-beta-20 2022-09-09 09:26:38 +02:00
Géry Debongnie 68541db5bd [IMP] update owl to v2.0.0-beta-19 2022-09-06 12:13:50 +02:00
Géry Debongnie 2796ea0341 [IMP] update owl to v2.0.0-beta-18 2022-09-02 14:57:44 +02:00
Géry Debongnie 3f1c1ea6a4 [IMP] update owl to v2.0.0-beta-17 2022-09-01 15:41:58 +02:00
Géry Debongnie c64086d9b6 [IMP] update owl to v2.0.0-beta-16 2022-07-22 09:44:11 +02:00
Géry Debongnie 4855977e7d [IMP] update owl to v2.0.0-beta-15 2022-07-20 10:02:51 +02:00
Géry Debongnie 196a0d3c8c [IMP] update owl to v2.0.0-beta-14 2022-07-08 16:12:07 +02:00
Géry Debongnie 8956aded5b [IMP] update owl to v2.0.0-beta-12 2022-06-29 11:13:31 +02:00
Géry Debongnie ca71e1b859 [IMP] update owl to v2.0.0-beta-11 2022-06-28 15:28:45 +02:00
Géry Debongnie 473f3f890f [IMP] update owl to v2.0.0-beta-10 2022-06-22 10:33:42 +02:00
Samuel Degueldre 97fb38eea5 [IMP] update owl to v2.0.0-beta-7 2022-04-27 11:08:59 +02:00
Samuel Degueldre db41b28d9c [IMP] update owl to v2.0.0-beta.3 2022-03-08 12:48:05 +01:00
Géry Debongnie 9d3ef2b541 [IMP] update owl to v2.0.0-beta.2 2022-03-03 16:22:30 +01:00
Géry Debongnie c1bc800fd2 [IMP] update owl to v2.0.0-beta.1 2022-03-02 13:29:27 +01:00
Géry Debongnie 7a3e50c86f [IMP] update owl to v2.0.0-alpha.3 2022-02-25 10:57:58 +01:00
Géry Debongnie d4267b55be [FIX] update playground code to last version 2022-02-14 13:59:05 +01:00
Géry Debongnie 78ba72d075 [IMP] update owl to v2.0.0-alpha.2 2022-02-14 13:47:48 +01:00
Samuel Degueldre 20b4c56c51 [IMP] update owl to v1.4.10 2021-12-07 15:32:38 +01:00
Samuel Degueldre c313ab9dd5 [IMP] update owl to v1.4.9 2021-12-07 10:32:08 +01:00
Géry Debongnie 2a8f5e95d2 [IMP] update owl to v1.4.8 2021-11-03 13:45:14 +01:00
Géry Debongnie 410388bece [IMP] update owl to v1.4.7 2021-10-19 16:48:12 +02:00
Géry Debongnie 9aba2cb8e1 [IMP] update owl to v1.4.6 2021-10-04 15:16:29 +02:00
Géry Debongnie 6bd4b66aff [IMP] update owl to v1.4.5 2021-09-22 16:34:24 +02:00
Géry Debongnie 9acedea612 [IMP] update owl to v1.4.4 2021-09-03 09:57:23 +02:00
Géry Debongnie eb1243d407 [IMP] update owl to v1.4.3 2021-07-08 11:54:58 +02:00
Géry Debongnie 8ac23e44a6 [IMP] update owl to v1.4.2 2021-07-07 14:57:13 +02:00
Géry Debongnie 93b4fc2c34 [IMP] update owl to v1.4.1 2021-07-07 11:11:07 +02:00
Géry Debongnie 0f4e20367c [IMP] update owl to v1.4.0 2021-07-06 10:28:21 +02:00
Géry Debongnie 2a53139313 [IMP] update owl to v1.3.2 2021-06-18 10:42:18 +02:00
Géry Debongnie c4474d5295 [IMP] update owl to v1.3.1 2021-06-10 10:37:24 +02:00
Géry Debongnie 5674866fbe [IMP] update owl to v1.3.0 2021-06-04 14:19:35 +02:00
Géry Debongnie 23679373b4 [IMP] update owl to v1.2.5 2021-05-18 10:01:54 +02:00
Géry Debongnie ce11bbba0a [IMP] owl: update to v1.2.1 2021-01-08 15:33:03 +01:00
Géry Debongnie bfcc27a356 [IMP] owl: update to v1.0.10 2020-09-21 16:19:11 +02:00
Géry Debongnie 82c6513d42 [IMP] owl: update to v1.0.5 2020-02-21 09:53:08 +01:00
Géry Debongnie bf2c2bff55 [IMP] owl: update to v1.0.0 2020-01-09 14:50:10 +01:00
Géry Debongnie 7aed9a6f82 [IMP] owl: update to v1.0.0-beta5 2019-12-20 11:29:34 +01:00
Géry Debongnie 7ee0f5e128 [IMP] owl: update to v1.0.0-beta4 2019-12-17 15:32:47 +01:00
Géry Debongnie 94461320aa [IMP] owl: update to v1.0.0-beta3 2019-12-16 10:04:15 +01:00
Géry Debongnie d66f5baead [IMP] owl: update to v1.0.0-beta2 2019-12-11 10:47:13 +01:00
Géry Debongnie 6d8c1cb987 [IMP] owl: update to v1.0.0-beta1 2019-12-03 09:02:11 +01:00
Géry Debongnie bf3f0ff91c [IMP] owl: update to v1.0.0alpha5 2019-11-29 16:19:27 +01:00
Géry Debongnie 583daa7902 [IMP] owl: update to v1.0.0-alpha4 2019-11-22 13:13:05 +01:00
Géry Debongnie 4c2bb7c0fc [IMP] owl: update to v1.0.0-alpha3 2019-11-13 15:46:19 +01:00
Géry Debongnie 573e379af4 [IMP] owl: update to v1.0.0-alpha2 2019-11-01 09:16:27 +01:00
Géry Debongnie 71a6091772 [IMP] owl: update to v1.0.0-alpha 2019-10-30 16:45:37 +01:00
Géry Debongnie d6aad9c673 [IMP] owl: update to v0.24.0 2019-10-25 17:11:09 +02:00
Pierre Rousseau 79a6891679 [IMP] owl: update to a3317ab997 2019-10-24 09:34:48 +02:00
Géry Debongnie d6e0a9dcba [IMP] owl: update to v0.23.0 2019-10-18 16:03:25 +02:00
Géry Debongnie 1571a16cfd [IMP] owl: update to v0.23.0 2019-10-16 22:42:20 +02:00
Géry Debongnie 58d03f6077 [FIX] index page had wrong information 2019-10-02 22:50:06 +02:00
Géry Debongnie 599492fa4b [IMP] owl: update to last version 2019-10-01 21:38:29 +02:00
Géry Debongnie b4eccb5149 [IMP] owl: update to v0.22.0 2019-10-01 21:15:14 +02:00
Géry Debongnie 65ec12532f [FIX] add missing TEMPLATES in example 2019-10-01 20:42:36 +02:00
Géry Debongnie 6ee9090174 [IMP] update main example 2019-10-01 18:41:37 +02:00
Géry Debongnie a1afcd21e8 [IMP] owl: update to v0.21.0 2019-09-12 14:42:32 +02:00
Géry Debongnie 6e818f9aa1 [IMP] owl: update to v0.20.0 2019-09-05 14:51:20 +02:00
Géry Debongnie 507163d708 [IMP] owl: update to v0.19.0 2019-08-28 12:02:00 +02:00
Géry Debongnie acc85a52fc [IMP] owl: update to v0.18.0 2019-08-22 15:05:45 +02:00
Géry Debongnie ef2268d601 [IMP] owl: update to v0.17.0 2019-07-13 17:17:49 +02:00
Géry Debongnie eccd1d4a93 [IMP] owl: update to v0.16.0 2019-06-28 11:41:03 +02:00
Pierre Paridans 4b86dfb4dd [IMP] pages: Simple and clean layout for the landing page 2019-06-27 16:43:44 +02:00
Géry Debongnie b7e377d815 [IMP] owl: update to last version 2019-06-21 21:47:49 +02:00
Géry Debongnie d746e01926 [IMP] update to last version 2019-06-18 15:03:22 +02:00
Géry Debongnie dc3a7a11e4 [FIX] samples: update to get fixed code 2019-06-17 16:17:12 +02:00
Géry Debongnie d7430e9f18 [IMP] owl: update to v0.15.0 2019-06-17 16:05:21 +02:00
Géry Debongnie 44e2de9709 [IMP] owl: update to v0.14.0 2019-06-13 12:20:25 +02:00
Géry Debongnie 84f810e6ae [IMP] owl: update lib 2019-06-08 16:31:10 +02:00
Géry Debongnie 449c9beaab [IMP] owl: update to v0.13.0 2019-06-05 10:58:52 +02:00
Géry Debongnie c394b955ed [IMP] static page: add some css 2019-05-29 13:02:52 +02:00
Géry Debongnie d048348e07 [IMP] owl: update to v0.12.0 2019-05-29 11:29:29 +02:00
Géry Debongnie 78b4938fbd [IMP] update owl/playground (to v0.11.0) 2019-05-17 23:39:41 +02:00
Géry Debongnie 5cd6f3ebb3 [IMP] owl: update to v0.10.0 2019-05-10 12:29:39 +02:00
Géry Debongnie 0be60ef372 [IMP] update owl/playground 2019-05-07 20:52:49 +02:00
Géry Debongnie 5f9a36f99a [IMP] update owl/playground 2019-05-03 14:37:05 +02:00
Géry Debongnie b15e1686e8 [REM] remove jshintrc 2019-05-03 13:33:17 +02:00
Géry Debongnie 62469c4264 [IMP] playground: update Owl to 0.9.0 2019-05-03 12:28:59 +02:00
Géry Debongnie 7642ce4e2c [FIX] playground: make vertical resize work better
closes #72
2019-05-03 10:30:41 +02:00
Géry Debongnie a25ebb8039 [FIX] playground: fix broken links 2019-04-30 12:05:51 +02:00
Géry Debongnie 651f920d27 [IMP] playground: use fontawesome for icons
closes #68
2019-04-30 11:16:42 +02:00
Géry Debongnie 9cc1dd0524 [REF] move libs out of playground, add server.py
So we can share it
2019-04-30 10:51:18 +02:00
Géry Debongnie 8b4a3e45a3 [FIX] playground: add warning if no static server
also, add a app.py file to start a static server.

closes #65
2019-04-29 11:49:52 +02:00
Géry Debongnie 841c7941c9 [FIX] playground: fix broken links 2019-04-29 11:17:53 +02:00
Géry Debongnie b0793ef8c5 [IMP] package.json: rename 'serve' into 'start'
So we have same script name as benchmarks branch
2019-04-29 11:11:06 +02:00
Géry Debongnie 791101e394 [IMP] playground: update owl 2019-04-26 15:33:13 +02:00
Géry Debongnie 02a0edb754 [IMP] playground: add download app feature
closes #60
2019-04-26 14:59:07 +02:00
Géry Debongnie 21cee6c0de [ADD] playground: add FileSaver and jszip libs 2019-04-26 14:24:02 +02:00
Géry Debongnie e637925430 [IMP] playground: update owl lib 2019-04-26 14:24:02 +02:00
Géry Debongnie e2b056e1dc [IMP] playground: add title attribute on some buttons 2019-04-25 23:21:25 +02:00
Géry Debongnie c62fa0eae5 [IMP] home: add some links in the main page 2019-04-25 15:01:11 +02:00
Géry Debongnie c608c9d07b [IMP] add jshintrc 2019-04-24 22:03:43 +02:00
Géry Debongnie 866e12dc27 [FIX] playground: remove error handler after 100ms
The error handler is intended to only detect errors at 'startup',  not
in the normal course of operation.
2019-04-24 16:17:27 +02:00
Géry Debongnie b8eaa649c6 [REF] playground: load templates from a xml file 2019-04-24 15:17:55 +02:00
Géry Debongnie d355573dd7 [IMP] playground: display script errors in right pane
closes #57
2019-04-24 12:31:09 +02:00
Géry Debongnie 621960a739 [IMP] playground: improve code samples 2019-04-24 12:15:42 +02:00
Géry Debongnie 7b9194819f [ADD] owl: add index page 2019-04-24 10:27:55 +02:00
Géry Debongnie f4e82996f1 [IMP] playground: update owl 2019-04-24 10:26:36 +02:00
Géry Debongnie 3064f37bc8 [REF] playground: move code to subfolder 2019-04-24 10:24:33 +02:00
Géry Debongnie b974d9d0cc [REF] rename Lab into Library 2019-04-18 10:20:53 +02:00
Géry Debongnie f466495f75 [FIX] strip XML comments out of templates
closes #47
2019-04-17 16:13:52 +02:00
Géry Debongnie 4a07e65102 [IMP] examples: update todo app 2019-04-17 15:34:57 +02:00
Géry Debongnie ba7ecf7446 [IMP] add simple feedback animation when clicking run
closes #42
2019-04-17 13:07:37 +02:00
Géry Debongnie cac301f0f7 [IMP] editor: set tab size dynamically
closes #46
2019-04-17 12:48:54 +02:00
Géry Debongnie 8156a9ef6f [IMP] owl: update lib + update samples 2019-04-17 11:47:48 +02:00
Géry Debongnie 83a5baf9a9 [IMP] owl: update library, and update examples 2019-04-16 17:37:17 +02:00
Géry Debongnie 050a4e5f3e [IMP] owl: update lib 2019-04-16 14:53:02 +02:00
Géry Debongnie 3c70961724 [FIX] samples: update state management example 2019-04-16 10:36:17 +02:00
Géry Debongnie 83f23487c3 [FIX] app: rename keep-alive -> keepalive 2019-04-16 10:23:06 +02:00
Géry Debongnie 7d55724839 [IMP] owl: update library 2019-04-16 10:20:59 +02:00
Géry Debongnie a9376670b5 [IMP] owl: update library 2019-04-12 15:26:03 +02:00
Géry Debongnie 725aa6ca3f imp: update owl to v0.6.0 2019-04-11 10:29:14 +02:00
Géry Debongnie eb7506d1a4 imp: update owl 2019-04-09 15:35:05 +02:00
Géry Debongnie b94127dca0 imp: add lifecycle demo example 2019-04-09 14:50:40 +02:00
Géry Debongnie 9220982cb1 update code to new owl semantics 2019-04-09 14:02:43 +02:00
Géry Debongnie 2dab6b67de imp: update owl.js 2019-04-09 13:56:50 +02:00
Géry Debongnie 0813c50b05 imp: update owl to v0.5.0 2019-04-04 14:22:42 +02:00
Géry Debongnie 5b5446bc14 add: new example with ES5 code 2019-04-04 13:09:52 +02:00
Géry Debongnie a286d8009b rem: remove old commented code 2019-04-04 13:06:38 +02:00
Géry Debongnie 27debda0e7 add: add responsive example 2019-04-02 16:19:43 +02:00
Géry Debongnie ff607315de imp: update owl library 2019-04-02 13:40:57 +02:00
Géry Debongnie ae48157985 imp: improve css 2019-04-02 13:40:01 +02:00
Géry Debongnie b96e4d3e54 imp: add some empty boilerplate to the empty sample 2019-04-01 16:26:19 +02:00
Géry Debongnie 213c83bab5 ref: update owl.js, use componentdidupdate 2019-04-01 16:03:26 +02:00
Géry Debongnie 89e6a00d3e fix: make sure ace editor adapts properly to different size 2019-04-01 14:10:37 +02:00
Géry Debongnie 11e7f5b733 imp: add state management example 2019-04-01 14:08:23 +02:00
Géry Debongnie 9d389d972e fix: a few small bugs, restore proper tab on layout change 2019-04-01 10:55:15 +02:00
Géry Debongnie cbd85f9cf3 imp: update css 2019-04-01 10:49:46 +02:00
Géry Debongnie a4ecdc541d imp: add vertical resize feature 2019-04-01 10:32:54 +02:00
Géry Debongnie 453080e9db imp: update code samples 2019-04-01 09:40:03 +02:00
Géry Debongnie 328043abf6 imp: use split layout by default 2019-04-01 09:22:12 +02:00
Géry Debongnie 14a8110b5d imp: add split editor mode to playground 2019-04-01 09:14:33 +02:00
Géry Debongnie cc33c9f5cf ref: split app widget in 2 2019-03-31 22:14:45 +02:00
Géry Debongnie f7e46784b4 imp: use arguments in sample code 2019-03-31 09:34:53 +02:00
Géry Debongnie ee4ecb5bd3 imp: add horizontal resize feature 2019-03-30 20:42:59 +01:00
Géry Debongnie f77625ffc1 imp: improve benchmark code 2019-03-29 16:48:16 +01:00
Géry Debongnie b18310db9b ref: rename playground => app
also, add version in title
2019-03-29 16:14:36 +01:00
Géry Debongnie c7c0cb7d44 imp: allow adding templates in xml tab 2019-03-29 16:11:18 +01:00
Géry Debongnie b262760b02 imp: update owl library 2019-03-29 15:38:34 +01:00
Géry Debongnie 8319e34f0e fix: update url in right pane 2019-03-29 15:18:52 +01:00
Géry Debongnie db6345e8af add: add script to start a static server 2019-03-29 15:18:26 +01:00
Géry Debongnie 064baedff8 imp: remove need for playground-iframe.html file 2019-03-29 13:44:21 +01:00
Géry Debongnie 9477faa52a add: add demo benchmark application 2019-03-29 13:23:36 +01:00
Géry Debongnie 24e4a9fc4b fix: fix issue with wrong editor content when changing sample 2019-03-29 13:13:36 +01:00
Géry Debongnie 2cabdf8d5c imp: properly inject css in iframe, as expected 2019-03-29 13:06:57 +01:00
Géry Debongnie 4be15febb9 imp: improve code samples to make them work on firefox 2019-03-29 10:14:02 +01:00
Géry Debongnie 3d18acb94d fix: links should not be absolutes 2019-03-29 10:06:23 +01:00
Géry Debongnie 8063316f1a ref: move playground at root of folder 2019-03-29 09:59:39 +01:00
Géry Debongnie 208c7d0b63 imp: add many features to playground 2019-03-29 09:57:19 +01:00
Géry Debongnie 9e7a8d9a63 add: add playground application 2019-03-28 17:39:24 +01:00
335 changed files with 13270 additions and 91941 deletions
-47
View File
@@ -1,47 +0,0 @@
{
"env": {
"browser": true,
"node": true,
"es2022": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"parserOptions": {
"sourceType": "module"
},
"root": true,
"rules": {
"no-restricted-globals": ["error", "event", "self"],
"no-const-assign": ["error"],
"no-debugger": ["error"],
"no-dupe-class-members": ["error"],
"no-dupe-keys": ["error"],
"no-dupe-args": ["error"],
"no-dupe-else-if": ["error"],
"no-unsafe-negation": ["error"],
"no-duplicate-imports": ["error"],
"valid-typeof": ["error"],
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false, "caughtErrors": "all" }],
"no-restricted-syntax": [
"error",
{
"selector": "MemberExpression[object.name='test'][property.name='only']",
"message": "test.only(...) is forbidden",
},
{
"selector": "MemberExpression[object.name='describe'][property.name='only']",
"message": "describe.only(...) is forbidden",
}
],
},
"globals": {
"describe": true,
"expect": true,
"test": true,
"beforeEach": true,
"beforeAll": true,
"afterEach": true,
"afterAll": true,
"jest": true,
},
}
-29
View File
@@ -1,29 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run test
- run: npm run check-formatting
- run: npm run lint
- run: npm run build
+2 -13
View File
@@ -1,6 +1,5 @@
/node_modules
/dist
npm-debug.log
# misc
@@ -14,15 +13,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
#ide's
.vscode
.idea
node_modules
release-notes.md
.rpt2_cache
# useful in some cases
/temp
package-lock.json
.vscode
-3
View File
@@ -1,3 +0,0 @@
{
"esversion": 9
}
-766
View File
@@ -1,766 +0,0 @@
# Changelog
This document contains an overview of all changes between Owl 1.x and
Owl 2.x, with some pointers on how to update the code.
Note that some of these changes can be magically implemented (for example, by
patching the `setup` method of `Component` to auto register all the lifecycle
methods as hooks). This will be done for the transition period, but will be
removed after.
## From Owl 1.x to Owl 2.0
All changes are documented here in no particular order.
**Components**
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
- breaking: component.el is removed ([details](#9-componentel-is-removed))
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
- new `onWillDestroy`, `onWillRender` and `onRendered` hooks ([doc](doc/reference/component.md#lifecycle))
- breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed))
- breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element))
- breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler))
- breaking: components can no longer be instantiated and mounted by hand ([details](#5-components-can-no-longer-be-instantiated-and-mounted-by-hand))
- breaking: components can no longer be unmounted/remounted ([details](#6-components-can-no-longer-be-unmountedremounted))
- breaking: template name is no longer inferred from the class name ([details](#7-template-name-is-no-longer-inferred-from-the-class-name))
- breaking: components no longer have a `shouldUpdate` method ([details](#8-components-no-longer-have-a-shouldupdate-method))
- breaking: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
- breaking: `render` method does not return a promise anymore ([details](#35-render-method-does-not-return-a-promise-anymore))
- breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook))
- breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed))
- new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation))
- breaking: prop validation system does not allow default prop on a mandatory (not optional) prop ([doc](doc/reference/props.md#props-validation))
- breaking: rendering a component does not necessarily render child components ([details](#40-rendering-a-component-does-not-necessarily-render-child-components))
**Templates**
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
- breaking: `t-raw` directive has been removed (replaced by `t-out`) ([details](#38-t-raw-directive-has-been-removed-replaced-by-t-out))
- new: add support for synthetic events ([doc](doc/reference/event_handling.md#synthetic-events))
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
- new: components can use the `.bind` suffix to bind function props ([doc](doc/reference/props.md#binding-function-props))
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
- new: an error is thrown if an handler defined in a `t-on-` directive is not a function (failed silently previously in some cases)
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
- new: the `this` variable in template expressions is now bound to the component
**Reactivity**
- finer grained reactivity: owl 2 tracks change per key/component
- finer grained reactivity: sub components can reobserve state ([doc](doc/reference/reactivity.md))
- new: `reactive` function: create reactive state (without being linked to a component) ([doc](doc/reference/reactivity.md#reactive))
- new: `markRaw` function: mark an object or array so that it is ignored by the reactivity system ([doc](doc/reference/reactivity.md#markraw))
- new: `toRaw` function: given a reactive objet, return the raw (non reactive) underlying object ([doc](doc/reference/reactivity.md#toraw))
**Slots**
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
- slots capabilities have been improved ([doc](doc/reference/slots.md))
- params can be give to slot content (to pass information from slot owner to slot user)
- slots are given as a `prop` (and can be manipulated/propagated to sub components )
- slots can define scopes (to pass information from slot user to slot owner)
**Portal**
- Portal are now defined with `t-portal` ([details](#33-portal-are-now-defined-with-t-portal))
- portals can now have arbitrary content (no longer restricted to one single child)
- breaking: does no longer transfer dom events ([details](#13-portal-does-no-longer-transfer-dom-events))
- breaking: does render as an empty text node instead of `<portal/>` ([details](#14-portal-does-render-as-an-empty-text-node-instead-of-portal))
**Miscellaneous**
- improved performance
- much simpler code
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
- breaking: `Context` is removed ([details](#15-context-is-removed))
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
- new hook: `useChildSubEnv` (only applies to child components) ([details](#27-usechildsubenv-only-applies-to-child-components))
- breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level))
- breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes))
- breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget))
- breaking: `Store` is removed ([details](#22-store-is-removed))
- breaking: `Router` is removed ([details](#23-router-is-removed))
- breaking: transition system is removed ([details](#24-transition-system-is-removed))
- breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates))
- breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed))
- breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
- breaking: `debounce` utility function has been removed ([details](#34-debounce-utility-function-has-been-removed))
- breaking: `browser` object has been removed ([details](#39-browser-object-has-been-removed))
## Details/Rationale/Migration
All changes are listed in no particular order.
### 1. component lifecycle methods are removed
There was two ways to define hooks: the component methods (`willStart`, `mounted`, ...) and the hooks (`onWillStart`, `onMounted`, ...). In Owl 2, the component methods have been removed.
Rationale: it makes the implementation simpler and slightly faster. Hooks are more composable
than component methods. It enforces a single entry point to check all the useful lifecycle
calls (instead of it being scattered in the component definition). It feels more "modern".
Migration: lifecycle methods should be defined in the `setup`:
```js
class MyComponent extends Component {
mounted() {
// do something
}
}
```
should become:
```js
class MyComponent extends Component {
setup() {
onMounted(() => {
// do something
});
}
}
```
Documentation: [Component Lifecycle](doc/reference/component.md#lifecycle)
### 2. components can no longer be mounted in a detached dom element
Nor document fragment.
Rationale: it is actually very difficult to do it: this implies that a component
can be mounted more than once, that we need to check every time different status,
that some elements is in the dom, and was a cause for bugs. Also, we don't use it
in practice. Removing this means that we have a much simpler mental model of what
happens.
Migration: well, not really easy. The code needs to be refactored in a different way.
### 3. **`t-set` will no longer work to define a slot**
The `t-set` directive cannot define a slot anymore. Only the `t-set-slot` directive
can do it.
Rationale: it was left for compatibility reason, but was deprecated anyway.
Migration: `t-set` should be changed to `t-set-slot` (when defining a slot)
Example:
```xml
<SideBar><t t-set="content">content</t></SideBar>
```
should become:
```xml
<SideBar><t t-set-slot="content">content</t></SideBar>
```
### 4. `mount` method API is simpler
Before, the `mount` method was used like this:
```js
await mount(Root, { target: document.body });
```
It is now simpler and takes the root component and a target argument:
```js
await mount(Root, document.body);
```
Rationale: the `mount` method is only useful anyway for small toy examples,
because real applications will need to configure the templates, the translations,
and other stuff. All complex usecases need to go through the new `App` class,
that encapsulates the root of an owl application.
Documentation: [Mounting a component](doc/reference/app.md#mount-helper)
### 5. components can no longer be instantiated and mounted by hand
In Owl 1, it was possible to instantiate a component by hand:
```js
const root = new Root();
await root.mount(document.body);
```
Now, it is no longer possible. All component instantiations should be done by
the owl framework itself.
Rationale: the `mount` method does not make sense for all non root components.
Also, the fact that it was possible for a component to be sometimes root,
sometimes a child made for a weird constructor signature. This changes makes it
simpler.
Migration: all code doing that should use either the `mount` method (if the use
case is simple enough, or the `App` class):
```js
const app = new App(Root);
app.configure({ templates: ..., ...});
await app.mount(document.body);
```
### 6. components can no longer be unmounted/remounted
Rationale: this is a very difficult feature to implement (it adds a lot of possible
state transitions), compared to its benefit.
Migration: all code using it should find a way to export and reimport the state
### 7. template name is no longer inferred from the class name
Before, it was possible to define a component without specifying its template:
```js
class Blabla extends Component {
// no static template here!
}
```
with the `Blabla` template. It also worked with subclasses. But then, this means
that the code had to look up all the super classes names to find the correct
template.
Rationale: in practice, it is not really useful, since all templates are usually
namespaced: `web.SomeComponent` anyway. All the trouble to do that was just not
worth it.
Migration: simply explicitely defines the template key everytime:
```js
class Blabla extends Component {}
Blabla.template = "Blabla";
```
### 8. components no longer have a `shouldUpdate` method
Rationale: `shouldUpdate` is a dangerous method to use, that may cause a lot of
issues. Vue does not have such a mechanism (see https://github.com/vuejs/vue/issues/4255),
because the reactivity system in Vue is smart enough to only rerender the minimal
subset of components that is subscribed to a piece of state. Now, Owl 2 features
a much more powerful reactivity system, so the same rationale applies: in a way,
it's like each Owl 2 component has a `shouldUpdate` method that precisely tracks
every value used by the component.
Migration code: remove the `shouldUpdate` methods, and it should work as well
as before.
### 9. component.el is removed
This comes from the fact that Owl 2 supports fragments (arbitrary content).
Migration: if one need a reference to the root htmlelement of a template, it is
suggested to simply add a `ref` on it, and access the reference as needed.
Documentation: [Refs](doc/reference/refs.md)
### 10. style/class on components are now regular props
Before, it was possible to do this in a template:
```xml
<Child style="..." class="..."/>
```
(or with `t-att-style` and `t-att-class`). This does no longer work, as they are
now considered normal props.
Rationale: with the move to fragments, the semantics of where the style/class
attribute should be set is unclear. Also, it is actually very hard to implement
properly, in particular with higher order components. And another issue is that
it (slightly) breaks the encapsulation of behaviour from the `Child` component
perspective.
Migration: each component that wishes to be customized should explicitely add
the `class` and `style` attributes in its template. Also, the parent component
should be aware that since we are talking about props, it should be a javascript expression:
In parent:
```xml
<Child class="'o_my_god'"/>
```
and in child:
```xml
<div t-att-class="props.class">
...
</div>
```
### 11. components can no longer be mounted with position=self
Rationale: this is due to the implementation of owl 2 virtual dom. The hack
necessary to support position=self does not work. This position also is not
compatible with the fact that a component can have a root `<div>` then later,
change it to something else, or even a text node.
Migration: no real way to do the same. Owl application needs to be appended or
prepended in something, maybe a `div`. Remember that you the root component
can have multiple roots
Documentation:
- [Fragments](doc/reference/templates.md#fragments)
- [Mounting a component](doc/reference/app.md#mount-helper)
### 13. Portal does no longer transfer DOM events
In Owl 1, a Portal component would listen to events emitted on its portalled
child, and redispatch them on itself. It no longer works.
Rationale: Portal now supports an arbitrary content (so, more than one child,
and potentially no html element), so it is already unclear what it should listen
to. Also, redispatching events was an hack. And this changes allows the portal
to render itself as a text node, which is nice. This is also in line with the
fact that modern Owl moves toward using callback instead of `t-on` for communication.
Migration: use callback if possible to communicate. Otherwise, use a sub env.
### 14. Portal does render as an empty text node instead of `<portal/>`
That is pretty nice. No real migration needed.
### 15. Context is removed
Context was an abstraction in Owl that was used to define some reactive state
and to let some components subscribe to it, then only them would be rerendered
if the context was updated. This has been removed.
Rationale: first, the Context api and code was kind of awkward, which is a sign
that the abstraction is not well thought. But the good news is that it is actually
completely replaced by the new reactivity system, which is even more powerful,
since it can tracks changes key by key.
Migration: replace all uses of Context with the new reactivity system.
```js
// somewhere, maybe in a service, or in the global env
const context = observe({some: "state"})
// in a component that would previously get a reference to the context:
setup() {
this.context = useState(context);
// now the component is subscribed to the context and will react to any
// change for any key read by the component, and only those changes
}
```
### 16. `env` is now totally empty
In Owl 1, the `env` object had to contain a QWeb instance. This was the way
components would get a reference to their template function. It no longer works
that way: the `env` object is now totally empty (from the perspective of Owl).
It is now a user space concept, useful for the application.
Rationale: first, there is no longer a QWeb class. Also, this changes simplifies
the way components works internally.
Migration: there is no proper way to get an equivalent. The closest is to get
a reference to the root App using `this.__owl__.app`. If you need to do this,
let us know. If this is a legitimate usecase, we may add a `useApp` hook.
Documentation: [Environment](doc/reference/environment.md)
### 17. `t-component` no longer accepts strings
In owl 1, we could write this:
```xml
<t t-component="Coucou"/>
```
This meant that Owl would look for the component class like this: `components["Coucou"]`,
so, essentially equivalent to `<Coucou/>`. In Owl 2, the `t-component` directive
is assumed to be an expression evaluating to a component class:
```js
class Parent extends Component {
static template = xml`<t t-component="Child"/>`;
Child = Child;
}
```
Rationale: it simply seems more consistent with the way directive works. Also,
the implementation is slightly simpler.
Migration: simply using `constructor.components.Coucou` instead of `Coucou` will
do the trick.
Documentation: [Component](doc/reference/component.md#dynamic-sub-components)
### 18. most exports are exported at top level
Most exports are flattened: for ex, `onMounted` is in owl, not in `owl.hooks`.
Rationale: this makes it easier to work with, instead of importing stuff from
`owl`, then `owl.hooks` and `owl.tags` for example.
Migration: all import code simply need to be slightly adapted.
### 19. Properties are no longer set as attributes
Formerly, html properties `<input type="checkbox" t-att-checked="blah"/>` were
set as property and as attribute, so, they would be visible in the DOM:
`<input type="checkbox" checked="blah"/>`. Now, they are treated as property only:
`<input type="checkbox"/>`.
Rationale: this is actually simple to do, is faster, and makes more sense to me.
### 20. `t-foreach` should always have a corresponding `t-key`
It was possible in Owl 1 to write a `t-foreach` without a `t-key`. In that case,
the index was used as key. Since it was clearly a possible bug, Owl 1 had a
warning in some cases, when it could detect that there was definitely not a `t-key`.
However, this was imperfect, and in some cases no warning was displayed. In Owl 2,
the tag with a `t-foreach` has to have a corresponding `t-key`.
Rationale: this makes it easier to avoid bugs.
Migration: simply move the `t-key` to the tag with the `t-foreach`. If this is
a situation where there is really not a need for a `t-key`, you can still add
it with the `_index` suffix:
```xml
<div t-foreach="items" t-as="item" t-key="item_index">
...
</div>
```
### 21. `EventBus` api changed: it is now an `EventTarget`
In Owl 1, the `EventBus` class was done manually, with a custom API. In Owl 2,
it simply extends `EventTarget` (the native Dom class), so its implementation
is basically only 5 lines long. This means that it has now the usual DOM interface:
```js
bus.addEventListener('event-name', callback);
```
Rationale: it makes it easier to have just one interface to remember, it makes
the code simpler
Migration: most bus methods need to be adapted. So, `bus.on("event-type", owner, (info) => {...})` has to be
rewritten like this: `bus.addEventListener("event-type", (({detail: info}) => {...}).bind(owner))`.
Do not forget to similarly replace `bus.off(...)` by `bus.removeEventListener(...)`
Documentation: [EventBus](doc/reference/utils.md#eventbus)
### 22. `Store` is removed
The Store system had been abandoned in owl 2.
Rationale: first, it was complicated to maintain. Second, it was not really
used in Odoo. Finally, the new reactivity system seems to be a pretty good basis
to write a store, and it should not take much work. Also, this can be done in
user space (so, not necessarily at the framework level). Another point is that
the store API was invented before the hooks, then was still a little awkward.
Migration:
- rewrite the code not to use a store
- probably use the reactivity system instead and build a store class and a few
hooks on top of it.
### 23. `Router` is removed
Rationale: Router was not used that much, and it felt like it did not fit in Owl 2.
Its API needs to be reworked, and we are not confident that it is a good
experience to use it. Also, it can be done in userspace (it does not need specific
integration at the framework level)
Migration: reimport all missing piece from the code in Owl 1.
### 24. transition system is removed
Rationale: this was a high ratio cost/value, with a lot of potential for bugs.
We feel like there should be a way to reimplement in userspace the simple cases.
Maybe something like: add a `t-ref` in the template, and define a hook `useFadeOut`
that takes the ref, and add a fadeout class at initial render, then in mounted,
wait for a micro tick and remove it.
Migration: try to reimplement it manually.
### 25. no more global components or templates
It was possible in Owl 1 to register globally a component or a template. This is
no longer the case in Owl 2.
Rationale: first, this was a tradeoff: ease of use was gained, but at the cost
of a higher complexity. Users had to know that there was a magic mechanism. Also,
it was not used much in practice, and the cost of having to import manually components
is low. Finally, this can be mostly done in user space (for example, by subclassing
`Component`).
Migration: import manually all required global components, or find a way to organize
the code to do it.
### 26. `AsyncRoot` utility component is removed
Rationale: it was difficult to understand, never used, and not really useful.
It seems better to control the asynchrony of an application by simply controlling
how/when the state is updated, and how each component is loading/updating itself.
Migration: remove the `AsyncRoot` component, then possibly, reorganize the code
to fetch data in a higher order component, and using a `t-if/t-else` to display
either a fallback when the data is not ready, or the actual component with data
as props. If there is no escape, and `AsyncRoot` is needed, please reach out to
us so we can study this usecase.
### 27. `useChildSubEnv` (only applies to child components)
In Owl, a call to `useSubEnv` would define a new environment for the children
AND the component. It is very useful, but in some cases, one only need to update
the children component environment. This can now be done with a new hook:
[`useChildSubEnv`](doc/reference/hooks.md#usesubenv-and-usechildsubenv)
### 28. `env` is now frozen
In Owl 2, the `env` object is frozen. It can no longer be modified (structurally)
arbitrarily.
Rationale: it seems like the `env` object purpose is to have a global channel of
communication between components. It is however scary if anyone can add something
to it. The usual use case is to add something to the environment for some child
components. This use case still works with `useSubEnv`.
Migration: use `useSubEnv` instead of writing directly to the env. Also, note
that the environment given to the App can initially contain anything.
Documentation: [Environment](doc/reference/environment.md)
### 29. `t-ref` does not work on component
Before, `t-ref` could be used to get a reference to a child component. It no
longer works.
Rationale: the possibility of having a ref to a child component breaks the
encapsulation provided by Owl components: a child component now has a private
and a public interface. Another issue is that it may be unclear when the ref
should be set: is the component active on setup, or on mounted? Also, it is
kind of awkward to implement.
Migration: the `env` and `props` should provide a communication channel wide enough:
the sub component can expose its public API by calling a callback at the proper
timing, or by triggering an event.
### 30. `t-on` does not accept expressions, only functions
In Owl 1, it was possible to define simple expressions inline, in a template:
```xml
<button t-on-click="state.value = state.value + 1">blabla</button>
<button t-on-click="someFunction(someVar)">blabla</button>
```
This does not work anymore. Now, the `t-on` directive assumes that what it get is
a function.
Rationale: the fact that owl 1 had to support expressions meant that it was not
possible to properly inject the event in general. With this restriction, Owl 2
can support more general use cases. Also, the examples above can simply be
wrapped in a lambda function.
Migration: use lambda functions. For example, the two examples above can be
adapted like this:
```xml
<button t-on-click="() => state.value = state.value + 1">blabla</button>
<button t-on-click="() => this.someFunction(someVar)">blabla</button>
```
Documentation: [Event Handling](doc/reference/event_handling.md)
### 31. components can now have arbitrary content
Before Owl 2, components had to limit themselves to one single htmlelement as
root. Now, the content is arbitrary: it can be empty, or multiple html elements.
So, the following template works for components:
```xml
<div>1</div>
<div>2</div>
hello
```
Documentation: [Fragments](doc/reference/templates.md#fragments)
### 32. `renderToString` on QWeb has been removed
Rationale: the `renderToString` function was a qweb method, which made sense because
the qweb instance knew all templates. But now, the closest analogy is the `App`
class, but it is not as convenient, since the `app` instance is no longer visible
to components (while before, `qweb` was in the environment).
Also, this can easily be done in userspace, by mounting a component in a div. For example:
```js
export async function renderToString(template, context) {
class C extends Component {
static template = template;
setup () {
Object.assign(this, context);
}
}
const div = document.createElement('div');
document.body.appendChild(div);
const app = new App(C);
await app.mount(div);
const result = div.innerHTML;
app.destroy();
div.remove();
return result;
}
```
The function above works for most cases, but is asynchronous. An alternative
function could look like this:
```js
const { App, blockDom } = owl;
const app = new App(Component); // act as a template repository
function renderToString(template, context = {}) {
app.addTemplate(template, template, { allowDuplicate: true });
const templateFn = app.getTemplate(template);
const bdom = templateFn(context, {});
const div = document.createElement('div')
blockDom.mount(bdom, div);
return div.innerHTML;
}
```
This is a synchronous function, so it will not work with components, but it should
be useful for most simple templates.
Also note that these two examples do not translate their templates. To do that,
they need to be modified to pass the proper translate function to the `App`
configuration.
### 33. Portal are now defined with `t-portal`
Before Owl 2, one could use the `Portal` component by importing it and using it.
Now, it is no longer available. Instead, we can simply use the `t-portal` directive:
```xml
<div>
some content
<span t-portal="'body'">
portalled content
</span>
<div>
```
Rationale: it makes it slightly simpler to use (just need the directive, instead
of having to import and use a sub component), it makes the implementation slightly
simpler as well. Also, it prevents subclassing the Portal component, which could
be dangerous, since it is really doing weird stuff under the hood, and could
easily be broken inadvertendly.
### 34. `debounce` utility function has been removed
Rationale: it did not really help that much, is available as utility function
elsewhere, so, we decided to have a smaller footprint by focusing Owl on what
it does best.
### 35. `render` method does not return a promise anymore
Rationale: using the `render` method directly and waiting for it to complete
was slightly un-declarative. Also, it can be done using the lifecycle hooks
any way.
Migration: if necessary, one can use the lifecycle hooks to execute code after
the next mounted/patched operation.
### 36. `catchError` method is replaced by `onError` hook
The `catchError` method was used to provide a way to components to handle errors
occurring during the component lifecycle. This has been replaced by a `onError`
hook, with a similar API.
Rationale: `catchError` felt a little big awkward, when most of the way we
interact with componentss is via hooks. Using hooks felt more natural and
consistent.
Migration: mostly replace all `catchError` methods by `onError` hooks in the
`setup` method.
Documentation: [Error Handling](doc/reference/error_handling.md)
## 37. Support for inline css (`css` tag and static `style`) has been removed
Rationale: Owl tries to focus on what it does best, and supporting inline css
was not a priority. It used to support some simplified scss language, but it
was feared that it would cause more trouble than it was worth. Also, it seems
like it can be done in userspace.
Migration: it seems possible to implement an equivalent solution using hooks. A
simple implementation could look like this:
```js
let cache = {};
function useStyle(css) {
if (!css in cache) {
const sheet = document.createElement("style");
sheet.innerHTML = css;
cache[css] = sheet;
document.head.appendChild(sheet);
}
}
```
## 38. `t-raw` directive has been removed (replaced by `t-out`)
To match the Odoo qweb server implementation, Owl does no longer implement `t-raw`.
It is replaced by the `t-out` directive, which is safer: it requires the data
to be marked explicitely as markup if it is to be inserted without escaping.
Otherwise, it will be escaped (just like `t-esc`).
Migration: replace all `t-raw` uses by `t-out`, and uses the `markup` function
to mark all the js values.
Documentation: [Outputting data](doc/reference/templates.md#outputting-data)
## 39. `browser` object has been removed
Rationale: the `browser` object caused more trouble than it was worth. Also, it
seems like this should be done in user space, not at the framework level.
Migration: code should just be adapted to either use another browser object,
or to use native browser function (and then, just mock them directly).
## 40. Rendering a component does not necessarily render child components
Before, if one had the following component tree:
```mermaid
graph TD;
A-->B;
A-->C;
```
when `A` would render, it would also render `B` and `C`. Now, in Owl 2, it will
(shallow) compare the before and after props, and `B` or `C` will only be rerendered
if their props have changed.
Now, the question is what happens if the props have changed, but in a deeper way?
In that case, Owl will know, because each props are now reactive. So, if some
inner value read by `B` was changed, then only `B` will be updated.
Rationale: This was just not possible in Owl 1, but it now possible. This is
due to the rewriteof the underlying rendering engine and the reactivity
system. The goal is to have a big performance boost in large screen with many
components: now Owl only rerender what is strictly useful.
-14
View File
@@ -1,14 +0,0 @@
Most of the files are
Copyright (c) 2004-2015 Odoo S.A.
Many files also contain contributions from third
parties. In this case the original copyright of
the contributions can be traced through the
history of the source version control system.
When that is not the case, the files contain a prominent
notice stating the original copyright and applicable
license, or come with their own dedicated COPYRIGHT
and/or LICENSE file.
-856
View File
@@ -1,856 +0,0 @@
OWL is published under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3
(LGPLv3), as included below. Since the LGPL is a set of additional
permissions on top of the GPL, the text of the GPL is included at the
bottom as well.
Some external libraries and contributions bundled with OWL may be published
under other GPL-compatible licenses. For these, please refer to the relevant
source files and/or license files, in the source code tree.
**************************************************************************
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
**************************************************************************
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
**************************************************************************
-149
View File
@@ -1,149 +0,0 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Owl Framework</a> 🦉</h1>
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![npm version](https://badge.fury.io/js/@odoo%2Fowl.svg)](https://badge.fury.io/js/@odoo%2Fowl)
[![Downloads](https://img.shields.io/npm/dm/@odoo%2Fowl.svg)](https://www.npmjs.com/package/@odoo/owl)
_Class based components with hooks, reactive state and concurrent mode_
**Try it online!** you can experiment with the Owl framework in an online [playground](https://odoo.github.io/owl/playground).
## Project Overview
The Odoo Web Library (Owl) is a smallish (~<20kb gzipped) UI framework built by
[Odoo](https://www.odoo.com/) for its products. Owl is a modern
framework, written in Typescript, taking the best ideas from React and Vue in a
simple and consistent way. Owl's main features are:
- a declarative component system,
- a fine grained reactivity system similar to Vue,
- hooks
- fragments
- asynchronous rendering
Owl components are defined with ES6 classes and xml templates, uses an
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
asynchronous.
Quick links:
- [documentation](#documentation),
- [changelog](CHANGELOG.md) (from Owl 1.x to 2.x),
- [playground](https://odoo.github.io/owl/playground)
## Example
Here is a short example to illustrate interactive components:
```javascript
const { Component, useState, mount, xml } = owl;
class Counter extends Component {
static template = xml`
<button t-on-click="() => state.value = state.value + props.increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
}
class Root extends Component {
static template = xml`
<span>Hello Owl</span>
<Counter increment="2"/>`;
static components = { Counter };
}
mount(Root, document.body);
```
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
Also, all examples here uses the [`xml` helper](doc/reference/templates.md#inline-templates) to define inline templates.
But this is not mandatory, many applications will load templates separately.
More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application.
## Documentation
### Learning Owl
Are you new to Owl? This is the place to start!
- [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md)
- [How to start an Owl project](doc/learning/quick_start.md)
- [How to test Components](doc/learning/how_to_test.md)
### Reference
- [Overview](doc/readme.md)
- [App](doc/reference/app.md)
- [Component](doc/reference/component.md)
- [Component Lifecycle](doc/reference/component.md#lifecycle)
- [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)
- [Environment](doc/reference/environment.md)
- [Error Handling](doc/reference/error_handling.md)
- [Event Handling](doc/reference/event_handling.md)
- [Form Input Bindings](doc/reference/input_bindings.md)
- [Fragments](doc/reference/templates.md#fragments)
- [Hooks](doc/reference/hooks.md)
- [Loading Templates](doc/reference/app.md#loading-templates)
- [Mounting a component](doc/reference/app.md#mount-helper)
- [Portal](doc/reference/portal.md)
- [Precompiling templates](doc/reference/precompiling_templates.md)
- [Props](doc/reference/props.md)
- [Props Validation](doc/reference/props.md#props-validation)
- [Reactivity](doc/reference/reactivity.md)
- [Rendering SVG](doc/reference/templates.md#rendering-svg)
- [Refs](doc/reference/refs.md)
- [Slots](doc/reference/slots.md)
- [Sub components](doc/reference/component.md#sub-components)
- [Sub templates](doc/reference/templates.md#sub-templates)
- [Templates (Qweb)](doc/reference/templates.md)
- [Translations](doc/reference/translations.md)
- [Utils](doc/reference/utils.md)
### Other Topics
- [Notes On Owl Architecture](doc/miscellaneous/architecture.md)
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
- [Owl devtools extension](doc/tools/devtools.md)
## Installing Owl
Owl is available on `npm` and can be installed with the following command:
```
npm install @odoo/owl
```
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl](https://github.com/odoo/owl/releases/latest)
## Installing Owl devtools
The Owl devtools browser extension is also available in the [release](https://github.com/odoo/owl/releases/latest):
Unzip the owl-devtools.zip file and follow the instructions depending on your browser:
### Chrome
Go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
Select the devtools-chrome folder and that's it, your extension is active!
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
### Firefox
Go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
Select any file in the devtools-firefox folder and that's it, your extension is active!
Here, you can use the reload button to refresh the extension.
Note that you may have to open another window or reload your tab to see the extension working.
Also note that the extension will only be active on pages that have a sufficient version of owl.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

-95
View File
@@ -1,95 +0,0 @@
# 🦉 How to test Components 🦉
## Content
- [Overview](#overview)
- [Unit Tests](#unit-tests)
## Overview
It is a good practice to test applications and components to ensure that they
behave as expected. There are many ways to test a user interface: manual
testing, integration testing, unit testing, ...
In this section, we will discuss how to write unit tests for components.
## Unit Tests
Writing unit tests for Owl components really depends on the testing framework
used in a project. But usually, it involves the following steps:
- create a test file: for example `SomeComponent.test.js`,
- in that file, import the code for `SomeComponent`,
- add a test case:
- create a real DOM element to use as test fixture,
- create a test environment
- create an instance of `SomeComponent`, mount it to the fixture
- interact with the component and assert some properties.
To help with this, it is useful to have a `helper.js` file that contains some
common utility functions:
```js
let lastFixture = null;
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
if (lastFixture) {
lastFixture.remove();
}
lastFixture = fixture;
return fixture;
}
export async function nextTick() {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
```
With such a file, a typical test suite for Jest will look like this:
```js
// in SomeComponent.test.js
import { SomeComponent } from "../../src/ui/SomeComponent";
import { nextTick, makeTestFixture } from '../helpers';
//------------------------------------------------------------------------------
// Setup
//------------------------------------------------------------------------------
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("SomeComponent", () => {
test("component behaves as expected", async () => {
const props = {...}; // depends on the component
const comp = await mount(SomeComponent, fixture, { props });
// do some assertions
expect(...).toBe(...);
fixture.querySelector('button').click();
await nextTick();
// some other assertions
expect(...).toBe(...);
});
});
```
Note that Owl does wait for the next animation frame to actually update the DOM.
This is why it is necessary to wait with the `nextTick` (or other methods) to
make sure that the DOM is up-to-date.
-399
View File
@@ -1,399 +0,0 @@
# 🦉 How to start an Owl project 🦉
## Content
- [Overview](#overview)
- [Simple html file](#simple-html-file)
- [With a static server](#with-a-static-server)
- [Standard Javascript project](#standard-javascript-project)
## Overview
Each software project has its specific needs. Many of these needs can be solved
with some tooling: `webpack`, `gulp`, css preprocessor, bundlers, transpilers, ...
Because of that, it is usually not simple to just start a project. Some
frameworks provide their own tooling to help with that. But then, you have to
integrate and learn how these applications work.
Owl is designed to be used with no tooling at all. Because of that, Owl can
"easily" be integrated in a modern build toolchain. In this section, we will
discuss a few different setups to start a project. Each of these setups has
advantages and disadvantages in different situations.
## Simple html file
The simplest possible setup is the following: a simple javascript file with your
code. To do that, let us create the following file structure:
```
hello_owl/
index.html
owl.js
app.js
```
The file `owl.js` can be downloaded from the last release published at
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
is a single javascript file which export all Owl into the global `owl` object.
Note that there are multiple files, and in this case, we need one of the two
files suffixed with `.iife`: they are built to be directly used in a browser.
Now, `index.html` should contain the following:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="app.js"></script>
</body>
</html>
```
And `app.js` should look like this:
```js
const { Component, mount, xml } = owl;
// Owl Components
class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
mount(Root, document.body);
```
Now, simply loading this html file in a browser should display a welcome message.
This setup is not fancy, but it is extremely simple. There are no tooling at
all required. It can be slightly optimized by using the minified build of Owl.
## With a static server
The previous setup has a big disadvantage: the application code is located in a
single file. Obviously, we could split it in several files and add multiple
`<script>` tags in the html page, but then we need to make sure the script are
inserted in the proper order, we need to export each file content in global
variables and we lose autocompletion across files.
There is a low tech solution to this issue: using native javascript modules.
This however has a requirement: for security reasons, browsers will not accept
modules on content served through the `file` protocol. This means that we need
to use a static server.
Let us start a new project with the following file structure:
```
hello_owl/
src/
index.html
main.js
owl.js
root.js
```
As previously, the file `owl.js` can be downloaded from the last release published at
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
Note that there are multiple files, and in this case, we need one of the two
files suffixed with `.iife`: they are built to be directly used in a browser.
Now, `index.html` should contain the following:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="main.js" type="module"></script>
</body>
</html>
```
Not that the `main.js` script tag has the `type="module"` attribute. This means
that the browser will parse the script as a module, and load all its dependencies.
Here is the content of `root.js` and `main.js`:
```js
// root.js ----------------------------------------------------------------------
const { Component, mount, xml } = owl;
export class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
// main.js ---------------------------------------------------------------------
import { Root } from "./root.js";
mount(Root, document.body);
```
The `main.js` file imports the `root.js` file. Note that the import statement has
a `.js` suffix, which is important. Most text editor can understand this syntax
and will provide autocompletion.
Now, to execute this code, we need to serve the `src` folder statically. A low
tech way to do that is to use for example the python `SimpleHTTPServer` feature:
```
$ cd src
$ python -m SimpleHTTPServer 8022 # now content is available at localhost:8022
```
Another more "javascripty" way to do it is to create a `npm` application. To do
that, we can add the following `package.json` file at the root of the project:
```json
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Starting Owl app",
"main": "src/index.html",
"scripts": {
"serve": "serve src"
},
"author": "John",
"license": "ISC",
"devDependencies": {
"serve": "^11.3.0"
}
}
```
We can now install the `serve` tool with the command `npm install`, and then,
start a static server with the simple `npm run serve` command.
## Standard Javascript project
The previous setup works, and is certainly good for some usecases, including
quick prototyping. However, it lacks some useful features, such as livereload,
a test suite, or bundling the code in a single file.
Each of these features, and many others, can be done in many different ways.
Since it is really not trivial to configure such a project, we provide here an
example that can be used as a starting point.
Our standard Owl project has the following file structure:
```
hello_owl/
public/
index.html
src/
components/
Root.js
main.js
tests/
components/
Root.test.js
helpers.js
.gitignore
package.json
webpack.config.js
```
This project as a `public` folder, meant to contain all static assets, such as
images and styles. The `src` folder has the javascript source code, and finally,
`tests` contains the test suite.
Here is the content of `index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
</head>
<body></body>
</html>
```
Note that there are no `<script>` tag here. They will be injected by webpack.
Now, let's have a look at the javascript files:
```js
// src/components/Root.js -------------------------------------------------------
import { Component, xml, useState } from "@odoo/owl";
export class Root extends Component {
static template = xml`
<div t-on-click="update">
Hello <t t-esc="state.text"/>
</div>`;
state = useState({ text: "Owl" });
update() {
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
}
}
// src/main.js -----------------------------------------------------------------
import { utils, mount } from "@odoo/owl";
import { Root } from "./components/Root";
mount(Root, document.body);
// tests/components/Root.test.js ------------------------------------------------
import { Root } from "../../src/components/Root";
import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("Root", () => {
test("Works as expected...", async () => {
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div");
await nextTick();
expect(fixture.innerHTML).toBe("<div>Hello World</div>");
});
});
// tests/helpers.js ------------------------------------------------------------
import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime";
export async function nextTick() {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
return fixture;
}
export function click(elem, selector) {
elem.querySelector(selector).dispatchEvent(new Event("click"));
}
```
Finally, here is the configuration files `.gitignore`, `package.json` and
`webpack.config.js`:
```
node_modules/
package-lock.json
dist/
```
```json
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Demo app",
"main": "src/index.html",
"scripts": {
"test": "jest",
"build": "webpack --mode production",
"dev": "webpack-dev-server --mode development"
},
"author": "Someone",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"babel-jest": "^25.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^25.1.0",
"regenerator-runtime": "^0.13.3",
"serve": "^11.3.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.2"
},
"dependencies": {
"@odoo/owl": "^1.0.4"
},
"babel": {
"plugins": ["@babel/plugin-proposal-class-properties"],
"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
},
"jest": {
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.js?$",
"moduleFileExtensions": ["js"],
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
}
}
}
```
```js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const host = process.env.HOST || "localhost";
module.exports = function (env, argv) {
const mode = argv.mode || "development";
return {
mode: mode,
entry: "./src/main.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".js", ".jsx"],
},
devServer: {
contentBase: path.resolve(__dirname, "public/index.html"),
compress: true,
hot: true,
host,
port: 3000,
publicPath: "/",
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, "public/index.html"),
}),
],
};
};
```
With this setup, we can now use the following script commands:
```
npm run build # build the full application in prod mode in dist/
npm run dev # start a dev server with livereload
npm run test # run the jest test suite
```
-997
View File
@@ -1,997 +0,0 @@
# 🦉 OWL Tutorial: TodoApp 🦉
For this tutorial, we will build a very simple Todo list application. The app
should satisfy the following requirements:
- let the user create and remove tasks
- tasks can be marked as completed
- tasks can be filtered to display active/completed tasks
This project will be an opportunity to discover and learn some important Owl
concepts, such as components, store, and how to organize an application.
## Content
1. [Setting up the project](#1-setting-up-the-project)
2. [Adding a first component](#2-adding-a-first-component)
3. [Displaying a list of tasks](#3-displaying-a-list-of-tasks)
4. [Layout: some basic css](#4-layout-some-basic-css)
5. [Extracting Task as a subcomponent](#5-extracting-task-as-a-subcomponent)
6. [Adding tasks (part 1)](#6-adding-tasks-part-1)
7. [Adding tasks (part 2)](#7-adding-tasks-part-2)
8. [Toggling tasks](#8-toggling-tasks)
9. [Deleting tasks](#9-deleting-tasks)
10. [Using a store](#10-using-a-store)
11. [Saving tasks in local storage](#11-saving-tasks-in-local-storage)
12. [Filtering tasks](#12-filtering-tasks)
13. [The Final Touch](#13-the-final-touch)
14. [Final Code](#final-code)
## 1. Setting up the project
For this tutorial, we will do a very simple project, with static files and
no additional tooling. The first step is to create the following file structure:
```
todoapp/
index.html
app.css
app.js
owl.js
```
The entry point for this application is the file `index.html`, which should have
the following content:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<script src="owl.js"></script>
<script src="app.js"></script>
</body>
</html>
```
Then, `app.css` can be left empty for now. It will be useful later on to style
our application. `app.js` is where we will write all our code. For now, let's
just put the following code:
```js
(function () {
console.log("hello owl", owl.__info__.version);
})();
```
Note that we put everything inside an immediately executed function to avoid leaking
anything to the global scope.
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files
are built to run directly on the browser, and rename it `owl.js` (other files such as `owl.cjs.js` are
built to be bundled by other tools).
Now, the project should be ready. Loading the `index.html` file into a browser
should show an empty page, with the title `Owl Todo App`, and it should log a
message such as `hello owl 2.x.y` in the console.
## 2. Adding a first component
An Owl application is made out of [components](../reference/component.md), with
a single root component. Let us start by defining a `Root` component. Replace the
content of the function in `app.js` by the following code:
```js
const { Component, mount, xml } = owl;
// Owl Components
class Root extends Component {
static template = xml`<div>todo app</div>`;
}
mount(Root, document.body);
```
Now, reloading the page in a browser should display a message.
The code is pretty simple: we define a component with an inline template, then
mount it in the document body.
Note 1: in a larger project, we would split the code in multiple files, with
components in a sub folder, and a main file that would initialize the application.
However, this is a very small project, and we want to keep it as simple as possible.
Note 2: this tutorial uses the static class field syntax. This is not yet
supported by all browsers. Most real projects will transpile their code, so this
is not a problem, but for this tutorial, if you need the code to work on every
browser, you will need to translate each `static` keyword to an assignation to
the class:
```js
class App extends Component {}
App.template = xml`<div>todo app</div>`;
```
Note 3: writing inline templates with the [`xml` helper](../reference/templates.md#inline-templates)
is nice, but there is no syntax highlighting, and this makes it very easy to
have malformed xml. Some editors support syntax highlighting for this situation.
For example, VS Code has an addon `Comment tagged template`, which, if installed,
will properly display tagged templates:
```js
static template = xml /* xml */`<div>todo app</div>`;
```
Note 4: Large applications will probably want to be able to translate templates.
Using inline templates makes it slightly harder, since we need additional tooling
to extract the xml from the code, and to replace it with the translated values.
## 3. Displaying a list of tasks
Now that the basics are done, it is time to start thinking about tasks. To
accomplish what we need, we will keep track of the tasks as an array of objects
with the following keys:
- `id`: a number. It is extremely useful to have a way to uniquely identify
tasks. Since the title is something created/edited by the user, it offers
no guarantee that it is unique. So, we will generate a unique `id` number for
each task.
- `text`: a string, to explain what the task is about.
- `isCompleted`: a boolean, to keep track of the status of the task
Now that we decided on the internal format of the state, let us add some demo
data and a template to the `App` component:
```js
class Root extends Component {
static template = xml/* xml */ `
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<div class="task">
<input type="checkbox" t-att-checked="task.isCompleted"/>
<span><t t-esc="task.text"/></span>
</div>
</t>
</div>`;
tasks = [
{
id: 1,
text: "buy milk",
isCompleted: true,
},
{
id: 2,
text: "clean house",
isCompleted: false,
},
];
}
```
The template contains a [`t-foreach`](../reference/templates.md#loops) loop to iterate
through the tasks. It can find the `tasks` list from the component, since the rendering
context contains the properties of the component. Note that we use the `id` of each task
as a `t-key`, which is very common. There are two css classes: `task-list` and `task`,
that we will use in the next section.
Finally, notice the use of the `t-att-checked` attribute:
prefixing an attribute by [`t-att`](../reference/templates.md#dynamic-attributes) makes
it dynamic. Owl will evaluate the expression and set it as the value of the
attribute.
## 4. Layout: some basic css
So far, our task list looks quite bad. Let us add the following to `app.css`:
```css
.task-list {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.task {
font-size: 18px;
color: #111111;
}
```
This is better. Now, let us add an extra feature: completed tasks should be
styled a little differently, to make it clearer that they are not as important.
To do that, we will add a dynamic css class on each task:
```xml
<div class="task" t-att-class="task.isCompleted ? 'done' : ''">
```
```css
.task.done {
opacity: 0.7;
}
```
Notice that we have here another use of a dynamic attribute.
## 5. Extracting Task as a subcomponent
It is now clear that there should be a `Task` component to encapsulate the look
and behavior of a task.
This `Task` component will display a task, but it cannot _own_ the state of the
task: a piece of data should only have one owner. Doing otherwise is asking for
trouble. So, the `Task` component will get its data as a `prop`. This means that
the data is still owned by the `App` component, but can be used by the `Task`
component (without modifying it).
Since we are moving code around, it is a good opportunity to refactor the code
a little bit:
```js
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml /* xml */`
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
<span><t t-esc="props.task.text"/></span>
</div>`;
static props = ["task"];
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml /* xml */`
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>`;
static components = { Task };
tasks = [
...
];
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
mount(Root, document.body, {dev: true});
```
A lot of stuff happened here:
- first, we have now a sub component `Task`, defined on top of the file,
- whenever we define a sub component, it needs to be added to the static
[`components`](../reference/component.md#static-properties)
key of its parent, so Owl can get a reference to it,
- the `Task` component has a `props` key: this is only useful for validation
purpose. It says that each `Task` should be given exactly one prop, named
`task`. If this is not the case, Owl will throw an
[error](../reference/props.md#props-validation). This is extremely
useful when refactoring components
- finally, to activate the props validation, we need to set Owl's
[mode](../reference/app.md#configuration) to `dev`. This is done in the last argument
of the `mount` function. Note that this should be removed when an app is used in a real
production environment, since `dev` mode is slightly slower, due to extra
checks and validations.
## 6. Adding tasks (part 1)
We still use a list of hardcoded tasks. It's really time to give the user a way
to add tasks himself. The first step is to add an input to the `Root` component.
But this input will be outside of the task list, so we need to adapt `Root`
template, js, and css:
```xml
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask"/>
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>
```
```js
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const text = ev.target.value.trim();
ev.target.value = "";
console.log('adding task', text);
// todo
}
}
```
```css
.todo-app {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.todo-app > input {
display: block;
margin: auto;
}
.task-list {
margin-top: 8px;
}
```
We now have a working input, which log to the console whenever the user adds a
task. Notice that when you load the page, the input is not focused. But adding
tasks is a core feature of a task list, so let us make it as fast as possible by
focusing the input.
We need to execute code when the `Root` component is ready (mounted). Let's do
that using the `onMounted` hook. We will also need to get a reference to the
input, by using the `t-ref` directive with the [`useRef`](../reference/hooks.md#useref) hook:
```xml
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
```
```js
// on top of file:
const { Component, mount, xml, useRef, onMounted } = owl;
```
```js
// in App
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
}
```
This is a very common situation: whenever we need to perform some actions depending
on the lifecycle of a component, we need to do it in the `setup` method, by using
one of the lifecycle hook. Here, we first get a reference to the `inputRef`,
then in the `onMounted` hook, we simply focus the html element.
## 7. Adding tasks (part 2)
In the previous section, we did everything except implement the code that actually
create tasks! So, let us do that now.
We need a way to generate unique `id` numbers. To do that, we will simply add a
`nextId` number in `App`. At the same time, let us remove the demo tasks in `App`:
```js
nextId = 1;
tasks = [];
```
Now, the `addTask` method can be implemented:
```js
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const text = ev.target.value.trim();
ev.target.value = "";
if (text) {
const newTask = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(newTask);
}
}
}
```
This almost works, but if you test it, you will notice that no new task is ever
displayed when the user press `Enter`. But if you add a `debugger` or a
`console.log` statement, you will see that the code is actually running as
expected. The problem is that Owl has no way of knowing that it needs to rerender
the user interface. We can fix the issue by making `tasks` reactive, with the
[`useState`](../reference/hooks.md#usestate) hook:
```js
// on top of the file
const { Component, mount, xml, useRef, onMounted, useState } = owl;
// replace the task definition in App with the following:
tasks = useState([]);
```
It now works as expected!
## 8. Toggling tasks
If you tried to mark a task as completed, you may have noticed that the text
did not change in opacity. This is because there is no code to modify the
`isCompleted` flag.
Now, this is an interesting situation: the task is displayed by the `Task`
component, but it is not the owner of its state, so ideally, it should not modify it.
However, for now, that's what we will do (this will be improved in a later step).
In `Task`, change the `input` to:
```xml
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
```
and add the `toggleTask` method:
```js
toggleTask() {
this.props.task.isCompleted = !this.props.task.isCompleted;
}
```
## 9. Deleting tasks
Let us now add the possibility do delete tasks. This is different from the previous
feature: deleting task has to be done on the task itself, but the actual operation
need to be done on the task list. So, we need to communicate the request to the
`Root` component. This is usually done by providing a callback in a prop.
First, let us update the `Task` template, css and js:
```xml
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
<span><t t-esc="props.task.text"/></span>
<span class="delete" t-on-click="deleteTask">🗑</span>
</div>
```
```css
.task {
font-size: 18px;
color: #111111;
display: grid;
grid-template-columns: 30px auto 30px;
}
.task > input {
margin: auto;
}
.delete {
opacity: 0;
cursor: pointer;
text-align: center;
}
.task:hover .delete {
opacity: 1;
}
```
```js
static props = ["task", "onDelete"];
deleteTask() {
this.props.onDelete(this.props.task);
}
```
And now, we need to provide the `onDelete` callback to each tasks in the `Root`
component:
```xml
<Task task="task" onDelete.bind="deleteTask"/>
```
```js
deleteTask(task) {
const index = this.tasks.findIndex(t => t.id === task.id);
this.tasks.splice(index, 1);
}
```
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
suffix that makes sure the function callback is bound to the component.
Notice also that we have two functions named `deleteTask`. The one in the Task
component just delegates the work to the Root component that owns the task list
via the `onDelete` property.
## 10. Using a store
Looking at the code, it is apparent that all the code handling tasks is scattered
all around the application. Also, it mixes UI code and business logic
code. Owl does not provide any high level abstraction to manage business logic,
but it is easy to do it with the basic reactivity primitives (`useState` and `reactive`).
Let us use it in our application to implement a central store. This is a pretty
large refactoring (for our application), since it involves extracting all task
related code out of the components. Here is the new content of the `app.js` file:
```js
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
function useStore() {
const env = useEnv();
return useState(env.store);
}
// -------------------------------------------------------------------------
// TaskList
// -------------------------------------------------------------------------
class TaskList {
nextId = 1;
tasks = [];
addTask(text) {
text = text.trim();
if (text) {
const task = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(task) {
task.isCompleted = !task.isCompleted;
}
deleteTask(task) {
const index = this.tasks.findIndex((t) => t.id === task.id);
this.tasks.splice(index, 1);
}
}
function createTaskStore() {
return reactive(new TaskList());
}
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml/* xml */ `
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="() => store.toggleTask(props.task)"/>
<span><t t-esc="props.task.text"/></span>
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
</div>`;
static props = ["task"];
setup() {
this.store = useStore();
}
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml/* xml */ `
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="store.tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>`;
static components = { Task };
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
this.store = useStore();
}
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
this.store.addTask(ev.target.value);
ev.target.value = "";
}
}
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
const env = {
store: createTaskStore(),
};
mount(Root, document.body, { dev: true, env });
```
## 11. Saving tasks in local storage
Now, our TodoApp works great, except if the user closes or refresh the browser!
It is really inconvenient to only keep the state of the application in memory.
To fix this, we will save the tasks in the local storage. With our current
codebase, it is a simple change: we need to save tasks to local storage and
listen to any change.
```js
class TaskList {
constructor(tasks) {
this.tasks = tasks || [];
const taskIds = this.tasks.map((t) => t.id);
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
}
// ...
}
function createTaskStore() {
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
saveTasks();
return taskStore;
}
```
The key point is that the `reactive` function takes a callback that will be called
every time an observed value is changed. Note that we need to call the `saveTasks`
method initially to make sure we observe all current values.
## 12. Filtering tasks
We are almost done, we can add/update/delete tasks. The only missing feature is
the possibility to display the task according to their completed status. We will
need to keep track of the state of the filter in `Root`, then filter the visible
tasks according to its value.
```js
class Root extends Component {
static template = xml /* xml */`
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
<div class="task-panel" t-if="store.tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt store.tasks.length">
/ <t t-esc="store.tasks.length"/>
</t>
task(s)
</div>
<div>
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="() => this.setFilter(f)"
t-esc="f"/>
</div>
</div>
</div>`;
setup() {
...
this.filter = useState({ value: "all" });
}
get displayedTasks() {
const tasks = this.store.tasks;
switch (this.filter.value) {
case "active": return tasks.filter(t => !t.isCompleted);
case "completed": return tasks.filter(t => t.isCompleted);
case "all": return tasks;
}
}
setFilter(filter) {
this.filter.value = filter;
}
}
```
```css
.task-panel {
color: #0088ff;
margin-top: 8px;
font-size: 14px;
display: flex;
}
.task-panel .task-counter {
flex-grow: 1;
}
.task-panel span {
padding: 5px;
cursor: pointer;
}
.task-panel span.active {
font-weight: bold;
}
```
Notice here that we set dynamically the css class of the filter with the object
syntax.
## 13. The Final Touch
Our list is feature complete. We can still add a few extra details to improve
the user experience.
1. Add a visual feedback when the user mouse is over a task:
```css
.task:hover {
background-color: #def0ff;
}
```
2. Make the text of a task clickable, to toggle its checkbox:
```xml
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-att-id="props.task.id"
t-on-click="() => store.toggleTask(props.task)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
```
3. Strike the text of completed task:
```css
.task.done label {
text-decoration: line-through;
}
```
## Final code
Our application is now complete. It works, the UI code is well separated from
the business logic code, it is testable, all under 150 lines of code (template
included!).
For reference, here is the final code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<script src="owl.js"></script>
<script src="app.js"></script>
</body>
</html>
```
```js
(function () {
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
function useStore() {
const env = useEnv();
return useState(env.store);
}
// -------------------------------------------------------------------------
// TaskList
// -------------------------------------------------------------------------
class TaskList {
constructor(tasks) {
this.tasks = tasks || [];
const taskIds = this.tasks.map((t) => t.id);
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
}
addTask(text) {
text = text.trim();
if (text) {
const task = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(task) {
task.isCompleted = !task.isCompleted;
}
deleteTask(task) {
const index = this.tasks.findIndex((t) => t.id === task.id);
this.tasks.splice(index, 1);
}
}
function createTaskStore() {
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
saveTasks();
return taskStore;
}
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml/* xml */ `
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox"
t-att-id="props.task.id"
t-att-checked="props.task.isCompleted"
t-on-click="() => store.toggleTask(props.task)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
</div>`;
static props = ["task"];
setup() {
this.store = useStore();
}
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml/* xml */ `
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
<div class="task-panel" t-if="store.tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt store.tasks.length">
/ <t t-esc="store.tasks.length"/>
</t>
task(s)
</div>
<div>
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="() => this.setFilter(f)"
t-esc="f"/>
</div>
</div>
</div>`;
static components = { Task };
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
this.store = useStore();
this.filter = useState({ value: "all" });
}
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
this.store.addTask(ev.target.value);
ev.target.value = "";
}
}
get displayedTasks() {
const tasks = this.store.tasks;
switch (this.filter.value) {
case "active":
return tasks.filter((t) => !t.isCompleted);
case "completed":
return tasks.filter((t) => t.isCompleted);
case "all":
return tasks;
}
}
setFilter(filter) {
this.filter.value = filter;
}
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
const env = { store: createTaskStore() };
mount(Root, document.body, { dev: true, env });
})();
```
```css
.todo-app {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.todo-app > input {
display: block;
margin: auto;
}
.task-list {
margin-top: 8px;
}
.task {
font-size: 18px;
color: #111111;
display: grid;
grid-template-columns: 30px auto 30px;
}
.task:hover {
background-color: #def0ff;
}
.task > input {
margin: auto;
}
.delete {
opacity: 0;
cursor: pointer;
text-align: center;
}
.task:hover .delete {
opacity: 1;
}
.task.done {
opacity: 0.7;
}
.task.done label {
text-decoration: line-through;
}
.task-panel {
color: #0088ff;
margin-top: 8px;
font-size: 14px;
display: flex;
}
.task-panel .task-counter {
flex-grow: 1;
}
.task-panel span {
padding: 5px;
cursor: pointer;
}
.task-panel span.active {
font-weight: bold;
}
```
-68
View File
@@ -1,68 +0,0 @@
# 🦉 Notes On Owl Architecture 🦉
We explain here how Owl is designed
Warning: these notes are technical by nature, and intended for people working
on Owl (or interested in understanding its design).
## Overview
Roughly speaking, Owl has 5 main parts:
- a virtual dom system (in `src/blockdom`)
- a component system (in `src/component`)
- a template compiler (located in the `src/compiler` folder)
- a small runtime code to tie them together (in `src/app`)
- a reactivity system (in `src/reactivity.ts`)
There are some other files, but the core of Owl can be understood with these
five main parts.
The virtual dom is an optimized virtual dom based on blocks, which supports
multi blocks (for fragments). Everything that owl renders is internally
represented by a virtual node. The job of the virtual dom is to efficiently
represent the current state of the application, and to build an actual DOM
representation when needed, or update the DOM whenever it is needed.
- some other helpers/smaller scale stuff
A rendering occurs in two phases:
- virtual rendering: this generates the virtual dom in memory, asynchronously
- patch: applies a virtual tree to the screen (synchronously)
There are several classes involved in a rendering:
- components
- a scheduler
- fibers: small objects containing some metadata, associated with a rendering of
a specific component
Components are organized in a dynamic component tree, visible in the user
interface. Whenever a rendering is initiated in a component `C`:
- a fiber is created on `C` with the rendering props information
- the virtual rendering phase starts on C (will asynchronously render all the
child components)
- the fiber is added to the scheduler, which will poll continuously, every
animation frame, if the fiber is done
- once it is done, the scheduler will call the task callback, which will apply
the patch (if it was not cancelled in the meantime).
# 🦉 VDom 🦉
Owl is a declarative component system: we declare the structure of the component
tree, and Owl will translate that to a list of imperative operations. This
translation is done by a virtual dom. This is the low level layer of Owl, most
developer will not need to call directly the virtual dom functions.
The main idea behind a virtual dom is to keep a in-memory representation of the
DOM (called a virtual node), and whenever some change is needed, to regenerate
a new representation, compute the difference between the old and the new, then
apply the changes.
`vdom` exports two functions:
- `h`: create a new virtual node
- `patch`: compare two virtual nodes, and apply the difference.
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
-255
View File
@@ -1,255 +0,0 @@
# Comparison with Vue/React
OWL, React and Vue have the same main feature: they allow developers to build
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
In this page, we try to highlight some of these differences. Obviously, a lot of
effort was put to be fair. However, if you disagree with some of the points
discussed, feel free to open an issue/submit a PR to correct this text.
## Content
- [Size](#size)
- [Class Based](#class-based)
- [Tooling/Build Step](#toolingbuild-step)
- [Templating](#templating)
- [Asynchronous rendering](#asynchronous-rendering)
- [Reactivity](#reactivity)
- [Hooks](#hooks)
## Size
OWL is intended to be small and to work at a slightly lower level of abstraction
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
| Framework | Size (minified, gzipped) |
| ------------------------ | ------------------------ |
| OWL | 18kb |
| Vue + VueX | 30kb |
| Vue + VueX + Vue Router | 39kb |
| React + ReactDOM + Redux | 40kb |
| jQuery | 30kb |
Note that those comparisons are not entirely fair, because we do not compare
the same exact set of features. For example, VueX and Vue Router support more
advanced use cases.
## Class Based
Both React and Vue moved away from defining components with classes. They prefer
a more functional approach, in particular, with the new `hooks` mechanisms.
This has some advantages and disadvantages. But the end result is that React
and Vue both offers multiple different ways of defining new components. In
contrast, Owl has only one mechanism: class-based components. We believe that Owl
components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us).
Also, functions or class based components are more than just syntax. Functions
come with a mindset of composition and class are about inheritance. Clearly,
both of these are important mechanisms for reusing code. Also, one does not
exclude the other.
It certainly looks like the world of UI frameworks is moving toward composition,
for many very good reasons. Owl is still good at composition (for example,
Owl supports slots, which is the primary mechanism to make generic reusable
components). But it can also use inheritance (and this is very important since
templates can also be inherited with `xpaths` transformations).
## Tooling/Build step
OWL is designed to be easy to use in a standalone way. For various reasons,
Odoo does not want to rely on standard web tools (such as webpack), and OWL can
be used by simply adding a script tag to a page.
```html
<script src="owl.min.js" />
```
In comparison, React encourages using JSX, which necessitate a build step, and
most Vue applications uses single file components, which also necessitate a build step.
On the flipside, external tooling may make it harder to use in some case, but it
also brings a lot of benefits. And React/Vue have both a large ecosystem.
Note that since Owl is not dependant on any external tool nor libraries, it is
very easy to integrate into any build toolchain. Also, since we cannot rely on
additional tools, we made a lot of effort to make the most of the web platform.
For example, Owl uses the standard `xml` parser that comes with every browser.
Because of that, Owl did not have to write its own template parser. Another
example is the [`xml`](../reference/templates.md#inline-templates) tag helper function, which makes use of
native template literals to allow in a natural way to write `xml` templates
directly in the javascript code. This can be easily integrated with editor
plugins to have autocompletion inside the template.
## Templating
OWL uses its own QWeb engine, which compiles templates on the
frontend, as they are needed. This is extremely convenient for our use case, in
particular because templates are described in XML files, and can be modified by
XPaths. Since Odoo is at its heart a modular application, this is an important
feature for us.
```xml
<div>
<button t-on-click="increment">Click Me! [<t t-esc="state.value"/>]</button>
</div>
```
Vue is actually kind of similar. Its template language is kind of close to QWeb,
with the `v` replaced by the `t`. However, it is also more fully featured. For
example, Vue templates have slots, or event modifiers. A large difference is that
most Vue applications will need to be built ahead of time, to compile the templates
into javascript functions. Note that Vue has a separate build which includes the
template compiler.
In contrast, most React applications do not use a templating language, but write
some JSX code, which is precompiled into plain JavaScript by a build step. This
example is done with the (kind of outdated) React class system:
```jsx
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
```
This has the advantage of having the full power of Javascript, but is less
structured than a template language. Note that the tooling is quite impressive:
there is a syntax highlighter for jsx here on github!
By comparison, here is the equivalent Owl component, written with the
[`xml`](../reference/templates.md#inline-templates) tag helper:
```js
class Clock extends Component {
static template = xml`
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
`;
}
```
## Asynchronous Rendering
This is actually a big difference between OWL and React/Vue: components in OWL
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
- `willStart` (before the component starts rendering)
- `willUpdateProps` (before new props are set)
Both these methods can be implemented and return a promise. The rendering will
then wait for these promises to be completed before patching the DOM. This is
useful for some use cases: for example, a component may want to fetch an external
library (a calendar component may need a specialized calendar rendering library),
in its willStart hook.
```javascript
class MyCalendarComponent extends owl.Component {
...
willStart() {
return utils.lazyLoad('static/libs/fullcalendar/fullcalendar.js');
}
...
}
```
This may be dangerous (to stop the rendering waiting for the network), but it is
extremely powerful as well, as demonstrated by the Odoo Web Client.
Lazy loading static libraries can obviously be done with React/Vue, but it is
more convoluted. For example, in Vue, you need to use a dynamic import keyword
that needs to be transpiled at build time in order for the component to be loaded
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
## Reactivity
React has a simple model: whenever the state changes, it is
replaced with a new state (via the `setState` method). Then, the DOM is patched.
This is simple, efficient, and a little bit awkward to write.
Vue is a little bit different: it replace magically the properties in the state
by getters/setters. With that, it can notify components whenever the state that
they read was changed.
Owl is closer to vue: it also tracks magically the state properties, but it does
only increment an internal counter whenever it changes. Note that it is done
with a `Proxy`, which means that it is totally transparent to the developers.
Adding new keys is supported. Once any part of the state has been changed, a
rendering is scheduled in the next microtask tick (promise queue).
## Hooks
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
the React world. They solve a lot of seemingly unconnected problems: attach
reusable behavior to a component, in a composable way, extract stateful logic
from a component or reuse stateful logic between component, without changing your
component hierarchy.
Here is an example of the React `useState` hook:
```js
import React, { useState } from "react";
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
```
Because of the way React designed the hooks API, they only work for functional
components. But in that case, they really are powerful. Every major React library
is in the process of redesigning their API with hooks (for example,
[Redux](https://react-redux.js.org/next/api/hooks)).
Vue 2 does not have hooks, but the Vue project is working on its next version,
which will feature its new [composition API](https://vue-composition-api-rfc.netlify.com/).
This work is based on the new ideas introduced by React hooks.
From the way React and Vue introduce their hooks, it may look like hooks are not
compatible with class components. However, this is not the case, as shown by
Owl [hooks](../reference/hooks.md). They are inspired by both React and Vue. For example,
the `useState` hook is named after React, but its API is closer to the `reactive`
Vue hook.
Here is what the `Counter` example above look like in Owl:
```js
import { Component, Owl } from "owl";
import { xml } from "owl/tags";
class Example extends Component {
static template = xml`
<div>
<p>You clicked {count.value} times</p>
<button t-on-click="increment">Click me</button>
</div>`;
count = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
Since the Owl framework had hooks from early in its life, its main APIs
are designed to be interacted with hooks from the start. For example, the
`Context` abstraction.
-98
View File
@@ -1,98 +0,0 @@
# 🦉 Notes On Owl Compiled Templates 🦉
This page will explain what an Owl compiled template look like. This is a
technical document intended for developers interested in understanding how Owl
works internally.
Broadly speaking, Owl compiles templates into a javascript function (a closure)
that returns a function (the "render" function). The point of the closure is to
have a place to store all values specific to the template (in particular, "blocks").
Once a template is compiled, its closure function is called once to get the
render function, and from then on, only the render function is used.
The render function takes some context (and some additional information) and
return a virtual dom representation of the rendered template, as a block tree.
A block tree is a very light weight representation that only contains the dynamic
part of the template, and its structure. It is actually independant of the
static part of the templates (which are contained in the blocks captured by the
closure). This means that the work performed at render time is only to collect
dynamic data, and to describe the block structure of the result.
It looks like this, in pseudo code:
```js
function closure(bdom, helpers) {
// here is some place to put stuff specific to the template, such as
// blocks
...
return function render(context, node, key) {
// only build here all dynamic parts of the template
// build a block tree
return tree;
}
}
```
Now, let us see an example. Consider the following template:
```xml
<div class="some-class">
<div class="blabla">
<span><t t-esc="state.value"/></span>
</div>
<t t-if="state.info">
<p class="info" t-att-class="someAttribute">
<t t-esc="state.info"/>
</p>
</t>
<SomeComponent value="value"/>
</div>
```
If you look carefully, there are 5 dynamic things:
- a text value (the first `t-esc`),
- a sub block (the `t-if`),
- a dynamic attribute (the `t-att-class` attribute),
- another text value (the second `t-esc`),
- and finally, a sub component
Here is the compiled code for this template:
```js
function closure(bdom, helpers) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(
`<div class="some-class"><div class="blabla"><span><block-text-0/></span></div><block-child-0/><block-child-1/></div>`
);
let block2 = createBlock(`<p class="info" block-attribute-0="class"><block-text-1/></p>`);
return function render(ctx, node, key = "") {
let b2, b3;
let txt1 = ctx["state"].value;
if (ctx["state"].info) {
let attr1 = ctx["someAttribute"];
let txt2 = ctx["state"].info;
b2 = block2([attr1, txt2]);
}
b3 = component(`SomeComponent`, { value: ctx["value"] }, key + `__1`, node, ctx);
return block1([txt1], [b2, b3]);
};
}
```
The values captured in the closure capture the static part of the template: we
define here two blocks (which contains a template node, that can be deep cloned
whenever a block is mounted). Then the render function only describes the block
tree structure of the result, depending on the context. This means that we
minimize the amount of work done at render time.
Then, when we want to patch the dom, Owl will uses the `patch` function from
blockdom, which then will diff the block tree, and deep clone new blocks whenever
a new block is inserted, keep track of dynamic parts of each block, and update
them accordingly.
With this design, the cost of rendering a template is proportional to the number
of dynamic values, and not to the size of the template.
-180
View File
@@ -1,180 +0,0 @@
# 🦉 Why Owl ? 🦉
The common wisdom is that one should not reinvent the wheel, because that would
waste effort and resources. It is certainly true in many cases. A javascript
framework is a considerable investment, so it is quite logical to ask the question:
why did Odoo decide to make OWL instead of using a standard/well known framework,
such as React or Vue?
As you might expect, the answer to that question is not simple. But most of the
reasons discussed in this page are a consequence from a single fact: Odoo is
extremely modular.
This means, for example, that the core parts of Odoo are not aware, before runtime,
of what files will be loaded/executed, or what will be the state of the UI. Because
of that, Odoo cannot rely on a standard build toolchain. Also, this implies that
the core parts of Odoo need to be extremely generic. In other words, Odoo is not
really an application with a user interface. It is an application which generates
a dynamic user interface. And most frameworks are not up to the task.
Betting on Owl was not an easy choice to make, because there certainly are a lot
of conflicting needs that we want to carefully balance. Choosing anything other
than a well known framework is bound to be controversial. This page will explain
some of the reason why we still believe that building Owl is a worthwile
endeavour.
## Strategy
It is true that we want to keep control of our technology, in the sense that we
do not want to depend on Facebook or Google, or any other large (or small)
company. If they decide to change their license, or to go in a direction that
will not work for us, this may be a problem. This is even more true because
Odoo is not a conventional javascript application, and our needs are probably
quite different as most other applications.
## Class components
It is clear that the biggest frameworks are moving away from class components.
There is an implicit assumption that class components are terrible, and that
functional programming is the way to go. React even goes as far as to say that
classes are confusing for developers.
While there is some truth to that, and to the fact that composition is certainly
a good mechanism for code reuse, we believe that classes and inheritance are
important tools.
Sharing code between generic components with inheritance is the way Odoo built
its web client. And it is clear that inheritance is not the root of all evils.
It is often a perfectly simple and appropriate solution. What matter most is
the architectural decisions.
Also, Odoo has another specific use out of class components: each method of a
class provides an extension point for addons. This may not be a clean architecture
pattern, but it is a pragmatic decision that served Odoo well: classes are
sometimes monkey-patched to add behaviour from the outside. A little bit like
mixins, but from the outside.
Using React or Vue would make it significantly harder to monkey patch components,
because a lot of the state is hidden in their internals.
## Tooling
React or Vue have a huge community, and a lot of effort have been made into their
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
since the assets are totally dynamic (and could change whenever the user installs
or removes an addon), we need to have all that kind of tooling on the production
servers. This is certainly not ideal.
Also, this makes it very complicated to setup Vue or React tools: Odoo code is
not a simple file that import other files. It changes all the time, assets
are bundled differently in different contexts. This is the reason why Odoo has
its own module system, which are resolved at runtime, by the browser. The
dynamic nature of Odoo means that we often need to delay work as late as possible
(in other word, we want a JIT user interface!)
Our ideal framework has minimal (mandatory) tooling, which makes it easier to
deploy. Using React without JSX, or Vue without vue file is not very appealing.
At the same time, Owl is designed to solve this issue: it compiles templates
by the browser, it doesn't need much code for that, since we use the XML parser
built into each browser. Owl works with or without any additional tooling. It
can use template strings to write single file components, and is easy to integrate
in any html page, with a simple `<script>` tag.
## Template based
Odoo stores templates as XML documents in a database. This is very powerful, since
this allow the use of xpaths to customize other templates. This is a very
important feature of odoo, and one of the key to Odoo modularity.
Because of that, we still expect to write our templates in an XML document.
Weirdly enough, no major framework uses XML to store templates, even though it
is extremely convenient.
So, using React or Vue means that we need to make a template compiler. For React,
that would be a compiler that would take a QWeb template, and convert it to a
React render function. For Vue, it would convert it to a Vue template. Then
we need to bundle the vue template compiler as well.
Not only this would be complex (compiling a templating language into another is
not an easy task), but it would negatively impact the developer experience as
well. Writing Vue or React components in a QWeb template would certainly be
awkward, and very confusing.
## Developer Experience
This brings us to the following point: developer experience. We see this choice
as an investment for the future, and we want to make onboarding developers as
easy as possible.
While many javascript professionals clearly think that react/vue is not difficult
(which is true to some extent), it is alsy true that many non js specialists are
overwhelmed with the frontend world: functional components, hooks, and many other
fancy words. Also, what is available in the compilation context may be difficult,
there is a lot of black magic going on in pretty much every framework. Vue
somehow join various namespaces into one, under the hood, and add various internal
keys. Svelte transform the code. React require that state transformations are
deep, and not shallow.
Owl is trying very hard to have a simple and familiar API. It uses classes. Its
reactivity system is explicit, not implicit. The scoping rules are obvious. In
case of doubt, we err on the side of not implementing a feature.
It is certainly different from React or Vue, but at the same time, kind of
familiar for experienced developers.
## JIT compilation
There is also a clear trend in the frontend world to compile code
as much as possible ahead of time. Most frameworks will compile templates ahead
of time. And now Svelte is trying to compile the JS code away, so it can remove
itself from the bundle.
This is certainly reasonable for many usecases. However, this is not what Odoo
needs: Odoo will fetch templates from the database and need to compile them only
at the last possible moment, so we can apply all necessary xpaths.
Even more: Odoo needs to be able to generate (and compile) templates at runtime.
Currently, Odoo form views interpret an xml description. But the form view code
then needs to do a lot of complicated operations. With Owl, we will be able to
transform a view description into a QWeb template, then compile that and use it
immediately.
## Reactivity
There are other design choices that we feel are not optimal in other frameworks.
For example, the reactivity system. We like the way Vue did it, but it has a
flaw: it is not really optional. There is actually a way to opt out of the reactivity
system by freezing the state, but then, it is freezed.
And there certainly are situations where we need a state, which is not read-only,
and not observed. For example, imagine a spreadsheet component. It may have a
very large internal state, and it knows exactly when it needs to be rendered
(basically, whenever the user performs some action). Then, observing its state
is a net performance loss, both for the CPU and the memory.
## Concurrency
Many applications are happy to simply display a spinner whenever a new asynchronous
action is performed, but Odoo wants a different user experience: most asynchronous
state changes are not displayed until ready. This is sometimes called a concurrent
mode: the UI is rendered in memory, and displayed only when it is ready (and
only if it has not been cancelled by subsequent user actions).
React has now an experimental concurrent mode, but it was not ready when Owl
started. Vue has not really an equivalent API (suspense is not what we need).
Also, React concurrent mode is complex to use. Concurrency was one of the rare
strong point of the former Odoo js framework (widgets), and we feel that Owl has
now a very strong concurrent mode, which is simple and powerful at the same time.
## Conclusion
This lengthy discussion showed that there are many small and not so small reasons
that current standard frameworks are not tailored to our needs. It is perfectly
fine, because they each chose a different set of tradeoffs.
However, we feel that there is still room in the framework world for something
that is different. For a framework that makes choices compatible with Odoo.
And that is why we built Owl 🦉.
-49
View File
@@ -1,49 +0,0 @@
# 🦉 Owl overview 🦉
Here is a list of everything exported by the Owl library:
Main entities:
- [`App`](reference/app.md): represent an Owl application (mainly a root component,a set of templates, and a config)
- [`Component`](reference/component.md): the main class to define a concrete Owl component
- [`mount`](reference/app.md#mount-helper): main entry point for most application: mount a component to a target
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
Reactivity
- [`useState`](reference/reactivity.md#usestate): create a reactive object (hook, linked to a specific component)
- [`reactive`](reference/reactivity.md#reactive): create a reactive object (not linked to any component)
- [`markRaw`](reference/reactivity.md#markraw): mark an object or array so that it is ignored by the reactivity system
- [`toRaw`](reference/reactivity.md#toraw): given a reactive objet, return the raw (non reactive) underlying object
Lifecycle hooks:
- [`onWillStart`](reference/component.md#willstart): hook to define asynchronous code that should be executed before component is rendered
- [`onMounted`](reference/component.md#mounted): hook to define code that should be executed when component is mounted
- [`onWillPatch`](reference/component.md#willpatch): hook to define code that should be executed before component is patched
- [`onWillUpdateProps`](reference/component.md#willupdateprops): hook to define code that should be executed before component is updated
- [`onPatched`](reference/component.md#patched): hook to define code that should be executed when component is patched
- [`onWillRender`](reference/component.md#willrender): hook to define code that should be executed before component is rendered
- [`onRendered`](reference/component.md#rendered): hook to define code that should be executed after component is rendered
- [`onWillUnmount`](reference/component.md#willunmount): hook to define code that should be executed before component is unmounted
- [`onWillDestroy`](reference/component.md#willdestroy): hook to define code that should be executed before component is destroyed
- [`onError`](reference/component.md#onerror): hook to define a Owl error handler
Other hooks:
- [`useComponent`](reference/hooks.md#usecomponent): return a reference to the current component (useful to create derived hooks)
- [`useEffect`](reference/hooks.md#useeffect): define an effect with its dependencies
- [`useEnv`](reference/hooks.md#useenv): return a reference to the current env
- [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM
- [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`)
- [`useChildSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for child components)
- [`useSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for current component and child components)
Utility/helpers:
- [`EventBus`](reference/utils.md#eventbus): a simple event bus
- [`loadFile`](reference/utils.md#loadfile): an helper to load a file from the server
- [`markup`](reference/templates.md#outputting-data): utility function to define strings that represent html (should not be escaped)
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
-123
View File
@@ -1,123 +0,0 @@
# 🦉 App 🦉
## Content
- [Overview](#overview)
- [API](#api)
- [Configuration](#configuration)
- [`mount` helper](#mount-helper)
- [Loading templates](#loading-templates)
## Overview
Every Owl application has a root element, a set of templates, an environment and
possibly a few other settings. The `App` class is a simple class that represents
all of these elements. Here is an example:
```js
const {Component, App } = owl;
class MyComponent extends Component { ... }
const app = new App(MyComponent, { props: {...}, templates: "..."});
app.mount(document.body);
```
The basic workflow is: create an `App` instance configured with the root
component, the templates, and possibly other settings. Then, we mount that
instance somewhere in the DOM.
## API
- **`constructor(Root[, config])`**: first argument should be a component class (not
an instance), and the optional second argument is a configuration object (see below).
- **`mount(target, options)`**: first argument is an html element, and the optional
second argument is an object with mounting options (see below). Mount the app
to a target in the DOM. Note that this is an asynchronous operation: the `mount`
method returns a promise that resolves to the component instance whenever it
is complete.
The `option` object is an object with the following keys:
- **`position (string)`**: either `first-child` or `last-child`. This option determines
the position of the application in the target: either first or last child.
- **`destroy()`**: destroys the application
## Configuration
The `config` object is an object with some of the following keys:
- **`env (object)`**: if given, this will be the shared `env` given to each component
- **`props (object)`**: the props given to the root component
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in
[`dev` mode](#dev-mode);
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
that Owl will not log a message to warn that Owl is in `dev` mode.
- **`translatableAttributes (string[])`**: a list of additional attributes that should
be translated (see [translations](translations.md))
- **`translateFn (function)`**: a function that will be called by owl to translate
templates (see [translations](translations.md))
- **`templates (string | xml document)`**: all the templates that will be used by
the components created by the application.
- **`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
Note that there is a `mount` helper to do that in just a line:
```js
const { mount, Component } = owl;
class MyComponent extends Component {
...
}
mount(MyComponent, document.body, { props: {...}, templates: "..."});
```
Here is the `mount` function signature:
**`mount(Component, target, config)`** with the following arguments:
- **`Component`**: a component class (Root component of the app)
- **`target`**: an html element, where the component will be mounted as last child
- **`config (optional)`**: a config object (the same as the App config object)
Most of the time, the `mount` helper is more convenient, but whenever one needs
a reference to the actual Owl App, then using the `App` class directly is
possible.
## Loading templates
Most applications will need to load templates whenever they start. Here is
what it could look like in practice:
```js
// in the main js file:
const { loadFile, mount } = owl;
// async, so we can use async/await
(async function setup() {
const templates = await loadFile(`/some/endpoint/that/return/templates`);
const env = {
_t: someTranslateFn,
templates,
// possibly other stuff
};
mount(Root, document.body, { env });
})();
```
## Dev mode
Dev mode activates some additional checks and developer amenities:
- [Props validation](./props.md#props-validation) is performed
- [t-foreach](./templates.md#loops) loops check for key unicity
- Lifecycle hooks are wrapped to report their errors in a more developer-friendly way
- onWillStart and onWillUpdateProps will emit a warning in the console when they
take longer than 3 seconds in an effort to ease debugging the presence of deadlocks
-456
View File
@@ -1,456 +0,0 @@
# 🦉 Owl Component 🦉
## Content
- [Overview](#overview)
- [Properties and methods](#properties-and-methods)
- [Static Properties](#static-properties)
- [Lifecycle](#lifecycle)
- [`setup`](#setup)
- [`willStart`](#willstart)
- [`willRender`](#willrender)
- [`rendered`](#rendered)
- [`mounted`](#mounted)
- [`willUpdateProps`](#willupdateprops)
- [`willPatch`](#willpatch)
- [`patched`](#patched)
- [`willUnmount`](#willunmount)
- [`willDestroy`](#willdestroy)
- [`onError`](#onerror)
- [Sub components](#sub-components)
- [Dynamic Sub components](#dynamic-sub-components)
- [`status` helper](#status-helper)
## Overview
An Owl component is a small class which represents some part of the user interface.
It is part of a component tree, and has an [environment](environment.md) (`env`),
which is propagated from a parent to its children.
OWL components are defined by subclassing the `Component` class. For example,
here is how a `Counter` component could be implemented:
```javascript
const { Component, xml, useState } = owl;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
In this example, we use the `xml` helper to define inline templates, and the
`useState` hook, which returns a reactive version of its argument (see the page
on reactivity).
## Properties and methods
The `Component` class has a very small API.
- **`env (object)`**: the component [environment](environment.md)
- **`props (object)`**: this is an object containing all the [props](props.md) given by
the parent to a child component
Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component (otherwise you risk
unintended effects, since the parent may not be aware of the change)!!
The `props` can be modified dynamically by the parent. In that case, the
component will go through the following lifecycle methods: `willUpdateProps`,
`willPatch` and `patched`.
* **`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.
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
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
- **`template (string)`**: this is the name of the template that
will render the component. Note that there is a helper `xml` to
make it easy to define an inline template.
* **`components (object, optional)`**: if given, this is an object that contains
the classes of any sub components needed by the template.
```js
class ParentComponent extends owl.Component {
static components = { SubComponent };
}
```
* **`props (object, optional)`**: if given, this is an object that describes the
type and shape of the (actual) props given to the component. If Owl mode is
`dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](props.md#props-validation) for more information.
```js
class Counter extends owl.Component {
static props = {
initialValue: Number,
optional: true,
};
}
```
- **`defaultProps (object, optional)`**: if given, this object define default
values for (top-level) props. Whenever `props` are given to the object, they
will be altered to add default value (if missing). Note that it does not
change the initial object, a new object will be created instead. See
[default props](props.md#default-props) for more information
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0,
};
}
```
## Lifecycle
A solid and robust component system needs a complete lifecycle system to help
developers write components. Here is a complete description of the lifecycle of
a Owl component:
| Method | Hook | Description |
| --------------------------------------- | ------------------- | ---------------------------------------------------------------------- |
| **[setup](#setup)** | none | setup |
| **[willStart](#willstart)** | `onWillStart` | async, before first rendering |
| **[willRender](#willrender)** | `onWillRender` | just before component is rendered |
| **[rendered](#rendered)** | `onRendered` | just after component is rendered |
| **[mounted](#mounted)** | `onMounted` | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdateprops)** | `onWillUpdateProps` | async, before props update |
| **[willPatch](#willpatch)** | `onWillPatch` | just before the DOM is patched |
| **[patched](#patched)** | `onPatched` | just after the DOM is patched |
| **[willUnmount](#willunmount)** | `onWillUnmount` | just before removing component from DOM |
| **[willDestroy](#willdestroy)** | `onWillDestroy` | just before component is destroyed |
| **[error](#onerror)** | `onError` | catch and handle errors (see [error handling page](error_handling.md)) |
### `setup`
_setup_ is run just after the component is constructed. It is a lifecycle method,
very similar to the _constructor_, except that it does not receive any argument.
It is the proper place to call hook functions. Note that one of the main reason to
have the `setup` hook in the component lifecycle is to make it possible to
monkey patch it. It is a common need in the Odoo ecosystem.
```javascript
setup() {
useSetupAutofocus();
}
```
### `willStart`
`willStart` is an asynchronous hook that can be implemented to
perform some (most of the time asynchronous) action before the initial rendering of a component.
It will be called exactly once before the initial rendering. It is useful
in some cases, for example, to load external assets (such as a JS library)
before the component is rendered. Another use case is to load data from a server.
The `onWillStart` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onWillStart(async () => {
this.data = await this.loadData()
});
}
```
At this point, the component is not yet rendered. Note that slow `willStart`
code will slow down the rendering of the user interface. Therefore, some care
should be made to make this method as fast as possible.
Note that if there are more than one `onWillStart` registered callback, then they
will all be run in parallel.
### `willRender`
It is uncommon but it may happen that one need to execute code just before a
component is rendered (more precisely, when its compiled template function is executed).
To do that, one can use the `onWillRender` hook:
```javascript
setup() {
onWillRender(() => {
// do something
});
}
```
`willRender` hooks are called just before rendering templates, parent first,
then children.
### `rendered`
It is uncommon but it may happen that one need to execute code just after a
component is rendered (more precisely, when its compiled template function is executed).
To do that, one can use the `onRendered` hook:
```javascript
setup() {
onRendered(() => {
// do something
});
}
```
`rendered` hooks are called just after rendering templates, parent first,
then children. Note that at this moment, the actual DOM may not exist yet (if
it is the first rendering), or is not updated yet. This will be dom in the next
animation frame as soon as all the components are ready.
### `mounted`
The `mounted` hook is called each time a component is attached to the
DOM, after the initial rendering. At this point, the component is considered
_active_. This is a good place to add some listeners, or to interact with the
DOM, if the component needs to perform some measure for example.
It is the opposite of `willUnmount`. If a component has been mounted, it will
always be unmounted at some point in the future.
The mounted method will be called recursively on each of its children. First,
children, then parents.
It is allowed (but not encouraged) to modify the state in the `mounted` hook.
Doing so will cause a rerender, which will not be perceptible by the user, but
will slightly slow down the component.
The `onMounted` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onMounted(() => {
// do something here
});
}
```
### `willUpdateProps`
The `willUpdateProps` is an asynchronous hook, called just before new props
are set. This is useful if the component needs to perform an asynchronous task,
depending on the props (for example, assuming that the props are
some record Id, fetching the record data).
The `onWillUpdateProps` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onWillUpdateProps(nextProps => {
return this.loadData({id: nextProps.id});
});
}
```
Notice that it receives the next props for the component.
This hook is not called during the first render (but `willStart` is called
and performs a similar job). Also, as most of the hooks, it is called in the
usual order: parents first, then children.
### `willPatch`
The willPatch hook is called just before the DOM patching process starts.
It is not called on the initial render. This is useful to read
information from the DOM. For example, the current position of the
scrollbar.
Note that modifying the state is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the component is not in the DOM.
The `onWillPatch` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onWillPatch(() => {
this.scrollState = this.getScrollSTate();
});
}
```
The `willPatch` is called in the usual parent->children order.
### `patched`
This hook is called whenever a component did actually update its DOM (most
likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the component is
not in the DOM.
The `onPatched` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onPatched(() => {
this.scrollState = this.getScrollSTate();
});
}
```
Updating the component state in this hook is possible, but not encouraged.
One needs to be careful, because updates here will create an additional rendering, which in
turn will cause other calls to the `patched` method. So, we need to be particularly
careful at avoiding endless cycles.
Like `mounted`, the `patched` hook is called in the order: children first, then
parent.
### `willUnmount`
`willUnmount` is a hook that is called each time just before a component is
unmounted from the DOM. This is a good place to remove listeners, for example.
The `onWillUnmount` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onMounted(() => {
// add some listener
});
onWillUnmount(() => {
// remove listener
});
}
```
This is the opposite method of `mounted`. Note that if a component is destroyed
before being mounted, the `willUnmount` method may not be called.
Parent `willUnmount` hooks will be called before children.
### `willDestroy`
Sometimes, components need to do some action in the `setup` and clean it up when
they are inactive. However, the `willUnmount` hook is not appropriate for the
cleaning operation, since the component may be destroyed before it has even been
mounted. The `willDestroy` hook is useful in that situation, since it is always
called.
The `onWillUnmount` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onWillDestroy(() => {
// do some cleanup
});
}
```
The `willDestroy` hooks are first called on children, then on parents.
### `onError`
Sadly, it may happen that components crashes at runtime. This is an unfortunate
reality, and this is why Owl needs to provide a way to handle these errors.
The `onError` hook is useful when we need to intercept and properly react
to errors that occur in some sub components. See the page on
[error handling](error_handling.md) for more detail.
```javascript
setup() {
onError(() => {
// do something
});
}
```
## Sub components
It is convenient to define a component using other (sub) components. This is
called composition, and is very powerful in practice. To do that in Owl, one
can just use a tag starting with a capital letter in its template, and register
the sub component class in its static `components` object:
```js
class Child extends Component {
static template = xml`<div>child component <t t-esc="props.value"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child value="1"/>
<Child value="2"/>
</div>`;
static components = { Child };
}
```
This example also shows how one can pass information from the parent component
to the child component, as props. See the [props section](props.md)
for more information.
## Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case,
the `t-component` directive can also be used to accept dynamic values. This should
be an expression that evaluates to a component class. For example:
```js
class A extends Component {
static template = xml`<div>child a</div>`;
}
class B extends Component {
static template = xml`<span>child b</span>`;
}
class Parent extends Component {
static template = xml`<t t-component="myComponent"/>`;
state = useState({ child: "a" });
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
## `status` helper
It is sometimes convenient to have a way to find out in which state a component
is currently. To do that, one can use the `status` helper:
```js
const { status } = owl;
// assume component is an instance of a Component
console.log(status(component));
// logs either:
// - 'new', if the component is new and has not been mounted yet
// - 'mounted', if the component is currently mounted
// - 'cancelled', if the component has not been mounted yet but will be destroyed soon
// - 'destroyed' if the component is currently destroyed
```
-163
View File
@@ -1,163 +0,0 @@
# 🦉 Concurrency Model 🦉
## Content
- [Overview](#overview)
- [Rendering Components](#rendering-components)
- [Semantics](#semantics)
- [Asynchronous Rendering](#asynchronous-rendering)
## Overview
Owl was designed from the very beginning with asynchronous components. This comes
from the `willStart` and the `willUpdateProps` lifecycle hooks. With these
asynchronous hooks, it is possible to build complex highly concurrent applications.
Owl concurrent mode has several benefits: it makes it possible to delay the
rendering until some asynchronous operation is complete, it makes it possible
to lazy load libraries, while keeping the previous screen completely functional.
It is also good for performance reasons: Owl uses it to only apply the result of
many different renderings only once in an animation frame. Owl can cancel
a rendering that is no longer relevant, restart it, reuse it in some cases.
But even though using concurrency is quite simple (and is the default behaviour),
asynchrony is difficult, because it introduces an additional dimension that
vastly increase the complexity of an application. This section will explain
how Owl manages this complexity, how concurrent rendering works in a general way.
## Rendering Components
The word _rendering_ is a little vague, so, let us explain more precisely the
process by which Owl components are displayed on a screen.
When a component is mounted or updated, a new rendering is started. It has
two phases: _virtual rendering_ and _patching_.
### Virtual rendering
This phase represent the process of rendering a template, in memory, which creates
a virtual representation of the desired component html). The output of this phase is a
virtual DOM.
It is asynchronous: each subcomponents needs to either be created (so, `willStart`
will need to be called), or updated (which is done with the `willUpdateProps`
method). This is completely a recursive process: a component is the root of a
component tree, and each sub component needs to be (virtually) rendered.
### Patching
Once a rendering is complete, it will be applied on the next animation frame.
This is done synchronously: the whole component tree is patched to the real
DOM.
## Semantics
We give here an informal description of the way components are created/updated
in an application. Here, ordered lists describe actions that are executed
sequentially, bullet lists describe actions that are executed in parallel.
**Scenario 1: initial rendering** Imagine we want to render the following component tree:
```
A
/ \
B C
/ \
D E
```
Here is what happen whenever we mount the root
component (with some code like `app.mount(document.body)`).
1. `willStart` is called on `A`
2. when it is done, template `A` is rendered.
- component `B` is created
1. `willStart` is called on `B`
2. template `B` is rendered
- component `C` is created
1. `willStart` is called on `C`
2. template `C` is rendered
- component `D` is created
1. `willStart` is called on `D`
2. template `D` is rendered
- component `E` is created
1. `willStart` is called on `E`
2. template `E` is rendered
3. each components are patched into a detached DOM element, in the following order:
`E`, `D`, `C`, `B`, `A`. (so the actual full DOM tree is created
in one pass)
4. the component `A` root element is actually appended to `document.body`
5. The method `mounted` is called recursively on all components in the following
order: `E`, `D`, `C`, `B`, `A`.
**Scenario 2: updating a component**. Now, let's assume that the user clicked on some
button in `C`, and this results in a state update, which is supposed to:
- update `D`,
- remove `E`,
- add new component `F`.
So, the component tree should look like this:
```
A
/ \
B C
/ \
D F
```
Here is what Owl will do:
1. because of a state change, the method `render` is called on `C`
2. template `C` is rendered again
- component `D` is updated:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- component `F` is created:
1. hook `willStart` is called on `F` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
because it is not mounted yet)
4. components `F`, `D` are patched in that order
5. component `C` is patched, which will cause recursively:
1. `willUnmount` hook on `E`
2. destruction of `E`,
6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C`
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`.
### Asynchronous Rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
different parts of a system are active at the same time, one needs to think
carefully about all possible interactions. Clearly, this is also true for Owl
components.
There are two different common problems with Owl asynchronous rendering model:
- any component can delay the rendering (initial and subsequent) of the whole
application
- for a given component, there are two independant situations that will trigger an
asynchronous rerendering: a change in the state, or a change in the props.
These changes may be done at different times, and Owl has no way of knowing
how to reconcile the resulting renderings.
Here are a few tips on how to work with asynchronous components:
1. Minimize the use of asynchronous components!
2. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once.
-76
View File
@@ -1,76 +0,0 @@
# 🦉 Environment 🦉
## Content
- [Overview](#overview)
- [Setting an Environment](#setting-an-environment)
- [Using a sub environment](#using-a-sub-environment)
- [Content of an Environment](#content-of-an-environment)
## Overview
An environment is a shared object given to all components in a tree. It is not
used by Owl itself, but it is useful for application developers to provide a
simple communication channel between components (in addition to the props).
The `env` given to the [`App`](app.md) is assigned to the `env` component
property.
```
Root
/ \
A B
```
Also, the `env` object is frozen when the application is started. This is done
to ensure a simpler mental model of what's happening in runtime. Note that it
is only shallowly frozen, so sub objects can be modified.
## Setting an environment
The correct way to customize an environment is to simply give it to the `App`,
whenever it is created.
```js
const env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
new App(Root, { env }).mount(document.body);
// or alternatively
mount(App, document.body, { env });
```
## Using a sub environment
It is sometimes useful to add one (or more) specific keys to the environment,
from the perspective of a specific component and its children. In that case, the
solution presented above will not work, since it sets the global environment.
There are two hooks for this situation: [`useSubEnv` and `useChildSubEnv`](hooks.md#usesubenv-and-usechildsubenv).
```js
class SomeComponent extends Component {
setup() {
useSubEnv({ myKey: someValue }); // myKey is now available for all child components
}
}
```
## Content of an Environment
The `env` object content is totally up to the application developer. However,
some good use cases for additional keys in the environment are:
- some configuration keys,
- session information,
- generic services (such as doing rpcs).
- other utility functions that one want to inject, such as a translation function.
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
-75
View File
@@ -1,75 +0,0 @@
# 🦉 Error Handling 🦉
## Content
- [Overview](#overview)
- [Managing Errors](#managing-errors)
- [Example](#example)
## Overview
By default, whenever an error occurs in the rendering of an Owl application, we
destroy the whole application. Otherwise, we cannot offer any guarantee on the
state of the resulting component tree. It might be hopelessly corrupted, but
without any user-visible feedback.
Clearly, it is usually a little bit extreme to destroy the application. This
is why we need a mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `onError` hook.
The main idea is that the `onError` hook register a function that will be called
with the error. This function need to handle the situation, most of the time by
updating some state and rerendering itself, so the application can return to a
normal state.
## Managing Errors
Whenever the `onError` lifecycle hook is used, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `onError` method. This allows us to properly handle the error, and to
not break the application.
There are important things to know:
- If an error that occured in the internal rendering cycle is not caught, then
Owl will destroy the full application. This is done on purpose, because Owl
cannot guarantee that the state is not corrupted from this point on.
- errors coming from event handlers are NOT managed by `onError` or any other
owl mechanism. This is up to the application developer to properly recover
from an error
- if an error handler is unable to properly handle an error, it can just rethrow
an error, and Owl will try looking for another error handler up the component
tree.
## Example
For example, here is how we could implement a generic component `ErrorBoundary`
that render its content, and a fallback if an error happened.
```js
class ErrorBoundary extends Component {
static template = xml`
<t t-if="error" t-slot="fallback">An error occurred</t>
<t t-else="" t-slot="content"`;
setup() {
this.state = useState({ error: false });
onError(() => (this.state.error = true));
}
}
```
Using the `ErrorBoundary` is then simple simple:
```xml
<ErrorBoundary>
<SomeOtherComponent/>
<t t-set-slot="fallback">Some specific error message</t>
</ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop (also, see the page on
[slots](slots.md) for more information on the `t-slot` directive).
-119
View File
@@ -1,119 +0,0 @@
# 🦉 Event Handling 🦉
## Content
- [Event Handling](#event-handling)
- [Modifiers](#modifiers)
- [Synthetic Events](#synthetic-events)
- [On Components](#on-components)
## Event Handling
In a component's template, it is useful to be able to register handlers on DOM
elements to some specific events. This is what makes a template _alive_. This
is done with the `t-on` directive. For example:
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", component.someMethod.bind(component));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event. The value of the `t-on` expression should be a valid javascript expression
that evaluates to a function in the context of the current component. So, one
can get a reference to the event, or pass some additional arguments. For example,
all the following expressions are valid:
```xml
<button t-on-click="someMethod">Do something</button>
<button t-on-click="() => this.increment(3)">Add 3</button>
<button t-on-click="ev => this.doStuff(ev, 'value')">Do something</button>
```
Notice the use of the `this` keyword in the lambda function: this is the
correct way to call a method on the component in a lambda function.
One could use the following expression:
```xml
<button t-on-click="() => increment(3)">Add 3</button>
```
But then, the increment function may be unbound (unless the component binds it
in its setup function, for example).
## Modifiers
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
| `.synthetic` | define a synthetic event handler (see below) |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
itself.
Finally, empty handlers are tolerated as they could be defined only to apply
modifiers. For example,
```xml
<button t-on-click.stop="">Do something</button>
```
This will simply stop the propagation of the event.
## Synthetic Events
In some cases, attaching an event handler for each element of large lists has
a non trivial cost. Owl provides a way to efficiently improve the performance:
with synthetic event, it actually adds only one handler on the document body,
and will properly call the handler, just as expected.
The only difference with regular events is that the event is caught at the document
body, so it cannot be stopped before it actually gets there. Since it may be
surprising in some cases, it is not enabled by default.
To enable it, one can just use the `.synthetic` suffix:
```xml
<div>
<t t-foreach="largeList" t-as="elem" t-key="elem.id">
<button t-on-click.synthetic="doSomething" ...>
<!-- some content -->
</button>
</t>
</div>
```
## On Components
The `t-on` directive also works on a child component:
```xml
<div>
in some template
<Child t-on-click="dosomething"/>
</div>
```
This will catch all click events on any html element contained in the `Child`
sub component. Note that if the child component is reduced to one (or more) text
nodes, then clicking on it will not call the handler, since the event will be
dispatched by the browser on the parent element (a `div` in this case).
-324
View File
@@ -1,324 +0,0 @@
# 🦉 Hooks 🦉
## Content
- [Overview](#overview)
- [The Hook Rule](#the-hook-rule)
- [Lifecycle hooks](#lifecycle-hooks)
- [Other hooks](#other-hooks)
- [`useState`](#usestate)
- [`useRef`](#useref)
- [`useSubEnv` and `useChildSubEnv`](#usesubenv-and-usechildsubenv)
- [`useExternalListener`](#useexternallistener)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [`useEffect`](#useeffect)
- [Example: Mouse Position](#example-mouse-position)
## Overview
Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components
- help organizing code by feature in complex components
- use state in functional components, without writing a class.
Owl hooks serve the same purpose, except that they work for class components
(note: React hooks do not work on class components, and maybe because of that,
there seems to be the misconception that hooks are in opposition to class. This
is clearly not true, as shown by Owl hooks).
Hooks work beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
## The Hook Rule
There is only one rule: every hook for a component has to be called in the _setup_ method, or in class fields:
```js
// ok
class SomeComponent extends Component {
state = useState({ value: 0 });
}
// also ok
class SomeComponent extends Component {
setup() {
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState({ value: 0 });
}
}
```
## Lifecycle Hooks
All lifecycle hooks are documented in detail in their specific [section](component.md#lifecycle).
| Hook | Description |
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
| **[onWillStart](component.md#willstart)** | async, before first rendering |
| **[onWillRender](component.md#willrender)** | just before component is rendered |
| **[onRendered](component.md#rendered)** | just after component is rendered |
| **[onMounted](component.md#mounted)** | just after component is rendered and added to the DOM |
| **[onWillUpdateProps](component.md#willupdateprops)** | async, before props update |
| **[onWillPatch](component.md#willpatch)** | just before the DOM is patched |
| **[onPatched](component.md#patched)** | just after the DOM is patched |
| **[onWillUnmount](component.md#willunmount)** | just before removing component from DOM |
| **[onWillDestroy](component.md#willdestroy)** | just before component is destroyed |
| **[onError](component.md#onerror)** | catch and handle errors (see [error handling page](error_handling.md)) |
## Other Hooks
### `useState`
The `useState` hook is certainly the most important hook for Owl components:
this is what allows a component to be reactive, to react to state change.
The `useState` hook has to be given an object or an array, and will return
an observed version of it (using a `Proxy`).
```javascript
const { useState, Component } = owl;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
It is important to remember that `useState` only works with objects or arrays. It
is necessary, since Owl needs to react to a change in state.
### `useRef`
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It only work on a html element tagged by the
`t-ref` directive:
```xml
<div>
<input t-ref="someInput"/>
<span>hello</span>
</div>
```
In this example, the component will be able to access the `input` with the `useRef` hook:
```js
class Parent extends Component {
inputRef = useRef("someInput");
someMethod() {
// here, if component is mounted, refs are active:
// - this.inputRef.el is the input HTMLElement
}
}
```
As shown by the example above, the actual HTMLElement instance is accessed with
the `el` key.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](templates.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>
```
Here, the references need to be set like this:
```js
this.ref1 = useRef("div_1");
this.ref2 = useRef("div_2");
```
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` on it will return `null`.
### `useSubEnv` and `useChildSubEnv`
The environment is sometimes useful to share some common information between
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub components, but not to the whole application.
This is where the `useChildSubEnv` hook may be useful: it lets a component add some
information to the environment in a way that only its children
can access it:
```js
class FormComponent extends Component {
setup() {
const model = makeModel();
// model will be available on this.env for this component and all children
useSubEnv({ model });
// someKey will be available on this.env for all children
useChildSubEnv({ someKey: "value" });
}
}
```
The `useSubEnv` and `useChildSubEnv` hooks take one argument: an object which
contains some key/value that will be added to the current environment. These hooks
will create a new env object with the new information:
- `useSubEnv` will assign this new `env` to itself and to all children components
- `useChildSubEnv` will only assign this new `env` to all children components.
As usual in Owl, [environments](environment.md) created with these two hooks are
frozen, to prevent unwanted modifications.
Note that both these hooks can be called an arbitrary number of times. The `env`
will then be updated accordingly.
### `useExternalListener`
The `useExternalListener` hook helps solve a very common problem: adding and removing
a listener on some target whenever a component is mounted/unmounted. For example,
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
to be closed:
```js
useExternalListener(window, "click", this.closeMenu);
```
### `useComponent`
The `useComponent` hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
```js
function useSomething() {
const component = useComponent();
// now, component is bound to the instance of the current component
}
```
### `useEnv`
The `useEnv` hook is useful as a building block for some customized hooks,
that may need a reference to the env of the component calling them.
```js
function useSomething() {
const env = useEnv();
// now, env is bound to the env of the current component
}
```
### `useEffect`
This hook will run a callback when a component is mounted and patched, and
will run a cleanup function before patching and before unmounting the
the component (only if some dependencies have changed).
It has almost the same API as the React `useEffect` hook, except that the dependencies
are defined by a function instead of just the dependencies.
The `useEffect` hook takes two function: the effect function and the dependency
function. The effect function perform some task and return (optionally) a cleanup
function. The dependency function returns a list of dependencies, these dependencies
are passed as parameters in the effect function . If any of these
dependencies changes, then the current effect will be cleaned up and reexecuted.
Here is an example without any dependencies:
```js
useEffect(
() => {
window.addEventListener("mousemove", someHandler);
return () => window.removeEventListener("mousemove", someHandler);
},
() => []
);
```
In the example above, the dependency list is empty, so the effect is only cleaned
up when the component is unmounted.
If the dependency function is skipped, then the effect will be cleaned up and
rerun at every patch.
Here is another example, of how one could implement a `useAutofocus` hook with
the `useEffect` hook:
```js
function useAutofocus(name) {
let ref = useRef(name);
useEffect(
(el) => el && el.focus(),
() => [ref.el]
);
}
```
This hook takes the name of a valid `t-ref` directive, which should be present
in the template. It then checks whenever the component is mounted or patched if
the reference is not valid, and in this case, it will focus the node element.
This hook can be used like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
setup() {
useAutofocus("myinput");
}
}
```
## Example: mouse position
Here is the classical example of a non trivial hook to track the mouse position.
```js
const { useState, onWillDestroy, Component } = owl;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
window.addEventListener("mousemove", update);
onWillDestroy(() => {
window.removeEventListener("mousemove", update);
});
return position;
}
// Main root component
class Root extends Component {
static template = xml`<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
-92
View File
@@ -1,92 +0,0 @@
# 🦉 Form Input Bindings 🦉
It is very common to need to be able to read the value out of an html `input` (or
`textarea`, or `select`) in order to use it (note: it does not need to be in a
form!). A possible way to do this is to do it by hand:
```js
class Form extends owl.Component {
state = useState({ text: "" });
_updateInputValue(event) {
this.state.text = event.target.value;
}
}
```
```xml
<div>
<input t-on-input="_updateInputValue" />
<span t-esc="state.text" />
</div>
```
This works. However, this requires a little bit of _plumbing_ code. Also, the
plumbing code is slightly different if you need to interact with a checkbox,
or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value
should be an observed value in the component (usually `state.someValue`). With
the `t-model` directive, we can write a shorter code, equivalent to the previous
example:
```js
class Form extends owl.Component {
state = { text: "" };
}
```
```xml
<div>
<input t-model="state.text" />
<span t-esc="state.text" />
</div>
```
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
`<input type="radio">`, `<textarea>` and `<select>`:
```xml
<div>
<div>Text in an input: <input t-model="state.someVal"/></div>
<div>Textarea: <textarea t-model="state.otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
<div>Selection:
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label>
</span>
</div>
</div>
```
Like event handling, the `t-model` directive accepts the following modifiers:
| Modifier | Description |
| --------- | -------------------------------------------------------------------- |
| `.lazy` | update the value on the `change` event (default is on `input` event) |
| `.number` | try to parse the value to a number (using `parseFloat`) |
| `.trim` | trim the resulting value |
For example:
```xml
<input t-model.lazy="state.someVal" />
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
update a number whenever the change is done.
Note: the online playground has an example to show how it works.
-17
View File
@@ -1,17 +0,0 @@
# 🦉 Portal 🦉
It is sometimes useful to be able to render some content outside the boundaries
of a component. To do that, Owl provides a special directive: `t-portal`:
```js
class SomeComponent extends Component {
static template = xml`
<div>this is inside the component</div>
<div t-portal="'body'">and this is outside</div>
`;
}
```
The `t-portal` directive takes a valid css selector as argument. The content of
the portalled template will be mounted at the corresponding location. Note that
Owl need to insert an empty text node at the location of the portalled content.
-30
View File
@@ -1,30 +0,0 @@
# 🦉 Precompiling templates 🦉
Owl is designed to be used by the Odoo javascript framework. Since Odoo handles
its assets in its own non standard way, it was decided/assumed that Owl would
compile templates at runtime.
However, in some cases, it is not optimal, or even worse, not possible to do that.
For example, browser extensions do not allow javascript code to create a new
function (using the `new Function(...)` syntax).
Therefore, in these cases, it is required to compile templates ahead of time. It
is possible to do that in Owl, but the tooling is still rough. For now, the
process is the following:
1. write your templates in xml files (with a `t-name` directive to declare the name
of the template)
2. Compile them in a `templates.js` file
3. get the `owl.iife.runtime.js` file (which is a owl build without the compiler)
4. bundle `owl.iife.runtime.js` and `template.js` with your assets (owl needs to
be positioned before the templates)
Here is a more detailed explanation on how to compile xml files into a js file:
1. clone the owl repository locally
2. `npm install` to install all the required tooling
3. `npm run build:runtime` to build the `owl.iife.runtime.js` file
4. `npm run build:compiler` to build the template compiler
5. `npm run compile_templates -- path/to/your/templates` will scan your target
folder, find all xml files, get all templates, compile them, and generate a
`templates.js` file.
-320
View File
@@ -1,320 +0,0 @@
# 🦉 Props 🦉
## Content
- [Overview](#overview)
- [Definition](#definition)
- [Props comparison](#props-comparison)
- [Binding function props](#binding-function-props)
- [Dynamic Props](#dynamic-props)
- [Default Props](#default-props)
- [Props validation](#props-validation)
- [Good Practices](#good-practices)
## Overview
In Owl, `props` (short for _properties_) is an object which contains every piece
of data given to a component by its parent.
```js
class Child extends Component {
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
}
class Parent extends Component {
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
```
In this example, the `Child` component receives two props from its parent: `a`
and `b`. They are collected into a `props` object by Owl, with each value being
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
`props.b` is equal to `'string'`.
Note that `props` is an object that only makes sense from the perspective of the
child component.
## Definition
The `props` object is made of every attributes defined on the template, with the
following exceptions:
- every attribute starting with `t-` are not props (they are QWeb directives),
In the following example:
```xml
<div>
<ComponentA a="state.a" b="'string'"/>
<ComponentB t-if="state.flag" model="model"/>
</div>
```
the `props` object contains the following keys:
- for `ComponentA`: `a` and `b`,
- for `ComponentB`: `model`,
## Props comparison
Whenever Owl encounters a subcomponent in a template, it performs a shallow
comparison of all props. If they are all referentially equal, then the subcomponent
will not even be updated. Otherwise, if at least one props has changed, then
Owl will update it.
However, in some cases, we know that two values are different, but they have the
same effect, and should not be considered different by Owl. For example, anonymous
functions in a template are always different, but most of them should not be
considered different:
```xml
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<Todo todo="todo" onDelete="() => deleteTodo(todo.id)" />
</t>
```
In that case, one can use the `.alike` suffix:
```xml
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<Todo todo="todo" onDelete.alike="() => deleteTodo(todo.id)" />
</t>
```
This tells Owl that this specific prop should always be considered equivalent
(or, in other words, should be removed from the list of comparable props).
Note that even if most anonymous functions should probably be considered `alike`,
it is not necessarily true in all cases. It depends on what values are captured
by the anonymous function. The following example shows a case where it is probably
wrong to use `.alike`.
```xml
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<!-- Probably wrong! todo.isCompleted may change -->
<Todo todo="todo" toggle.alike="() => toggleTodo(todo.isCompleted)" />
</t>
```
## Binding function props
It is common to have the need to pass a callback as a prop. Since Owl components
are class based, the callback frequently needs to be bound to its owner component.
So, one can do this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback="doSomething"/>
</div>`;
setup() {
this.doSomething = this.doSomething.bind(this);
}
doSomething() {
// ...
}
}
```
However, this is such a common use case that Owl provides a special suffix to do
just that: `.bind`. This looks like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback.bind="doSomething"/>
</div>`;
doSomething() {
// ...
}
}
```
The `.bind` suffix also implies `.alike`, so these props will not cause additional
renderings.
## Dynamic Props
The `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
## Default Props
If the static `defaultProps` property is defined, it will be used to complete
props received by the parent, if missing.
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0,
};
...
}
```
In the example above, the `initialValue` props is now by default set to 0.
## Props Validation
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system solves both issues, by describing the types and shapes
of the props. Here is how it works in Owl:
- `props` key is a static key (so, different from `this.props` in a component instance)
- it is optional: it is ok for a component to not define a `props` key.
- props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [how to configure an app](app.md#configuration))
- if a key does not match the description, an error is thrown
- it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error (unless the special prop `*` is present).
- it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props. Also, if the name ends with `?`, it is considered optional.
- all props are by default required, unless they are defined with `optional: true`
(in that case, it is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number
- an object describing a value as type. This is done by using the `value` key. For example, `{value: false}` specifies that the corresponding value should be equal to false.
- a list of constructors. In that case, this means that we allow more than one
type. For example, `id: [Number, String]` means that `id` can be either a string
or a number.
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
- `type`: the main type of the prop being validated
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
- `values`: if the type was `Object`, then the `values` key describes the interface of values in the object, this allows validating objects that are used as mappings, where keys are not known in advance but the shape of the values is.
- `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic.
- `optional`: if true, the prop is not mandatory
There is a special `*` prop that means that additional prop are allowed. This is
sometimes useful for generic components that will propagate some or all their
props to their child components.
Note that default values cannot be defined for a mandatory props. Doing so will
result in a prop validation error.
Examples:
```js
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: String }
},
date: Date,
combinedVal: [Number, Boolean],
optionalProp: { type: Number, optional: true }
};
...
}
```
```js
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
```
```js
// only the existence of those 3 keys is documented. any other key is allowed.
static props = ['message', 'id', 'date', '*'];
```
```js
// size is optional
static props = ['message', 'size?'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someObj3: {
type: Object,
values: { type: Array, element: String },
}, // object with arbitary keys where values are arrays of strings
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
kindofsmallnumber: {
type: Number,
validate: n => (0 <= n && n <= 10)
},
size: {
validate: e => ["small", "medium", "large"].includes(e)
},
someId: [Number, {value: false}], // either a number or false
};
```
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
## Good Practices
A `props` object is a collection of values that come from the parent. As such,
they are owned by the parent, and should never be modified by the child:
```js
class MyComponent extends Component {
constructor(parent, props) {
super(parent, props);
props.a.b = 43; // Never do that!!!
}
}
```
Props should be considered readonly, from the perspective of the child component.
If there is a need to modify them, then the request to update them should be
sent to the parent (for example, with an event).
Any value can go in a props. Strings, objects, classes, or even callbacks could
be given to a child component (but then, in the case of callbacks, communicating
with events seems more appropriate).
-395
View File
@@ -1,395 +0,0 @@
# 🦉 Reactivity 🦉
## Content
- [Introduction](#introduction)
- [`useState`](#usestate)
- [`reactive`](#reactive)
- [`Escape hatches`](#escape-hatches)
- [`Advanced usage`](#advanced-usage)
## Introduction
Reactivity is a big topic in javascript frameworks. The goal is to provide a
simple way to manipulate state, in such a way that the interface updates automatically
according to state changes, and to do so in a performant manner.
To this end, Owl provides a proxy-based reactivity system, based on the `reactive` primitive.
The `reactive` function takes an object as a first argument, and an optional callback as its second
argument, it returns a proxy of the object. This proxy tracks what properties are read
through the proxy, and calls the provided callback whenever one of these properties is changed
through any reactive version of the same object. It does so in depth, by returning reactive versions
of the subobjects when they are read.
## `useState`
While the `reactive` primitive is very powerful, its usage in components follow a very standard pattern:
components want to be rerendered when part of the state which they depend on for rendering changes. To
this end, owl provides a standard hook: `useState`. To put it simply, this hook simply calls reactive
with the provided object, and the current component's render function as its callback. This will cause
it to rerender whenever any part of the state object that has been read by this component is modified.
Here is a simple example of how `useState` can be used:
```js
class Counter extends Component {
static template = xml`
<div t-on-click="() => this.state.value++">
<t t-esc="state.value"/>
</div>`;
setup() {
this.state = useState({ value: 0 });
}
}
```
This component reads `state.value` when it renders, subscribing it to changes to that key. Whenever
the value changes, Owl will update the component. Note that there is nothing special about the
`state` property, you can name your state variables whatever you want, and you can have multiple of
them on the same component if it makes sense to do so. This also allows `useState` to be used in custom
hooks that may require state that is specific to that hook.
### Reactive props
Since version 2.0, Owl renders are no longer "deep" by default: a component is only rerendered by its
parent if its props have changed (using a simple equality test). What if the contents of a props have
changed in a deeper property? If that prop is reactive, owl will rerender the child components that
need to be updated automatically, and only those components, it does so by reobserving reactive
objects passed as props to components. Consider the following example:
```js
class Counter extends Component {
static template = xml`
<div t-on-click="() => props.state.value++">
<t t-esc="props.state.value"/>
</div>`;
}
class Parent extends Component {
static template = xml`
<Counter state="this.state"/>
<button t-on-click="() => this.state.value = 0">Reset counter</button>
<button t-on-click="() => this.state.test++" t-esc="this.state.test"/>`;
setup() {
this.state = useState({ value: 0, test: 1 });
}
}
```
When clicking on the counter button, only the Counter rerenders, because the Parent has never read
the "value" key in the state. When clicking on the "Reset Counter" button, the same thing happens:
only the Counter component rerenders. What matters is not _where_ the state is updated, but which
parts of the state are updated, and which components depend on them. This is achieved by Owl by
automatically calling `useState` on reactive objects passed as props to a child component.
When clicking on the last button, the parent is rerendered, but the child does not care about the
`test` key: it has not read it. The props that we give it (`this.state`) have also not changed,
as such, the parent updates but the child doesn't.
For most day-to-day operations, `useState` should cover all of your needs. If
you are curious about more advanced use cases and technical details, read on.
### Debugging subscriptions
Owl provides a way to show which reactive objects and keys a component is subscribed to: you can
look at `component.__owl__.subscriptions`. Note that this is on the internal `__owl__` field, and
should not be used in any type of production code as the name of this property or any of its properties
or methods are subject to change at any point, even in stable versions of Owl, and may become available
only in debug mode in the future.
## `reactive`
The `reactive` function is the basic reactivity primitive. It takes an object
or an array as first argument, and optionally, a function as the second argument.
The function is called whenever any tracked value is updated.
```js
const obj = reactive({ a: 1 }, () => console.log("changed"));
obj.a = 2; // does not log anything: the 'a' key has not been read yet
console.log(obj.a); // logs 2 and reads the 'a' key => it is now tracked
obj.a = 3; // logs 'changed' because we updated a tracked value
```
An important property of reactive objects is that they can be reobserved: this
will create an independent proxy that tracks another set of keys:
```js
const obj1 = reactive({ a: 1, b: 2 }, () => console.log("observer 1"));
const obj2 = reactive(obj1, () => console.log("observer 2"));
console.log(obj1.a); // logs 1, and reads the 'a' key => it is now tracked by observer 1
console.log(obj2.b); // logs 2, and 'b' is now tracked by observer 2
obj2.a = 3; // only logs 'observer1', because observer2 does not track a
obj2.b = 3; // only logs 'observer2', because observer1 does not track b
console.log(obj2.a, obj1.b); // logs 3 and 3, while the object is observed independently, it is still a single object
```
Because `useState` returns a normal reactive object, it is possible to call `reactive` on the result
of a `useState` to observe changes to that object while outside the context of a component, or to
call `useState` on reactive objects created outside of components. In those cases, one needs to be
careful with regards to the lifetime of those reactive objects, as holding references to these
objects may prevent garbage collection of the component and its data even if Owl has destroyed it.
### Subscriptions are ephemereal
Subscription to state changes are ephemereal, whenever an observer is notified that a state object
has changed, all of its subscriptions are cleared, meaning that if it still cares about it, it
should read the properties it cares about again. For example:
```js
const obj = reactive({ a: 1 }, () => console.log("observer called"));
console.log(obj.a); // logs 1, and reads the 'a' key => it is now tracked by the observer
obj.a = 3; // logs 'observer1' and clears the subscriptions of the observer
obj.a = 4; // doesn't log anything, the key is no longer observed
```
This may seem counter-intuitive, but it makes perfect sense in the context of components:
```js
class DoubleCounter extends Component {
static template = xml`
<t t-esc="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({
a: 1,
obj: someObject,
});
console.log(state.obj.b); // attempt to subscribe to the "b" key in someObject
state.obj.b = 2; // No rerender will occur here
console.log(someObject === state.obj); // true
```
This is useful in some rare cases. One such example would be if you want to use an array of objects
that is potentially large to render a list, but those objects are known to be immutable:
```js
this.items = useState([
{ label: "some text", value: 42 },
// ... 1000 total objects
]);
```
in the template:
```xml
<t t-foreach="items" t-as="item" t-key="item.label" t-esc="item.label + item.value"/>
```
Here, on every render, we go and read one thousand keys from a reactive object, which causes
one thousand reactive objects to be created. If we know that the content of these objects
cannot change, this is wasted work. If instead all of these objects are marked as raw, we avoid
all of this work while keeping the ability to lean on the reactivity to track the presence and
identity of these objects:
```js
this.items = useState([
markRaw({ label: "some text", value: 42 }),
// ... 1000 total objects
]);
```
However, use this function with caution: this is an escape hatch from the reactivity
system, and as such, using it may cause subtle and unintended issues! For example:
```js
// This will cause a rerender
this.items.push(markRaw({ label: "another label", value: 1337 }));
// THIS WILL NOT CAUSE A RENDER!
this.items[17].value = 3;
// The UI is now desynced from component's state until the next render caused by something else
```
In short: only use `markRaw` if your application is slowing down noticeably and profiling reveals
that a lot of time is spent creating useless reactive objects.
### `toRaw`
While `markRaw` marks an object so that it is never made reactive, `toRaw` takes an object and
returns the underlying non-reactive object. It can be useful in some niche cases. In particular,
because the reactivity system returns a proxy, the returned object does not compare equal to the
original object:
```js
const obj = {};
const reactiveObj = reactive(obj);
console.log(obj === reactiveObj); // false
console.log(obj === toRaw(reactiveObj)); // true
```
It can also be useful during debugging, as unfolding proxies recursively in debuggers can be confusing.
## Advanced usage
The following is a collection of small snippets that leverage the reactivity system in
"non-standard" ways to help you understand its power and where using it might make your code simpler.
### Notification manager
Showing notifications is a pretty common need in web applications, you may want to show a
notification from any other component within the application, and the notifications should stack on
top of one another regardless of which component spawned them, here is how we can leverage the
reactivity to accomplish this:
```js
let notificationId = 1;
const notifications = reactive({});
class NotificationContainer extends Component {
static template = xml`
<t t-foreach="notifications" t-as="notification" t-key="notification_key" t-esc="notification"/>
`;
setup() {
this.notifications = useState(notifications);
}
}
export function addNotification(label) {
const id = notificationId++;
notifications[id] = label;
return () => {
delete notifications[id];
};
}
```
Here, the `notifications` variable is a reactive object. Notice how we didn't give `reactive` a
callback: this is because in this case, all we care about is that adding or removing notifications
in the `addNotification` function goes through the reactivity system. The `NotificationContainer`
component reobserves this object with `useState`, and is updated whenever notifications are
added or removed.
### Store
Centralizing application state is a pretty common want/need in web applications. Because of the way
the reactivity system works, you can treat any reactive object as a store, and if you call `useState`
on it, components automatically observe only the part of the store that they're interested in:
```js
export const store = reactive({
list: [],
add(item) {
this.list.push(item);
},
});
export function useStore() {
return useState(store);
}
```
In any component:
```js
import { useStore } from "./store";
class List extends Component {
static template = xml`
<t t-foreach="store.list" t-as="item" t-key="item" t-esc="item"/>
`;
setup() {
this.store = useStore();
}
}
```
Anywhere in the application:
```js
import { store } from "./store";
// Will cause any instance of the List component in the app to update
store.add("New list item!");
```
Notice how we can make objects with methods into reactive objects, and when these methods are used
to mutate the store contents, it works as expected. And while stores are generally one-off objects,
it is entirely possible to make class instances reactive:
```js
class Store {
list = [];
add(item) {
this.list.push(item);
}
}
// Essentially equivalent to the previous code
export const store = reactive(new Store());
```
Which can be useful to unit test the class separately.
### Local storage synchronization
Sometimes, you want to persist some state accross reloads, you can do this by storing it in the
`localStorage`, but what if you want to update the `localStorage` item every time the state changes,
so that you don't have to manually synchronize the states? Well, you can use the reactivity system
to write a custom hook that will do that for you:
```js
function useStoredState(key, initialState) {
const state = JSON.parse(localStorage.getItem(key)) || initialState;
const store = (obj) => localStorage.setItem(key, JSON.stringify(obj));
const reactiveState = reactive(state, () => store(reactiveState));
store(reactiveState);
return useState(state);
}
class MyComponent extends Component {
setup() {
this.state = useStoredState("MyComponent.state", { value: 1 });
}
}
```
One important thing to notice is that both times we call `store`, we call it with `reactiveState`,
not `state`: we need `store` to read the keys through a reactive object for it to correctly
subscribe to state changes. Notice also that we call `store` the first time by hand, as otherwise it
will not be subscribed to anything, and no amount of change in the object will cause the reactive
callback to be invoked.
-37
View File
@@ -1,37 +0,0 @@
# 🦉 References 🦉
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
targeted by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
more detail.
As a short example, here is how we could set the focus on a given input:
```xml
<div>
<input t-ref="input"/>
<button t-on-click="focusInput">Click</button>
</div>
```
```js
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
inputRef = useRef("input");
focusInput() {
this.inputRef.el.focus();
}
}
```
Be aware that the `el` property will only be set when the target of the `t-ref`
directive is mounted in the DOM. Otherwise, it will be set to `null`.
The `useRef` hook cannot be used to get a reference to an instance of a sub
component.
Note that this example uses the suffix `ref` to name the reference. This
is not mandatory, but it is a useful convention, so we do not forget that it is
a reference object.
-260
View File
@@ -1,260 +0,0 @@
# 🦉 Slots 🦉
## Content
- [Overview](#overview)
- [Named slots](#named-slots)
- [Rendering Context](#rendering-context)
- [Default Slot](#default-slot)
- [Default Content](#default-content)
- [Dynamic slots](#dynamic-slots)
- [Slots and props](#slots-and-props)
- [Slot params](#slot-params)
- [Slot scopes](#slot-scopes)
## Overview
Owl is a template based component system. There is therefore a need to be able
to make generic components. For example, imagine a generic `Navbar`
component, which displays a navbar, but with some customizable content. Since
the specific content is only known to the user of the `Navbar`, it would be nice
to specify it in the template where `Navbar` is used:
```xml
<div>
<Navbar>
<span>Hello Owl</span>
</Navbar>
</div>
```
This is exactly the way slots work! In the example above, the user of the `Navbar`
component specify some content (here, in the default slot). The `Navbar`
component can insert that content in its own template at the appropriate location.
An important information to notice is that the content of the slot is rendered in
the parent context, not in the navbar. As such, it can access values and methods
from the parent component.
Here is how the `Navbar` component could be defined, with the `t-slot` directive:
```xml
<div class="navbar">
<t t-slot="default"/>
<ul>
<!-- rest of the navbar here -->
</ul>
</div>
```
## Named slots
Default slots are very useful, but sometimes, we may need more than one slot.
This is what named slots are for! For example, suppose we implement a component
`InfoBox` that display a title and some specific content. Its template could look
like this:
```xml
<div class="info-box">
<div class="info-box-title">
<t t-slot="title"/>
<span class="info-box-close-button" t-on-click="close">X</span>
</div>
<div class="info-box-content">
<t t-slot="content"/>
</div>
</div>
```
And one could use it with the `t-set-slot` directive:
```xml
<InfoBox>
<t t-set-slot="title">
Specific Title. It could be html also.
</t>
<t t-set-slot="content">
<!-- some template here, with html, events, whatever -->
</t>
</InfoBox>
```
## Rendering context
The content of the slots is actually rendered with the rendering context corresponding
to where it was defined, not where it is positioned. This allows the user to define
event handlers that will be bound to the correct component (usually, the
grandparent of the slot content).
## Default Slot
All elements inside the component which are not a named slot will be treated as
part of the content of the `default` slot. For example:
```xml
<div t-name="Parent">
<Child>
<span>some content</span>
</Child>
</div>
<div t-name="Child">
<t t-slot="default"/>
</div>
```
One can mix default slot and named slots:
```xml
<div>
<Child>
default content
<t t-set-slot="footer">
content for footer slot here
</t>
</Child>
</div>
```
## Default content
Slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
<Child/>
</div>
<span t-name="Child">
<t t-slot="default">default content</t>
</span>
<!-- will be rendered as: <div><span>default content</span></div> -->
```
## Dynamic Slots
The `t-slot` directive is actually able to use any expressions, using string
interplolation:
```xml
<t t-slot="{{current}}" />
```
This will evaluate the `current` expression, and insert the corresponding slot
at the place of the `t-slot` directive.
## Slots and props
In a sense, slots are almost the same as a prop: they define some information
to pass to the child component. To make it possible to use it, and to pass it
down to sub component, Owl actually define a special prop `slots` that contains
all slot information given to the component. It looks like this:
```js
{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }
```
So, a component can pass its slots to a subcomponent like this:
```xml
<Child slots="props.slots"/>
```
## Slot params
For advanced usecases, it may be necessary to pass additional information to a
slot. This can be done by providing extra key/value pairs to the `t-set-slot`
directive. Then, the generic component can read them in its prop `slots`.
For example, here is how a Notebook component could be implemented (a component
with multiple page, and a tab bar, which only render the current active page,
and each page has a title).
```js
class Notebook extends Component {
static template = xml`
<div class="notebook">
<div class="tabs">
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab_index">
<t t-esc="props.slots[tab].title"/>
</span>
</t>
</div>
<div class="page">
<t t-slot="{{currentSlot}}"/>
</div>
</div>`;
setup() {
this.state = useState({ activeTab: 0 });
this.tabNames = Object.keys(this.props.slots);
}
get currentSlot() {
return this.tabNames[this.state.activeTab];
}
}
```
Notice how one can read the `title` value for each slots. Here is how one could
use this `Notebook` component:
```xml
<Notebook>
<t t-set-slot="page1" title="'Page 1'">
<div>this is in the page 1</div>
</t>
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
<div>this is in the page 2</div>
</t>
</Notebook>
```
Slot params works like normal props, so one can use the `.bind` suffix to
bind a function if needed.
## Slot scopes
For other kinds of advanced use cases, the content of a slot may depends on some
information specific to the generic component. This is the opposite of the slot
params.
To solve this kind of problems, one can use the `t-slot-scope` directive along
with the `t-set-slot`. This defines the name of a variable that can access
everything given by the child component:
```xml
<MyComponent>
<t t-set-slot="foo" t-slot-scope="scope">
content
<t t-esc="scope.bool"/>
<t t-esc="scope.num"/>
</t>
</MyComponent>
```
And the child component that includes the slot can provide values like this:
```xml
<t t-slot="foo" bool="other_var" num="5">
```
or this:
```xml
<t t-slot="foo" t-props="someObject">
```
In the case of the default slot, you may declare the slot scope directly on the
component itself:
```xml
<MyComponent t-slot-scope="scope">
content
<t t-esc="scope.bool"/>
<t t-esc="scope.num"/>
</MyComponent>
```
Slot values works like normal props, so one can use the `.bind` suffix to
bind a function if needed.
-703
View File
@@ -1,703 +0,0 @@
# 🦉 Templates 🦉
## Content
- [Overview](#overview)
- [Directives](#directives)
- [QWeb Template reference](#qweb-template-reference)
- [White Spaces](#white-spaces)
- [Expression Evaluation](#expression-evaluation)
- [Static html Nodes](#static-html-nodes)
- [Outputting Data](#outputting-data)
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes)
- [Dynamic Class Attribute](#dynamic-class-attribute)
- [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops)
- [Sub Templates](#sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
- [Debugging](#debugging)
- [Fragments](#fragments)
- [Inline templates](#inline-templates)
- [Rendering svg](#rendering-svg)
- [Restrictions](#restrictions)
## Overview
Owl templates are describe using the [QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) specification. It is based on the XML format, and used
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML. Also, since Owl is a live
component system, there are additional directives specific to Owl (such as `t-on`).
```xml
<div>
<span t-if="somecondition">Some string</span>
<ul t-else="">
<li t-foreach="messages" t-as="message">
<t t-esc="message"/>
</li>
</ul>
</div>
```
Template directives are specified as XML attributes prefixed with `t-`, for
instance `t-if` for conditionals, with elements and other attributes being
rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which
executes its directive but doesnt generate any output in and of itself.
We present in this section the templating language, including its Owl specific
extensions.
## Directives
For reference, here is a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | --------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](translations.md) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| -------------------------------------- | --------------------------------------------------------------- |
| `t-component`, `t-props` | [Defining a sub component](component.md#sub-components) |
| `t-ref` | [Setting a reference to a dom node or a sub component](refs.md) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
| `t-on-*` | [Event handling](event_handling.md) |
| `t-portal` | [Portal](portal.md) |
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](input_bindings.md) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
## QWeb Template Reference
### White Spaces
White spaces in a template are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Expression Evaluation
QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced with a lookup in the context (so, the
component). For example, `a + b.c(d)` will be converted into:
```js
context["a"] + context["b"].c(context["d"]);
```
It is useful to explain the various rules that apply on these expressions:
1. it should be a simple expression which returns a value. It cannot be a statement.
```xml
<div><p t-if="1 + 2 === 3">ok</p></div>
```
is valid, but the following is not valid:
```xml
<div><p t-if="console.log(1)">NOT valid</p></div>
```
2. it can use anything in the rendering context (which typically contains the properties of the component):
```xml
<p t-if="user.birthday === today()">Happy bithday!</p>
```
is valid, and will read the `user` object from the context, and call the
`today` function.
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | replaced with |
| ----- | ------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
```xml
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### Static Html Nodes
Normal, regular html nodes are rendered into themselves:
```xml
<div>hello</div> <!–– rendered as itself ––>
```
### Outputting Data
The `t-esc` directive is necessary whenever you want to add a dynamic text
expression in a template. The text is escaped to avoid security issues.
```xml
<p><t t-esc="value"/></p>
```
rendered with the value `value` set to `42` in the rendering context yields:
```html
<p>42</p>
```
The `t-out` directive is almost the same as `t-esc`, but possibly without the
escaping. The difference is that the value received by the `t-out` directive
will only be not-escaped if it has been marked as such, using the `markup`
utility function:
For example, in the following component:
```js
const { markup, Component, xml } = owl;
class SomeComponent extends Component {
static template = xml`
<t t-out="value1"/>
<t t-out="value2"/>`;
value1 = "<div>some text 1</div>";
value2 = markup("<div>some text 2</div>");
}
```
The first `t-out` will act as a `t-esc` directive, which means that the content
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
this will be injected as html.
### Setting Variables
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
This is done via the `t-set` directive, which takes the name of the variable to create. The value to set can be provided in two ways:
1. a `t-value` attribute containing an expression, and the result of its
evaluation will be set:
```xml
<t t-set="foo" t-value="2 + 1"/>
<t t-esc="foo"/>
```
will print `3`. Note that the evaluation is done at rendering time, not at
compilte time.
2. if there is no `t-value` attribute, the nodes body is saved and its value is
set as the variables value:
```xml
<t t-set="foo">
<li>ok</li>
</t>
<t t-esc="foo"/>
```
will generate `&lt;li&gt;ok&lt;/li&gt;` (the content is escaped as we used the `t-esc` directive)
The `t-set` directive acts like a regular variable in most programming language.
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
### Conditionals
The `t-if` directive is useful to conditionally render something. It evaluates
the expression given as attribute value, and then acts accordingly.
```xml
<div>
<t t-if="condition">
<p>ok</p>
</t>
</div>
```
The element is rendered if the condition (evaluated with the current rendering
context) is true:
```xml
<div>
<p>ok</p>
</div>
```
but if the condition is false it is removed from the result:
```xml
<div>
</div>
```
The conditional rendering applies to the bearer of the directive, which does not
have to be `<t>`:
```xml
<div>
<p t-if="condition">ok</p>
</div>
```
will give the same results as the previous example.
Extra conditional branching directives `t-elif` and `t-else` are also available:
```xml
<div>
<p t-if="user.birthday == today()">Happy bithday!</p>
<p t-elif="user.login == 'root'">Welcome master!</p>
<p t-else="">Welcome!</p>
</div>
```
### Dynamic Attributes
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
evaluate an expression (at rendering time) and bind an attribute to its result:
For example, if we have `id` set to 32 in the rendering context,
```xml
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
```
If an expression evaluates to a falsy value, it will not be set at all:
```xml
<div t-att-foo="false"/> <!-- result: <div></div> -->
```
It is sometimes convenient to format an attribute with string interpolation. In
that case, the `t-attf-` directive can be used. It is useful when we need to mix
literal and dynamic elements, such as css classes. The dynamic elements can be
specified with either `{{...}}` or `#{...}`:
```xml
<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> -->
```
If we need completely dynamic attribute names, then there is an additional
directive: `t-att`, which takes either an object (with keys mapping to their
values) or a pair `[key, value]`. For example:
```xml
<div t-att="{'a': 1, 'b': 2}"/> <!-- result: <div a="1" b="2"></div> -->
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
```
### Dynamic class attribute
For convenience, Owl supports a special case for the `t-att-class` case: one can
use an object with keys describing the classes, and values boolean value denoting
if the class is or is not present:
```xml
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
```
Note that it can be combined with normal class attribute:
```xml
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
```
### Dynamic tag names
When writing generic components or templates, the specific concrete tag for an
HTML element is not known yet. In those situations, the `t-tag` directive is
useful. It simply evaluates dynamically an expression to use as a tag name. The
template:
```xml
<t t-tag="tag">
<span>content</span>
</t>
```
will be rendered as `<div><span>content</span></div>` if the `tag` context key
is set to `div`.
### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the
collection to iterate on, and a second parameter `t-as` providing the name to use
for the current item of the iteration:
```xml
<t t-foreach="[1, 2, 3]" t-as="i" t-key="i">
<p><t t-esc="i"/></p>
</t>
```
will be rendered as:
```xml
<p>1</p>
<p>2</p>
<p>3</p>
```
Like conditions, `t-foreach` applies to the element bearing the directives attribute, and
```xml
<p t-foreach="[1, 2, 3]" t-as="i" t-key="i">
<t t-esc="i"/>
</p>
```
is equivalent to the previous example.
An important difference should be made with the usual `QWeb` behaviour: Owl
requires the presence of a `t-key` directive, to be able to properly reconcile
renderings.
`t-foreach` can iterate on any iterable, and also has special support for objects
and maps, it will expose the key of the current iteration as the contents of the
`t-as`, and the corresponding value with the same name and the suffix `_value`.
In addition to the name passed via t-as, `t-foreach` provides a few other useful
variables (note: `$as` will be replaced with the name passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for arrays and
other iterables, but for objects and maps, it provides the value (where `$as`
provides the key)
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
- `$as_first`: whether the current item is the first of the iteration
(equivalent to `$as_index == 0`)
- `$as_last`: whether the current item is the last of the iteration
(equivalent to `$as_index + 1 == $as_size`), requires the iteratees size be
available
These extra variables provided and all new variables created into the `t-foreach`
are only available in the scope of the `t-foreach`. If the variable exists outside
the context of the `t-foreach`, the value is copied at the end of the foreach
into the global context.
```xml
<t t-set="existing_variable" t-value="false"/>
<!-- existing_variable now False -->
<p t-foreach="Array(3)" t-as="i" t-key="i">
<t t-set="existing_variable" t-value="true"/>
<t t-set="new_variable" t-value="true"/>
<!-- existing_variable and new_variable now true -->
</p>
<!-- existing_variable always true -->
<!-- new_variable undefined -->
```
Even though Owl tries to be as declarative as possible, the DOM does not fully
expose its state declaratively in the DOM tree. For example, the scrolling state,
the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to make sure we keep the actual DOM node instead of replacing it with
a new one.
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml
<p t-foreach="items" t-as="item" t-key="item_index"><t t-esc="item.text"/></p>
```
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
and rerender the template, Owl needs to know what the intent is:
- should Owl actually swap the DOM nodes,
- or should it keep the DOM nodes, but with an updated text content?
This might look trivial, but it actually matters. These two possibilities lead
to different results in some cases. For example, if the user selected the text
of the first `p`, swapping them will keep the selection while updating the
text content will not.
There are many other cases where this is important: `input` tags with their
value, css classes and animations, scroll position...
So, the `t-key` directive is used to give an identity to an element. It allows
Owl to understand if different elements of a list are actually different or not.
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
Then, the template could look like this:
```xml
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
```
The `t-key` directive is useful for lists (`t-foreach`). A key should be
a unique number or string (objects will not work: they will be cast to the
`"[object Object]"` string, which is obviously not unique).
Also, the key can be set on a `t` tag or on its children. The following variations
are all equivalent:
```xml
<p t-foreach="items" t-as="item" t-key="item.id">
<t t-esc="item.text"/>
</p>
<t t-foreach="items" t-as="item" t-key="item.id">
<p t-esc="item.text"/>
</t>
<t t-foreach="items" t-as="item">
<p t-key="item.id" t-esc="item.text"/>
</t>
```
If there is no `t-key` directive, Owl will use the index as a default key.
Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does
not work with other iterables, such as `Set`. However, it is only a matter of
using the `...` javascript operator. For example:
```xml
<t t-foreach="[...items]" t-as="item">...</t>
```
The `...` operator will convert the `Set` (or any other iterables) into a list,
which will work with Owl QWeb.
### Sub Templates
QWeb templates can be used for top level rendering, but they can also be used
from within another template (to avoid duplication or give names to parts of
templates), using the `t-call` directive:
```xml
<div t-name="other-template">
<p><t t-value="var"/></p>
</div>
<div t-name="main-template">
<t t-set="var" t-value="owl"/>
<t t-call="other-template"/>
</div>
```
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
template is rendered with the execution context of the parent. The sub template
is actually inlined in the main template, but in a sub scope: variables defined
in the sub template do not escape.
Sometimes, one might want to pass information to the sub template. In that case,
the content of the body of the `t-call` directive is available as a special
magic variable `0`:
```xml
<t t-name="other-template">
This template was called with content:
<t t-raw="0"/>
</t>
<div t-name="main-template">
<t t-call="other-template">
<em>content</em>
</t>
</div>
```
will result in :
```xml
<div>
This template was called with content:
<em>content</em>
</div>
```
This can be used to define variables scoped to a sub template:
```xml
<t t-call="other-template">
<t t-set="var" t-value="1"/>
</t>
<!-- "var" does not exist here -->
```
Note: by default, the rendering context for a sub template is simply the current
rendering context. However, it may be useful to be able to specify a specific
object as context. This can be done by using the `t-call-context` directive:
```xml
<t t-call="other-template" t-call-context="obj"/>
```
### Dynamic sub templates
The `t-call` directive can also be used to dynamically call a sub template,
using string interpolation. For example:
```xml
<div t-name="main-template">
<t t-call="{{template}}">
<em>content</em>
</t>
</div>
```
Here, the name of the template is obtained from the `template` value in the
template rendering context.
### Debugging
The javascript QWeb implementation provides two useful debugging directives:
`t-debug` adds a debugger statement during template rendering:
```xml
<t t-if="a_test">
<t t-debug=""/>
</t>
```
will stop execution if the browser dev tools are open.
`t-log` takes an expression parameter, evaluates the expression during rendering and logs its result with console.log:
```xml
<t t-set="foo" t-value="42"/>
<t t-log="foo"/>
```
will print 42 to the console.
## Fragments
Owl 2 supports templates with an arbitrary number of root elements, or even just
a text node. So, the following templates are all valid:
```xml
hello owl. This is just a text node!
```
```xml
<div>hello</div>
```
```xml
<div>hello</div>
<div>ola</div>
```
```xml
<div t-if="someCondition"><SomeChildComponent/></div>
```
```xml
<t t-if="someCondition"><SomeChildComponent/></t>
```
## Inline templates
Most real applications will define their templates in a XML file, to benefit
from the XML ecosystem, and to do some additional processing, such as translating
them. However, in some cases, it is convenient to be able to define a template
inline. To do so, one can use the `xml` helper function:
```js
const { Component, xml } = owl;
class MyComponent extends Component {
static template = xml`
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
...
}
mount(MyComponent, document.body);
```
This function simply generates an unique string id, and register the template
under that id in the internals of Owl, then return the id.
## Rendering svg
Owl components can be used to generate dynamic SVG graphs:
```js
class Node extends Component {
static template = xml`
<g>
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
<t t-set="childx" t-value="props.x + 100"/>
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
<t t-foreach="props.node.children || []" t-as="child">
<t t-set="childy" t-value="props.y + child_index*height"/>
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
<Node x="childx" y="childy" node="child" height="height"/>
</t>
</g>
`;
static components = { Node };
}
class RootNode extends Component {
static template = xml`
<svg height="180">
<Node node="graph" x="10" y="20" height="180"/>
</svg>
`;
static components = { Node };
graph = {
label: "a",
children: [
{ label: "b" },
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
{ label: "f", children: [{ label: "g" }] },
],
};
}
```
This `RootNode` component will then display a live SVG representation of the
graph described by the `graph` property. Note that there is a recursive structure
here: the `Node` component uses itself as a subcomponent.
**Important note:** Owl needs to properly set the namespace for each svg elements.
Since Owl compile each template separately, it is not able to determine easily
if a template is supposed to be included in a svg namespace or not. Therefore,
Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will
be considered as svg. In practice, this means that each component or each sub
templates (included with `t-call`) should have one of these tag as root tag.
## Restrictions
Note that Owl templates forbid the use of tag and or attributes starting with
the `block-` string. This restriction prevents name collision with the internal
code of Owl.
```xml
<div><block-1>this will not be accepted by Owl</block-1></div>
```
-70
View File
@@ -1,70 +0,0 @@
# 🦉 Translations 🦉
If properly setup, Owl can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non",
};
const translateFn = (str) => translations[str] || str;
const app = new App(Root, { templates, tranaslateFn });
// ...
```
See the [app configuration page](app.md#configuration) for more info on how to
configure an Owl application.
Once setup, all rendered templates will be translated using `translateFn`:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
So, with the above `translateFn`, the following templates:
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
In some case, it is useful to be able to extend the list of translatable attributes.
For example, one may want to also translate `data-title` attributes. To do that,
we can define additional attributes with the `translatableAttributes` option:
```js
const app = new App(Root, { templates, tranaslateFn, translatableAttributes: ["data-title"] });
// ...
```
It is also possible to remove an attribute from the default list by prefixing it with `-`:
```js
const app = new App(Root, {
templates,
tranaslateFn,
translatableAttributes: ["data-title", "-title"],
});
// data-title attribute will be translated, but not title attribute...
```
-80
View File
@@ -1,80 +0,0 @@
# 🦉 Utils 🦉
Owl export a few useful utility functions, to help with common issues. Those
functions are all available in the `owl.utils` namespace.
## Content
- [`whenReady`](#whenready): executing code when DOM is ready
- [`loadFile`](#loadfile): loading a file (useful for templates)
- [`EventBus`](#eventbus): a simple EventBus
- [`validate`](#validate): a validation function
## `whenReady`
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
const { whenReady } = owl;
await whenReady();
// do something
```
or alternatively:
```js
whenReady(function () {
// do something
});
```
## `loadFile`
`loadFile` is a helper function to fetch a file. It simply
performs a `GET` request and returns the resulting string in a promise. The
initial usecase for this function is to load a template file. For example:
```js
const { loadFile } = owl;
async function makeEnv() {
const templates = await loadFile("templates.xml");
// do something
}
```
## `EventBus`
It is a simple `EventBus`, with the same API as usual DOM elements, and an
additional `trigger` method to dispatch events:
```js
const bus = new EventBus();
bus.addEventListener("event", () => console.log("something happened"));
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),
```
-56
View File
@@ -1,56 +0,0 @@
# Owl Devtools Browser extension
The owl devtools browser extension is an extension available on chrome or firefox which adds an owl tab
to the browser devtools in order to inspect all owl apps that are present on any web page, their components
and allows to interract with their data to a certain extend. There is also a profiler available to visualize
the components' lifecycle and be able to trace their origin.
See the [`devtools doc`](devtools_guide.md) for more information.
## Install the extension manually (for devs)
In the owl root folder:
```bash
npm install
```
For chrome:
```bash
npm run build:devtools-chrome
```
For firefox:
```bash
npm run build:devtools-firefox
```
You can also run:
```bash
npm run dev:devtools-chrome
```
or
```bash
npm run dev:devtools-firefox
```
to avoid recompiling owl and gain time if it has already been done.
To run the extension:
In google chrome: go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
Select the output folder (dist/devtools) and that's it, your extension is active!
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
In firefox: go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
Select any file of the output folder (dist/devtools) and that's it, your extension is active!
Here, you can use the reload button to refresh the extension.
Note that you may have to open another window or reload your tab to see the extension working.
Also note that the extension will only be active on pages that have a sufficient version of owl.
-150
View File
@@ -1,150 +0,0 @@
# Owl Devtools Guide
## Information popup
After having installed the extension, a new icon will be added to your extension bar.
If you don't see it, you can pin the extension using the extensions popup.
<img src="screenshots/extensions.png"/>
Clicking on the owl icon will open the information popup. This popup is useful
to know in advance whether owl is loaded in the tab or not. This is also indicated
by the icon itself: if it is flipped upside-down, it means that owl is not loaded in the
active tab. Do note that old versions of owl are not supported by the extension and will
therefore be indicated either as obsolete or absent by the extension popup.
<img src="screenshots/popup.png"/>
## First steps
When you are on a page where owl is detected, you can open your devtools either with
right-click -> Inspect or using F12. In the devtools menu, you can search for the Owl
tab which is added by the extension. It will be present by default at the end of the list but
you can drag and drop it at the position you want for easier navigation in the future.
<img src="screenshots/find_owl_tab.png"/>
When you open the tab, you arrive on the Components view by default which is one of the
two available tabs at the top. Here is an example of the devtools on the Odoo CRM app:
<img src="screenshots/crm.png"/>
## Components tab
The components tab is separated into two sub windows: the components tree in the left and
the component details in the right. The components tree will display all the different
components that are present in the tab in the form of a tree. The root of this tree is
actually the app which is not a component but can still be inspected by the devtools like
one. There can also be multiple apps loaded in the page like in 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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

-15
View File
@@ -1,15 +0,0 @@
import { mount, Component, useState, loadFile } from "@odoo/owl";
class Counter extends Component {
static template = "Counter";
setup() {
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
const templates = await loadFile("./counter.xml");
mount(Counter, document.getElementById("app-container"), { templates });
-7
View File
@@ -1,7 +0,0 @@
<templates>
<t t-name="Counter">
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</t>
</templates>
-9
View File
@@ -1,9 +0,0 @@
import { loadFile } from "@odoo/owl";
for (const [className, type] of [["xml", "xml"], ["javascript", "js"]]) {
loadFile(`./counter.${type}`).then(code => {
const el = document.querySelector(`code.${className}`);
el.textContent = code;
hljs.highlightBlock(el);
})
}
-5999
View File
File diff suppressed because it is too large Load Diff
-18
View File
@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL Playground</title>
<link rel="icon" href="data:,">
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="playground.js"></script>
</head>
<body>
</body>
</html>
@@ -1,11 +0,0 @@
tr.danger {
font-weight: bold;
}
.remove:hover {
font-weight: bold;
}
.remove {
cursor: pointer;
}
@@ -1,124 +0,0 @@
import { Component, mount, useState, onPatched} from "@odoo/owl";
// -----------------------------------------------------------------------------
// Data generation
// -----------------------------------------------------------------------------
let idCounter = 1;
const adjectives = [
"pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
"quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
"unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
function _random (max) { return Math.round(Math.random() * 1000) % max; };
function buildData(count) {
const data = new Array(count);
for (let i = 0; i < count; i++) {
const label = `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`;
data[i] = {
id: idCounter++,
label,
};
}
return data;
}
// -----------------------------------------------------------------------------
// Components
// -----------------------------------------------------------------------------
class Button extends Component {
static template = "Button";
}
class Row extends Component {
static template = "Row";
}
class Root extends Component {
static template = "Root";
static components = { Button, Row };
setup() {
this.state = useState({
rows: [],
selectedRowId: null
});
this.benchmarking = false;
onPatched(() => {
if (this.benchmarking) {
this.stop();
}
});
}
start(descr) {
this.benchmarking = `[${descr}]`;
console.time(this.benchmarking);
}
stop() {
console.timeEnd(this.benchmarking);
this.benchmarking = false;
}
run() {
this.start('add1000');
this.state.rows = buildData(1000);
this.state.selectedRowId = null;
}
runLots() {
this.start('add10_000');
this.state.rows = buildData(10_000);
this.state.selectedRowId = null;
}
add() {
this.start('append1000');
this.state.rows = this.state.rows.concat(buildData(1000));
}
update() {
this.start('update1/10th');
let index = 0;
const rows = this.state.rows;
while (index < rows.length) {
rows[index].label = rows[index].label + " !!!";
index += 10;
}
}
clear() {
this.start('clear');
this.state.rows = [];
this.state.selectedRowId = null;
}
swapRows() {
this.start('swap');
const rows = this.state.rows;
if (rows.length > 998) {
let tmp = rows[1];
rows[1] = rows[998];
rows[998] = tmp;
}
}
selectRow(id) {
this.start('select');
this.state.selectedRowId = id;
}
removeRow(id) {
this.start('remove1');
const rows = this.state.rows;
rows.splice(rows.findIndex(row => row.id === id), 1);
}
}
// dev=false for benchmarking. we don't want to benchmark dev code!
mount(Root, document.body, { templates: TEMPLATES, dev: false });
@@ -1,52 +0,0 @@
<templates>
<t t-name="Button">
<div class='col-sm-6 smallpad'>
<button t-att-id="props.id" class='btn btn-primary btn-block' type='button' t-on-click="props.onClick">
<t t-esc="props.text"/>
</button>
</div>
</t>
<t t-name="Row">
<tr t-att-class="props.isSelected ? 'danger' : ''">
<td class="col-md-1" t-esc="props.row.id" />
<td class="col-md-4">
<a t-on-click="() => props.onSelect(props.row.id)" t-esc="props.row.label" />
</td>
<td class="col-md-1">
<a t-on-click="() => props.onRemove(props.row.id)" class="remove">[x]
<span class='glyphicon glyphicon-remove' aria-hidden="true" />
</a>
</td>
<td class='col-md-6'/>
</tr>
</t>
<t t-name="Root">
<div class='container'>
<div class='jumbotron'>
<div class='row'>
<div class='col-md-6'>
<h1>Owl Keyed</h1>
</div>
<div class='col-md-6'>
<div class='row'>
<Button id="'run'" onClick.bind="run" text="'Create 1,000 rows'" />
<Button id="'runlots'" onClick.bind="runLots" text="'Create 10,000 rows'" />
<Button id="'add'" onClick.bind="add" text="'Append 1,000 rows'" />
<Button id="'update'" onClick.bind="update" text="'Update every 10th row'" />
<Button id="'clear'" onClick.bind="clear" text="'Clear'" />
<Button id="'swaprows'" onClick.bind="swapRows" text="'Swap Rows'" />
</div>
</div>
</div>
</div>
<table class='table table-hover table-striped test-data'>
<tbody>
<t t-foreach="state.rows" t-as="row" t-key="row.id">
<Row row="row" isSelected="row.id === state.selectedRowId" onSelect.bind="selectRow" onRemove.bind="removeRow"/>
</t>
</tbody>
</table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
</div>
</t>
</templates>
@@ -1,10 +0,0 @@
.greeter {
font-size: 20px;
width: 300px;
height: 100px;
margin: 5px;
text-align: center;
line-height: 100px;
background-color: #eeeeee;
user-select: none;
}
@@ -1,26 +0,0 @@
// In this example, we show how components can be defined and created.
import { Component, useState, mount } from "@odoo/owl";
class Greeter extends Component {
static template = "Greeter";
setup() {
this.state = useState({ word: 'Hello' });
}
toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
}
}
// Main root component
class Root extends Component {
static components = { Greeter };
static template = "Root"
setup() {
this.state = useState({ name: 'World'});
}
}
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -1,9 +0,0 @@
<templates>
<div t-name="Greeter" class="greeter" t-on-click="toggle">
<t t-esc="state.word"/>, <t t-esc="props.name"/>
</div>
<t t-name="Root">
<Greeter name="state.name"/>
</t>
</templates>
@@ -1,6 +0,0 @@
button {
width: 120px;
height: 35px;
font-size: 16px;
}
@@ -1,40 +0,0 @@
// In this example, we show how hooks can be used or defined.
import { Component, mount, useState, onWillDestroy } from "@odoo/owl";
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
window.addEventListener('mousemove', update);
onWillDestroy(() => {
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
class Root extends Component {
static template = "Root";
setup() {
// simple state hook (reactive object)
this.counter = useState({ value: 0 });
// this hooks is bound to the 'mouse' property.
this.mouse = useMouse();
}
increment() {
this.counter.value++;
}
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -1,7 +0,0 @@
<templates>
<div t-name="Root">
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>
</templates>
-22
View File
@@ -1,22 +0,0 @@
// This example illustrate how the t-model directive can be used to synchronize
// data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally
// synchronized.
import { Component, useState, mount } from "@odoo/owl";
class Form extends Component {
static template = "Form";
setup() {
this.state = useState({
text: "",
othertext: "",
number: 11,
color: "",
bool: false
});
}
}
// Application setup
mount(Form, document.body, { templates: TEMPLATES, dev: true });
-37
View File
@@ -1,37 +0,0 @@
<templates>
<div t-name="Form">
<h1>Form</h1>
<div>
Text (immediate): <input t-model="state.text"/>
</div>
<div>
Other text (lazy): <input t-model.lazy="state.othertext"/>
</div>
<div>
Number: <input t-model.number="state.number"/>
</div>
<div>
Boolean: <input type="checkbox" t-model="state.bool"/>
</div>
<div>
Color, with a select:
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Color, with radio buttons:
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
</div>
<hr/>
<h1>State</h1>
<div>Text: <t t-esc="state.text"/></div>
<div>Other Text: <t t-esc="state.othertext"/></div>
<div>Number: <t t-esc="state.number"/></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
<div>Color: <t t-esc="state.color"/></div>
</div>
</templates>
-7
View File
@@ -1,7 +0,0 @@
{
"compilerOptions": {
"paths": {
"owl": ["../../../src/index.ts"],
},
}
}
@@ -1,11 +0,0 @@
button {
font-size: 18px;
margin: 5px;
}
.demo {
margin: 10px;
padding: 10px;
background-color: #dddddd;
width: 250px;
}
@@ -1,68 +0,0 @@
// This example shows all the possible lifecycle hooks
//
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
// methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what
// happens.
import {
Component,
useState,
mount,
useComponent,
onWillStart,
onMounted,
onWillUnmount,
onWillUpdateProps,
onPatched,
onWillPatch,
onWillRender,
onRendered,
onWillDestroy,
} from "@odoo/owl";
function useLogLifecycle() {
const component = useComponent();
const name = component.constructor.name;
onWillStart(() => console.log(`${name}:willStart`));
onMounted(() => console.log(`${name}:mounted`));
onWillUpdateProps(() => console.log(`${name}:willUpdateProps`));
onWillRender(() => console.log(`${name}:willRender`));
onRendered(() => console.log(`${name}:rendered`));
onWillPatch(() => console.log(`${name}:willPatch`));
onPatched(() => console.log(`${name}:patched`));
onWillUnmount(() => console.log(`${name}:willUnmount`));
onWillDestroy(() => console.log(`${name}:willDestroy`));
}
class DemoComponent extends Component {
static template = "DemoComponent";
setup() {
useLogLifecycle();
this.state = useState({ n: 0 });
}
increment() {
this.state.n++;
}
}
class Root extends Component {
static template = "Root";
static components = { DemoComponent };
setup() {
useLogLifecycle();
this.state = useState({ n: 0, flag: true });
}
increment() {
this.state.n++;
}
toggleSubComponent() {
this.state.flag = !this.state.flag;
}
}
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -1,15 +0,0 @@
<templates>
<div t-name="DemoComponent" t-on-click="increment" class="demo">
<div>Demo Sub Component</div>
<div>(click on me to update me)</div>
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
</div>
<div t-name="Root">
<button t-on-click="increment">Increment Parent State</button>
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
<div t-if="state.flag">
<DemoComponent n="state.n"/>
</div>
</div>
</templates>
@@ -1,56 +0,0 @@
body {
margin: 0;
}
.app {
height: 100%;
flex-direction: column;
}
.app.desktop {
display: flex;
}
.navbar {
flex: 0 0 30px;
height: 30px;
background-color: cadetblue;
color: white;
line-height: 30px;
}
.controlpanel {
flex: 0 0 100px;
height: 100px;
background-color: #dddddd;
padding: 8px;
}
.content-wrapper {
flex: 1 1 auto;
position: relative;
}
.content {
display: flex;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.formview {
overflow-y: auto;
flex: 1 1 60%;
min-height: 200px;
padding: 8px;
}
.chatter {
overflow-y: auto;
flex: 1 1 40%;
background-color: #eeeeee;
color: #333333;
padding: 8px;
}
@@ -1,118 +0,0 @@
// In this example, we show how one can design an application that is responsive:
// its UI is different in mobile mode or in desktop mode.
//
// The main idea is to have a "isMobile" key in the environment, then listen
// to resize events and update the env if needed. Then, the whole interface
// will be updated, creating and destroying components as needed.
//
// To see this in action, try resizing the window. The application will switch
// to mobile mode whenever it has less than 768px.
import { Component, useState, mount, reactive, useEnv } from "@odoo/owl";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
function debounce(func, wait, immediate) {
let timeout;
return function () {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
//------------------------------------------------------------------------------
// Responsive hook
//------------------------------------------------------------------------------
function createUI() {
const getIsMobile = () => window.innerWidth <= 768;
const ui = reactive({ isMobile: getIsMobile() });
const updateEnv = debounce(() => {
const isMobile = getIsMobile();
if (ui.isMobile !== isMobile) {
ui.isMobile = isMobile;
}
}, 15);
window.addEventListener("resize", updateEnv);
return ui;
}
function useUI() {
const env = useEnv();
return useState(env.ui);
}
//------------------------------------------------------------------------------
// Components
//------------------------------------------------------------------------------
class Navbar extends Component {
static template = "Navbar";
}
class MobileSearchView extends Component {
static template = "MobileSearchView";
}
class ControlPanel extends Component {
static template = "ControlPanel";
static components = { MobileSearchView };
setup() {
this.ui = useUI();
}
}
class AdvancedComponent extends Component {
static template = "AdvancedComponent";
}
class FormView extends Component {
static template = "FormView";
static components = { AdvancedComponent };
setup() {
this.ui = useUI();
}
}
class Chatter extends Component {
static template = "Chatter";
setup() {
this.messages = Array.from(Array(100).keys());
}
}
class Root extends Component {
static template = "Root";
static components = { Navbar, ControlPanel, FormView, Chatter };
setup() {
this.ui = useUI();
}
}
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
const env = {
ui: createUI()
};
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
@@ -1,42 +0,0 @@
<templates>
<div t-name="Navbar" class="navbar">Navbar</div>
<div t-name="ControlPanel" class="controlpanel">
<h2>Control Panel</h2>
<MobileSearchView t-if="ui.isMobile" />
</div>
<div t-name="FormView" class="formview">
<h2>Form View</h2>
<AdvancedComponent t-if="!ui.isMobile" />
</div>
<div t-name="Chatter" class="chatter">
<h2>Chatter</h2>
<t t-foreach="messages" t-as="item" t-key="item"><div>Message <t t-esc="item"/></div></t>
</div>
<div t-name="MobileSearchView">Mobile searchview</div>
<div t-name="AdvancedComponent">
This component is only created in desktop mode.
<button>Button!</button>
</div>
<t t-name="maincontent">
<FormView />
<Chatter />
</t>
<div t-name="Root" class="app" t-att-class="{mobile: ui.isMobile, desktop: !ui.isMobile}">
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!ui.isMobile">
<div class="content">
<t t-call="maincontent"/>
</div>
</div>
<t t-else="">
<t t-call="maincontent"/>
</t>
</div>
</templates>
@@ -1,28 +0,0 @@
// This example illustrates how one can write Owl components with
// inline templates.
import { Component, useState, xml, mount } from "@odoo/owl";
// Counter component
class Counter extends Component {
static template = xml`
<button t-on-click="() => state.value++">
Click! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 })
}
// Root
class Root extends Component {
static template = xml`
<div>
<Counter/>
<Counter/>
</div>`;
static components = { Counter };
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true});
-43
View File
@@ -1,43 +0,0 @@
.main {
display: flex;
}
.card {
display: flex;
flex-direction: column;
background-color: #eeeeee;
width: 200px;
height: 100px;
margin: 10px;
border: 1px solid gray;
}
.card.full {
height: 100px;
}
.card.small {
height: 25px;
}
.card-title {
flex: 0 0 25px;
font-weight: bold;
background-color: darkcyan;
color: white;
padding: 2px;
}
.card-title button {
float: right;
}
.card-content {
flex: 1 1 auto;
padding: 5px;
border-top: 1px solid white;
}
.card-footer {
border-top: 1px solid white;
}
-49
View File
@@ -1,49 +0,0 @@
// We show here how slots can be used to create generic components.
// In this example, the Card component is basically only a container. It is not
// aware of its content. It just knows where it should be (with t-slot).
// The parent component define the content with t-set-slot.
//
// Note that the t-on-click event, defined in the Root template, is executed in
// the context of the Root component, even though it is inside the Card component
import { Component, useState, mount } from "@odoo/owl";
class Card extends Component {
static template = "Card";
setup() {
this.state = useState({ showContent: true });
}
toggleDisplay() {
this.state.showContent = !this.state.showContent;
}
}
class Counter extends Component {
static template = "Counter";
setup() {
this.state = useState({val: 1});
}
inc() {
this.state.val++;
}
}
// Main root component
class Root extends Component {
static template = "Root"
static components = { Card, Counter };
setup() {
this.state = useState({a: 1, b: 3});
}
inc(key, delta) {
this.state[key] += delta;
}
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true});
-33
View File
@@ -1,33 +0,0 @@
<templates>
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
<div class="card-title">
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
</div>
<t t-if="state.showContent">
<div class="card-content" >
<t t-slot="content"/>
</div>
<div class="card-footer">
<t t-slot="footer"/>
</div>
</t>
</div>
<div t-name="Counter">
<t t-esc="state.val"/><button t-on-click="inc">Inc</button>
</div>
<div t-name="Root" class="main">
<Card title="'Title card A'">
<t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set-slot="footer"><button t-on-click="() => this.inc('a', 1)">Increment A</button></t>
</Card>
<Card title="'Title card B'">
<t t-set-slot="content">
<div>Card 2... [<t t-esc="state.b"/>]</div>
<Counter />
</t>
<t t-set-slot="footer"><button t-on-click="() => this.inc('b', -1)">Decrement B</button></t>
</Card>
</div>
</templates>
@@ -1,379 +0,0 @@
html,body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all + label:before {
content: "";
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked + label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: "×";
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
.filters a {
cursor: pointer;
}
@@ -1,178 +0,0 @@
// This example is an implementation of the TodoList application, from the
// www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence.
//
// In this implementation, we use the owl reactivity mechanism.
import { Component, useState, mount, useRef, reactive, useEnv, useEffect } from "@odoo/owl";
//------------------------------------------------------------------------------
// Constants, helpers
//------------------------------------------------------------------------------
const ENTER_KEY = 13;
const ESC_KEY = 27;
function useAutofocus(name) {
let ref = useRef(name);
useEffect(el => el && el.focus(), () => [ref.el]);
}
function useStore() {
const env = useEnv();
return useState(env.store);
}
//------------------------------------------------------------------------------
// Task store
//------------------------------------------------------------------------------
class TaskList {
constructor(tasks) {
this.tasks = tasks || [];
const taskIds = this.tasks.map((t) => t.id);
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
}
addTask(text) {
text = text.trim();
if (text) {
const task = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(id) {
const task = this.tasks.find(t => t.id === id);
task.isCompleted = !task.isCompleted;
}
toggleAll(value) {
for (let task of this.tasks) {
task.isCompleted = value;
}
}
clearCompleted() {
const tasks = this.tasks.filter(t => t.isCompleted);
for (let task of tasks) {
this.deleteTask(task.id);
}
}
deleteTask(id) {
const index = this.tasks.findIndex((t) => t.id === id);
this.tasks.splice(index, 1);
}
updateTask(id, text) {
const value = text.trim();
if (!value) {
this.deleteTask(id);
} else {
const task = this.tasks.find(t => t.id === id);
task.text = value;
}
}
}
function createTaskStore() {
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
saveTasks();
return taskStore;
}
//------------------------------------------------------------------------------
// Todo
//------------------------------------------------------------------------------
class Todo extends Component {
static template = "Todo";
setup() {
useAutofocus("input");
this.store = useStore();
this.state = useState({
isEditing: false
});
}
handleKeyup(ev) {
if (ev.keyCode === ENTER_KEY) {
this.updateText(ev.target.value);
}
if (ev.keyCode === ESC_KEY) {
ev.target.value = this.props.text;
this.state.isEditing = false;
}
}
handleBlur(ev) {
this.updateText(ev.target.value);
}
updateText(text) {
this.store.updateTask(this.props.id, text);
this.state.isEditing = false;
}
}
//------------------------------------------------------------------------------
// TodoList
//------------------------------------------------------------------------------
class TodoList extends Component {
static template = "TodoList";
static components = { Todo };
setup() {
this.store = useStore();
this.state = useState({ filter: "all" });
}
get displayedTasks() {
const tasks = this.store.tasks;
switch (this.state.filter) {
case "active":
return tasks.filter((t) => !t.isCompleted);
case "completed":
return tasks.filter((t) => t.isCompleted);
case "all":
return tasks;
}
}
get allChecked() {
return this.store.tasks.every(todo => todo.isCompleted);
}
get remaining() {
return this.store.tasks.filter(todo => !todo.isCompleted).length;
}
get remainingText() {
const items = this.remaining < 2 ? "item" : "items";
return ` ${items} left`;
}
addTodo(ev) {
if (ev.keyCode === ENTER_KEY) {
const text = ev.target.value;
if (text.trim()) {
this.store.addTask(text);
}
ev.target.value = "";
}
}
setFilter(filter) {
this.state.filter = filter;
}
}
//------------------------------------------------------------------------------
// App Initialization
//------------------------------------------------------------------------------
const env = { store: createTaskStore() };
mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
@@ -1,50 +0,0 @@
<templates>
<section t-name="TodoList" class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header>
<section class="main" t-if="store.tasks.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="() => store.toggleAll(!allChecked)"/>
<label for="toggle-all"></label>
<ul class="todo-list">
<t t-foreach="displayedTasks" t-as="todo" t-key="todo.id">
<Todo id="todo.id" isCompleted="todo.isCompleted" text="todo.text"/>
</t>
</ul>
</section>
<footer class="footer" t-if="store.tasks.length">
<span class="todo-count">
<strong>
<t t-esc="remaining"/>
</strong>
<t t-esc="remainingText"/>
</span>
<ul class="filters">
<li>
<a t-on-click="() => this.setFilter('all')" t-att-class="{selected: state.filter === 'all'}">All</a>
</li>
<li>
<a t-on-click="() => this.setFilter('active')" t-att-class="{selected: state.filter === 'active'}">Active</a>
</li>
<li>
<a t-on-click="() => this.setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li>
</ul>
<button class="clear-completed" t-if="store.tasks.length gt remaining" t-on-click="() => store.clearCompleted()">
Clear completed
</button>
</footer>
</section>
<li t-name="Todo" class="todo" t-att-class="{completed: props.isCompleted, editing: state.isEditing}">
<div class="view">
<input class="toggle" type="checkbox" t-on-change="() => store.toggleTask(props.id)" t-att-checked="props.completed"/>
<label t-on-dblclick="() => state.isEditing = true">
<t t-esc="props.text"/>
</label>
<button class="destroy" t-on-click="() => store.deleteTask(props.id)"></button>
</div>
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.text" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
</li>
</templates>
@@ -1,70 +0,0 @@
body {
margin: 0;
}
.app {
width: 100%;
height: 100%;
display: grid;
grid-template-rows: auto 50px;
}
.window-manager {
position: relative;
width: 100%;
height: 100%;
background-color: #eeeeee;
overflow: hidden;
}
.menubar {
background-color: #875a7b;
color: white;
}
.menubar button {
height: 40px;
font-size: 18px;
margin: 5px;
}
.window {
display: grid;
grid-template-rows: 30px auto;
border: 1px solid gray;
background-color: white;
position: absolute;
box-shadow: 1px 1px 2px 1px grey;
}
.window.dragging {
opacity: 0.75;
}
.window .header {
background-color: #875a7b;
display: grid;
grid-template-columns: auto 24px;
color: white;
line-height: 30px;
padding-left: 5px;
cursor: default;
user-select: none;
}
.window .header .close {
cursor: pointer;
font-size: 22px;
padding-left: 4px;
padding-right: 4px;
font-weight: bold;
}
.counter {
font-size: 20px;
}
.counter button {
width: 80px;
height:40px;
font-size: 20px;
}
@@ -1,192 +0,0 @@
// This example is slightly more complex than usual. We demonstrate
// here a way to manage sub windows in Owl, declaratively. This is still just a
// demonstration. Managing windows can be as complex as we want. For example,
// we could implement the following features:
// - resizing windows
// - minimizing windows
// - configuration options for windows to make a window non resizeable
// - minimal width/height
// - better heuristic for initial window position
// - ...
import { Component, useState, mount, useRef, reactive, useEnv, onMounted } from "@odoo/owl";
// -----------------------------------------------------------------------------
// Window manager code
// -----------------------------------------------------------------------------
class WindowManager {
// contains all components with metadata
static Windows = {};
windows = {}; // mapping id => info
nextId = 1;
add(type) {
const Comp = WindowManager.Windows[type];
const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth));
const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight));
const id = this.nextId++;
this.windows[id] = {
id,
title: Comp.defaultTitle,
width: Comp.defaultWidth,
height: Comp.defaultHeight,
left,
top,
Component: Comp,
};
}
close(id) {
delete this.windows[id];
}
updatePosition(id, left, top) {
const w = this.windows[id];
w.left = left;
w.top = top;
}
getWindows() {
return Object.values(this.windows);
}
}
function createWindowService() {
return reactive(new WindowManager());
}
function useWindowService() {
const env = useEnv();
return useState(env.windowService);
}
// -----------------------------------------------------------------------------
// Generic Window Component
// -----------------------------------------------------------------------------
class Window extends Component {
static template = "Window";
static nextZIndex = 1;
zIndex = 0;
setup() {
this.windowService = useWindowService();
this.root = useRef('root');
onMounted(this.updateZIndex);
}
get style() {
let { width, height, top, left } = this.props.info;
return `width: ${width}px;height: ${height}px;top:${top}px;left:${left}px;z-index:${this.zIndex}`;
}
close() {
this.windowService.close(this.props.info.id);
}
startDragAndDrop(ev) {
this.updateZIndex();
const self = this;
const root = this.root;
const el = root.el;
el.classList.add('dragging');
const current = this.props.info;
const offsetX = current.left - ev.pageX;
const offsetY = current.top - ev.pageY;
let left, top;
window.addEventListener("mousemove", moveWindow);
window.addEventListener("mouseup", stopDnD, { once: true });
function moveWindow(ev) {
left = Math.max(offsetX + ev.pageX, 0);
top = Math.max(offsetY + ev.pageY, 0);
el.style.left = `${left}px`;
el.style.top = `${top}px`;
}
function stopDnD() {
window.removeEventListener("mousemove", moveWindow);
el.classList.remove('dragging');
if (top !== undefined && left !== undefined) {
self.windowService.updatePosition(current.id, left, top);
}
}
}
updateZIndex() {
this.zIndex = Window.nextZIndex++;
this.root.el.style['z-index'] = this.zIndex;
}
}
// -----------------------------------------------------------------------------
// Two concrete Window type implementations
// -----------------------------------------------------------------------------
class HelloWorld extends Component {
static template = "HelloWorld";
static defaultTitle = "Hello Owl!";
static defaultWidth = 200;
static defaultHeight = 100;
}
class Counter extends Component {
static template = "Counter";
static defaultTitle = "Click Counter";
static defaultWidth = 300;
static defaultHeight = 120;
state = useState({ value: 0 });
inc() {
this.state.value++;
}
}
// register window components
WindowManager.Windows.Hello = HelloWorld;
WindowManager.Windows.Counter = Counter;
// -----------------------------------------------------------------------------
// Window Container
// -----------------------------------------------------------------------------
class WindowContainer extends Component {
static template = "WindowContainer";
static components = { Window };
setup() {
this.windowService = useWindowService();
}
}
// -----------------------------------------------------------------------------
// Root Component
// -----------------------------------------------------------------------------
class Root extends Component {
static template = "Root";
static components = { WindowContainer };
setup() {
this.windowService = useWindowService();
}
addWindow(type) {
this.windowService.add(type);
}
}
// -----------------------------------------------------------------------------
// Setup
// -----------------------------------------------------------------------------
const env = {
windowService: createWindowService(),
};
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
@@ -1,32 +0,0 @@
<templates>
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex" t-ref="root">
<div class="header">
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
<span class="close" t-on-click.stop="close">×</span>
</div>
<t t-slot="default"/>
</div>
<div t-name="WindowContainer" class="window-manager">
<Window t-foreach="windowService.getWindows()" t-as="w" t-key="w.id" info="w">
<t t-component="w.Component"/>
</Window>
</div>
<div t-name="Root" class="app">
<WindowContainer/>
<div class="menubar">
<button t-on-click="() => this.addWindow('Hello')">Say Hello</button>
<button t-on-click="() => this.addWindow('Counter')">Counter</button>
</div>
</div>
<div t-name="HelloWorld">
Some content here...
</div>
<div t-name="Counter" class="counter">
<button t-on-click="inc">Click</button>
<span><t t-esc="state.value"/></span>
</div>
</templates>
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
httpd = None
def start_server():
global httpd
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="app.css">
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
<script type="module" src="app.js"></script>
</head>
<body>
<pre>
This app requires a web server, simply opening the html file using your browser will not work.
You can run a simple web server with one of the following:
- if you have python3 installed, run app.py with python3 or run 'python3 -m http.server' in the app folder
- if you have node/npm installed, run 'npx serve' in the app folder
- if you have php installed, run 'php -S localhost:8080 -t .' in the app folder
</pre>
<script>if (window.location.protocol !== "file:") document.querySelector("pre").remove()</script>
</body>
</html>
-5
View File
@@ -1,5 +0,0 @@
# Github page and playground
This folder contains the code for owl's [github page](https://odoo.github.io/owl)
and the owl [playground](https://odoo.github.io/owl/playground/). If you're
looking for the owl documentation, click [here](/doc)
+41 -6
View File
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="assets/milligram.css">
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
<link rel="stylesheet" href="assets/main.css">
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
<script src="./owl.js"></script>
</head>
<body>
<header class="container">
@@ -54,11 +54,47 @@
<h2>Example</h2>
<div class="row">
<section class="column">
<pre><code class="javascript"></code></pre>
<pre><code class="javascript">import { mount, Component, useState } from "owl";
class Counter extends Component {
setup() {
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
mount(Counter, document.body);</code></pre>
</section>
<section class="column">
<pre><code class="xml"></code></pre>
<pre><code class="xml">&lt;templates&gt;
&lt;t t-name=&quot;Counter&quot;&gt;
&lt;button t-on-click=&quot;increment&quot;&gt;
Click Me! [&lt;t t-esc=&quot;state.value&quot;/&gt;]
&lt;/button&gt;
&lt;/t&gt;
&lt;/templates&gt;</code></pre>
<div id="app-container"></div>
<script>
const { mount, Component, useState, xml } = owl;
class Counter extends Component {
setup() {
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
Counter.template = xml`<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
mount(Counter, document.getElementById("app-container"));
</script>
</section>
</div>
</div>
@@ -68,7 +104,6 @@
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
</footer>
<script src="assets/highlight.pack.js"></script>
<script type="module" src="display_code.js"></script>
<script type="module" src="counter.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</body>
</html>
</html>
+5967
View File
File diff suppressed because it is too large Load Diff
-5985
View File
File diff suppressed because it is too large Load Diff
-101
View File
@@ -1,101 +0,0 @@
{
"name": "@odoo/owl",
"version": "2.2.4",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
"types": "dist/types/owl.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=12.18.3"
},
"scripts": {
"build:bundle": "rollup -c --failAfterWarnings",
"build:runtime": "rollup -c --failAfterWarnings runtime",
"build:compiler": "rollup -c --failAfterWarnings compiler",
"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:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch",
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
"playground": "npm run build && npm run playground:serve",
"preplayground:watch": "npm run build",
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"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,tools/devtools/**/*.js} --check",
"lint": "eslint src/**/*.ts tests/**/*.ts",
"release": "node tools/release.js",
"compile_templates": "node tools/compile_xml.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/odoo/owl.git"
},
"author": "Odoo",
"license": "LGPL-3.0-only",
"bugs": {
"url": "https://github.com/odoo/owl/issues"
},
"homepage": "https://github.com/odoo/owl#readme",
"devDependencies": {
"@types/jest": "^27.0.1",
"@types/node": "^14.11.8",
"@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.1",
"chalk": "^3.0.0",
"current-git-branch": "^1.1.0",
"eslint": "8.31.0",
"git-rev-sync": "^3.0.2",
"github-api": "^3.3.0",
"jest": "^27.1.0",
"jest-diff": "^27.3.1",
"jest-environment-jsdom": "^27.1.0",
"npm-run-all": "^4.1.5",
"prettier": "2.4.1",
"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-typescript2": "^0.31.1",
"source-map-support": "^0.5.10",
"ts-jest": "^27.0.5",
"typescript": "4.5.2"
},
"jest": {
"testEnvironment": "jsdom",
"roots": [
"<rootDir>/src",
"<rootDir>/tests"
],
"setupFiles": [
"./tests/mocks/mockEventTarget.js"
],
"transform": {
"^.+\\.ts?$": "ts-jest"
},
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.ts?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
},
"prettier": {
"printWidth": 100,
"endOfLine": "auto"
}
}
@@ -1,19 +1,7 @@
import { SAMPLES } from "./samples.js";
import { debounce, loadJS } from "./utils.js";
import {
App,
Component,
useState,
useRef,
onMounted,
onWillUnmount,
onPatched,
onWillUpdateProps,
loadFile as _loadFile,
whenReady,
__info__,
useEffect,
onWillStart,
} from "../owl.js";
import { exportStandaloneApp } from "./export_snippet.js";
const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl;
//------------------------------------------------------------------------------
// Constants, helpers, utils
@@ -28,38 +16,40 @@ const MODES = {
const DEFAULT_XML = `<templates>
</templates>`;
// Memoize loadFile so that samples aren't reloaded whenever a sample is selected
const fileCache = {};
const loadFile = (path) => {
if (!(path in fileCache)) {
fileCache[path] = _loadFile(path);
}
return fileCache[path];
}
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml) {
// escape backticks in the xml so they don't close the template string
const escapedXml = xml.replace(/`/g, '\\\`');
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`');
// create iframe
const iframe = document.createElement("iframe");
iframe.onload = () => {
const doc = iframe.contentDocument;
const utilsMod = doc.createElement("script");
utilsMod.setAttribute("type", "module");
utilsMod.setAttribute("src", "utils.js");
doc.head.appendChild(utilsMod);
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "module";
const content = `
(async function(TEMPLATES) {
${js}
})(\`${sanitizedXML}\`)`;
script.innerHTML = content;
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
const importMap = doc.createElement("script");
importMap.type = "importmap";
importMap.textContent = `{ "imports": { "@odoo/owl": "../owl.js" } }`;
doc.head.appendChild(importMap);
const script = doc.createElement("script");
script.type = "module";
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
doc.body.appendChild(script);
// inject css
const style = document.createElement("style");
style.innerText = css;
style.innerHTML = css;
doc.head.appendChild(style);
};
return iframe;
@@ -68,84 +58,53 @@ function makeCodeIframe(js, css, xml) {
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
const SAMPLES = [
{
description: "Components",
folder: "components",
code: ["js", "xml", "css"],
},
{
description: "Form Input Bindings",
folder: "form",
code: ["js", "xml"],
},
{
description: "Inline templates",
folder: "single_file_component",
code: ["js"],
},
{
description: "Lifecycle demo",
folder: "lifecycle_demo",
code: ["js", "xml", "css"],
},
{
description: "Customized hook",
folder: "custom_hooks",
code: ["js", "xml", "css"],
},
{
description: "Todo List App (with reactivity)",
folder: "todo_app",
code: ["js", "xml", "css"],
},
{
description: "Responsive app",
folder: "responsive_app",
code: ["js", "xml", "css"],
},
{
description: "Slots And Generic Components",
folder: "slots",
code: ["js", "xml", "css"],
},
{
description: "Window Management System",
folder: "window_manager",
code: ["js", "xml", "css"],
},
{
description: "Benchmark example",
folder: "benchmark",
code: ["js", "xml", "css"],
},
]
function loadSamples() {
const result = SAMPLES.map(({ description, folder, code }) => ({
description,
code: async () => Object.fromEntries(
await Promise.all(
code.map(async (type) => [type, await loadFile(`./samples/${folder}/${folder}.${type}`)])
)
),
}));
let result = SAMPLES.slice();
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: () => Promise.resolve({ js, xml, css }),
code: js,
xml,
css
});
}
return result;
}
function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample");
}
function useSamples() {
const samples = loadSamples();
const component = owl.useComponent();
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends Component {
class TabbedEditor extends owl.Component {
setup() {
const props = this.props;
this.state = useState({
@@ -209,7 +168,7 @@ class TabbedEditor extends Component {
const resizer = ev => {
const delta = ev.clientY - y;
y = ev.clientY;
this.props.updatePanelHeight({ delta });
this.trigger("updatePanelHeight", { delta });
};
document.body.addEventListener("mousemove", resizer);
document.body.addEventListener("mouseup", () => {
@@ -246,53 +205,36 @@ TabbedEditor.template = "TabbedEditor";
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class Playground extends Component {
static template = "Playground";
static components = { TabbedEditor };
class App extends owl.Component {
setup() {
this.version = __info__.version;
this.version = owl.__info__.version;
this.SAMPLES = useSamples();
this.isDirty = false;
this.state = useState({
js: "",
css: "",
xml: DEFAULT_XML,
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
});
this.samples = loadSamples();
this.toggleLayout = debounce(this.toggleLayout, 250, true);
this.runCode = debounce(this.runCode, 250, true);
this.downloadCode = debounce(this.downloadCode, 250, true);
this.content = useRef("content");
this.updateCode = this.updateCode.bind(this);
if (window.location.hash) {
try {
const { js, css, xml } = JSON.parse(atob(decodeURIComponent(window.location.hash.slice(1))));
if ([js, css, xml].every(item => typeof item === "string")) {
Object.assign(this.state, { js, css, xml });
if (![js, css, xml].every(item => typeof item === "string")) {
return;
}
Object.assign(this.state, { js, css, xml });
} catch {}
}
onWillStart(async () => {
if (!this.state.js) {
this.setSample(await this.samples[0].code());
}
})
useEffect(() => {
const interval = setInterval(() => {
if (this.isDirty) {
const { js, css, xml } = this.state;
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
}, 1000);
return () => clearInterval(interval);
}, () => []);
this.toggleLayout = debounce(this.toggleLayout, 250, true);
this.runCode = debounce(this.runCode, 250, true);
this.exportStandaloneApp = debounce(this.exportStandaloneApp, 250, true);
this.content = useRef("content");
this.updateCode = this.updateCode.bind(this);
}
runCode() {
@@ -316,11 +258,12 @@ class Playground extends Component {
window.location.href = link.href;
}
setSample(sample) {
this.state.js = sample.js;
setSample(ev) {
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
localStorage.removeItem("owl-playground-local-sample");
deleteLocalSample();
this.isDirty = false;
}
@@ -328,12 +271,12 @@ class Playground extends Component {
return `width:${this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
async onSampleChange(ev) {
this.setSample(await this.samples.find(s => s.description === ev.target.value).code());
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
onMouseDown() {
@@ -362,40 +305,39 @@ class Playground extends Component {
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight({ delta }) {
let height = this.state.topPanelHeight || document.querySelector(".tabbed-editor").clientHeight;
this.state.topPanelHeight = height + delta;
updatePanelHeight(ev) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.detail.delta;
}
async exportStandaloneApp() {
async downloadCode() {
const { js, css, xml } = this.state;
await loadJS("libs/jszip.min.js");
const zip = new JSZip();
zip.file("app.py", await loadFile("./standalone_app/app.py"));
zip.file("index.html", await loadFile("./standalone_app/index.html"));
zip.file("owl.js", await loadFile("../owl.js"));
zip.file("app.js", `const TEMPLATES = await (await fetch('app.xml')).text();\n${js}`);
zip.file("app.css", css);
zip.file("app.xml", xml);
const content = await exportStandaloneApp(js, css, xml);
await loadJS("libs/FileSaver.min.js");
saveAs(await zip.generateAsync({ type: "blob" }), "app.zip");
saveAs(content, "app.zip");
}
}
App.components = { TabbedEditor };
App.template = "App";
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
document.title = `${document.title} (v${__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${__info__.hash}`;
document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
loadFile("templates.xml"),
whenReady()
owl.loadFile("templates.xml"),
owl.whenReady()
]);
const rootApp = new App(Playground, { name: "Owl Playground" });
const rootApp = new owl.App(App);
rootApp.addTemplates(templates);
await rootApp.mount(document.body);
+123
View File
@@ -0,0 +1,123 @@
import { loadJS } from "./utils.js";
const sourceCache = new Map();
async function getSourceCode(filePath) {
if (sourceCache.has(filePath)) {
return sourceCache.get(filePath);
}
let source = await fetch(`${filePath}.js`);
source = await source.text();
sourceCache.set(filePath, source);
return source;
}
const DEFAULT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<script src="owl.js"></script>
<link rel="stylesheet" href="app.css">
<script type="module" src="app.js"></script>
<script type="module" src="utils.js"></script>
</head>
<body>
</body>
</html>
`;
const APP_PY = `#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
def start_server():
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
`;
/**
* Make a zip file containing a functioning application
*/
export async function exportStandaloneApp(js, css, xml) {
await loadJS("libs/jszip.min.js");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const JS = `
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
import * as utils from "./utils.js";
async function app() {
${processedJS}
}
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
function prepareOWLApp(templates) {
const _configure = owl.App.prototype.configure;
owl.App.prototype.configure = function configureOverriden(config) {
config = Object.assign({ dev: true }, config);
this.addTemplates(templates);
return _configure.call(this, config);
}
}
async function start() {
let templates;
try {
templates = await owl.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
prepareOWLApp(templates);
app();
}
start();
`;
const [owlSrc, utilsSrc] = await Promise.all([
getSourceCode("../owl"),
getSourceCode("./utils"),
]);
zip.file("app.js", JS);
zip.file("utils.js", utilsSrc);
zip.file("app.css", css);
zip.file("app.py", APP_PY);
zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSrc);
return zip.generateAsync({ type: "blob" });
}

Some files were not shown because too many files have changed in this diff Show More