├── .eslintrc ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .jshintrc ├── CHANGELOG.md ├── COPYRIGHT ├── LICENSE ├── README.md ├── doc ├── learning │ ├── how_to_test.md │ ├── quick_start.md │ └── tutorial_todoapp.md ├── miscellaneous │ ├── architecture.md │ ├── comparison.md │ ├── compiled_template.md │ └── why_owl.md ├── readme.md ├── reference │ ├── app.md │ ├── component.md │ ├── concurrency_model.md │ ├── environment.md │ ├── error_handling.md │ ├── event_handling.md │ ├── hooks.md │ ├── input_bindings.md │ ├── portal.md │ ├── precompiling_templates.md │ ├── props.md │ ├── reactivity.md │ ├── refs.md │ ├── slots.md │ ├── templates.md │ ├── translations.md │ └── utils.md └── tools │ ├── devtools.md │ ├── devtools_guide.md │ └── screenshots │ ├── crm.png │ ├── darkmode.png │ ├── edit.png │ ├── events_log.png │ ├── extensions.png │ ├── find_owl_tab.png │ ├── function_menu.png │ ├── hooks.png │ ├── iframes.png │ ├── menu.png │ ├── multi_apps.png │ ├── observe_variables.png │ ├── picker.png │ ├── popup.png │ ├── profiler.png │ ├── record.png │ ├── states.png │ ├── trace_rendering.png │ ├── trace_subscriptions.png │ └── tree_actions.png ├── docs ├── assets │ ├── highlight.pack.js │ ├── highlight.tomorrow.css │ ├── main.css │ ├── milligram.css │ ├── normalize.css │ └── owl_1f989.png ├── counter.js ├── counter.xml ├── display_code.js ├── index.html ├── owl.js ├── playground │ ├── index.html │ ├── libs │ │ ├── FileSaver.min.js │ │ ├── ace.js │ │ ├── jszip.min.js │ │ ├── mode-css.js │ │ ├── mode-javascript.js │ │ ├── mode-xml.js │ │ ├── theme-monokai.js │ │ └── worker-javascript.js │ ├── playground.css │ ├── playground.js │ ├── samples │ │ ├── benchmark │ │ │ ├── benchmark.css │ │ │ ├── benchmark.js │ │ │ └── benchmark.xml │ │ ├── components │ │ │ ├── components.css │ │ │ ├── components.js │ │ │ └── components.xml │ │ ├── custom_hooks │ │ │ ├── custom_hooks.css │ │ │ ├── custom_hooks.js │ │ │ └── custom_hooks.xml │ │ ├── form │ │ │ ├── form.js │ │ │ └── form.xml │ │ ├── jsconfig.json │ │ ├── lifecycle_demo │ │ │ ├── lifecycle_demo.css │ │ │ ├── lifecycle_demo.js │ │ │ └── lifecycle_demo.xml │ │ ├── responsive_app │ │ │ ├── responsive_app.css │ │ │ ├── responsive_app.js │ │ │ └── responsive_app.xml │ │ ├── single_file_component │ │ │ └── single_file_component.js │ │ ├── slots │ │ │ ├── slots.css │ │ │ ├── slots.js │ │ │ └── slots.xml │ │ ├── todo_app │ │ │ ├── todo_app.css │ │ │ ├── todo_app.js │ │ │ └── todo_app.xml │ │ └── window_manager │ │ │ ├── window_manager.css │ │ │ ├── window_manager.js │ │ │ └── window_manager.xml │ ├── standalone_app │ │ ├── app.py │ │ └── index.html │ ├── templates.xml │ └── utils.js └── readme.md ├── package-lock.json ├── package.json ├── roadmap.md ├── rollup.config.js ├── src ├── common │ ├── owl_error.ts │ ├── types.ts │ └── utils.ts ├── compiler │ ├── code_generator.ts │ ├── index.ts │ ├── inline_expressions.ts │ ├── parser.ts │ └── standalone │ │ ├── index.ts │ │ └── setup_jsdom.ts ├── index.ts ├── runtime │ ├── app.ts │ ├── blockdom │ │ ├── attributes.ts │ │ ├── block_compiler.ts │ │ ├── config.ts │ │ ├── event_catcher.ts │ │ ├── events.ts │ │ ├── html.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── multi.ts │ │ ├── text.ts │ │ └── toggler.ts │ ├── component.ts │ ├── component_node.ts │ ├── error_handling.ts │ ├── event_handling.ts │ ├── fibers.ts │ ├── hooks.ts │ ├── index.ts │ ├── lifecycle_hooks.ts │ ├── portal.ts │ ├── reactivity.ts │ ├── scheduler.ts │ ├── status.ts │ ├── template_helpers.ts │ ├── template_set.ts │ ├── utils.ts │ └── validation.ts └── version.ts ├── tests ├── __snapshots__ │ └── reactivity.test.ts.snap ├── app │ ├── __snapshots__ │ │ ├── app.test.ts.snap │ │ └── sub_root.test.ts.snap │ ├── app.test.ts │ └── sub_root.test.ts ├── blockdom │ ├── block.test.ts │ ├── block_attributes.test.ts │ ├── block_event_handling.test.ts │ ├── block_properties.test.ts │ ├── block_refs.test.ts │ ├── comment.test.ts │ ├── event_catcher.test.ts │ ├── helpers.ts │ ├── html.test.ts │ ├── list.test.ts │ ├── multi.test.ts │ ├── namespace.test.ts │ ├── remove_hook.test.ts │ ├── text.test.ts │ └── toggler.test.ts ├── compiler │ ├── __snapshots__ │ │ ├── attributes.test.ts.snap │ │ ├── comments.test.ts.snap │ │ ├── event_handling.test.ts.snap │ │ ├── misc.test.ts.snap │ │ ├── parser.test.ts.snap │ │ ├── properties.test.ts.snap │ │ ├── qweb_memory.test.ts.snap │ │ ├── simple_templates.test.ts.snap │ │ ├── svg.test.ts.snap │ │ ├── t_call.test.ts.snap │ │ ├── t_custom.test.ts.snap │ │ ├── t_debug_log.test.ts.snap │ │ ├── t_esc.test.ts.snap │ │ ├── t_foreach.test.ts.snap │ │ ├── t_if.test.ts.snap │ │ ├── t_key.test.ts.snap │ │ ├── t_out.test.ts.snap │ │ ├── t_ref.test.ts.snap │ │ ├── t_set.test.ts.snap │ │ ├── t_slot.test.ts.snap │ │ ├── t_tag.test.ts.snap │ │ ├── template_set.test.ts.snap │ │ ├── translation.test.ts.snap │ │ └── white_space.test.ts.snap │ ├── attributes.test.ts │ ├── blacklist.test.ts │ ├── comments.test.ts │ ├── error_handling.test.ts │ ├── event_handling.test.ts │ ├── inline_expressions.test.ts │ ├── misc.test.ts │ ├── parser.test.ts │ ├── properties.test.ts │ ├── qweb_memory.test.ts │ ├── simple_templates.test.ts │ ├── svg.test.ts │ ├── t_call.test.ts │ ├── t_custom.test.ts │ ├── t_debug_log.test.ts │ ├── t_esc.test.ts │ ├── t_foreach.test.ts │ ├── t_if.test.ts │ ├── t_key.test.ts │ ├── t_out.test.ts │ ├── t_ref.test.ts │ ├── t_set.test.ts │ ├── t_slot.test.ts │ ├── t_tag.test.ts │ ├── template_set.test.ts │ ├── translation.test.ts │ ├── validation.test.ts │ └── white_space.test.ts ├── components │ ├── __snapshots__ │ │ ├── basics.test.ts.snap │ │ ├── concurrency.test.ts.snap │ │ ├── error_handling.test.ts.snap │ │ ├── event_handling.test.ts.snap │ │ ├── higher_order_component.test.ts.snap │ │ ├── hooks.test.ts.snap │ │ ├── lifecycle.test.ts.snap │ │ ├── props.test.ts.snap │ │ ├── props_validation.test.ts.snap │ │ ├── reactivity.test.ts.snap │ │ ├── refs.test.ts.snap │ │ ├── rendering.test.ts.snap │ │ ├── slots.test.ts.snap │ │ ├── style_class.test.ts.snap │ │ ├── t_call.test.ts.snap │ │ ├── t_call_block.test.ts.snap │ │ ├── t_component.test.ts.snap │ │ ├── t_foreach.test.ts.snap │ │ ├── t_key.test.ts.snap │ │ ├── t_model.test.ts.snap │ │ ├── t_on.test.ts.snap │ │ ├── t_out.test.ts.snap │ │ ├── t_props.test.ts.snap │ │ └── t_set.test.ts.snap │ ├── basics.test.ts │ ├── concurrency.test.ts │ ├── env.test.ts │ ├── error_handling.test.ts │ ├── event_handling.test.ts │ ├── higher_order_component.test.ts │ ├── hooks.test.ts │ ├── lifecycle.test.ts │ ├── props.test.ts │ ├── props_validation.test.ts │ ├── reactivity.test.ts │ ├── refs.test.ts │ ├── rendering.test.ts │ ├── slots.test.ts │ ├── style_class.test.ts │ ├── t_call.test.ts │ ├── t_call_block.test.ts │ ├── t_component.test.ts │ ├── t_foreach.test.ts │ ├── t_key.test.ts │ ├── t_model.test.ts │ ├── t_on.test.ts │ ├── t_out.test.ts │ ├── t_props.test.ts │ └── t_set.test.ts ├── helpers.ts ├── misc │ ├── __snapshots__ │ │ └── portal.test.ts.snap │ └── portal.test.ts ├── mocks │ └── mockEventTarget.js ├── reactivity.test.ts ├── shadow_dom │ ├── __snapshots__ │ │ └── shadow_dom.test.ts.snap │ └── shadow_dom.test.ts ├── tooling │ └── doc_link_checker.test.ts ├── utils.test.ts └── validation.test.ts ├── tools ├── compile_owl_templates.mjs ├── devtools │ ├── assets │ │ ├── FiraMono-Medium.ttf │ │ ├── bootstrap.min.css │ │ ├── font-awesome.min.css │ │ ├── icon128.png │ │ ├── icon16.png │ │ ├── icon48.png │ │ └── icon_disabled128.png │ ├── manifest-chrome.json │ ├── manifest-firefox.json │ ├── rollup.config.js │ └── src │ │ ├── background.html │ │ ├── background.js │ │ ├── content.js │ │ ├── devtools_app │ │ ├── context_menu │ │ │ ├── context_menu.js │ │ │ └── context_menu.xml │ │ ├── devtools.html │ │ ├── devtools.js │ │ ├── devtools_panel.html │ │ ├── devtools_panel.js │ │ ├── devtools_window │ │ │ ├── components_tab │ │ │ │ ├── component_search_bar │ │ │ │ │ ├── component_search_bar.js │ │ │ │ │ └── component_search_bar.xml │ │ │ │ ├── components_tab.js │ │ │ │ ├── components_tab.xml │ │ │ │ ├── details_window │ │ │ │ │ ├── details_window.js │ │ │ │ │ ├── details_window.xml │ │ │ │ │ └── object_tree_element │ │ │ │ │ │ ├── object_tree_element.js │ │ │ │ │ │ └── object_tree_element.xml │ │ │ │ └── tree_element │ │ │ │ │ ├── highlight_text │ │ │ │ │ ├── highlight_text.js │ │ │ │ │ └── highlight_text.xml │ │ │ │ │ ├── tree_element.js │ │ │ │ │ └── tree_element.xml │ │ │ ├── devtools_window.js │ │ │ ├── devtools_window.xml │ │ │ ├── profiler_tab │ │ │ │ ├── event │ │ │ │ │ ├── event.js │ │ │ │ │ └── event.xml │ │ │ │ ├── event_node │ │ │ │ │ ├── event_node.js │ │ │ │ │ └── event_node.xml │ │ │ │ ├── event_search_bar │ │ │ │ │ ├── event_search_bar.js │ │ │ │ │ └── event_search_bar.xml │ │ │ │ ├── profiler_tab.js │ │ │ │ └── profiler_tab.xml │ │ │ └── tab │ │ │ │ ├── tab.js │ │ │ │ └── tab.xml │ │ └── store │ │ │ └── store.js │ │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ │ ├── main.css │ │ ├── page_scripts │ │ └── owl_devtools_global_hook.js │ │ ├── popup_app │ │ ├── popup.html │ │ ├── popup.js │ │ └── popup_app.xml │ │ └── utils.js ├── owl-vision │ ├── .eslintrc.json │ ├── .vscode │ │ ├── launch.json │ │ └── tasks.json │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── assets │ │ ├── icon128.png │ │ └── syntax_highlight.png │ ├── package-lock.json │ ├── package.json │ ├── scripts │ │ ├── owl_template_syntax.mjs │ │ ├── syntax_builder_utils.mjs │ │ └── syntax_parts │ │ │ ├── owl_attributes.mjs │ │ │ └── xpath.mjs │ ├── snippets │ │ └── component.json │ ├── src │ │ ├── extension.ts │ │ ├── language_features │ │ │ ├── items.ts │ │ │ ├── language_features_provider.ts │ │ │ └── parser.ts │ │ ├── search.ts │ │ └── utils.ts │ ├── syntaxes │ │ ├── owl.markup.inline.json │ │ ├── owl.template.inline.json │ │ └── owl.template.json │ └── tsconfig.json ├── playground_server.py └── release.js └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es2022": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint"], 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "root": true, 13 | "rules": { 14 | "no-restricted-globals": ["error", "event", "self"], 15 | "no-const-assign": ["error"], 16 | "no-debugger": ["error"], 17 | "no-dupe-class-members": ["error"], 18 | "no-dupe-keys": ["error"], 19 | "no-dupe-args": ["error"], 20 | "no-dupe-else-if": ["error"], 21 | "no-unsafe-negation": ["error"], 22 | "no-duplicate-imports": ["error"], 23 | "valid-typeof": ["error"], 24 | "@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false, "caughtErrors": "all" }], 25 | "no-restricted-syntax": [ 26 | "error", 27 | { 28 | "selector": "MemberExpression[object.name='test'][property.name='only']", 29 | "message": "test.only(...) is forbidden", 30 | }, 31 | { 32 | "selector": "MemberExpression[object.name='describe'][property.name='only']", 33 | "message": "describe.only(...) is forbidden", 34 | } 35 | ], 36 | }, 37 | "globals": { 38 | "describe": true, 39 | "expect": true, 40 | "test": true, 41 | "beforeEach": true, 42 | "beforeAll": true, 43 | "afterEach": true, 44 | "afterAll": true, 45 | "jest": true, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [20.x, 22.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm ci 26 | - run: npm run test 27 | - run: npm run check-formatting 28 | - run: npm run lint 29 | - run: npm run build 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | 4 | npm-debug.log 5 | 6 | # misc 7 | .DS_Store 8 | .env.local 9 | .env.development.local 10 | .env.test.local 11 | .env.production.local 12 | 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | #ide's 18 | **/.vscode/* 19 | .idea 20 | 21 | node_modules 22 | 23 | release-notes.md 24 | 25 | .rpt2_cache 26 | 27 | # useful in some cases 28 | /temp 29 | 30 | # owl-vision 31 | */owl-vision/out/ 32 | */owl-vision/.vs/ 33 | **/*.vsix 34 | !*/owl-vision/.vscode/launch.json 35 | !*/owl-vision/.vscode/tasks.json 36 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 9 3 | } 4 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | 2 | Most of the files are 3 | 4 | Copyright (c) 2004-2015 Odoo S.A. 5 | 6 | Many files also contain contributions from third 7 | parties. In this case the original copyright of 8 | the contributions can be traced through the 9 | history of the source version control system. 10 | 11 | When that is not the case, the files contain a prominent 12 | notice stating the original copyright and applicable 13 | license, or come with their own dedicated COPYRIGHT 14 | and/or LICENSE file. 15 | -------------------------------------------------------------------------------- /doc/learning/how_to_test.md: -------------------------------------------------------------------------------- 1 | # 🦉 How to test Components 🦉 2 | 3 | ## Content 4 | 5 | - [Overview](#overview) 6 | - [Unit Tests](#unit-tests) 7 | 8 | ## Overview 9 | 10 | It is a good practice to test applications and components to ensure that they 11 | behave as expected. There are many ways to test a user interface: manual 12 | testing, integration testing, unit testing, ... 13 | 14 | In this section, we will discuss how to write unit tests for components. 15 | 16 | ## Unit Tests 17 | 18 | Writing unit tests for Owl components really depends on the testing framework 19 | used in a project. But usually, it involves the following steps: 20 | 21 | - create a test file: for example `SomeComponent.test.js`, 22 | - in that file, import the code for `SomeComponent`, 23 | - add a test case: 24 | - create a real DOM element to use as test fixture, 25 | - create a test environment 26 | - create an instance of `SomeComponent`, mount it to the fixture 27 | - interact with the component and assert some properties. 28 | 29 | To help with this, it is useful to have a `helper.js` file that contains some 30 | common utility functions: 31 | 32 | ```js 33 | let lastFixture = null; 34 | 35 | export function makeTestFixture() { 36 | let fixture = document.createElement("div"); 37 | document.body.appendChild(fixture); 38 | if (lastFixture) { 39 | lastFixture.remove(); 40 | } 41 | lastFixture = fixture; 42 | return fixture; 43 | } 44 | 45 | export async function nextTick() { 46 | await new Promise((resolve) => setTimeout(resolve)); 47 | await new Promise((resolve) => requestAnimationFrame(resolve)); 48 | } 49 | ``` 50 | 51 | With such a file, a typical test suite for Jest will look like this: 52 | 53 | ```js 54 | // in SomeComponent.test.js 55 | import { SomeComponent } from "../../src/ui/SomeComponent"; 56 | import { nextTick, makeTestFixture } from '../helpers'; 57 | 58 | 59 | //------------------------------------------------------------------------------ 60 | // Setup 61 | //------------------------------------------------------------------------------ 62 | let fixture: HTMLElement; 63 | let env: Env; 64 | 65 | beforeEach(() => { 66 | fixture = makeTestFixture(); 67 | }); 68 | 69 | afterEach(() => { 70 | fixture.remove(); 71 | }); 72 | 73 | //------------------------------------------------------------------------------ 74 | // Tests 75 | //------------------------------------------------------------------------------ 76 | describe("SomeComponent", () => { 77 | test("component behaves as expected", async () => { 78 | const props = {...}; // depends on the component 79 | const comp = await mount(SomeComponent, fixture, { props }); 80 | 81 | // do some assertions 82 | expect(...).toBe(...); 83 | 84 | fixture.querySelector('button').click(); 85 | await nextTick(); 86 | 87 | // some other assertions 88 | expect(...).toBe(...); 89 | }); 90 | }); 91 | ``` 92 | 93 | Note that Owl does wait for the next animation frame to actually update the DOM. 94 | This is why it is necessary to wait with the `nextTick` (or other methods) to 95 | make sure that the DOM is up-to-date. 96 | -------------------------------------------------------------------------------- /doc/miscellaneous/architecture.md: -------------------------------------------------------------------------------- 1 | # 🦉 Notes On Owl Architecture 🦉 2 | 3 | We explain here how Owl is designed 4 | 5 | Warning: these notes are technical by nature, and intended for people working 6 | on Owl (or interested in understanding its design). 7 | 8 | ## Overview 9 | 10 | Roughly speaking, Owl has 5 main parts: 11 | 12 | - a virtual dom system (in `src/blockdom`) 13 | - a component system (in `src/component`) 14 | - a template compiler (located in the `src/compiler` folder) 15 | - a small runtime code to tie them together (in `src/app`) 16 | - a reactivity system (in `src/reactivity.ts`) 17 | 18 | There are some other files, but the core of Owl can be understood with these 19 | five main parts. 20 | 21 | The virtual dom is an optimized virtual dom based on blocks, which supports 22 | multi blocks (for fragments). Everything that owl renders is internally 23 | represented by a virtual node. The job of the virtual dom is to efficiently 24 | represent the current state of the application, and to build an actual DOM 25 | representation when needed, or update the DOM whenever it is needed. 26 | 27 | - some other helpers/smaller scale stuff 28 | A rendering occurs in two phases: 29 | 30 | - virtual rendering: this generates the virtual dom in memory, asynchronously 31 | - patch: applies a virtual tree to the screen (synchronously) 32 | 33 | There are several classes involved in a rendering: 34 | 35 | - components 36 | - a scheduler 37 | - fibers: small objects containing some metadata, associated with a rendering of 38 | a specific component 39 | 40 | Components are organized in a dynamic component tree, visible in the user 41 | interface. Whenever a rendering is initiated in a component `C`: 42 | 43 | - a fiber is created on `C` with the rendering props information 44 | - the virtual rendering phase starts on C (will asynchronously render all the 45 | child components) 46 | - the fiber is added to the scheduler, which will poll continuously, every 47 | animation frame, if the fiber is done 48 | - once it is done, the scheduler will call the task callback, which will apply 49 | the patch (if it was not cancelled in the meantime). 50 | 51 | # 🦉 VDom 🦉 52 | 53 | Owl is a declarative component system: we declare the structure of the component 54 | tree, and Owl will translate that to a list of imperative operations. This 55 | translation is done by a virtual dom. This is the low level layer of Owl, most 56 | developer will not need to call directly the virtual dom functions. 57 | 58 | The main idea behind a virtual dom is to keep a in-memory representation of the 59 | DOM (called a virtual node), and whenever some change is needed, to regenerate 60 | a new representation, compute the difference between the old and the new, then 61 | apply the changes. 62 | 63 | `vdom` exports two functions: 64 | 65 | - `h`: create a new virtual node 66 | - `patch`: compare two virtual nodes, and apply the difference. 67 | 68 | Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom). 69 | -------------------------------------------------------------------------------- /doc/reference/environment.md: -------------------------------------------------------------------------------- 1 | # 🦉 Environment 🦉 2 | 3 | ## Content 4 | 5 | - [Overview](#overview) 6 | - [Setting an Environment](#setting-an-environment) 7 | - [Using a sub environment](#using-a-sub-environment) 8 | - [Content of an Environment](#content-of-an-environment) 9 | 10 | ## Overview 11 | 12 | An environment is a shared object given to all components in a tree. It is not 13 | used by Owl itself, but it is useful for application developers to provide a 14 | simple communication channel between components (in addition to the props). 15 | 16 | The `env` given to the [`App`](app.md) is assigned to the `env` component 17 | property. 18 | 19 | ``` 20 | Root 21 | / \ 22 | A B 23 | ``` 24 | 25 | Also, the `env` object is frozen when the application is started. This is done 26 | to ensure a simpler mental model of what's happening in runtime. Note that it 27 | is only shallowly frozen, so sub objects can be modified. 28 | 29 | ## Setting an environment 30 | 31 | The correct way to customize an environment is to simply give it to the `App`, 32 | whenever it is created. 33 | 34 | ```js 35 | const env = { 36 | _t: myTranslateFunction, 37 | user: {...}, 38 | services: { 39 | ... 40 | }, 41 | }; 42 | 43 | new App(Root, { env }).mount(document.body); 44 | 45 | // or alternatively 46 | mount(App, document.body, { env }); 47 | ``` 48 | 49 | ## Using a sub environment 50 | 51 | It is sometimes useful to add one (or more) specific keys to the environment, 52 | from the perspective of a specific component and its children. In that case, the 53 | solution presented above will not work, since it sets the global environment. 54 | 55 | There are two hooks for this situation: [`useSubEnv` and `useChildSubEnv`](hooks.md#usesubenv-and-usechildsubenv). 56 | 57 | ```js 58 | class SomeComponent extends Component { 59 | setup() { 60 | useSubEnv({ myKey: someValue }); // myKey is now available for all child components 61 | } 62 | } 63 | ``` 64 | 65 | ## Content of an Environment 66 | 67 | The `env` object content is totally up to the application developer. However, 68 | some good use cases for additional keys in the environment are: 69 | 70 | - some configuration keys, 71 | - session information, 72 | - generic services (such as doing rpcs). 73 | - other utility functions that one want to inject, such as a translation function. 74 | 75 | Doing it this way means that components are easily testable: we can simply 76 | create a test environment with mock services. 77 | -------------------------------------------------------------------------------- /doc/reference/error_handling.md: -------------------------------------------------------------------------------- 1 | # 🦉 Error Handling 🦉 2 | 3 | ## Content 4 | 5 | - [Overview](#overview) 6 | - [Managing Errors](#managing-errors) 7 | - [Example](#example) 8 | 9 | ## Overview 10 | 11 | By default, whenever an error occurs in the rendering of an Owl application, we 12 | destroy the whole application. Otherwise, we cannot offer any guarantee on the 13 | state of the resulting component tree. It might be hopelessly corrupted, but 14 | without any user-visible feedback. 15 | 16 | Clearly, it is usually a little bit extreme to destroy the application. This 17 | is why we need a mechanism to handle rendering errors (and errors coming 18 | from lifecycle hooks): the `onError` hook. 19 | 20 | The main idea is that the `onError` hook register a function that will be called 21 | with the error. This function need to handle the situation, most of the time by 22 | updating some state and rerendering itself, so the application can return to a 23 | normal state. 24 | 25 | ## Managing Errors 26 | 27 | Whenever the `onError` lifecycle hook is used, all errors coming from 28 | sub components rendering and/or lifecycle method calls will be caught and given 29 | to the `onError` method. This allows us to properly handle the error, and to 30 | not break the application. 31 | 32 | There are important things to know: 33 | 34 | - If an error that occured in the internal rendering cycle is not caught, then 35 | Owl will destroy the full application. This is done on purpose, because Owl 36 | cannot guarantee that the state is not corrupted from this point on. 37 | 38 | - errors coming from event handlers are NOT managed by `onError` or any other 39 | owl mechanism. This is up to the application developer to properly recover 40 | from an error 41 | 42 | - if an error handler is unable to properly handle an error, it can just rethrow 43 | an error, and Owl will try looking for another error handler up the component 44 | tree. 45 | 46 | ## Example 47 | 48 | For example, here is how we could implement a generic component `ErrorBoundary` 49 | that render its content, and a fallback if an error happened. 50 | 51 | ```js 52 | class ErrorBoundary extends Component { 53 | static template = xml` 54 | An error occurred 55 | `; 56 | 57 | setup() { 58 | this.state = useState({ error: false }); 59 | onError(() => (this.state.error = true)); 60 | } 61 | } 62 | ``` 63 | 64 | Using the `ErrorBoundary` is then simple simple: 65 | 66 | ```xml 67 | 68 | 69 | Some specific error message 70 | 71 | ``` 72 | 73 | Note that we need to be careful here: the fallback UI should not throw any 74 | error, otherwise we risk going into an infinite loop (also, see the page on 75 | [slots](slots.md) for more information on the `t-slot` directive). 76 | -------------------------------------------------------------------------------- /doc/reference/portal.md: -------------------------------------------------------------------------------- 1 | # 🦉 Portal 🦉 2 | 3 | It is sometimes useful to be able to render some content outside the boundaries 4 | of a component. To do that, Owl provides a special directive: `t-portal`: 5 | 6 | ```js 7 | class SomeComponent extends Component { 8 | static template = xml` 9 |
this is inside the component
10 |
and this is outside
11 | `; 12 | } 13 | ``` 14 | 15 | The `t-portal` directive takes a valid css selector as argument. The content of 16 | the portalled template will be mounted at the corresponding location. Note that 17 | Owl need to insert an empty text node at the location of the portalled content. 18 | -------------------------------------------------------------------------------- /doc/reference/precompiling_templates.md: -------------------------------------------------------------------------------- 1 | # 🦉 Precompiling templates 🦉 2 | 3 | Owl is designed to be used by the Odoo javascript framework. Since Odoo handles 4 | its assets in its own non standard way, it was decided/assumed that Owl would 5 | compile templates at runtime. 6 | 7 | However, in some cases, it is not optimal, or even worse, not possible to do that. 8 | For example, browser extensions do not allow javascript code to create a new 9 | function (using the `new Function(...)` syntax). 10 | 11 | Therefore, in these cases, it is required to compile templates ahead of time. It 12 | is possible to do that in Owl, but the tooling is still rough. For now, the 13 | process is the following: 14 | 15 | 1. write your templates in xml files (with a `t-name` directive to declare the name 16 | of the template) 17 | 2. Compile them in a `templates.js` file 18 | 3. get the `owl.iife.runtime.js` file (which is a owl build without the compiler) 19 | 4. bundle `owl.iife.runtime.js` and `template.js` with your assets (owl needs to 20 | be positioned before the templates) 21 | 22 | Here is a more detailed explanation on how to compile xml files into a js file: 23 | 24 | 1. clone the owl repository locally 25 | 2. `npm install` to install all the required tooling 26 | 3. `npm run build:runtime` to build the `owl.iife.runtime.js` file 27 | 4. `npm run build:compiler` to build the template compiler 28 | 5. `npm run compile_templates -- path/to/your/templates` will scan your target 29 | folder, find all xml files, get all templates, compile them, and generate a 30 | `templates.js` file. 31 | -------------------------------------------------------------------------------- /doc/reference/refs.md: -------------------------------------------------------------------------------- 1 | # 🦉 References 🦉 2 | 3 | The `useRef` hook is useful when we need a way to interact with some inside part 4 | of a component, rendered by Owl. It can work either on a DOM node, or on a component, 5 | targeted by the `t-ref` directive. See the [hooks section](hooks.md#useref) for 6 | more detail. 7 | 8 | As a short example, here is how we could set the focus on a given input: 9 | 10 | ```xml 11 |
12 | 13 | 14 |
15 | ``` 16 | 17 | ```js 18 | import { useRef } from "owl/hooks"; 19 | 20 | class SomeComponent extends Component { 21 | inputRef = useRef("input"); 22 | 23 | focusInput() { 24 | this.inputRef.el.focus(); 25 | } 26 | } 27 | ``` 28 | 29 | Be aware that the `el` property will only be set when the target of the `t-ref` 30 | directive is mounted in the DOM. Otherwise, it will be set to `null`. 31 | 32 | The `useRef` hook cannot be used to get a reference to an instance of a sub 33 | component. 34 | 35 | Note that this example uses the suffix `ref` to name the reference. This 36 | is not mandatory, but it is a useful convention, so we do not forget that it is 37 | a reference object. 38 | -------------------------------------------------------------------------------- /doc/reference/utils.md: -------------------------------------------------------------------------------- 1 | # 🦉 Utils 🦉 2 | 3 | Owl export a few useful utility functions, to help with common issues. Those 4 | functions are all available in the `owl.utils` namespace. 5 | 6 | ## Content 7 | 8 | - [`whenReady`](#whenready): executing code when DOM is ready 9 | - [`loadFile`](#loadfile): loading a file (useful for templates) 10 | - [`EventBus`](#eventbus): a simple EventBus 11 | - [`validate`](#validate): a validation function 12 | - [`batched`](#batched): batch function calls 13 | 14 | ## `whenReady` 15 | 16 | The function `whenReady` returns a `Promise` resolved when the DOM is ready (if 17 | not ready yet, resolved directly otherwise). If called with a callback as 18 | argument, it executes it as soon as the DOM ready (or directly). 19 | 20 | ```js 21 | const { whenReady } = owl; 22 | 23 | await whenReady(); 24 | // do something 25 | ``` 26 | 27 | or alternatively: 28 | 29 | ```js 30 | whenReady(function () { 31 | // do something 32 | }); 33 | ``` 34 | 35 | ## `loadFile` 36 | 37 | `loadFile` is a helper function to fetch a file. It simply 38 | performs a `GET` request and returns the resulting string in a promise. The 39 | initial usecase for this function is to load a template file. For example: 40 | 41 | ```js 42 | const { loadFile } = owl; 43 | 44 | async function makeEnv() { 45 | const templates = await loadFile("templates.xml"); 46 | // do something 47 | } 48 | ``` 49 | 50 | ## `EventBus` 51 | 52 | It is a simple `EventBus`, with the same API as usual DOM elements, and an 53 | additional `trigger` method to dispatch events: 54 | 55 | ```js 56 | const bus = new EventBus(); 57 | bus.addEventListener("event", () => console.log("something happened")); 58 | 59 | bus.trigger("event"); // 'something happened' is logged 60 | ``` 61 | 62 | ## `validate` 63 | 64 | The `validate` function is a function that validates if a given object satisfies a 65 | specified schema. It is actually used by Owl itself to perform 66 | [props validation](props.md#props-validation). For example: 67 | 68 | ```js 69 | validate( 70 | { a: "hey" }, 71 | { 72 | id: Number, 73 | url: [Boolean, { type: Array, element: Number }], 74 | } 75 | ); 76 | 77 | // throws an error with the following information: 78 | // - unknown key 'a', 79 | // - 'id' is missing (should be a number), 80 | // - 'url' is missing (should be a boolean or list of numbers), 81 | ``` 82 | 83 | ## `batched` 84 | 85 | The `batched` function creates a batched version of a callback so that multiple calls to it within the same microtick will only result in a single invocation of the original callback. 86 | 87 | ```js 88 | function hello() { 89 | console.log("hello"); 90 | } 91 | 92 | const batchedHello = batched(hello); 93 | batchedHello(); 94 | // Nothing is logged 95 | batchedHello(); 96 | // Still not logged 97 | 98 | await Promise.resolve(); // Await the next microtick 99 | // "hello" is logged only once 100 | ``` 101 | -------------------------------------------------------------------------------- /doc/tools/devtools.md: -------------------------------------------------------------------------------- 1 | # Owl Devtools Browser extension 2 | 3 | The owl devtools browser extension is an extension available on chrome or firefox which adds an owl tab 4 | to the browser devtools in order to inspect all owl apps that are present on any web page, their components 5 | and allows to interract with their data to a certain extend. There is also a profiler available to visualize 6 | the components' lifecycle and be able to trace their origin. 7 | 8 | See the [`devtools doc`](devtools_guide.md) for more information. 9 | 10 | ## Install the extension manually (for devs) 11 | 12 | In the owl root folder: 13 | 14 | ```bash 15 | npm install 16 | ``` 17 | 18 | For chrome: 19 | 20 | ```bash 21 | npm run build:devtools-chrome 22 | ``` 23 | 24 | For firefox: 25 | 26 | ```bash 27 | npm run build:devtools-firefox 28 | ``` 29 | 30 | You can also run: 31 | 32 | ```bash 33 | npm run dev:devtools-chrome 34 | ``` 35 | 36 | or 37 | 38 | ```bash 39 | npm run dev:devtools-firefox 40 | ``` 41 | 42 | to avoid recompiling owl and gain time if it has already been done. 43 | 44 | To run the extension: 45 | 46 | In google chrome: go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`. 47 | Select the output folder (dist/devtools) and that's it, your extension is active! 48 | There is a convenient refresh button on the extension card (still on the same admin page) to update your code. 49 | Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension. 50 | 51 | In firefox: go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`. 52 | Select any file of the output folder (dist/devtools) and that's it, your extension is active! 53 | Here, you can use the reload button to refresh the extension. 54 | 55 | Note that you may have to open another window or reload your tab to see the extension working. 56 | Also note that the extension will only be active on pages that have a sufficient version of owl. 57 | -------------------------------------------------------------------------------- /doc/tools/screenshots/crm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/crm.png -------------------------------------------------------------------------------- /doc/tools/screenshots/darkmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/darkmode.png -------------------------------------------------------------------------------- /doc/tools/screenshots/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/edit.png -------------------------------------------------------------------------------- /doc/tools/screenshots/events_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/events_log.png -------------------------------------------------------------------------------- /doc/tools/screenshots/extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/extensions.png -------------------------------------------------------------------------------- /doc/tools/screenshots/find_owl_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/find_owl_tab.png -------------------------------------------------------------------------------- /doc/tools/screenshots/function_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/function_menu.png -------------------------------------------------------------------------------- /doc/tools/screenshots/hooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/hooks.png -------------------------------------------------------------------------------- /doc/tools/screenshots/iframes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/iframes.png -------------------------------------------------------------------------------- /doc/tools/screenshots/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/menu.png -------------------------------------------------------------------------------- /doc/tools/screenshots/multi_apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/multi_apps.png -------------------------------------------------------------------------------- /doc/tools/screenshots/observe_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/observe_variables.png -------------------------------------------------------------------------------- /doc/tools/screenshots/picker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/picker.png -------------------------------------------------------------------------------- /doc/tools/screenshots/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/popup.png -------------------------------------------------------------------------------- /doc/tools/screenshots/profiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/profiler.png -------------------------------------------------------------------------------- /doc/tools/screenshots/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/record.png -------------------------------------------------------------------------------- /doc/tools/screenshots/states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/states.png -------------------------------------------------------------------------------- /doc/tools/screenshots/trace_rendering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/trace_rendering.png -------------------------------------------------------------------------------- /doc/tools/screenshots/trace_subscriptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/trace_subscriptions.png -------------------------------------------------------------------------------- /doc/tools/screenshots/tree_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/doc/tools/screenshots/tree_actions.png -------------------------------------------------------------------------------- /docs/assets/highlight.tomorrow.css: -------------------------------------------------------------------------------- 1 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 2 | 3 | /* Tomorrow Comment */ 4 | .hljs-comment, 5 | .hljs-quote { 6 | color: #8e908c; 7 | } 8 | 9 | /* Tomorrow Red */ 10 | .hljs-variable, 11 | .hljs-template-variable, 12 | .hljs-tag, 13 | .hljs-name, 14 | .hljs-selector-id, 15 | .hljs-selector-class, 16 | .hljs-regexp, 17 | .hljs-deletion { 18 | color: #c82829; 19 | } 20 | 21 | /* Tomorrow Orange */ 22 | .hljs-number, 23 | .hljs-built_in, 24 | .hljs-builtin-name, 25 | .hljs-literal, 26 | .hljs-type, 27 | .hljs-params, 28 | .hljs-meta, 29 | .hljs-link { 30 | color: #f5871f; 31 | } 32 | 33 | /* Tomorrow Yellow */ 34 | .hljs-attribute { 35 | color: #eab700; 36 | } 37 | 38 | /* Tomorrow Green */ 39 | .hljs-string, 40 | .hljs-symbol, 41 | .hljs-bullet, 42 | .hljs-addition { 43 | color: #718c00; 44 | } 45 | 46 | /* Tomorrow Blue */ 47 | .hljs-title, 48 | .hljs-section { 49 | color: #4271ae; 50 | } 51 | 52 | /* Tomorrow Purple */ 53 | .hljs-keyword, 54 | .hljs-selector-tag { 55 | color: #8959a8; 56 | } 57 | 58 | .hljs { 59 | display: block; 60 | overflow-x: auto; 61 | background: white; 62 | color: #4d4d4c; 63 | padding: 0.5em; 64 | } 65 | 66 | .hljs-emphasis { 67 | font-style: italic; 68 | } 69 | 70 | .hljs-strong { 71 | font-weight: bold; 72 | } 73 | -------------------------------------------------------------------------------- /docs/assets/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color: #fefefe; 3 | font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 4 | height: 100%; 5 | margin: 0; 6 | text-rendering: optimizeLegibility; 7 | } 8 | 9 | body { 10 | display: grid; 11 | grid-gap: 3rem; 12 | grid-template-areas: "header" "nav" "main" "footer"; 13 | grid-template-columns: 1fr; 14 | height: 100%; 15 | grid-template-rows: 1fr 3rem 1fr 3rem; 16 | } 17 | 18 | @media screen and (max-width: 800px) { 19 | body { 20 | grid-template-rows: 1fr 3rem 1fr 5rem; 21 | } 22 | } 23 | 24 | a { 25 | color: #00A09D; 26 | text-decoration: none; 27 | } 28 | 29 | a:hover, a:active, a:visited { 30 | text-decoration: underline; 31 | } 32 | 33 | header { 34 | grid-area: header; 35 | display: flex; 36 | align-items: center; 37 | flex-direction: column; 38 | justify-content: flex-end; 39 | text-align: center; 40 | padding-top: 3rem !important; 41 | } 42 | 43 | hgroup > p { 44 | margin-bottom: auto; 45 | } 46 | 47 | nav { 48 | grid-area: nav; 49 | } 50 | 51 | main { 52 | grid-area: main; 53 | /*justify-self: center;*/ 54 | grid-column: 1 / -1; 55 | /*max-width: 80rem !important;*/ 56 | /*margin: 3rem 0;*/ 57 | } 58 | 59 | footer { 60 | grid-area: footer; 61 | padding-bottom: 3rem !important; 62 | } 63 | 64 | header, nav, footer { 65 | text-align: center; 66 | } 67 | 68 | article { 69 | padding: 3rem 0; 70 | } 71 | 72 | article h3 { 73 | font-size: 2rem; 74 | font-weight: 700; 75 | } 76 | 77 | article.brand { 78 | align-items: center; 79 | display: flex; 80 | flex-direction: column; 81 | justify-content: center; 82 | text-align: center; 83 | } 84 | 85 | article.design { 86 | background-color: #875A7B; 87 | color: #f0eeee; 88 | } 89 | 90 | header .homepage-logo { 91 | margin-bottom: 2rem; 92 | } 93 | 94 | @media (min-width: 40rem) { 95 | .row { 96 | flex-direction: column; 97 | margin-left: initial; 98 | width: 100%; 99 | } 100 | 101 | .row .column { 102 | margin-bottom: initial; 103 | padding: initial; 104 | } 105 | } 106 | @media (min-width: 60rem) { 107 | .row { 108 | flex-direction: row; 109 | margin-left: -1.0rem; 110 | width: calc(100% + 2.0rem); 111 | } 112 | .row .column { 113 | margin-bottom: inherit; 114 | padding: 0 1.0rem; 115 | } 116 | } -------------------------------------------------------------------------------- /docs/assets/owl_1f989.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/docs/assets/owl_1f989.png -------------------------------------------------------------------------------- /docs/counter.js: -------------------------------------------------------------------------------- 1 | import { mount, Component, useState, loadFile } from "@odoo/owl"; 2 | 3 | class Counter extends Component { 4 | static template = "Counter"; 5 | setup() { 6 | this.state = useState({ value: 0 }); 7 | } 8 | 9 | increment() { 10 | this.state.value++; 11 | } 12 | } 13 | const templates = await loadFile("./counter.xml"); 14 | 15 | mount(Counter, document.getElementById("app-container"), { templates }); 16 | -------------------------------------------------------------------------------- /docs/counter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/display_code.js: -------------------------------------------------------------------------------- 1 | import { loadFile } from "@odoo/owl"; 2 | 3 | for (const [className, type] of [["xml", "xml"], ["javascript", "js"]]) { 4 | loadFile(`./counter.${type}`).then(code => { 5 | const el = document.querySelector(`code.${className}`); 6 | el.textContent = code; 7 | hljs.highlightBlock(el); 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /docs/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OWL Playground 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/playground/libs/FileSaver.min.js: -------------------------------------------------------------------------------- 1 | (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Depricated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(b,c,d){var e=new XMLHttpRequest;e.open("GET",b),e.responseType="blob",e.onload=function(){a(e.response,c,d)},e.onerror=function(){console.error("could not download file")},e.send()}function d(a){var b=new XMLHttpRequest;return b.open("HEAD",a,!1),b.send(),200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(a,b,d,e){if(e=e||open("","_blank"),e&&(e.document.title=e.document.body.innerText="downloading..."),"string"==typeof a)return c(a,b,d);var g="application/octet-stream"===a.type,h=/constructor/i.test(f.HTMLElement)||f.safari,i=/CriOS\/[\d]+/.test(navigator.userAgent);if((i||g&&h)&&"object"==typeof FileReader){var j=new FileReader;j.onloadend=function(){var a=j.result;a=i?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),e?e.location.href=a:location=a,e=null},j.readAsDataURL(a)}else{var k=f.URL||f.webkitURL,l=k.createObjectURL(a);e?e.location=l:location.href=l,e=null,setTimeout(function(){k.revokeObjectURL(l)},4E4)}});f.saveAs=a.saveAs=a,"undefined"!=typeof module&&(module.exports=a)}); 2 | 3 | //# sourceMappingURL=FileSaver.min.js.map -------------------------------------------------------------------------------- /docs/playground/samples/benchmark/benchmark.css: -------------------------------------------------------------------------------- 1 | tr.danger { 2 | font-weight: bold; 3 | } 4 | 5 | .remove:hover { 6 | font-weight: bold; 7 | } 8 | .remove { 9 | cursor: pointer; 10 | } 11 | -------------------------------------------------------------------------------- /docs/playground/samples/benchmark/benchmark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | [x] 17 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |

Owl Keyed

29 |
30 |
31 |
32 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /docs/playground/samples/components/components.css: -------------------------------------------------------------------------------- 1 | .greeter { 2 | font-size: 20px; 3 | width: 300px; 4 | height: 100px; 5 | margin: 5px; 6 | text-align: center; 7 | line-height: 100px; 8 | background-color: #eeeeee; 9 | user-select: none; 10 | } 11 | -------------------------------------------------------------------------------- /docs/playground/samples/components/components.js: -------------------------------------------------------------------------------- 1 | // In this example, we show how components can be defined and created. 2 | import { Component, useState, mount } from "@odoo/owl"; 3 | 4 | class Greeter extends Component { 5 | static template = "Greeter"; 6 | 7 | setup() { 8 | this.state = useState({ word: 'Hello' }); 9 | } 10 | 11 | toggle() { 12 | this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'; 13 | } 14 | } 15 | 16 | // Main root component 17 | class Root extends Component { 18 | static components = { Greeter }; 19 | static template = "Root" 20 | 21 | setup() { 22 | this.state = useState({ name: 'World'}); 23 | } 24 | } 25 | 26 | mount(Root, document.body, { templates: TEMPLATES, dev: true }); 27 | -------------------------------------------------------------------------------- /docs/playground/samples/components/components.xml: -------------------------------------------------------------------------------- 1 | 2 |
3 | , 4 |
5 | 6 | 7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /docs/playground/samples/custom_hooks/custom_hooks.css: -------------------------------------------------------------------------------- 1 | 2 | button { 3 | width: 120px; 4 | height: 35px; 5 | font-size: 16px; 6 | } 7 | -------------------------------------------------------------------------------- /docs/playground/samples/custom_hooks/custom_hooks.js: -------------------------------------------------------------------------------- 1 | // In this example, we show how hooks can be used or defined. 2 | import { Component, mount, useState, onWillDestroy } from "@odoo/owl"; 3 | 4 | // We define here a custom behaviour: this hook tracks the state of the mouse 5 | // position 6 | function useMouse() { 7 | const position = useState({x:0, y: 0}); 8 | 9 | function update(e) { 10 | position.x = e.clientX; 11 | position.y = e.clientY; 12 | } 13 | window.addEventListener('mousemove', update); 14 | onWillDestroy(() => { 15 | window.removeEventListener('mousemove', update); 16 | }); 17 | 18 | return position; 19 | } 20 | 21 | 22 | // Main root component 23 | class Root extends Component { 24 | static template = "Root"; 25 | 26 | setup() { 27 | // simple state hook (reactive object) 28 | this.counter = useState({ value: 0 }); 29 | 30 | // this hooks is bound to the 'mouse' property. 31 | this.mouse = useMouse(); 32 | } 33 | 34 | increment() { 35 | this.counter.value++; 36 | } 37 | } 38 | 39 | // Application setup 40 | mount(Root, document.body, { templates: TEMPLATES, dev: true }); 41 | -------------------------------------------------------------------------------- /docs/playground/samples/custom_hooks/custom_hooks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
Mouse: ,
6 |
7 |
8 | -------------------------------------------------------------------------------- /docs/playground/samples/form/form.js: -------------------------------------------------------------------------------- 1 | // This example illustrate how the t-model directive can be used to synchronize 2 | // data between html inputs (and select/textareas) and the state of a component. 3 | // Note that there are two controls with t-model="color": they are totally 4 | // synchronized. 5 | import { Component, useState, mount } from "@odoo/owl"; 6 | 7 | class Form extends Component { 8 | static template = "Form"; 9 | 10 | setup() { 11 | this.state = useState({ 12 | text: "", 13 | othertext: "", 14 | number: 11, 15 | color: "", 16 | bool: false 17 | }); 18 | } 19 | } 20 | 21 | // Application setup 22 | mount(Form, document.body, { templates: TEMPLATES, dev: true }); 23 | -------------------------------------------------------------------------------- /docs/playground/samples/form/form.xml: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Form

4 |
5 | Text (immediate): 6 |
7 |
8 | Other text (lazy): 9 |
10 |
11 | Number: 12 |
13 |
14 | Boolean: 15 |
16 |
17 | Color, with a select: 18 | 23 |
24 |
25 | Color, with radio buttons: 26 | 27 | 28 |
29 |
30 |

State

31 |
Text:
32 |
Other Text:
33 |
Number:
34 |
Boolean: TrueFalse
35 |
Color:
36 |
37 |
38 | -------------------------------------------------------------------------------- /docs/playground/samples/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "owl": ["../../../src/index.ts"], 5 | }, 6 | } 7 | } -------------------------------------------------------------------------------- /docs/playground/samples/lifecycle_demo/lifecycle_demo.css: -------------------------------------------------------------------------------- 1 | button { 2 | font-size: 18px; 3 | margin: 5px; 4 | } 5 | 6 | .demo { 7 | margin: 10px; 8 | padding: 10px; 9 | background-color: #dddddd; 10 | width: 250px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/playground/samples/lifecycle_demo/lifecycle_demo.js: -------------------------------------------------------------------------------- 1 | // This example shows all the possible lifecycle hooks 2 | // 3 | // The root component controls a sub component (DemoComponent). It logs all its lifecycle 4 | // methods in the console. Try modifying its state by clicking on it, or by 5 | // clicking on the two main buttons, and look into the console to see what 6 | // happens. 7 | import { 8 | Component, 9 | useState, 10 | mount, 11 | useComponent, 12 | onWillStart, 13 | onMounted, 14 | onWillUnmount, 15 | onWillUpdateProps, 16 | onPatched, 17 | onWillPatch, 18 | onWillRender, 19 | onRendered, 20 | onWillDestroy, 21 | } from "@odoo/owl"; 22 | 23 | function useLogLifecycle() { 24 | const component = useComponent(); 25 | const name = component.constructor.name; 26 | onWillStart(() => console.log(`${name}:willStart`)); 27 | onMounted(() => console.log(`${name}:mounted`)); 28 | onWillUpdateProps(() => console.log(`${name}:willUpdateProps`)); 29 | onWillRender(() => console.log(`${name}:willRender`)); 30 | onRendered(() => console.log(`${name}:rendered`)); 31 | onWillPatch(() => console.log(`${name}:willPatch`)); 32 | onPatched(() => console.log(`${name}:patched`)); 33 | onWillUnmount(() => console.log(`${name}:willUnmount`)); 34 | onWillDestroy(() => console.log(`${name}:willDestroy`)); 35 | } 36 | 37 | class DemoComponent extends Component { 38 | static template = "DemoComponent"; 39 | 40 | setup() { 41 | useLogLifecycle(); 42 | this.state = useState({ n: 0 }); 43 | } 44 | increment() { 45 | this.state.n++; 46 | } 47 | } 48 | 49 | class Root extends Component { 50 | static template = "Root"; 51 | static components = { DemoComponent }; 52 | 53 | setup() { 54 | useLogLifecycle(); 55 | this.state = useState({ n: 0, flag: true }); 56 | } 57 | 58 | increment() { 59 | this.state.n++; 60 | } 61 | 62 | toggleSubComponent() { 63 | this.state.flag = !this.state.flag; 64 | } 65 | } 66 | 67 | mount(Root, document.body, { templates: TEMPLATES, dev: true }); 68 | -------------------------------------------------------------------------------- /docs/playground/samples/lifecycle_demo/lifecycle_demo.xml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
Demo Sub Component
4 |
(click on me to update me)
5 |
Props: , State: .
6 |
7 | 8 |
9 | 10 | 11 |
12 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /docs/playground/samples/responsive_app/responsive_app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .app { 6 | height: 100%; 7 | flex-direction: column; 8 | } 9 | 10 | .app.desktop { 11 | display: flex; 12 | } 13 | 14 | .navbar { 15 | flex: 0 0 30px; 16 | height: 30px; 17 | background-color: cadetblue; 18 | color: white; 19 | line-height: 30px; 20 | } 21 | 22 | .controlpanel { 23 | flex: 0 0 100px; 24 | height: 100px; 25 | background-color: #dddddd; 26 | padding: 8px; 27 | } 28 | 29 | .content-wrapper { 30 | flex: 1 1 auto; 31 | position: relative; 32 | } 33 | 34 | .content { 35 | display: flex; 36 | position: absolute; 37 | top: 0; 38 | right: 0; 39 | bottom: 0; 40 | left: 0; 41 | } 42 | 43 | .formview { 44 | overflow-y: auto; 45 | flex: 1 1 60%; 46 | min-height: 200px; 47 | padding: 8px; 48 | } 49 | 50 | .chatter { 51 | overflow-y: auto; 52 | flex: 1 1 40%; 53 | background-color: #eeeeee; 54 | color: #333333; 55 | padding: 8px; 56 | } 57 | -------------------------------------------------------------------------------- /docs/playground/samples/responsive_app/responsive_app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

Control Panel

6 | 7 |
8 | 9 |
10 |

Form View

11 | 12 |
13 | 14 |
15 |

Chatter

16 |
Message
17 |
18 | 19 |
Mobile searchview
20 | 21 |
22 | This component is only created in desktop mode. 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 |
42 |
43 | -------------------------------------------------------------------------------- /docs/playground/samples/single_file_component/single_file_component.js: -------------------------------------------------------------------------------- 1 | // This example illustrates how one can write Owl components with 2 | // inline templates. 3 | 4 | import { Component, useState, xml, mount } from "@odoo/owl"; 5 | 6 | // Counter component 7 | class Counter extends Component { 8 | static template = xml` 9 | `; 12 | 13 | state = useState({ value: 0 }) 14 | } 15 | 16 | // Root 17 | class Root extends Component { 18 | static template = xml` 19 |
20 | 21 | 22 |
`; 23 | 24 | static components = { Counter }; 25 | } 26 | 27 | // Application setup 28 | mount(Root, document.body, { templates: TEMPLATES, dev: true}); 29 | -------------------------------------------------------------------------------- /docs/playground/samples/slots/slots.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | } 4 | 5 | .card { 6 | display: flex; 7 | flex-direction: column; 8 | background-color: #eeeeee; 9 | width: 200px; 10 | height: 100px; 11 | margin: 10px; 12 | border: 1px solid gray; 13 | } 14 | 15 | .card.full { 16 | height: 100px; 17 | } 18 | 19 | .card.small { 20 | height: 25px; 21 | } 22 | 23 | .card-title { 24 | flex: 0 0 25px; 25 | font-weight: bold; 26 | background-color: darkcyan; 27 | color: white; 28 | padding: 2px; 29 | } 30 | 31 | .card-title button { 32 | float: right; 33 | } 34 | 35 | .card-content { 36 | flex: 1 1 auto; 37 | padding: 5px; 38 | border-top: 1px solid white; 39 | } 40 | 41 | .card-footer { 42 | border-top: 1px solid white; 43 | } 44 | -------------------------------------------------------------------------------- /docs/playground/samples/slots/slots.js: -------------------------------------------------------------------------------- 1 | // We show here how slots can be used to create generic components. 2 | // In this example, the Card component is basically only a container. It is not 3 | // aware of its content. It just knows where it should be (with t-slot). 4 | // The parent component define the content with t-set-slot. 5 | // 6 | // Note that the t-on-click event, defined in the Root template, is executed in 7 | // the context of the Root component, even though it is inside the Card component 8 | import { Component, useState, mount } from "@odoo/owl"; 9 | 10 | class Card extends Component { 11 | static template = "Card"; 12 | 13 | setup() { 14 | this.state = useState({ showContent: true }); 15 | } 16 | 17 | toggleDisplay() { 18 | this.state.showContent = !this.state.showContent; 19 | } 20 | } 21 | 22 | class Counter extends Component { 23 | static template = "Counter"; 24 | 25 | setup() { 26 | this.state = useState({val: 1}); 27 | } 28 | 29 | inc() { 30 | this.state.val++; 31 | } 32 | } 33 | 34 | // Main root component 35 | class Root extends Component { 36 | static template = "Root" 37 | static components = { Card, Counter }; 38 | 39 | setup() { 40 | this.state = useState({a: 1, b: 3}); 41 | } 42 | 43 | inc(key, delta) { 44 | this.state[key] += delta; 45 | } 46 | } 47 | 48 | // Application setup 49 | mount(Root, document.body, { templates: TEMPLATES, dev: true}); 50 | -------------------------------------------------------------------------------- /docs/playground/samples/slots/slots.xml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 | 7 |
8 | 9 |
10 | 13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 | Content of card 1... [] 23 | 24 | 25 | 26 | 27 |
Card 2... []
28 | 29 |
30 | 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /docs/playground/samples/todo_app/todo_app.xml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

todos

5 | 6 |
7 |
8 | 9 | 10 |
    11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 34 | 37 |
38 |
39 | 40 |
  • 41 |
    42 | 43 | 46 | 47 |
    48 | 49 |
  • 50 |
    51 | -------------------------------------------------------------------------------- /docs/playground/samples/window_manager/window_manager.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .app { 6 | width: 100%; 7 | height: 100%; 8 | display: grid; 9 | grid-template-rows: auto 50px; 10 | } 11 | 12 | .window-manager { 13 | position: relative; 14 | width: 100%; 15 | height: 100%; 16 | background-color: #eeeeee; 17 | overflow: hidden; 18 | } 19 | 20 | .menubar { 21 | background-color: #875a7b; 22 | color: white; 23 | } 24 | 25 | .menubar button { 26 | height: 40px; 27 | font-size: 18px; 28 | margin: 5px; 29 | } 30 | 31 | .window { 32 | display: grid; 33 | grid-template-rows: 30px auto; 34 | border: 1px solid gray; 35 | background-color: white; 36 | position: absolute; 37 | box-shadow: 1px 1px 2px 1px grey; 38 | } 39 | 40 | .window.dragging { 41 | opacity: 0.75; 42 | } 43 | 44 | .window .header { 45 | background-color: #875a7b; 46 | display: grid; 47 | grid-template-columns: auto 24px; 48 | color: white; 49 | line-height: 30px; 50 | padding-left: 5px; 51 | cursor: default; 52 | user-select: none; 53 | } 54 | 55 | .window .header .close { 56 | cursor: pointer; 57 | font-size: 22px; 58 | padding-left: 4px; 59 | padding-right: 4px; 60 | font-weight: bold; 61 | } 62 | 63 | .counter { 64 | font-size: 20px; 65 | } 66 | .counter button { 67 | width: 80px; 68 | height:40px; 69 | font-size: 20px; 70 | } 71 | -------------------------------------------------------------------------------- /docs/playground/samples/window_manager/window_manager.xml: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 | 5 | × 6 |
    7 | 8 |
    9 | 10 |
    11 | 12 | 13 | 14 |
    15 | 16 |
    17 | 18 | 22 |
    23 | 24 |
    25 | Some content here... 26 |
    27 | 28 |
    29 | 30 | 31 |
    32 |
    33 | -------------------------------------------------------------------------------- /docs/playground/standalone_app/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import threading 4 | import time 5 | 6 | from http.server import SimpleHTTPRequestHandler, HTTPServer 7 | 8 | httpd = None 9 | def start_server(): 10 | global httpd 11 | SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript' 12 | httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler) 13 | httpd.serve_forever() 14 | 15 | url = 'http://127.0.0.1:3600' 16 | 17 | if __name__ == "__main__": 18 | print("Owl Application") 19 | print("---------------") 20 | print("Server running on: {}".format(url)) 21 | threading.Thread(target=start_server, daemon=True).start() 22 | 23 | while True: 24 | try: 25 | time.sleep(1) 26 | except KeyboardInterrupt: 27 | httpd.server_close() 28 | quit(0) 29 | -------------------------------------------------------------------------------- /docs/playground/standalone_app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OWL App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 | This app requires a web server, simply opening the html file using your browser will not work.
    15 | You can run a simple web server with one of the following:
    16 | - if you have python3 installed, run app.py with python3 or run 'python3 -m http.server' in the app folder
    17 | - if you have node/npm installed, run 'npx serve' in the app folder
    18 | - if you have php installed, run 'php -S localhost:8080 -t .' in the app folder
    19 |     
    20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/playground/templates.xml: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    11 |
    12 | 13 |
    14 |
    16 | 27 | 33 | 34 |
    35 | 42 | 43 |
    44 |
    45 |
    46 |
    47 |
    🦉 Odoo Web Library 🦉
    48 |
    v
    49 | 50 |
    51 |

    52 | Note: these examples make use of recent features of Javascript, and require a recent browser to work without a transpilation step! Among other things, it uses class fields and import maps. If you encounter issues, make sure your browser is up to date. 53 |

    54 |
    55 |
    56 |
    57 |
    58 |
    59 | -------------------------------------------------------------------------------- /docs/playground/utils.js: -------------------------------------------------------------------------------- 1 | export function debounce(func, wait, immediate) { 2 | let timeout; 3 | return function () { 4 | const context = this; 5 | const args = arguments; 6 | function later() { 7 | timeout = null; 8 | if (!immediate) { 9 | func.apply(context, args); 10 | } 11 | } 12 | const callNow = immediate && !timeout; 13 | clearTimeout(timeout); 14 | timeout = setTimeout(later, wait); 15 | if (callNow) { 16 | func.apply(context, args); 17 | } 18 | }; 19 | } 20 | 21 | 22 | const loadedScripts = {}; 23 | 24 | export function loadJS(url) { 25 | if (url in loadedScripts) { 26 | return loadedScripts[url]; 27 | } 28 | const promise = new Promise(function (resolve, reject) { 29 | const script = document.createElement("script"); 30 | script.type = "text/javascript"; 31 | script.src = url; 32 | script.onload = function () { 33 | resolve(); 34 | }; 35 | script.onerror = function () { 36 | reject(`Error loading file '${url}'`); 37 | }; 38 | const head = document.head || document.getElementsByTagName("head")[0]; 39 | head.appendChild(script); 40 | }); 41 | loadedScripts[url] = promise; 42 | return promise; 43 | } 44 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Github page and playground 2 | 3 | This folder contains the code for owl's [github page](https://odoo.github.io/owl) 4 | and the owl [playground](https://odoo.github.io/owl/playground/). If you're 5 | looking for the owl documentation, click [here](/doc) 6 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # 🦉 OWL Roadmap 🦉 2 | 3 | - Current version: 2.X 4 | - Status: stable 5 | 6 | Owl is currently stable. No (large) improvements is expected in the near future. 7 | 8 | Note that we intend to keep maintaining owl, and as such, improvements and/or 9 | breaking changes may require a version bump in the future. 10 | -------------------------------------------------------------------------------- /src/common/owl_error.ts: -------------------------------------------------------------------------------- 1 | // Custom error class that wraps error that happen in the owl lifecycle 2 | export class OwlError extends Error { 3 | cause?: any; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | export type customDirectives = Record< 2 | string, 3 | (node: Element, value: string, modifier: string[]) => void 4 | >; 5 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { OwlError } from "./owl_error"; 2 | 3 | /** 4 | * Parses an XML string into an XML document, throwing errors on parser errors 5 | * instead of returning an XML document containing the parseerror. 6 | * 7 | * @param xml the string to parse 8 | * @returns an XML document corresponding to the content of the string 9 | */ 10 | export function parseXML(xml: string): XMLDocument { 11 | const parser = new DOMParser(); 12 | const doc = parser.parseFromString(xml, "text/xml"); 13 | if (doc.getElementsByTagName("parsererror").length) { 14 | let msg = "Invalid XML in template."; 15 | const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent; 16 | if (parsererrorText) { 17 | msg += "\nThe parser has produced the following error message:\n" + parsererrorText; 18 | const re = /\d+/g; 19 | const firstMatch = re.exec(parsererrorText); 20 | if (firstMatch) { 21 | const lineNumber = Number(firstMatch[0]); 22 | const line = xml.split("\n")[lineNumber - 1]; 23 | const secondMatch = re.exec(parsererrorText); 24 | if (line && secondMatch) { 25 | const columnIndex = Number(secondMatch[0]) - 1; 26 | if (line[columnIndex]) { 27 | msg += 28 | `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` + 29 | `${line}\n${"-".repeat(columnIndex - 1)}^`; 30 | } 31 | } 32 | } 33 | } 34 | throw new OwlError(msg); 35 | } 36 | 37 | return doc; 38 | } 39 | -------------------------------------------------------------------------------- /src/compiler/index.ts: -------------------------------------------------------------------------------- 1 | import type { customDirectives } from "../common/types"; 2 | import type { TemplateSet } from "../runtime/template_set"; 3 | import type { BDom } from "../runtime/blockdom"; 4 | import { CodeGenerator, Config } from "./code_generator"; 5 | import { parse } from "./parser"; 6 | import { OwlError } from "../common/owl_error"; 7 | 8 | export type Template = (context: any, vnode: any, key?: string) => BDom; 9 | 10 | export type TemplateFunction = (app: TemplateSet, bdom: any, helpers: any) => Template; 11 | 12 | interface CompileOptions extends Config { 13 | name?: string; 14 | customDirectives?: customDirectives; 15 | hasGlobalValues: boolean; 16 | } 17 | export function compile( 18 | template: string | Element, 19 | options: CompileOptions = { 20 | hasGlobalValues: false, 21 | } 22 | ): TemplateFunction { 23 | // parsing 24 | const ast = parse(template, options.customDirectives); 25 | 26 | // some work 27 | const hasSafeContext = 28 | template instanceof Node 29 | ? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null 30 | : !template.includes("t-set") && !template.includes("t-call"); 31 | 32 | // code generation 33 | const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext }); 34 | const code = codeGenerator.generateCode(); 35 | // template function 36 | try { 37 | return new Function("app, bdom, helpers", code) as TemplateFunction; 38 | } catch (originalError: any) { 39 | const { name } = options; 40 | const nameStr = name ? `template "${name}"` : "anonymous template"; 41 | const err = new OwlError( 42 | `Failed to compile ${nameStr}: ${originalError.message}\n\ngenerated code:\nfunction(app, bdom, helpers) {\n${code}\n}` 43 | ); 44 | err.cause = originalError; 45 | throw err; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/compiler/standalone/setup_jsdom.ts: -------------------------------------------------------------------------------- 1 | import jsdom from "jsdom"; 2 | 3 | // ----------------------------------------------------------------------------- 4 | // add global DOM stuff for compiler. Needs to be in a separate file so rollup 5 | // doesn't hoist the owl imports above this block of code. 6 | // ----------------------------------------------------------------------------- 7 | var document = new jsdom.JSDOM("", {}); 8 | var window = document.window; 9 | global.document = window.document; 10 | global.window = window as unknown as Window & typeof globalThis; 11 | global.DOMParser = window.DOMParser; 12 | global.Element = window.Element; 13 | global.Node = window.Node; 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { TemplateSet } from "./runtime/template_set"; 2 | import { compile } from "./compiler"; 3 | 4 | export * from "./runtime"; 5 | 6 | TemplateSet.prototype._compileTemplate = function _compileTemplate( 7 | name: string, 8 | template: string | Element 9 | ) { 10 | return compile(template, { 11 | name, 12 | dev: this.dev, 13 | translateFn: this.translateFn, 14 | translatableAttributes: this.translatableAttributes, 15 | customDirectives: this.customDirectives, 16 | hasGlobalValues: this.hasGlobalValues, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/runtime/blockdom/config.ts: -------------------------------------------------------------------------------- 1 | export function filterOutModifiersFromData(dataList: any[]): { modifiers: string[]; data: any[] } { 2 | dataList = dataList.slice(); 3 | const modifiers = []; 4 | let elm; 5 | while ((elm = dataList[0]) && typeof elm === "string") { 6 | modifiers.push(dataList.shift()); 7 | } 8 | return { modifiers, data: dataList }; 9 | } 10 | 11 | export const config = { 12 | // whether or not blockdom should normalize DOM whenever a block is created. 13 | // Normalizing dom mean removing empty text nodes (or containing only spaces) 14 | shouldNormalizeDom: true, 15 | 16 | // this is the main event handler. Every event handler registered with blockdom 17 | // will go through this function, giving it the data registered in the block 18 | // and the event 19 | mainEventHandler: (data: any, ev: Event, currentTarget?: EventTarget | null): boolean => { 20 | if (typeof data === "function") { 21 | data(ev); 22 | } else if (Array.isArray(data)) { 23 | data = filterOutModifiersFromData(data).data; 24 | data[0](data[1], ev); 25 | } 26 | return false; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/runtime/blockdom/html.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from "./index"; 2 | 3 | const nodeProto = Node.prototype; 4 | 5 | const nodeInsertBefore = nodeProto.insertBefore; 6 | const nodeRemoveChild = nodeProto.removeChild; 7 | 8 | class VHtml { 9 | html: string; 10 | parentEl?: HTMLElement | undefined; 11 | content: ChildNode[] = []; 12 | 13 | constructor(html: string) { 14 | this.html = html; 15 | } 16 | 17 | mount(parent: HTMLElement, afterNode: Node | null) { 18 | this.parentEl = parent; 19 | const template = document.createElement("template"); 20 | template.innerHTML = this.html; 21 | this.content = [...(template.content.childNodes as any)]; 22 | for (let elem of this.content) { 23 | nodeInsertBefore.call(parent, elem, afterNode); 24 | } 25 | if (!this.content.length) { 26 | const textNode = document.createTextNode(""); 27 | this.content.push(textNode); 28 | nodeInsertBefore.call(parent, textNode, afterNode); 29 | } 30 | } 31 | 32 | moveBeforeDOMNode(node: Node | null, parent = this.parentEl) { 33 | this.parentEl = parent; 34 | for (let elem of this.content) { 35 | nodeInsertBefore.call(parent, elem, node); 36 | } 37 | } 38 | 39 | moveBeforeVNode(other: VHtml | null, afterNode: Node | null) { 40 | const target = other ? other.content[0] : afterNode; 41 | this.moveBeforeDOMNode(target); 42 | } 43 | 44 | patch(other: VHtml) { 45 | if (this === other) { 46 | return; 47 | } 48 | const html2 = other.html; 49 | if (this.html !== html2) { 50 | const parent = this.parentEl; 51 | // insert new html in front of current 52 | const afterNode = this.content[0]; 53 | const template = document.createElement("template"); 54 | template.innerHTML = html2; 55 | const content = [...(template.content.childNodes as any)]; 56 | for (let elem of content) { 57 | nodeInsertBefore.call(parent, elem, afterNode); 58 | } 59 | if (!content.length) { 60 | const textNode = document.createTextNode(""); 61 | content.push(textNode); 62 | nodeInsertBefore.call(parent, textNode, afterNode); 63 | } 64 | 65 | // remove current content 66 | this.remove(); 67 | this.content = content; 68 | this.html = other.html; 69 | } 70 | } 71 | 72 | beforeRemove() {} 73 | 74 | remove() { 75 | const parent = this.parentEl; 76 | for (let elem of this.content) { 77 | nodeRemoveChild.call(parent, elem); 78 | } 79 | } 80 | 81 | firstNode(): Node { 82 | return this.content[0]!; 83 | } 84 | 85 | toString() { 86 | return this.html; 87 | } 88 | } 89 | 90 | export function html(str: string): VNode { 91 | return new VHtml(str); 92 | } 93 | -------------------------------------------------------------------------------- /src/runtime/blockdom/index.ts: -------------------------------------------------------------------------------- 1 | export { config } from "./config"; 2 | 3 | export { toggler } from "./toggler"; 4 | export { createBlock } from "./block_compiler"; 5 | export { list } from "./list"; 6 | export { multi } from "./multi"; 7 | export { text, comment } from "./text"; 8 | export { html } from "./html"; 9 | export { createCatcher } from "./event_catcher"; 10 | 11 | export interface VNode { 12 | mount(parent: HTMLElement, afterNode: Node | null): void; 13 | moveBeforeDOMNode(node: Node | null, parent?: HTMLElement): void; 14 | moveBeforeVNode(other: T | null, afterNode: Node | null): void; 15 | patch(other: T, withBeforeRemove: boolean): void; 16 | beforeRemove(): void; 17 | remove(): void; 18 | firstNode(): Node | undefined; 19 | 20 | el?: undefined | HTMLElement | Text; 21 | parentEl?: undefined | HTMLElement; 22 | isOnlyChild?: boolean | undefined; 23 | key?: any; 24 | } 25 | 26 | export type BDom = VNode; 27 | 28 | export function mount(vnode: VNode, fixture: HTMLElement, afterNode: Node | null = null) { 29 | vnode.mount(fixture, afterNode); 30 | } 31 | 32 | export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) { 33 | vnode1.patch(vnode2, withBeforeRemove); 34 | } 35 | 36 | export function remove(vnode: VNode, withBeforeRemove: boolean = false) { 37 | if (withBeforeRemove) { 38 | vnode.beforeRemove(); 39 | } 40 | vnode.remove(); 41 | } 42 | 43 | export function withKey(vnode: VNode, key: any) { 44 | vnode.key = key; 45 | return vnode; 46 | } 47 | -------------------------------------------------------------------------------- /src/runtime/blockdom/text.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from "./index"; 2 | 3 | const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!; 4 | const nodeProto = Node.prototype; 5 | const characterDataProto = CharacterData.prototype; 6 | 7 | const nodeInsertBefore = nodeProto.insertBefore; 8 | const characterDataSetData = getDescriptor(characterDataProto, "data").set!; 9 | const nodeRemoveChild = nodeProto.removeChild; 10 | 11 | abstract class VSimpleNode { 12 | text: string | String; 13 | parentEl?: HTMLElement | undefined; 14 | el?: any; 15 | 16 | constructor(text: string | String) { 17 | this.text = text; 18 | } 19 | 20 | mountNode(node: Node, parent: HTMLElement, afterNode: Node | null) { 21 | this.parentEl = parent; 22 | nodeInsertBefore.call(parent, node, afterNode); 23 | this.el = node; 24 | } 25 | 26 | moveBeforeDOMNode(node: Node | null, parent = this.parentEl) { 27 | this.parentEl = parent; 28 | nodeInsertBefore.call(parent, this.el!, node); 29 | } 30 | 31 | moveBeforeVNode(other: VText | null, afterNode: Node | null) { 32 | nodeInsertBefore.call(this.parentEl, this.el!, other ? other.el! : afterNode); 33 | } 34 | 35 | beforeRemove() {} 36 | 37 | remove() { 38 | nodeRemoveChild.call(this.parentEl, this.el!); 39 | } 40 | 41 | firstNode(): Node { 42 | return this.el!; 43 | } 44 | 45 | toString() { 46 | return this.text; 47 | } 48 | } 49 | 50 | class VText extends VSimpleNode { 51 | mount(parent: HTMLElement, afterNode: Node | null) { 52 | this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode); 53 | } 54 | 55 | patch(other: VText) { 56 | const text2 = other.text; 57 | if (this.text !== text2) { 58 | characterDataSetData.call(this.el!, toText(text2)); 59 | this.text = text2; 60 | } 61 | } 62 | } 63 | 64 | class VComment extends VSimpleNode { 65 | mount(parent: HTMLElement, afterNode: Node | null) { 66 | this.mountNode(document.createComment(toText(this.text)), parent, afterNode); 67 | } 68 | 69 | patch() {} 70 | } 71 | 72 | export function text(str: string | String): VNode { 73 | return new VText(str); 74 | } 75 | 76 | export function comment(str: string): VNode { 77 | return new VComment(str); 78 | } 79 | 80 | export function toText(value: any): string { 81 | switch (typeof value) { 82 | case "string": 83 | return value; 84 | case "number": 85 | return String(value); 86 | case "boolean": 87 | return value ? "true" : "false"; 88 | default: 89 | return value || ""; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/runtime/blockdom/toggler.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from "./index"; 2 | 3 | // ----------------------------------------------------------------------------- 4 | // Toggler node 5 | // ----------------------------------------------------------------------------- 6 | 7 | class VToggler { 8 | key: string; 9 | child: VNode; 10 | 11 | parentEl?: HTMLElement | undefined; 12 | 13 | constructor(key: string, child: VNode) { 14 | this.key = key; 15 | this.child = child; 16 | } 17 | 18 | mount(parent: HTMLElement, afterNode: Node | null) { 19 | this.parentEl = parent; 20 | this.child.mount(parent, afterNode); 21 | } 22 | 23 | moveBeforeDOMNode(node: Node | null, parent?: HTMLElement) { 24 | this.child.moveBeforeDOMNode(node, parent); 25 | } 26 | 27 | moveBeforeVNode(other: VToggler | null, afterNode: Node | null) { 28 | this.moveBeforeDOMNode((other && other.firstNode()) || afterNode); 29 | } 30 | 31 | patch(other: VToggler, withBeforeRemove: boolean) { 32 | if (this === other) { 33 | return; 34 | } 35 | let child1 = this.child; 36 | let child2 = other.child; 37 | if (this.key === other.key) { 38 | child1.patch(child2, withBeforeRemove); 39 | } else { 40 | child2.mount(this.parentEl!, child1.firstNode()!); 41 | if (withBeforeRemove) { 42 | child1.beforeRemove(); 43 | } 44 | child1.remove(); 45 | this.child = child2; 46 | this.key = other.key; 47 | } 48 | } 49 | 50 | beforeRemove() { 51 | this.child.beforeRemove(); 52 | } 53 | 54 | remove() { 55 | this.child.remove(); 56 | } 57 | 58 | firstNode(): Node | undefined { 59 | return this.child.firstNode(); 60 | } 61 | 62 | toString(): string { 63 | return this.child.toString(); 64 | } 65 | } 66 | 67 | export function toggler(key: string, child: VNode): VNode { 68 | return new VToggler(key, child); 69 | } 70 | -------------------------------------------------------------------------------- /src/runtime/component.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "./validation"; 2 | import type { ComponentNode } from "./component_node"; 3 | 4 | // ----------------------------------------------------------------------------- 5 | // Component Class 6 | // ----------------------------------------------------------------------------- 7 | 8 | export type Props = { [key: string]: any }; 9 | 10 | interface StaticComponentProperties { 11 | template: string; 12 | defaultProps?: any; 13 | props?: Schema; 14 | components?: { [componentName: string]: ComponentConstructor }; 15 | } 16 | 17 | export type ComponentConstructor

    = (new ( 18 | props: P, 19 | env: E, 20 | node: ComponentNode 21 | ) => Component) & 22 | StaticComponentProperties; 23 | 24 | export class Component { 25 | static template: string = ""; 26 | static props?: Schema; 27 | static defaultProps?: any; 28 | 29 | props: Props; 30 | env: Env; 31 | __owl__: ComponentNode; 32 | 33 | constructor(props: Props, env: Env, node: ComponentNode) { 34 | this.props = props; 35 | this.env = env; 36 | this.__owl__ = node; 37 | } 38 | 39 | setup() {} 40 | 41 | render(deep: boolean = false) { 42 | this.__owl__.render(deep === true); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/runtime/error_handling.ts: -------------------------------------------------------------------------------- 1 | import { OwlError } from "../common/owl_error"; 2 | import type { ComponentNode } from "./component_node"; 3 | import type { Fiber } from "./fibers"; 4 | 5 | // Maps fibers to thrown errors 6 | export const fibersInError: WeakMap = new WeakMap(); 7 | export const nodeErrorHandlers: WeakMap void)[]> = new WeakMap(); 8 | 9 | function _handleError(node: ComponentNode | null, error: any): boolean { 10 | if (!node) { 11 | return false; 12 | } 13 | const fiber = node.fiber; 14 | if (fiber) { 15 | fibersInError.set(fiber, error); 16 | } 17 | 18 | const errorHandlers = nodeErrorHandlers.get(node); 19 | if (errorHandlers) { 20 | let handled = false; 21 | // execute in the opposite order 22 | for (let i = errorHandlers.length - 1; i >= 0; i--) { 23 | try { 24 | errorHandlers[i](error); 25 | handled = true; 26 | break; 27 | } catch (e) { 28 | error = e; 29 | } 30 | } 31 | 32 | if (handled) { 33 | return true; 34 | } 35 | } 36 | return _handleError(node.parent, error); 37 | } 38 | 39 | type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber }); 40 | export function handleError(params: ErrorParams) { 41 | let { error } = params; 42 | // Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode) 43 | if (!(error instanceof OwlError)) { 44 | error = Object.assign( 45 | new OwlError(`An error occured in the owl lifecycle (see this Error's "cause" property)`), 46 | { cause: error } 47 | ); 48 | } 49 | const node = "node" in params ? params.node : params.fiber.node; 50 | const fiber = "fiber" in params ? params.fiber : node.fiber; 51 | 52 | if (fiber) { 53 | // resets the fibers on components if possible. This is important so that 54 | // new renderings can be properly included in the initial one, if any. 55 | let current: Fiber | null = fiber; 56 | do { 57 | current.node.fiber = current; 58 | current = current.parent; 59 | } while (current); 60 | 61 | fibersInError.set(fiber.root!, error); 62 | } 63 | 64 | const handled = _handleError(node, error); 65 | if (!handled) { 66 | console.warn(`[Owl] Unhandled error. Destroying the root component`); 67 | try { 68 | node.app.destroy(); 69 | } catch (e) { 70 | console.error(e); 71 | } 72 | throw error; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/runtime/event_handling.ts: -------------------------------------------------------------------------------- 1 | import { filterOutModifiersFromData } from "./blockdom/config"; 2 | import { STATUS } from "./status"; 3 | import { OwlError } from "../common/owl_error"; 4 | 5 | export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => { 6 | const { data: _data, modifiers } = filterOutModifiersFromData(data); 7 | data = _data; 8 | let stopped = false; 9 | if (modifiers.length) { 10 | let selfMode = false; 11 | const isSelf = ev.target === currentTarget; 12 | for (const mod of modifiers) { 13 | switch (mod) { 14 | case "self": 15 | selfMode = true; 16 | if (isSelf) { 17 | continue; 18 | } else { 19 | return stopped; 20 | } 21 | case "prevent": 22 | if ((selfMode && isSelf) || !selfMode) ev.preventDefault(); 23 | continue; 24 | case "stop": 25 | if ((selfMode && isSelf) || !selfMode) ev.stopPropagation(); 26 | stopped = true; 27 | continue; 28 | } 29 | } 30 | } 31 | // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0 32 | // We check this rather than data[0] being truthy (or typeof function) so that it crashes 33 | // as expected when there is a handler expression that evaluates to a falsy value 34 | if (Object.hasOwnProperty.call(data, 0)) { 35 | const handler = data[0]; 36 | if (typeof handler !== "function") { 37 | throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`); 38 | } 39 | let node = data[1] ? data[1].__owl__ : null; 40 | if (node ? node.status === STATUS.MOUNTED : true) { 41 | handler.call(node ? node.component : null, ev); 42 | } 43 | } 44 | return stopped; 45 | }; 46 | -------------------------------------------------------------------------------- /src/runtime/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "./app"; 2 | import { 3 | config, 4 | createBlock, 5 | html, 6 | list, 7 | mount as blockMount, 8 | multi, 9 | patch, 10 | remove, 11 | text, 12 | toggler, 13 | comment, 14 | } from "./blockdom"; 15 | import { mainEventHandler } from "./event_handling"; 16 | 17 | config.shouldNormalizeDom = false; 18 | config.mainEventHandler = mainEventHandler; 19 | 20 | export const blockDom = { 21 | config, 22 | // bdom entry points 23 | mount: blockMount, 24 | patch, 25 | remove, 26 | // bdom block types 27 | list, 28 | multi, 29 | text, 30 | toggler, 31 | createBlock, 32 | html, 33 | comment, 34 | }; 35 | 36 | export { App, mount } from "./app"; 37 | export { xml } from "./template_set"; 38 | export { Component } from "./component"; 39 | export type { ComponentConstructor } from "./component"; 40 | export { useComponent, useState } from "./component_node"; 41 | export { status } from "./status"; 42 | export { reactive, markRaw, toRaw } from "./reactivity"; 43 | export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks"; 44 | export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils"; 45 | export { 46 | onWillStart, 47 | onMounted, 48 | onWillUnmount, 49 | onWillUpdateProps, 50 | onWillPatch, 51 | onPatched, 52 | onWillRender, 53 | onRendered, 54 | onWillDestroy, 55 | onError, 56 | } from "./lifecycle_hooks"; 57 | export { validate, validateType } from "./validation"; 58 | export { OwlError } from "../common/owl_error"; 59 | 60 | export const __info__ = { 61 | version: App.version, 62 | }; 63 | -------------------------------------------------------------------------------- /src/runtime/portal.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onWillUnmount } from "./lifecycle_hooks"; 2 | import { BDom, text, VNode } from "./blockdom"; 3 | import { Component } from "./component"; 4 | import { OwlError } from "../common/owl_error"; 5 | 6 | const VText: any = text("").constructor; 7 | 8 | class VPortal extends VText implements Partial> { 9 | content: BDom | null; 10 | selector: string; 11 | target: HTMLElement | null = null; 12 | 13 | constructor(selector: string, content: BDom) { 14 | super(""); 15 | this.selector = selector; 16 | this.content = content; 17 | } 18 | 19 | mount(parent: HTMLElement, anchor: ChildNode) { 20 | super.mount(parent, anchor); 21 | this.target = document.querySelector(this.selector) as any; 22 | if (this.target) { 23 | this.content!.mount(this.target!, null); 24 | } else { 25 | this.content!.mount(parent, anchor); 26 | } 27 | } 28 | 29 | beforeRemove() { 30 | this.content!.beforeRemove(); 31 | } 32 | remove() { 33 | if (this.content) { 34 | super.remove(); 35 | this.content!.remove(); 36 | this.content = null; 37 | } 38 | } 39 | 40 | patch(other: VPortal) { 41 | super.patch(other); 42 | if (this.content) { 43 | this.content.patch(other.content!, true); 44 | } else { 45 | this.content = other.content; 46 | this.content!.mount(this.target!, null); 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * kind of similar to , but it wraps it around a VPortal 53 | */ 54 | export function portalTemplate(app: any, bdom: any, helpers: any) { 55 | let { callSlot } = helpers; 56 | return function template(ctx: any, node: any, key = ""): any { 57 | return new VPortal(ctx.props.target, callSlot(ctx, node, key, "default", false, null)); 58 | }; 59 | } 60 | 61 | export class Portal extends Component { 62 | static template = "__portal__"; 63 | static props = { 64 | target: { 65 | type: String, 66 | }, 67 | slots: true, 68 | } as const; 69 | 70 | setup() { 71 | const node: any = this.__owl__; 72 | 73 | onMounted(() => { 74 | const portal: VPortal = node.bdom; 75 | if (!portal.target) { 76 | const target: HTMLElement = document.querySelector(this.props.target); 77 | if (target) { 78 | portal.content!.moveBeforeDOMNode(target.firstChild, target); 79 | } else { 80 | throw new OwlError("invalid portal target"); 81 | } 82 | } 83 | }); 84 | 85 | onWillUnmount(() => { 86 | const portal: VPortal = node.bdom; 87 | portal.remove(); 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/runtime/status.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "./component"; 2 | 3 | // ----------------------------------------------------------------------------- 4 | // Status 5 | // ----------------------------------------------------------------------------- 6 | 7 | export const enum STATUS { 8 | NEW, 9 | MOUNTED, // is ready, and in DOM. It has a valid el 10 | // component has been created, but has been replaced by a newer component before being mounted 11 | // it is cancelled until the next animation frame where it will be destroyed 12 | CANCELLED, 13 | DESTROYED, 14 | } 15 | 16 | type STATUS_DESCR = "new" | "mounted" | "cancelled" | "destroyed"; 17 | 18 | export function status(component: Component): STATUS_DESCR { 19 | switch (component.__owl__.status) { 20 | case STATUS.NEW: 21 | return "new"; 22 | case STATUS.CANCELLED: 23 | return "cancelled"; 24 | case STATUS.MOUNTED: 25 | return "mounted"; 26 | case STATUS.DESTROYED: 27 | return "destroyed"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | // do not modify manually. This file is generated by the release script. 2 | export const version = "2.7.0"; 3 | -------------------------------------------------------------------------------- /tests/blockdom/block_properties.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, patch, createBlock } from "../../src/runtime/blockdom"; 2 | import { makeTestFixture } from "./helpers"; 3 | 4 | //------------------------------------------------------------------------------ 5 | // Setup and helpers 6 | //------------------------------------------------------------------------------ 7 | 8 | let fixture: HTMLElement; 9 | 10 | beforeEach(() => { 11 | fixture = makeTestFixture(); 12 | }); 13 | 14 | afterEach(() => { 15 | fixture.remove(); 16 | }); 17 | 18 | test("input with value property", () => { 19 | // render input with initial value 20 | const block = createBlock(``); 21 | 22 | const tree = block(["zucchini"]); 23 | mount(tree, fixture); 24 | // const bnode1 = renderToBdom(template, { v: "zucchini" }); 25 | // const fixture = makeTestFixture(); 26 | // mount(bnode1, fixture); 27 | const input = fixture.querySelector("input")!; 28 | expect(input.value).toBe("zucchini"); 29 | 30 | // change value manually in input, to simulate user input 31 | input.value = "tomato"; 32 | expect(input.value).toBe("tomato"); 33 | 34 | // rerender with a different value, and patch actual dom, to check that 35 | // input value was properly reset by owl 36 | patch(tree, block(["potato"])); 37 | expect(input.value).toBe("potato"); 38 | }); 39 | 40 | test("input with value property, and falsy value given", () => { 41 | const block = createBlock(``); 42 | 43 | const tree = block([undefined]); 44 | mount(tree, fixture); 45 | const input = fixture.querySelector("input")!; 46 | expect(input.value).toBe(""); 47 | 48 | patch(tree, block([null])); 49 | expect(input.value).toBe(""); 50 | 51 | patch(tree, block([0])); 52 | expect(input.value).toBe("0"); 53 | 54 | patch(tree, block([""])); 55 | expect(input.value).toBe(""); 56 | 57 | patch(tree, block([false])); 58 | expect(input.value).toBe(""); 59 | }); 60 | 61 | test("input type=checkbox with checked property", () => { 62 | // render input with initial value 63 | const block = createBlock(``); 64 | 65 | const tree = block([true]); 66 | mount(tree, fixture); 67 | expect(fixture.innerHTML).toBe(``); 68 | const input = fixture.querySelector("input")!; 69 | expect(input.checked).toBe(true); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/blockdom/block_refs.test.ts: -------------------------------------------------------------------------------- 1 | import { createBlock, mount, patch, remove } from "../../src/runtime/blockdom"; 2 | import { logStep } from "../helpers"; 3 | import { makeTestFixture } from "./helpers"; 4 | 5 | //------------------------------------------------------------------------------ 6 | // Setup and helpers 7 | //------------------------------------------------------------------------------ 8 | 9 | let fixture: HTMLElement; 10 | 11 | beforeEach(() => { 12 | fixture = makeTestFixture(); 13 | }); 14 | 15 | afterEach(() => { 16 | fixture.remove(); 17 | }); 18 | 19 | test("simple callback ref", async () => { 20 | const block = createBlock('

    hey
    '); 21 | let arg: any = undefined; 22 | let n = 0; 23 | const refFn = (_arg: any) => { 24 | n++; 25 | arg = _arg; 26 | }; 27 | 28 | const tree = block([refFn]); 29 | 30 | expect(arg).toBeUndefined(); 31 | expect(n).toBe(0); 32 | mount(tree, fixture); 33 | expect(fixture.innerHTML).toBe("
    hey
    "); 34 | expect(n).toBe(1); 35 | 36 | expect(arg).toBeInstanceOf(HTMLSpanElement); 37 | expect(arg!.innerHTML).toBe("hey"); 38 | 39 | patch(tree, block([refFn])); 40 | expect(n).toBe(1); 41 | 42 | remove(tree); 43 | expect(fixture.innerHTML).toBe(""); 44 | expect(arg).toBeNull(); 45 | expect(n).toBe(2); 46 | }); 47 | 48 | test("is in dom when callback is called", async () => { 49 | expect.assertions(1); 50 | const block = createBlock('
    hey
    '); 51 | const refFn = (span: any) => { 52 | expect(document.body.contains(span)).toBeTruthy(); 53 | }; 54 | 55 | const tree = block([refFn]); 56 | 57 | mount(tree, fixture); 58 | }); 59 | 60 | test("callback ref in callback ref with same block", async () => { 61 | const block = createBlock('

    '); 62 | let refFn = (el: HTMLParagraphElement) => logStep(el.outerHTML); 63 | 64 | const child = block([refFn, "child"], []); 65 | const parent = block([refFn, "parent"], [child]); 66 | mount(parent, fixture); 67 | 68 | expect(fixture.innerHTML).toBe("

    parent

    child

    "); 69 | expect(["

    child

    ", "

    parent

    child

    "]).toBeLogged(); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/blockdom/comment.test.ts: -------------------------------------------------------------------------------- 1 | import { comment, mount } from "../../src/runtime/blockdom"; 2 | import { makeTestFixture } from "./helpers"; 3 | 4 | //------------------------------------------------------------------------------ 5 | // Setup and helpers 6 | //------------------------------------------------------------------------------ 7 | 8 | let fixture: HTMLElement; 9 | 10 | beforeEach(() => { 11 | fixture = makeTestFixture(); 12 | }); 13 | 14 | afterEach(() => { 15 | fixture.remove(); 16 | }); 17 | 18 | test("simple comment node", async () => { 19 | const tree = comment("foo"); 20 | expect(tree.el).toBe(undefined); 21 | mount(tree, fixture); 22 | expect(fixture.innerHTML).toBe(""); 23 | expect(tree.el).not.toBe(undefined); 24 | tree.remove(); 25 | expect(fixture.innerHTML).toBe(""); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/blockdom/event_catcher.test.ts: -------------------------------------------------------------------------------- 1 | import { config, createBlock, createCatcher, mount } from "../../src/runtime/blockdom"; 2 | import { makeTestFixture } from "./helpers"; 3 | import { mainEventHandler } from "../../src/runtime/event_handling"; 4 | 5 | //------------------------------------------------------------------------------ 6 | // Setup and helpers 7 | //------------------------------------------------------------------------------ 8 | 9 | let fixture: HTMLElement; 10 | config.mainEventHandler = mainEventHandler; 11 | 12 | beforeEach(() => { 13 | fixture = makeTestFixture(); 14 | }); 15 | 16 | afterEach(() => { 17 | fixture.remove(); 18 | }); 19 | 20 | test("simple event catcher", async () => { 21 | const catcher = createCatcher({ click: 0 }); 22 | const block = createBlock("
    "); 23 | let n = 0; 24 | let ctx = {}; 25 | let handler = [() => n++, ctx]; 26 | const tree = catcher(block(), [handler]); 27 | 28 | mount(tree, fixture); 29 | expect(fixture.innerHTML).toBe("
    "); 30 | 31 | expect(fixture.firstChild).toBeInstanceOf(HTMLDivElement); 32 | expect(n).toBe(0); 33 | (fixture.firstChild as HTMLDivElement).click(); 34 | expect(n).toBe(1); 35 | }); 36 | 37 | test("do not catch events outside of itself", async () => { 38 | const catcher = createCatcher({ click: 0 }); 39 | const childBlock = createBlock("
    "); 40 | const parentBlock = createBlock(""); 41 | let n = 0; 42 | let ctx = {}; 43 | let handler = [() => n++, ctx]; 44 | const tree = parentBlock([], [catcher(childBlock(), [handler])]); 45 | 46 | mount(tree, fixture); 47 | expect(fixture.innerHTML).toBe(""); 48 | 49 | expect(n).toBe(0); 50 | fixture.querySelector("div")!.click(); 51 | expect(n).toBe(1); 52 | fixture.querySelector("button")!.click(); 53 | expect(n).toBe(1); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/blockdom/helpers.ts: -------------------------------------------------------------------------------- 1 | let lastFixture: any = null; 2 | 3 | export function makeTestFixture() { 4 | let fixture = document.createElement("div"); 5 | document.body.appendChild(fixture); 6 | if (lastFixture) { 7 | lastFixture.remove(); 8 | } 9 | lastFixture = fixture; 10 | return fixture; 11 | } 12 | -------------------------------------------------------------------------------- /tests/blockdom/html.test.ts: -------------------------------------------------------------------------------- 1 | import { html, mount, patch, text } from "../../src/runtime/blockdom"; 2 | import { makeTestFixture } from "./helpers"; 3 | 4 | //------------------------------------------------------------------------------ 5 | // Setup and helpers 6 | //------------------------------------------------------------------------------ 7 | 8 | let fixture: HTMLElement; 9 | 10 | beforeEach(() => { 11 | fixture = makeTestFixture(); 12 | }); 13 | 14 | afterEach(() => { 15 | fixture.remove(); 16 | }); 17 | 18 | //------------------------------------------------------------------------------ 19 | // Tests 20 | //------------------------------------------------------------------------------ 21 | 22 | describe("html block", () => { 23 | test("can be mounted and patched", async () => { 24 | const tree = html("12"); 25 | mount(tree, fixture); 26 | expect(fixture.innerHTML).toBe("12"); 27 | 28 | patch(tree, html("
    coucou
    ")); 29 | expect(fixture.innerHTML).toBe("
    coucou
    "); 30 | }); 31 | 32 | test("html vnode can be used as text", () => { 33 | mount(text(html("

    a

    ") as any), fixture); 34 | expect(fixture.textContent).toBe("

    a

    "); 35 | }); 36 | 37 | test("html vnode can represent ", () => { 38 | const fixture = document.createElement("table"); 39 | // const block = createBlock('
    '); 40 | // const tree = block([], [html(`tomato`)]); 41 | const tree = html(`tomato`); 42 | mount(tree, fixture); 43 | expect(fixture.innerHTML).toBe("tomato"); 44 | 45 | patch(tree, html(`potato`)); 46 | expect(fixture.innerHTML).toBe("potato"); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/blockdom/multi.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, multi, patch, text, createBlock } from "../../src/runtime/blockdom"; 2 | import { makeTestFixture } from "./helpers"; 3 | 4 | //------------------------------------------------------------------------------ 5 | // Setup and helpers 6 | //------------------------------------------------------------------------------ 7 | 8 | let fixture: HTMLElement; 9 | 10 | beforeEach(() => { 11 | fixture = makeTestFixture(); 12 | }); 13 | 14 | afterEach(() => { 15 | fixture.remove(); 16 | }); 17 | 18 | describe("multi blocks", () => { 19 | test("multiblock with 2 text blocks", async () => { 20 | const tree = multi([text("foo"), text("bar")]); 21 | mount(tree, fixture); 22 | expect(fixture.innerHTML).toBe("foobar"); 23 | }); 24 | 25 | test("a multiblock can be removed and leaves no extra text nodes", async () => { 26 | const block1 = createBlock("
    foo
    "); 27 | const block2 = createBlock("bar"); 28 | 29 | const tree = multi([block1(), block2()]); 30 | 31 | expect(fixture.childNodes.length).toBe(0); 32 | mount(tree, fixture); 33 | expect(fixture.childNodes.length).toBe(2); 34 | tree.remove(); 35 | expect(fixture.childNodes.length).toBe(0); 36 | }); 37 | 38 | test("multiblock with an empty children", async () => { 39 | const block = createBlock("
    foo
    "); 40 | const tree = multi([block(), undefined]); 41 | 42 | mount(tree, fixture); 43 | expect(fixture.innerHTML).toBe("
    foo
    "); 44 | }); 45 | 46 | test("multi block in a regular block", async () => { 47 | const block1 = createBlock("
    "); 48 | const block2 = createBlock("yip yip"); 49 | 50 | const tree = block1([], [multi([block2()])]); 51 | 52 | mount(tree, fixture); 53 | expect(fixture.innerHTML).toBe("
    yip yip
    "); 54 | }); 55 | 56 | test("patching a multiblock ", async () => { 57 | const tree = multi([text("foo"), text("bar")]); 58 | mount(tree, fixture); 59 | expect(fixture.innerHTML).toBe("foobar"); 60 | 61 | patch(tree, multi([text("blip"), text("bar")])); 62 | expect(fixture.innerHTML).toBe("blipbar"); 63 | }); 64 | 65 | test("simple multi with multiple roots", async () => { 66 | const block1 = createBlock("
    foo
    "); 67 | const block2 = createBlock("bar"); 68 | 69 | const tree = multi([block1(), block2()]); 70 | 71 | mount(tree, fixture); 72 | expect(fixture.innerHTML).toBe("
    foo
    bar"); 73 | }); 74 | 75 | test("multi vnode can be used as text", () => { 76 | mount(text(multi([text("a"), text("b")]) as any), fixture); 77 | expect(fixture.innerHTML).toBe("ab"); 78 | }); 79 | 80 | test("multi vnode can be used as text", () => { 81 | mount(text(multi([text("a"), undefined]) as any), fixture); 82 | expect(fixture.innerHTML).toBe("a"); 83 | }); 84 | 85 | test("multi inside a block", async () => { 86 | const block = createBlock("
    "); 87 | const tree = block([], [multi([text("foo"), text("bar")])]); 88 | mount(tree, fixture); 89 | expect(fixture.innerHTML).toBe("
    foobar
    "); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/blockdom/namespace.test.ts: -------------------------------------------------------------------------------- 1 | import { createBlock, mount } from "../../src/runtime/blockdom"; 2 | import { makeTestFixture } from "./helpers"; 3 | 4 | //------------------------------------------------------------------------------ 5 | // Setup and helpers 6 | //------------------------------------------------------------------------------ 7 | 8 | const XHTML_URI = "http://www.w3.org/1999/xhtml"; 9 | const SVG_URI = "http://www.w3.org/2000/svg"; 10 | let fixture: HTMLElement; 11 | 12 | beforeEach(() => { 13 | fixture = makeTestFixture(); 14 | }); 15 | 16 | afterEach(() => { 17 | fixture.remove(); 18 | }); 19 | 20 | //------------------------------------------------------------------------------ 21 | // Tests 22 | //------------------------------------------------------------------------------ 23 | 24 | describe("namespace", () => { 25 | test("default namespace is xhtml", () => { 26 | const block = createBlock(``); 27 | const tree = block(); 28 | mount(tree, fixture); 29 | expect(fixture.innerHTML).toBe(""); 30 | expect(fixture.firstElementChild!.namespaceURI).toBe(XHTML_URI); 31 | }); 32 | 33 | test("namespace can be changed with xmlns", () => { 34 | const block = createBlock(``); 35 | const tree = block(); 36 | mount(tree, fixture); 37 | expect(fixture.innerHTML).toBe(``); 38 | expect(fixture.firstElementChild!.namespaceURI).toBe(SVG_URI); 39 | }); 40 | 41 | test("namespace is kept for children", () => { 42 | const block = createBlock( 43 | `` 44 | ); 45 | const tree = block(); 46 | mount(tree, fixture); 47 | expect(fixture.innerHTML).toBe( 48 | `` 49 | ); 50 | const parent = fixture.firstElementChild!; 51 | const child1 = parent.firstElementChild!; 52 | const subchild = child1.firstElementChild!; 53 | const child2 = child1.nextElementSibling!; 54 | expect(parent.namespaceURI).toBe(SVG_URI); 55 | expect(child1.namespaceURI).toBe(SVG_URI); 56 | expect(child2.namespaceURI).toBe(SVG_URI); 57 | expect(subchild.namespaceURI).toBe(SVG_URI); 58 | }); 59 | 60 | test("various namespaces in same block", () => { 61 | const block = createBlock(``); 62 | const tree = block(); 63 | mount(tree, fixture); 64 | expect(fixture.innerHTML).toBe(''); 65 | const none = fixture.firstElementChild!; 66 | const one = none.firstElementChild!; 67 | const two = one.nextElementSibling!; 68 | expect(none.namespaceURI).toBe(XHTML_URI); 69 | expect(one.namespaceURI).toBe("one"); 70 | expect(two.namespaceURI).toBe("two"); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/blockdom/remove_hook.test.ts: -------------------------------------------------------------------------------- 1 | import { createBlock, mount, multi, patch } from "../../src/runtime/blockdom"; 2 | import { makeTestFixture } from "./helpers"; 3 | 4 | //------------------------------------------------------------------------------ 5 | // Setup and helpers 6 | //------------------------------------------------------------------------------ 7 | 8 | let fixture: HTMLElement; 9 | 10 | beforeEach(() => { 11 | fixture = makeTestFixture(); 12 | }); 13 | 14 | afterEach(() => { 15 | fixture.remove(); 16 | }); 17 | 18 | describe("before remove is called", () => { 19 | test("simple removal", async () => { 20 | const block1 = createBlock("
    "); 21 | const block2 = createBlock("

    coucou

    "); 22 | 23 | const child = block2(); 24 | const tree = block1([], [child]); 25 | mount(tree, fixture); 26 | expect(fixture.innerHTML).toBe("

    coucou

    "); 27 | 28 | let n = 1; 29 | child.beforeRemove = () => n++; 30 | patch(tree, block1([], []), true); 31 | expect(fixture.innerHTML).toBe("
    "); 32 | expect(n).toBe(2); 33 | }); 34 | 35 | test("removal, variation with grandchild", async () => { 36 | const block1 = createBlock("

    "); 37 | const block2 = createBlock("coucou"); 38 | 39 | const child = block2(); 40 | const tree = block1([], [block1([], [child])]); 41 | mount(tree, fixture); 42 | expect(fixture.innerHTML).toBe("

    coucou

    "); 43 | 44 | let n = 1; 45 | child.beforeRemove = () => n++; 46 | patch(tree, block1([], []), true); 47 | expect(fixture.innerHTML).toBe("

    "); 48 | expect(n).toBe(2); 49 | }); 50 | 51 | test("remove a child of a multi", async () => { 52 | const block1 = createBlock("
    "); 53 | const block2 = createBlock("

    coucou

    "); 54 | 55 | const child1 = block2(); 56 | const child2 = block2(); 57 | const tree = block1([], [multi([child1, child2])]); 58 | mount(tree, fixture); 59 | expect(fixture.innerHTML).toBe("

    coucou

    coucou

    "); 60 | 61 | let n = 1; 62 | child1.beforeRemove = () => n++; 63 | patch(tree, block1([], [multi([])]), true); 64 | expect(fixture.innerHTML).toBe("
    "); 65 | expect(n).toBe(2); 66 | }); 67 | 68 | test("remove a multi", async () => { 69 | const block1 = createBlock("
    "); 70 | const block2 = createBlock("

    coucou

    "); 71 | 72 | const child1 = block2(); 73 | const child2 = block2(); 74 | const tree = block1([], [multi([child1, child2])]); 75 | mount(tree, fixture); 76 | expect(fixture.innerHTML).toBe("

    coucou

    coucou

    "); 77 | 78 | let n = 1; 79 | child1.beforeRemove = () => n++; 80 | patch(tree, block1([], []), true); 81 | expect(fixture.innerHTML).toBe("
    "); 82 | expect(n).toBe(2); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tests/blockdom/text.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, patch, remove, text } from "../../src/runtime/blockdom"; 2 | import { makeTestFixture } from "./helpers"; 3 | 4 | //------------------------------------------------------------------------------ 5 | // Setup and helpers 6 | //------------------------------------------------------------------------------ 7 | 8 | let fixture: HTMLElement; 9 | 10 | beforeEach(() => { 11 | fixture = makeTestFixture(); 12 | }); 13 | 14 | afterEach(() => { 15 | fixture.remove(); 16 | }); 17 | 18 | describe("adding/patching text", () => { 19 | test("simple text node", async () => { 20 | const tree = text("foo"); 21 | expect(tree.el).toBe(undefined); 22 | mount(tree, fixture); 23 | expect(fixture.innerHTML).toBe("foo"); 24 | expect(tree.el).not.toBe(undefined); 25 | }); 26 | 27 | test("patching a simple text node", async () => { 28 | const tree = text("foo"); 29 | mount(tree, fixture); 30 | expect(fixture.innerHTML).toBe("foo"); 31 | 32 | patch(tree, text("bar")); 33 | expect(fixture.innerHTML).toBe("bar"); 34 | }); 35 | 36 | test("falsy values in text nodes", () => { 37 | const cases = [ 38 | [false, "false"], 39 | [undefined, ""], 40 | [null, ""], 41 | [0, "0"], 42 | ["", ""], 43 | ]; 44 | for (let [value, result] of cases) { 45 | const fixture = makeTestFixture(); 46 | mount(text(value as any), fixture); 47 | expect(fixture.innerHTML).toBe(result); 48 | } 49 | }); 50 | 51 | test("vtext node can be used as text", () => { 52 | const t = text("foo") as any; 53 | mount(text(t), fixture); 54 | expect(fixture.innerHTML).toBe("foo"); 55 | }); 56 | }); 57 | 58 | describe("remove text blocks", () => { 59 | test("a text block can be removed", async () => { 60 | const tree = text("cat"); 61 | expect(fixture.childNodes.length).toBe(0); 62 | mount(tree, fixture); 63 | expect(fixture.innerHTML).toBe("cat"); 64 | expect(fixture.childNodes.length).toBe(1); 65 | remove(tree); 66 | expect(fixture.innerHTML).toBe(""); 67 | expect(fixture.childNodes.length).toBe(0); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/compiler/__snapshots__/comments.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`comments comment node with backslash at top level 1`] = ` 4 | "function anonymous(app, bdom, helpers 5 | ) { 6 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 7 | 8 | return function template(ctx, node, key = \\"\\") { 9 | return comment(\` \\\\\\\\ \`); 10 | } 11 | }" 12 | `; 13 | 14 | exports[`comments comment node with backtick at top-level 1`] = ` 15 | "function anonymous(app, bdom, helpers 16 | ) { 17 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 18 | 19 | return function template(ctx, node, key = \\"\\") { 20 | return comment(\` \\\\\` \`); 21 | } 22 | }" 23 | `; 24 | 25 | exports[`comments comment node with interpolation sigil at top level 1`] = ` 26 | "function anonymous(app, bdom, helpers 27 | ) { 28 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 29 | 30 | return function template(ctx, node, key = \\"\\") { 31 | return comment(\` \\\\\${very cool} \`); 32 | } 33 | }" 34 | `; 35 | 36 | exports[`comments only a comment 1`] = ` 37 | "function anonymous(app, bdom, helpers 38 | ) { 39 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 40 | 41 | return function template(ctx, node, key = \\"\\") { 42 | return comment(\` comment\`); 43 | } 44 | }" 45 | `; 46 | 47 | exports[`comments properly handle comments 1`] = ` 48 | "function anonymous(app, bdom, helpers 49 | ) { 50 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 51 | 52 | let block1 = createBlock(\`
    hello owl
    \`); 53 | 54 | return function template(ctx, node, key = \\"\\") { 55 | return block1(); 56 | } 57 | }" 58 | `; 59 | 60 | exports[`comments properly handle comments between t-if/t-else 1`] = ` 61 | "function anonymous(app, bdom, helpers 62 | ) { 63 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 64 | 65 | let block1 = createBlock(\`
    \`); 66 | let block2 = createBlock(\`true\`); 67 | let block3 = createBlock(\`owl\`); 68 | 69 | return function template(ctx, node, key = \\"\\") { 70 | let b2, b3; 71 | if (true) { 72 | b2 = block2(); 73 | } else { 74 | b3 = block3(); 75 | } 76 | return block1([], [b2, b3]); 77 | } 78 | }" 79 | `; 80 | -------------------------------------------------------------------------------- /tests/compiler/__snapshots__/parser.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`qweb parser simple t-foreach expression, t-key mandatory 1`] = `"\\"Directive t-foreach should always be used with a t-key!\\" (expression: t-foreach=\\"list\\" t-as=\\"item\\")"`; 4 | -------------------------------------------------------------------------------- /tests/compiler/__snapshots__/qweb_memory.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`memory t-foreach does not leak stuff in global scope 1`] = ` 4 | "function anonymous(app, bdom, helpers 5 | ) { 6 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 7 | let { prepareList, withKey } = helpers; 8 | 9 | let block1 = createBlock(\`

    \`); 10 | 11 | return function template(ctx, node, key = \\"\\") { 12 | ctx = Object.create(ctx); 13 | const [k_block2, v_block2, l_block2, c_block2] = prepareList([3,2,1]);; 14 | for (let i1 = 0; i1 < l_block2; i1++) { 15 | ctx[\`item\`] = k_block2[i1]; 16 | ctx[\`item_index\`] = i1; 17 | const key1 = ctx['item_index']; 18 | c_block2[i1] = withKey(text(ctx['item']), key1); 19 | } 20 | const b2 = list(c_block2); 21 | return block1([], [b2]); 22 | } 23 | }" 24 | `; 25 | -------------------------------------------------------------------------------- /tests/compiler/__snapshots__/t_custom.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`t-custom can use t-custom directive on a node 1`] = ` 4 | "function anonymous(app, bdom, helpers 5 | ) { 6 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 7 | 8 | let block1 = createBlock(\`
    \`); 9 | 10 | return function template(ctx, node, key = \\"\\") { 11 | let hdlr1 = [ctx['click'], ctx]; 12 | return block1([hdlr1]); 13 | } 14 | }" 15 | `; 16 | 17 | exports[`t-custom can use t-custom directive with modifiers on a node 1`] = ` 18 | "function anonymous(app, bdom, helpers 19 | ) { 20 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 21 | 22 | let block1 = createBlock(\`
    \`); 23 | 24 | return function template(ctx, node, key = \\"\\") { 25 | let hdlr1 = [ctx['click'], ctx]; 26 | return block1([hdlr1]); 27 | } 28 | }" 29 | `; 30 | -------------------------------------------------------------------------------- /tests/compiler/__snapshots__/t_debug_log.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`debugging t-debug 1`] = ` 4 | "function anonymous(app, bdom, helpers 5 | ) { 6 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 7 | 8 | let block1 = createBlock(\`
    \`); 9 | let block2 = createBlock(\`hey\`); 10 | 11 | return function template(ctx, node, key = \\"\\") { 12 | debugger; 13 | let b2; 14 | if (true) { 15 | debugger; 16 | b2 = block2(); 17 | } 18 | return block1([], [b2]); 19 | } 20 | }" 21 | `; 22 | 23 | exports[`debugging t-debug on sub template 1`] = ` 24 | "function anonymous(app, bdom, helpers 25 | ) { 26 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 27 | 28 | let block1 = createBlock(\`

    coucou

    \`); 29 | 30 | return function template(ctx, node, key = \\"\\") { 31 | debugger; 32 | return block1(); 33 | } 34 | }" 35 | `; 36 | 37 | exports[`debugging t-debug on sub template 2`] = ` 38 | "function anonymous(app, bdom, helpers 39 | ) { 40 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 41 | const callTemplate_1 = app.getTemplate(\`sub\`); 42 | 43 | let block1 = createBlock(\`
    \`); 44 | 45 | return function template(ctx, node, key = \\"\\") { 46 | const b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`); 47 | return block1([], [b2]); 48 | } 49 | }" 50 | `; 51 | 52 | exports[`debugging t-log 1`] = ` 53 | "function anonymous(app, bdom, helpers 54 | ) { 55 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 56 | let { isBoundary, withDefault, setContextValue } = helpers; 57 | 58 | let block1 = createBlock(\`
    \`); 59 | 60 | return function template(ctx, node, key = \\"\\") { 61 | ctx = Object.create(ctx); 62 | ctx[isBoundary] = 1 63 | setContextValue(ctx, \\"foo\\", 42); 64 | console.log(ctx['foo']+3); 65 | return block1(); 66 | } 67 | }" 68 | `; 69 | -------------------------------------------------------------------------------- /tests/compiler/__snapshots__/t_slot.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`t-slot compile t-props correctly multiple time 1`] = ` 4 | "function anonymous(app, bdom, helpers 5 | ) { 6 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 7 | let { callSlot } = helpers; 8 | 9 | return function template(ctx, node, key = \\"\\") { 10 | return callSlot(ctx, node, key, 'default', false, Object.assign({}, {a:1})); 11 | } 12 | }" 13 | `; 14 | -------------------------------------------------------------------------------- /tests/compiler/__snapshots__/white_space.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`white space handling consecutives whitespaces are condensed into a single space 1`] = ` 4 | "function anonymous(app, bdom, helpers 5 | ) { 6 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 7 | 8 | let block1 = createBlock(\`
    abc
    \`); 9 | 10 | return function template(ctx, node, key = \\"\\") { 11 | return block1(); 12 | } 13 | }" 14 | `; 15 | 16 | exports[`white space handling nothing is done in pre tags 1`] = ` 17 | "function anonymous(app, bdom, helpers 18 | ) { 19 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 20 | 21 | let block1 = createBlock(\`
       
    \`); 22 | 23 | return function template(ctx, node, key = \\"\\") { 24 | return block1(); 25 | } 26 | }" 27 | `; 28 | 29 | exports[`white space handling nothing is done in pre tags 2`] = ` 30 | "function anonymous(app, bdom, helpers 31 | ) { 32 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 33 | 34 | let block1 = createBlock(\`
    35 |           some text
    36 |         
    \`); 37 | 38 | return function template(ctx, node, key = \\"\\") { 39 | return block1(); 40 | } 41 | }" 42 | `; 43 | 44 | exports[`white space handling nothing is done in pre tags 3`] = ` 45 | "function anonymous(app, bdom, helpers 46 | ) { 47 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 48 | 49 | let block1 = createBlock(\`
    50 |           
    51 |         
    \`); 52 | 53 | return function template(ctx, node, key = \\"\\") { 54 | return block1(); 55 | } 56 | }" 57 | `; 58 | 59 | exports[`white space handling pre inside a div with a new line 1`] = ` 60 | "function anonymous(app, bdom, helpers 61 | ) { 62 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 63 | 64 | let block1 = createBlock(\`
    SomeText
    \`); 65 | 66 | return function template(ctx, node, key = \\"\\") { 67 | return block1(); 68 | } 69 | }" 70 | `; 71 | 72 | exports[`white space handling white space only text nodes are condensed into a single space 1`] = ` 73 | "function anonymous(app, bdom, helpers 74 | ) { 75 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 76 | 77 | let block1 = createBlock(\`
    \`); 78 | 79 | return function template(ctx, node, key = \\"\\") { 80 | return block1(); 81 | } 82 | }" 83 | `; 84 | 85 | exports[`white space handling whitespace only text nodes with newlines are removed 1`] = ` 86 | "function anonymous(app, bdom, helpers 87 | ) { 88 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 89 | 90 | let block1 = createBlock(\`
    abc
    \`); 91 | 92 | return function template(ctx, node, key = \\"\\") { 93 | return block1(); 94 | } 95 | }" 96 | `; 97 | -------------------------------------------------------------------------------- /tests/compiler/blacklist.test.ts: -------------------------------------------------------------------------------- 1 | import { renderToString } from "../helpers"; 2 | 3 | describe("blacklisted tags and attributes", () => { 4 | test("template with block-text tag", () => { 5 | const template = `
    hello
    `; 6 | expect(() => renderToString(template)).toThrow("Invalid tag name: 'block-text-0'"); 7 | }); 8 | 9 | test("template with block-handler tag", () => { 10 | const template = `
    hello
    `; 11 | expect(() => renderToString(template)).toThrow("Invalid attribute: 'block-handler-0'"); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/compiler/comments.test.ts: -------------------------------------------------------------------------------- 1 | import { renderToString, snapshotEverything } from "../helpers"; 2 | 3 | snapshotEverything(); 4 | 5 | // ----------------------------------------------------------------------------- 6 | // comments 7 | // ----------------------------------------------------------------------------- 8 | 9 | describe("comments", () => { 10 | test("properly handle comments", () => { 11 | const template = `
    hello owl
    `; 12 | expect(renderToString(template)).toBe("
    hello owl
    "); 13 | }); 14 | 15 | test("only a comment", () => { 16 | const template = ``; 17 | expect(renderToString(template)).toBe(``); 18 | }); 19 | 20 | test("properly handle comments between t-if/t-else", () => { 21 | const template = ` 22 |
    23 | true 24 | 25 | owl 26 |
    `; 27 | expect(renderToString(template)).toBe("
    true
    "); 28 | }); 29 | 30 | test("comment node with backslash at top level", () => { 31 | const template = ""; 32 | expect(renderToString(template)).toBe(""); 33 | }); 34 | 35 | test("comment node with backtick at top-level", () => { 36 | const template = ""; 37 | expect(renderToString(template)).toBe(""); 38 | }); 39 | 40 | test("comment node with interpolation sigil at top level", () => { 41 | const template = ""; 42 | expect(renderToString(template)).toBe(""); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/compiler/error_handling.test.ts: -------------------------------------------------------------------------------- 1 | import { renderToString, snapshotEverything, TestContext } from "../helpers"; 2 | 3 | snapshotEverything(); 4 | 5 | describe("error handling", () => { 6 | test("invalid xml", () => { 7 | expect(() => renderToString("
    ")).toThrow("Invalid XML in template"); 8 | }); 9 | 10 | test("nice warning if no template with given name", () => { 11 | const context = new TestContext(); 12 | expect(() => context.renderToString("invalidname")).toThrow("Missing template"); 13 | }); 14 | 15 | test("addTemplates throw if parser error", () => { 16 | const context = new TestContext(); 17 | expect(() => { 18 | context.addTemplates(">"); 19 | }).toThrow("Invalid XML in template"); 20 | }); 21 | 22 | test("nice error when t-on is evaluated with a missing event", () => { 23 | expect(() => renderToString(`
    `)).toThrow( 24 | "Missing event name with t-on directive" 25 | ); 26 | }); 27 | 28 | test("error when unknown directive", () => { 29 | expect(() => renderToString(`
    test
    `)).toThrow( 30 | "Unknown QWeb directive: 't-best-beer'" 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/compiler/qweb_memory.test.ts: -------------------------------------------------------------------------------- 1 | import { renderToString, snapshotEverything } from "../helpers"; 2 | 3 | snapshotEverything(); 4 | 5 | describe("memory", () => { 6 | test("t-foreach does not leak stuff in global scope", () => { 7 | const initialNumberOfGlobals = Object.keys(window).length; 8 | const template = `

    `; 9 | expect(renderToString(template)).toBe("

    321

    "); 10 | expect(Object.keys(window).length).toBe(initialNumberOfGlobals); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/compiler/t_custom.test.ts: -------------------------------------------------------------------------------- 1 | import { App, Component, xml } from "../../src"; 2 | import { makeTestFixture, snapshotEverything } from "../helpers"; 3 | 4 | let fixture: HTMLElement; 5 | 6 | snapshotEverything(); 7 | 8 | beforeEach(() => { 9 | fixture = makeTestFixture(); 10 | }); 11 | 12 | describe("t-custom", () => { 13 | test("can use t-custom directive on a node", async () => { 14 | const steps: string[] = []; 15 | class SomeComponent extends Component { 16 | static template = xml`
    `; 17 | click() { 18 | steps.push("clicked"); 19 | } 20 | } 21 | const app = new App(SomeComponent, { 22 | customDirectives: { 23 | plop: (node, value) => { 24 | node.setAttribute("t-on-click", value); 25 | }, 26 | }, 27 | }); 28 | await app.mount(fixture); 29 | expect(fixture.innerHTML).toBe(`
    `); 30 | fixture.querySelector("div")!.click(); 31 | expect(steps).toEqual(["clicked"]); 32 | }); 33 | 34 | test("can use t-custom directive with modifiers on a node", async () => { 35 | const steps: string[] = []; 36 | class SomeComponent extends Component { 37 | static template = xml`
    `; 38 | click() { 39 | steps.push("clicked"); 40 | } 41 | } 42 | const app = new App(SomeComponent, { 43 | customDirectives: { 44 | plop: (node, value, modifiers) => { 45 | node.setAttribute("t-on-click", value); 46 | for (let mod of modifiers) { 47 | steps.push(mod); 48 | } 49 | }, 50 | }, 51 | }); 52 | await app.mount(fixture); 53 | expect(fixture.innerHTML).toBe(`
    `); 54 | fixture.querySelector("div")!.click(); 55 | expect(steps).toEqual(["mouse", "stop", "clicked"]); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/compiler/t_debug_log.test.ts: -------------------------------------------------------------------------------- 1 | import { renderToString, snapshotTemplate } from "../helpers"; 2 | 3 | // ----------------------------------------------------------------------------- 4 | // debugging 5 | // ----------------------------------------------------------------------------- 6 | 7 | describe("debugging", () => { 8 | test("t-debug", () => { 9 | const consoleLog = console.log; 10 | console.log = jest.fn(); 11 | const template = `
    hey
    `; 12 | snapshotTemplate(template); 13 | expect(console.log).toHaveBeenCalledTimes(1); 14 | console.log = consoleLog; 15 | }); 16 | 17 | test("t-debug on sub template", () => { 18 | const consoleLog = console.log; 19 | console.log = jest.fn(); 20 | let template = `

    coucou

    `; 21 | snapshotTemplate(template); 22 | template = `
    `; 23 | snapshotTemplate(template); 24 | expect(console.log).toHaveBeenCalledTimes(1); 25 | console.log = consoleLog; 26 | }); 27 | 28 | test("t-log", () => { 29 | const consoleLog = console.log; 30 | console.log = jest.fn(); 31 | 32 | const template = `
    33 | 34 | 35 |
    `; 36 | snapshotTemplate(template); 37 | renderToString(template); 38 | expect(console.log).toHaveBeenCalledWith(45); 39 | console.log = consoleLog; 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/compiler/t_key.test.ts: -------------------------------------------------------------------------------- 1 | import { renderToString, renderToBdom, snapshotEverything, makeTestFixture } from "../helpers"; 2 | import { mount, patch } from "../../src/runtime/blockdom/index"; 3 | 4 | snapshotEverything(); 5 | 6 | describe("t-key", () => { 7 | test("can use t-key directive on a node", () => { 8 | const template = `
    `; 9 | expect(renderToString(template, { beer: { id: 12, name: "Chimay Rouge" } })).toBe( 10 | "
    Chimay Rouge
    " 11 | ); 12 | }); 13 | 14 | test("can use t-key directive on a node as a function", () => { 15 | const template = `
    `; 16 | const getKey = (arg: any) => arg.id; 17 | expect(renderToString(template, { getKey, beer: { id: 12, name: "Chimay Rouge" } })).toBe( 18 | "
    Chimay Rouge
    " 19 | ); 20 | }); 21 | 22 | test("can use t-key directive on a node 2", async () => { 23 | const template = `
    `; 24 | const bd = renderToBdom(template, { beer: { id: 12, name: "Chimay Rouge" } }); 25 | const fixture = makeTestFixture(); 26 | await mount(bd, fixture); 27 | const div = fixture.firstChild; 28 | 29 | expect((div as HTMLElement).outerHTML).toBe("
    Chimay Rouge
    "); 30 | const bd2 = renderToBdom(template, { beer: { id: 13, name: "Chimay Rouge" } }); 31 | await patch(bd, bd2); 32 | expect(div !== fixture.firstChild).toBeTruthy(); 33 | expect((div as HTMLElement).outerHTML).toBe("
    Chimay Rouge
    "); 34 | }); 35 | 36 | test("t-key directive in a list", () => { 37 | const template = `
      38 |
    • 39 |
    `; 40 | expect( 41 | renderToString(template, { 42 | beers: [{ id: 12, name: "Chimay Rouge" }], 43 | }) 44 | ).toBe("
    • Chimay Rouge
    "); 45 | }); 46 | 47 | test("t-key on sub dom node pushes a child block in its parent", async () => { 48 | const template = ` 49 |
    50 | 51 |

    52 |
    53 | `; 54 | 55 | expect(renderToString(template, { key: "1" })).toBe("

    "); 56 | expect(renderToString(template, { hasSpan: true, key: "1" })).toBe( 57 | "

    " 58 | ); 59 | 60 | const template2 = ` 61 |

    62 | `; 63 | 64 | expect(renderToString(template2, { key: "1" })).toBe("

    "); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/compiler/t_slot.test.ts: -------------------------------------------------------------------------------- 1 | import { parseXML } from "../../src/common/utils"; 2 | import { compile } from "../../src/compiler"; 3 | 4 | describe("t-slot", () => { 5 | test("compile t-props correctly multiple time", () => { 6 | const template = ``; 7 | const parsedTemplate = parseXML(template).firstChild as Element; 8 | 9 | const fn1 = compile(parsedTemplate); 10 | expect(fn1.toString()).toMatchSnapshot(); 11 | 12 | const fn2 = compile(parsedTemplate); 13 | expect(fn2.toString()).toBe(fn1.toString()); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/compiler/t_tag.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, patch } from "../../src/runtime/blockdom"; 2 | import { makeTestFixture, renderToBdom, renderToString, snapshotEverything } from "../helpers"; 3 | 4 | snapshotEverything(); 5 | 6 | let fixture: HTMLElement; 7 | 8 | beforeEach(() => { 9 | fixture = makeTestFixture(); 10 | }); 11 | 12 | afterEach(() => { 13 | fixture.remove(); 14 | }); 15 | 16 | describe("qweb t-tag", () => { 17 | test("simple usecases", () => { 18 | expect(renderToString(``)).toBe("
    "); 19 | expect(renderToString(`text`, { tag: "span" })).toBe("text"); 20 | }); 21 | 22 | test("with multiple child nodes", () => { 23 | const template = ` 24 | 25 | pear 26 | apple 27 | strawberry 28 | `; 29 | expect(renderToString(template, { tag: "div" })).toBe( 30 | "
    pear apple strawberry
    " 31 | ); 32 | }); 33 | 34 | test("with multiple attributes", () => { 35 | const template = `gooseberry`; 36 | const expected = `
    gooseberry
    `; 37 | expect(renderToString(template, { tag: "div" })).toBe(expected); 38 | }); 39 | 40 | test("can fallback if falsy tag", () => { 41 | const template = ``; 42 | expect(renderToString(template, { tag: "div" })).toBe(`
    `); 43 | expect(renderToString(template, { tag: "" })).toBe(``); 44 | expect(renderToString(template, { tag: undefined })).toBe(``); 45 | expect(renderToString(template, { tag: null })).toBe(``); 46 | expect(renderToString(template, { tag: false })).toBe(``); 47 | }); 48 | 49 | test("with multiple t-tag in same template", () => { 50 | const template = `baz`; 51 | expect(renderToString(template, { outer: "foo", inner: "bar" })).toBe( 52 | `baz` 53 | ); 54 | }); 55 | 56 | test("with multiple t-tag in same template, part 2", () => { 57 | const template = `barbaz`; 58 | expect(renderToString(template, { brother: "foo" })).toBe(`barbaz`); 59 | }); 60 | 61 | test("can update", () => { 62 | const template = ``; 63 | const bdom = renderToBdom(template, { tag: "yop" }); 64 | mount(bdom, fixture); 65 | expect(fixture.innerHTML).toBe(""); 66 | patch(bdom, renderToBdom(template, { tag: "gnap" })); 67 | expect(fixture.innerHTML).toBe(""); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/compiler/validation.test.ts: -------------------------------------------------------------------------------- 1 | import { TemplateSet } from "../../src/runtime/template_set"; 2 | import { renderToString, snapshotTemplate, TestContext } from "../helpers"; 3 | 4 | // ----------------------------------------------------------------------------- 5 | // basic validation 6 | // ----------------------------------------------------------------------------- 7 | 8 | describe("basic validation", () => { 9 | test("error if no template with given name", () => { 10 | const context = new TemplateSet(); 11 | expect(() => context.getTemplate("invalidname")).toThrow("Missing template"); 12 | }); 13 | 14 | test("cannot add a different template with the same name in dev mode", () => { 15 | const context = new TemplateSet({ dev: true }); 16 | context.addTemplate("test", ``); 17 | // Same template with the same name is fine 18 | expect(() => context.addTemplate("test", "")).not.toThrow(); 19 | // Different template with the same name crashes 20 | expect(() => context.addTemplate("test", "
    ")).toThrow("already defined"); 21 | }); 22 | 23 | test("adding different template with same name outside dev mode silently ignores it", () => { 24 | const context = new TemplateSet({ dev: false }); 25 | context.addTemplate("test", ``); 26 | expect(() => context.addTemplate("test", "
    ")).not.toThrow(); 27 | expect(context.rawTemplates.test).toBe(""); 28 | }); 29 | 30 | test("invalid xml", () => { 31 | const template = "
    "; 32 | expect(() => snapshotTemplate(template)).toThrow("Invalid XML in template"); 33 | }); 34 | 35 | test("missing template in template set", () => { 36 | const context = new TestContext(); 37 | const template = ``; 38 | 39 | context.addTemplate("template", template); 40 | expect(() => context.renderToString("template")).toThrowError("Missing"); 41 | }); 42 | 43 | test("error when unknown directive", () => { 44 | const template = `
    test
    `; 45 | expect(() => renderToString(template)).toThrow("Unknown QWeb directive: 't-best-beer'"); 46 | }); 47 | 48 | test("compilation error", () => { 49 | const template = `
    test
    `; 50 | expect(() => renderToString(template)) 51 | .toThrow(`Failed to compile anonymous template: Unexpected identifier 'ctx' 52 | 53 | generated code: 54 | function(app, bdom, helpers) { 55 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 56 | 57 | let block1 = createBlock(\`
    test
    \`); 58 | 59 | return function template(ctx, node, key = "") { 60 | let attr1 = ctx['a']ctx['b']; 61 | return block1([attr1]); 62 | } 63 | }`); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/compiler/white_space.test.ts: -------------------------------------------------------------------------------- 1 | import { renderToString, snapshotEverything } from "../helpers"; 2 | 3 | snapshotEverything(); 4 | 5 | // ----------------------------------------------------------------------------- 6 | // white space 7 | // ----------------------------------------------------------------------------- 8 | 9 | describe("white space handling", () => { 10 | test("white space only text nodes are condensed into a single space", () => { 11 | const template = `
    `; 12 | expect(renderToString(template)).toBe("
    "); 13 | }); 14 | 15 | test("consecutives whitespaces are condensed into a single space", () => { 16 | const template = `
    abc
    `; 17 | expect(renderToString(template)).toBe("
    abc
    "); 18 | }); 19 | 20 | test("whitespace only text nodes with newlines are removed", () => { 21 | const template = `
    22 | abc 23 |
    `; 24 | 25 | expect(renderToString(template)).toBe("
    abc
    "); 26 | }); 27 | 28 | test("nothing is done in pre tags", () => { 29 | const template1 = `
       
    `; 30 | expect(renderToString(template1)).toBe(template1); 31 | 32 | const template2 = `
    33 |           some text
    34 |         
    `; 35 | expect(renderToString(template2)).toBe(template2); 36 | 37 | const template3 = `
    38 |           
    39 |         
    `; 40 | expect(renderToString(template3)).toBe(template3); 41 | }); 42 | 43 | test("pre inside a div with a new line", () => { 44 | expect(renderToString(`
    SomeText
    \n
    `)).toBe( 45 | "
    SomeText
    " 46 | ); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/components/__snapshots__/t_call_block.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`t-call-block simple t-call-block with static text 1`] = ` 4 | "function anonymous(app, bdom, helpers 5 | ) { 6 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 7 | 8 | let block1 = createBlock(\`
    \`); 9 | 10 | return function template(ctx, node, key = \\"\\") { 11 | const b2 = ctx['myBlock'](); 12 | return block1([], [b2]); 13 | } 14 | }" 15 | `; 16 | -------------------------------------------------------------------------------- /tests/components/__snapshots__/t_out.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components in t-out simple list 1`] = ` 4 | "function anonymous(app, bdom, helpers 5 | ) { 6 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 7 | let { prepareList, isBoundary, withDefault, LazyValue, safeOutput, withKey } = helpers; 8 | const comp1 = app.createComponent(\`Child\`, true, false, false, []); 9 | 10 | function value1(ctx, node, key = \\"\\") { 11 | return comp1({}, key + \`__1\`, node, this, null); 12 | } 13 | 14 | return function template(ctx, node, key = \\"\\") { 15 | ctx = Object.create(ctx); 16 | ctx[isBoundary] = 1 17 | ctx = Object.create(ctx); 18 | const [k_block1, v_block1, l_block1, c_block1] = prepareList([1,2]);; 19 | for (let i1 = 0; i1 < l_block1; i1++) { 20 | ctx[\`n\`] = k_block1[i1]; 21 | const key1 = ctx['n']; 22 | ctx[\`blabla\`] = new LazyValue(value1, ctx, this, node, key1); 23 | c_block1[i1] = withKey(safeOutput(ctx['blabla']), key1); 24 | } 25 | return list(c_block1); 26 | } 27 | }" 28 | `; 29 | 30 | exports[`components in t-out simple list 2`] = ` 31 | "function anonymous(app, bdom, helpers 32 | ) { 33 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 34 | 35 | return function template(ctx, node, key = \\"\\") { 36 | return text(\`child\`); 37 | } 38 | }" 39 | `; 40 | -------------------------------------------------------------------------------- /tests/components/env.test.ts: -------------------------------------------------------------------------------- 1 | import { App, Component, mount, xml } from "../../src"; 2 | import { makeTestFixture } from "../helpers"; 3 | 4 | let fixture: HTMLElement; 5 | 6 | beforeEach(() => { 7 | fixture = makeTestFixture(); 8 | }); 9 | 10 | describe("env handling", () => { 11 | test("has an env by default", async () => { 12 | class Test extends Component { 13 | static template = xml`
    `; 14 | } 15 | const component = await mount(Test, fixture); 16 | expect(component.env).toEqual({}); 17 | }); 18 | 19 | test("env is shallow frozen", async () => { 20 | const env = { foo: 42, bar: { value: 42 } }; 21 | class Test extends Component { 22 | static template = xml`
    `; 23 | } 24 | const component = await new App(Test, { env }).mount(fixture); 25 | expect(Object.isFrozen(component.env)).toBeTruthy(); 26 | expect(component.env).toEqual({ foo: 42, bar: { value: 42 } }); 27 | expect(() => { 28 | component.env.foo = 23; 29 | }).toThrow(/Cannot assign to read only property 'foo' of object/); 30 | component.env.bar.value = 23; 31 | expect(component.env).toEqual({ foo: 42, bar: { value: 23 } }); 32 | }); 33 | 34 | test("parent env is propagated to child components", async () => { 35 | const env = { foo: 42, bar: { value: 42 } }; 36 | let child: any = null; 37 | 38 | class Child extends Component { 39 | static template = xml`
    `; 40 | setup() { 41 | child = this; 42 | } 43 | } 44 | 45 | class Test extends Component { 46 | static template = xml``; 47 | static components = { Child }; 48 | } 49 | 50 | await new App(Test, { env }).mount(fixture); 51 | expect(child.env).toEqual(env); 52 | // we check that the frozen env maintain the same prototype chain 53 | expect(Object.getPrototypeOf(child.env)).toBe(Object.getPrototypeOf(env)); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/components/t_call_block.test.ts: -------------------------------------------------------------------------------- 1 | import { text } from "../../src/runtime/blockdom"; 2 | import { Component, mount, xml } from "../../src/index"; 3 | import { makeTestFixture, snapshotEverything } from "../helpers"; 4 | 5 | snapshotEverything(); 6 | 7 | let fixture: HTMLElement; 8 | 9 | beforeEach(() => { 10 | fixture = makeTestFixture(); 11 | }); 12 | 13 | describe("t-call-block", () => { 14 | test("simple t-call-block with static text", async () => { 15 | class Test extends Component { 16 | static template = xml`
    `; 17 | myBlock() { 18 | return text("hello"); 19 | } 20 | } 21 | 22 | await mount(Test, fixture); 23 | expect(fixture.innerHTML).toBe("
    hello
    "); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/components/t_out.test.ts: -------------------------------------------------------------------------------- 1 | import { Component, mount, xml } from "../../src/index"; 2 | import { makeTestFixture, snapshotEverything } from "../helpers"; 3 | 4 | snapshotEverything(); 5 | 6 | // ----------------------------------------------------------------------------- 7 | // t-out 8 | // ----------------------------------------------------------------------------- 9 | 10 | describe("components in t-out", () => { 11 | let fixture: HTMLElement; 12 | 13 | beforeEach(() => { 14 | fixture = makeTestFixture(); 15 | }); 16 | 17 | test("simple list", async () => { 18 | class Child extends Component { 19 | static template = xml`child`; 20 | } 21 | 22 | class Parent extends Component { 23 | static template = xml` 24 | 25 | 26 | 27 | 28 | 29 | `; 30 | static components = { Child }; 31 | } 32 | 33 | await mount(Parent, fixture); 34 | expect(fixture.innerHTML).toBe("childchild"); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/mocks/mockEventTarget.js: -------------------------------------------------------------------------------- 1 | // mockEventTarget.js 2 | 3 | // copy the code from https://developer.mozilla.org/en-US/docs/Web/API/EventTarget#Simple_implementation_of_EventTarget 4 | var EventTarget = function() { 5 | this.listeners = {}; 6 | }; 7 | 8 | EventTarget.prototype.listeners = null; 9 | EventTarget.prototype.addEventListener = function(type, callback) { 10 | if (!(type in this.listeners)) { 11 | this.listeners[type] = []; 12 | } 13 | this.listeners[type].push(callback); 14 | }; 15 | 16 | EventTarget.prototype.removeEventListener = function(type, callback) { 17 | if (!(type in this.listeners)) { 18 | return; 19 | } 20 | var stack = this.listeners[type]; 21 | for (var i = 0, l = stack.length; i < l; i++) { 22 | if (stack[i] === callback){ 23 | stack.splice(i, 1); 24 | return; 25 | } 26 | } 27 | }; 28 | 29 | EventTarget.prototype.dispatchEvent = function(event) { 30 | if (!(event.type in this.listeners)) { 31 | return true; 32 | } 33 | var stack = this.listeners[event.type].slice(); 34 | 35 | for (var i = 0, l = stack.length; i < l; i++) { 36 | stack[i].call(this, event); 37 | } 38 | return !event.defaultPrevented; 39 | }; 40 | 41 | // make the EventTarget global 42 | global.EventTarget = EventTarget -------------------------------------------------------------------------------- /tests/shadow_dom/__snapshots__/shadow_dom.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`shadow_dom can bind event handler 1`] = ` 4 | "function anonymous(app, bdom, helpers 5 | ) { 6 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 7 | 8 | let block1 = createBlock(\`\`); 9 | 10 | return function template(ctx, node, key = \\"\\") { 11 | let hdlr1 = [ctx['add'], ctx]; 12 | return block1([hdlr1]); 13 | } 14 | }" 15 | `; 16 | 17 | exports[`shadow_dom can mount app 1`] = ` 18 | "function anonymous(app, bdom, helpers 19 | ) { 20 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 21 | 22 | let block1 = createBlock(\`
    \`); 23 | 24 | return function template(ctx, node, key = \\"\\") { 25 | return block1(); 26 | } 27 | }" 28 | `; 29 | 30 | exports[`shadow_dom can mount app in closed shadow dom 1`] = ` 31 | "function anonymous(app, bdom, helpers 32 | ) { 33 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 34 | 35 | let block1 = createBlock(\`
    \`); 36 | 37 | return function template(ctx, node, key = \\"\\") { 38 | return block1(); 39 | } 40 | }" 41 | `; 42 | 43 | exports[`shadow_dom can mount app inside a separate HTML document 1`] = ` 44 | "function anonymous(app, bdom, helpers 45 | ) { 46 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 47 | 48 | let block1 = createBlock(\`
    \`); 49 | 50 | return function template(ctx, node, key = \\"\\") { 51 | return block1(); 52 | } 53 | }" 54 | `; 55 | 56 | exports[`shadow_dom can mount app inside a shadow child element 1`] = ` 57 | "function anonymous(app, bdom, helpers 58 | ) { 59 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 60 | 61 | let block1 = createBlock(\`
    \`); 62 | 63 | return function template(ctx, node, key = \\"\\") { 64 | return block1(); 65 | } 66 | }" 67 | `; 68 | 69 | exports[`shadow_dom can mount app inside an element in a shadow root inside an iframe 1`] = ` 70 | "function anonymous(app, bdom, helpers 71 | ) { 72 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 73 | 74 | let block1 = createBlock(\`
    \`); 75 | 76 | return function template(ctx, node, key = \\"\\") { 77 | return block1(); 78 | } 79 | }" 80 | `; 81 | 82 | exports[`shadow_dom useRef hook 1`] = ` 83 | "function anonymous(app, bdom, helpers 84 | ) { 85 | let { text, createBlock, list, multi, html, toggler, comment } = bdom; 86 | 87 | let block1 = createBlock(\`
    \`); 88 | 89 | return function template(ctx, node, key = \\"\\") { 90 | let ref1 = (el) => this.__owl__.setRef((\`refName\`), el); 91 | return block1([ref1]); 92 | } 93 | }" 94 | `; 95 | -------------------------------------------------------------------------------- /tools/compile_owl_templates.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // this is the "compile_owl_templates" command that owl makes available when 4 | // installed as a node_module. 5 | import { existsSync, mkdirSync, writeFileSync } from "fs"; 6 | import { dirname } from "path"; 7 | import { compileTemplates } from "../dist/compile_templates.mjs"; 8 | import { parseArgs } from "util"; 9 | 10 | const { values, positionals } = parseArgs({ 11 | allowPositionals: true, 12 | options: { 13 | output: { 14 | type: "string", 15 | short: "o", 16 | default: "templates.js", 17 | }, 18 | }, 19 | }); 20 | 21 | if (positionals.length) { 22 | const result = await compileTemplates(positionals); 23 | const outputPath = values.output; 24 | const dir = dirname(outputPath); 25 | if (!existsSync(dir)) { 26 | mkdirSync(dir, { recursive: true }); 27 | } 28 | writeFileSync(outputPath, result); 29 | } else { 30 | console.log("Please provide a path"); 31 | } 32 | -------------------------------------------------------------------------------- /tools/devtools/assets/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/assets/FiraMono-Medium.ttf -------------------------------------------------------------------------------- /tools/devtools/assets/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/assets/icon128.png -------------------------------------------------------------------------------- /tools/devtools/assets/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/assets/icon16.png -------------------------------------------------------------------------------- /tools/devtools/assets/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/assets/icon48.png -------------------------------------------------------------------------------- /tools/devtools/assets/icon_disabled128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/assets/icon_disabled128.png -------------------------------------------------------------------------------- /tools/devtools/manifest-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Owl devtools", 3 | "version": "1.3.0", 4 | "manifest_version": 3, 5 | "description": "Chrome devtools extension for Odoo Owl framework", 6 | "icons": { 7 | "128": "assets/icon128.png" 8 | }, 9 | "action": { 10 | "default_icon": { 11 | "128": "assets/icon_disabled128.png" 12 | }, 13 | "default_title": "Owl devtools", 14 | "default_popup": "popup_app/popup.html" 15 | }, 16 | "permissions": ["scripting", "storage"], 17 | "host_permissions": ["http://*/*", "https://*/*", "file://*"], 18 | "content_security_policy": { 19 | "script-src": "self", 20 | "object-src": "self" 21 | }, 22 | "background": { 23 | "service_worker": "background.js" 24 | }, 25 | "devtools_page": "devtools_app/devtools.html", 26 | "content_scripts": [ 27 | { 28 | "matches": [""], 29 | "js": ["content.js"] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tools/devtools/manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Owl devtools", 3 | "version": "1.1.0", 4 | "description": "Firefox devtools extension for Odoo Owl framework", 5 | "manifest_version": 2, 6 | "browser_specific_settings": { 7 | "gecko": { 8 | "id": "@owl-devtools", 9 | "strict_min_version": "74.0" 10 | } 11 | }, 12 | "icons": { 13 | "128": "assets/icon128.png" 14 | }, 15 | "browser_action": { 16 | "default_icon": { 17 | "128": "assets/icon_disabled128.png" 18 | }, 19 | "default_title": "Owl devtools", 20 | "default_popup": "popup_app/popup.html" 21 | }, 22 | "web_accessible_resources": [ 23 | "popup_app/popup.html", 24 | "devtools_app/devtools.html", 25 | "devtools_app/components_panel.html" 26 | ], 27 | "permissions": ["storage", "scripting", ""], 28 | "background": { 29 | "scripts": ["background.js"] 30 | }, 31 | "devtools_page": "devtools_app/devtools.html", 32 | "content_security_policy": "script-src 'self'; object-src 'self'", 33 | "content_scripts": [ 34 | { 35 | "matches": [""], 36 | "js": ["content.js"] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /tools/devtools/src/background.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tools/devtools/src/content.js: -------------------------------------------------------------------------------- 1 | import globalHook from "./page_scripts/owl_devtools_global_hook"; 2 | import { IS_FIREFOX, browserInstance } from "./utils"; 3 | 4 | // Relays the owlDevtools__... type top window messages to the background script so that it can relay it to the devtools app 5 | window.addEventListener( 6 | "message", 7 | function (event) { 8 | if (event.data.type && event.data.source === "owl-devtools") { 9 | try { 10 | browserInstance.runtime.sendMessage( 11 | event.data.data 12 | ? { type: event.data.type, data: event.data.data, origin: event.data.origin } 13 | : { type: event.data.type, origin: event.data.origin } 14 | ); 15 | } catch (e) { 16 | // Extension context invalidated, cannot be handled here since the whole communication system 17 | // inside the extension is dead in this case. 18 | } 19 | } 20 | }, 21 | false 22 | ); 23 | 24 | // Load the devtools global hook this way when running on firefox 25 | if (IS_FIREFOX) { 26 | const script = document.createElement("script"); 27 | script.textContent = globalHook; 28 | document.documentElement.appendChild(script); 29 | script.parentNode.removeChild(script); 30 | } 31 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/context_menu/context_menu.js: -------------------------------------------------------------------------------- 1 | import { useStore } from "../store/store"; 2 | 3 | const { Component, useEffect, useRef } = owl; 4 | 5 | export class ContextMenu extends Component { 6 | static template = "devtools.ContextMenu"; 7 | static props = { 8 | items: Array, 9 | }; 10 | setup() { 11 | this.store = useStore(); 12 | this.contextMenu = useRef("contextmenu"); 13 | useEffect( 14 | (position) => { 15 | const menu = this.contextMenu.el; 16 | const menuWidth = menu.offsetWidth; 17 | const menuHeight = menu.offsetHeight; 18 | let { x, y } = position; 19 | if (x + menuWidth > window.innerWidth) { 20 | x = window.innerWidth - menuWidth; 21 | } 22 | if (y + menuHeight > window.innerHeight) { 23 | y = window.innerHeight - menuHeight; 24 | } 25 | menu.style.left = x + "px"; 26 | // Need 25px offset because of the main navbar from the browser devtools 27 | menu.style.top = y + "px"; 28 | }, 29 | () => [this.store.contextMenu?.position] 30 | ); 31 | } 32 | onClickItem(action) { 33 | action(); 34 | this.store.contextMenu = null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/context_menu/context_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 |
      6 |
    • 7 |
    8 |
    9 |
    10 |
    11 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools.js: -------------------------------------------------------------------------------- 1 | import { IS_FIREFOX, browserInstance } from "../utils"; 2 | 3 | let created = false; 4 | 5 | // Try to load the owl panel each 1000 ms in case it (re)appears on the page later on 6 | const checkInterval = setInterval(createPanelsIfOwl, 1000); 7 | 8 | createPanelsIfOwl(); 9 | 10 | // Create the owl devtools panel if owl on the page is available at a sufficient version 11 | function createPanelsIfOwl() { 12 | if (created) { 13 | clearInterval(checkInterval); 14 | return; 15 | } 16 | browserInstance.devtools.inspectedWindow.eval( 17 | "window.__OWL_DEVTOOLS__?.Fiber !== undefined;", 18 | async (hasOwl) => { 19 | if (!hasOwl || created) { 20 | return; 21 | } 22 | clearInterval(checkInterval); 23 | created = true; 24 | browserInstance.devtools.panels.create( 25 | "Owl", 26 | "../../assets/icon128.png", 27 | IS_FIREFOX ? "devtools_panel.html" : "devtools_app/devtools_panel.html" 28 | ); 29 | } 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_panel.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module **/ 2 | 3 | import { DevtoolsWindow } from "./devtools_window/devtools_window"; 4 | const { mount } = owl; 5 | import { templates } from "../../assets/templates.js"; 6 | 7 | for (const template in templates) { 8 | owl.App.registerTemplate(template, templates[template]); 9 | } 10 | mount(DevtoolsWindow, document.body, { dev: true }); 11 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/components_tab/component_search_bar/component_search_bar.js: -------------------------------------------------------------------------------- 1 | import { useStore } from "../../../store/store"; 2 | 3 | const { Component } = owl; 4 | 5 | export class ComponentSearchBar extends Component { 6 | static template = "devtools.ComponentSearchBar"; 7 | 8 | setup() { 9 | this.store = useStore(); 10 | } 11 | 12 | updateSearch(event) { 13 | if (event.key !== "Enter") { 14 | this.store.updateSearch(event.target.value); 15 | } 16 | } 17 | 18 | // Go to the next search result repeatedly while enter is pressed 19 | onSearchKeyDown(event) { 20 | if (event.key === "Enter") { 21 | this.store.componentSearch.getNextSearch(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/components_tab/component_search_bar/component_search_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 |
    7 |
    8 |
    9 | 10 | 11 | 12 | | 13 | 14 | 15 | 16 | 17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/components_tab/components_tab.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module **/ 2 | 3 | const { Component, onWillUnmount, useExternalListener } = owl; 4 | import { TreeElement } from "./tree_element/tree_element"; 5 | import { DetailsWindow } from "./details_window/details_window"; 6 | import { ComponentSearchBar } from "./component_search_bar/component_search_bar"; 7 | import { useStore } from "../../store/store"; 8 | 9 | export class ComponentsTab extends Component { 10 | static template = "devtools.ComponentsTab"; 11 | 12 | static components = { TreeElement, DetailsWindow, ComponentSearchBar }; 13 | 14 | setup() { 15 | this.store = useStore(); 16 | this.flushRendersTimeout = false; 17 | useExternalListener(document, "keydown", this.onKeyboardEvent); 18 | useExternalListener(window, "resize", this.onWindowResize); 19 | 20 | onWillUnmount(() => { 21 | window.removeEventListener("mousemove", this.onMouseMove); 22 | window.removeEventListener("mouseup", this.onMouseUp); 23 | }); 24 | } 25 | 26 | // Apply the right action depending on which arrow key is pressed (on keydown) 27 | onKeyboardEvent(event) { 28 | switch (event.key) { 29 | case "ArrowLeft": 30 | this.store.toggleOrSelectPrevElement(true); 31 | event.preventDefault(); 32 | break; 33 | case "ArrowUp": 34 | this.store.toggleOrSelectPrevElement(false); 35 | event.preventDefault(); 36 | break; 37 | case "ArrowRight": 38 | this.store.toggleOrSelectNextElement(true); 39 | event.preventDefault(); 40 | break; 41 | case "ArrowDown": 42 | this.store.toggleOrSelectNextElement(false); 43 | event.preventDefault(); 44 | break; 45 | } 46 | } 47 | 48 | onMouseDown = () => { 49 | // Add event listeners for mouse move and mouse up events 50 | // to allow the user to drag the split screen border 51 | window.addEventListener("mousemove", this.onMouseMove); 52 | window.addEventListener("mouseup", this.onMouseUp); 53 | }; 54 | 55 | // Adjust the position of the split between the left and right right window of the components tab 56 | onMouseMove = (event) => { 57 | const minWidth = (147 / window.innerWidth) * 100; 58 | const maxWidth = 100 - (100 / window.innerWidth) * 100; 59 | this.store.splitPosition = Math.max( 60 | Math.min((event.clientX / window.innerWidth) * 100, maxWidth), 61 | minWidth 62 | ); 63 | }; 64 | 65 | onMouseUp = () => { 66 | // Remove the event listeners when the user releases the mouse button 67 | window.removeEventListener("mousemove", this.onMouseMove); 68 | window.removeEventListener("mouseup", this.onMouseUp); 69 | }; 70 | 71 | onWindowResize = () => { 72 | const minWidth = (147 / window.innerWidth) * 100; 73 | if (minWidth <= 100) { 74 | this.store.splitPosition = Math.max(this.store.splitPosition, minWidth); 75 | } 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/components_tab/components_tab.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 | There are no apps currently running. 7 |
    8 |
    9 | 10 |
    11 |
    12 |
    13 | 14 |
    15 |
    16 |
    17 | 18 | 19 | 20 |
    21 |
    22 |
    23 |
    27 |
    28 | 29 | 30 |
    31 | There was an error while processing this component. 32 |
    33 |
    34 |
    35 |
    36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/details_window.js: -------------------------------------------------------------------------------- 1 | const { Component } = owl; 2 | import { useStore } from "../../../store/store"; 3 | import { ObjectTreeElement } from "./object_tree_element/object_tree_element"; 4 | 5 | export class DetailsWindow extends Component { 6 | static template = "devtools.DetailsWindow"; 7 | static components = { ObjectTreeElement }; 8 | setup() { 9 | this.store = useStore(); 10 | } 11 | 12 | get contextMenuItems() { 13 | return [ 14 | { 15 | title: "Inspect source code", 16 | show: true, 17 | action: () => this.store.inspectComponent("source", this.store.activeComponent.path), 18 | }, 19 | { 20 | title: "Store as global variable", 21 | show: this.store.activeComponent.path.length !== 1, 22 | action: () => 23 | this.store.logObjectInConsole([ 24 | ...this.store.activeComponent.path, 25 | { type: "item", value: "component" }, 26 | ]), 27 | }, 28 | { 29 | title: "Inspect in Elements tab", 30 | show: this.store.activeComponent.path.length !== 1, 31 | action: () => this.store.inspectComponent("DOM", this.store.activeComponent.path), 32 | }, 33 | { 34 | title: "Force rerender", 35 | show: this.store.activeComponent.path.length !== 1, 36 | action: () => this.store.refreshComponent(this.store.activeComponent.path), 37 | }, 38 | { 39 | title: "Store observed states as global variable", 40 | show: this.store.activeComponent.path.length !== 1, 41 | action: () => 42 | this.store.logObjectInConsole([ 43 | ...this.store.activeComponent.path, 44 | { type: "item", value: "subscriptions" }, 45 | ]), 46 | }, 47 | { 48 | title: "Inspect compiled template", 49 | show: this.store.activeComponent.path.length !== 1, 50 | action: () => 51 | this.store.inspectComponent("compiled template", this.store.activeComponent.path), 52 | }, 53 | { 54 | title: "Log raw template", 55 | show: this.store.activeComponent.path.length !== 1, 56 | action: () => this.store.inspectComponent("raw template", this.store.activeComponent.path), 57 | }, 58 | { 59 | title: "Store as global variable", 60 | show: this.store.activeComponent.path.length === 1, 61 | action: () => this.store.logObjectInConsole([...this.store.activeComponent.path]), 62 | }, 63 | ]; 64 | } 65 | 66 | openMenu(ev) { 67 | this.store.openContextMenu(ev, this.contextMenuItems); 68 | } 69 | 70 | toggleCategory(ev, category) { 71 | this.store.activeComponent[category].toggled = !this.store.activeComponent[category].toggled; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/object_tree_element/object_tree_element.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    9 |
    10 | 14 | 15 | : 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | +/- 32 |
    33 |
    34 | 35 | 36 | 37 | 38 | 39 |
    40 |
    41 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/components_tab/tree_element/highlight_text/highlight_text.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module **/ 2 | 3 | const { Component, onWillRender } = owl; 4 | 5 | export class HighlightText extends Component { 6 | setup() { 7 | onWillRender(() => { 8 | const splitText = this.splitFuzzySearch(this.props.originalText, this.props.searchValue); 9 | this.splitText = 10 | this.props.searchValue.length && splitText.length > 1 11 | ? splitText 12 | : [this.props.originalText]; 13 | }); 14 | } 15 | 16 | // Logic to split the text to highlight it according to a fuzzy search pattern 17 | splitFuzzySearch(text, search) { 18 | if (!search || search.length === 0) { 19 | return [text]; 20 | } 21 | let splits = [""]; 22 | let searchIndex = 0; 23 | for (const letter of text) { 24 | if ( 25 | !(searchIndex >= search.length) && 26 | (letter === search[searchIndex] || letter === search[searchIndex].toUpperCase()) 27 | ) { 28 | if (splits.length % 2) { 29 | splits.push(letter); 30 | } else { 31 | splits[splits.length - 1] += letter; 32 | } 33 | searchIndex++; 34 | } else { 35 | if (splits.length % 2) { 36 | splits[splits.length - 1] += letter; 37 | } else { 38 | splits.push(letter); 39 | } 40 | } 41 | } 42 | return splits; 43 | } 44 | } 45 | HighlightText.template = "utils.HighlightText"; 46 | HighlightText.props = { 47 | originalText: String, 48 | searchValue: String, 49 | }; 50 | HighlightText.highlightClass = "highlight-search"; 51 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/components_tab/tree_element/highlight_text/highlight_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/components_tab/tree_element/tree_element.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    11 |
    12 | 17 | < 18 | 19 | 20 | 21 | key= 22 | 23 | 24 | 25 | 26 | > 27 | owl= 28 |
    29 |
    30 | 31 | 32 | 33 | 34 | 35 |
    36 |
    37 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/devtools_window.js: -------------------------------------------------------------------------------- 1 | const { Component } = owl; 2 | import { ContextMenu } from "../context_menu/context_menu"; 3 | import { useStore } from "../store/store"; 4 | import { ComponentsTab } from "./components_tab/components_tab"; 5 | import { ProfilerTab } from "./profiler_tab/profiler_tab"; 6 | import { Tab } from "./tab/tab"; 7 | 8 | export class DevtoolsWindow extends Component { 9 | static props = []; 10 | static template = "devtools.DevtoolsWindow"; 11 | static components = { ComponentsTab, Tab, ProfilerTab, ContextMenu }; 12 | setup() { 13 | this.store = useStore(); 14 | } 15 | 16 | selectFrame(ev) { 17 | const val = ev.target.value; 18 | this.store.selectFrame(val); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/devtools_window.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 |
    7 | Extension context is invalid. Please close the devtools and reload the page. 8 |
    9 |
    10 | 11 |
    12 | 13 | 14 | 19 | 20 | 21 | 22 |
    23 | 24 | 25 |
    26 | 27 |
    28 | Owl is not loaded on this page. 29 |
    30 |
    31 | 32 |
    33 |
    34 |
    35 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/profiler_tab/event/event.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 |
    6 |
    7 | 11 | : 12 | < 18 | 19 | key= 20 | 21 | 22 | > 23 | 24 | (ms) 25 | 26 |
    27 |
    28 | 29 |
    30 | 31 | 32 | origin: 33 | < 38 | 39 | key= 40 | 41 | 42 | > 43 | 44 |
    45 |
    46 |
    47 |
    48 |
    49 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/profiler_tab/event_node/event_node.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    8 |
    9 | 13 | 14 | : 15 | < 20 | 21 | key= 22 | 23 | 24 | > 25 | 26 | (ms) 27 | 28 | 29 |
    30 |
    31 | 32 | 33 | 34 | 35 | 36 |
    37 |
    38 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/profiler_tab/event_search_bar/event_search_bar.js: -------------------------------------------------------------------------------- 1 | import { useStore } from "../../../store/store"; 2 | 3 | const { Component } = owl; 4 | 5 | export class EventSearchBar extends Component { 6 | static template = "devtools.EventSearchBar"; 7 | 8 | setup() { 9 | this.store = useStore(); 10 | } 11 | 12 | // On keyup 13 | updateSearch(event) { 14 | if (!(event.keyCode === 13)) { 15 | const search = event.target.value; 16 | this.store.updateSearch(search); 17 | } 18 | } 19 | 20 | clearSearch() { 21 | this.store.updateSearch(""); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/profiler_tab/event_search_bar/event_search_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 | 7 | 8 | 9 | 10 |
    11 |
    12 |
    -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/profiler_tab/profiler_tab.js: -------------------------------------------------------------------------------- 1 | const { Component } = owl; 2 | import { useStore } from "../../store/store"; 3 | import { Event } from "./event/event"; 4 | import { EventNode } from "./event_node/event_node"; 5 | import { EventSearchBar } from "./event_search_bar/event_search_bar"; 6 | 7 | export class ProfilerTab extends Component { 8 | static template = "devtools.ProfilerTab"; 9 | 10 | static components = { Event, EventNode, EventSearchBar }; 11 | 12 | setup() { 13 | this.store = useStore(); 14 | } 15 | 16 | showHelp() { 17 | return this.store.events.length < 1 && !this.store.activeRecorder; 18 | } 19 | 20 | selectDisplayMode(ev) { 21 | const val = ev.target.value; 22 | if (val === "Tree") { 23 | this.store.buildEventsTree(); 24 | this.store.eventsTreeView = true; 25 | } else { 26 | this.store.eventsTreeView = false; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/profiler_tab/profiler_tab.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 |
    6 | 7 | 8 |
    9 | 13 | 14 |
    15 | 18 | 21 | 22 |
    23 |
    24 | 25 |
    26 |
    27 | Click on the button to start recording events. 28 |
    29 |
    30 |
    31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 |
    45 | 46 | 47 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/tab/tab.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module **/ 2 | 3 | import { useStore } from "../../store/store"; 4 | 5 | const { Component } = owl; 6 | 7 | export class Tab extends Component { 8 | static props = ["tabName"]; 9 | 10 | static template = "devtools.Tab"; 11 | 12 | setup() { 13 | this.store = useStore(); 14 | } 15 | 16 | get active() { 17 | return this.props.tabName === this.store.page; 18 | } 19 | 20 | get name() { 21 | switch (this.props.tabName) { 22 | case "ComponentsTab": 23 | return "Components"; 24 | case "ProfilerTab": 25 | return "Profiler"; 26 | } 27 | } 28 | 29 | selectTab(ev) { 30 | this.store.switchTab(this.props.tabName); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tools/devtools/src/devtools_app/devtools_window/tab/tab.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /tools/devtools/src/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/src/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /tools/devtools/src/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/src/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /tools/devtools/src/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/src/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /tools/devtools/src/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/src/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /tools/devtools/src/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/devtools/src/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /tools/devtools/src/popup_app/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tools/devtools/src/popup_app/popup.js: -------------------------------------------------------------------------------- 1 | import { templates } from "../../assets/templates.js"; 2 | import { getOwlStatus } from "../utils"; 3 | const { Component, useState, onWillStart, mount, App } = owl; 4 | 5 | class PopUpApp extends Component { 6 | static template = "popup.PopUpApp"; 7 | 8 | setup() { 9 | this.state = useState({ status: 0 }); 10 | onWillStart(async () => { 11 | try { 12 | this.state.status = await getOwlStatus(); 13 | } catch (e) { 14 | this.state.status = -1; 15 | } 16 | }); 17 | } 18 | } 19 | 20 | for (const template in templates) { 21 | App.registerTemplate(template, templates[template]); 22 | } 23 | mount(PopUpApp, document.body, { dev: true }); 24 | -------------------------------------------------------------------------------- /tools/devtools/src/popup_app/popup_app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 |

    6 | Owl is detected on this page but the version seems to be outdated. 7 | Please upgrade to a newer version in order to use the devtools. 8 |

    9 |

    10 | Owl is detected on this page. 11 | Open DevTools and look for the Owl panel. 12 |

    13 |

    14 | Owl is not detected on this page. 15 |

    16 |
    17 |
    18 |
    -------------------------------------------------------------------------------- /tools/devtools/src/utils.js: -------------------------------------------------------------------------------- 1 | export const IS_FIREFOX = navigator.userAgent.indexOf("Firefox") !== -1; 2 | 3 | export const browserInstance = IS_FIREFOX ? browser : chrome; 4 | 5 | export async function getOwlStatus() { 6 | const response = await browserInstance.runtime.sendMessage({ type: "getOwlStatus" }); 7 | return response.result; 8 | } 9 | 10 | export async function getActiveTabURL() { 11 | const window = await browserInstance.windows.getLastFocused({ populate: true }); 12 | const activeTab = window.tabs.find((tab) => tab.active); 13 | return activeTab.id; 14 | } 15 | 16 | // inspired from https://www.tutorialspoint.com/fuzzy-search-algorithm-in-javascript 17 | // Check if the query matches with the base in a fuzzy search way 18 | export function fuzzySearch(baseString, queryString) { 19 | const base = baseString.toLowerCase(); 20 | const query = queryString.toLowerCase(); 21 | let queryIndex = 0; 22 | let baseIndex = -1; 23 | let character; 24 | // Loop through each character in the query string 25 | while ((character = query[queryIndex++])) { 26 | // Find the index of the character in the base string, starting from the previous index plus 1 27 | baseIndex = base.indexOf(character, baseIndex + 1); 28 | // If the character is not found, return false 29 | if (baseIndex === -1) { 30 | return false; 31 | } 32 | } 33 | // All characters in the query string were found in the base string, so return true 34 | return true; 35 | } 36 | 37 | // Check if the given element is vertically centered in the user view (between 25 and 75% of the height) 38 | export function isElementInCenterViewport(el) { 39 | const rect = el.getBoundingClientRect(); 40 | return ( 41 | rect.top >= 0 && 42 | rect.bottom >= 0.25 * (window.innerHeight || document.documentElement.clientHeight) && 43 | rect.bottom <= 0.75 * (window.innerHeight || document.documentElement.clientHeight) 44 | ); 45 | } 46 | 47 | // Formatting for displaying the key of the component 48 | export function minimizeKey(key) { 49 | if (key.startsWith("__")) { 50 | const split = key.split("__"); 51 | if (split.length > 2) { 52 | key = key.substring(4 + split[1].length, key.length); 53 | } else { 54 | key = ""; 55 | } 56 | return key; 57 | } 58 | return key; 59 | } 60 | -------------------------------------------------------------------------------- /tools/owl-vision/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tools/owl-vision/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}" 10 | ], 11 | "outFiles": [ 12 | "${workspaceFolder}/out/**/*.js" 13 | ], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "args": [ 21 | "--extensionDevelopmentPath=${workspaceFolder}", 22 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 23 | ], 24 | "outFiles": [ 25 | "${workspaceFolder}/out/test/**/*.js" 26 | ], 27 | "preLaunchTask": "${defaultBuildTask}" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /tools/owl-vision/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /tools/owl-vision/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | **/*.vsix 12 | scripts/** 13 | -------------------------------------------------------------------------------- /tools/owl-vision/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "owl-vision" extension will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | 7 | ## [0.1.0] - 2024-11-06 8 | 9 | ### Added 10 | 11 | - Basic autocomplete in xml files. This includes autocompletion for elements, components, 12 | props, attributes, and javascript expressions. 13 | 14 | The current implementation, while relatively simple, has a couple of drawbacks: 15 | - Javascript imports are not resolved by the xml autocomplete, this means that it does not 16 | understand the types of imported functions or objects. That said, I've added custom support 17 | for frequently used Owl imports, namely `useState` and `useRef`. You can add more in 18 | the settings if needed. 19 | - The autocomplete is limited to templates directly linked to components, sub-templates 20 | used via t-call will not get autocompletion as no component/context can be bound to them. 21 | 22 | - "Go To Definition" support for props and javascript expressions in xml 23 | - Support for the following directives: t-att, t-model, t-tag, t-debug, t-log 24 | 25 | ### Fixed 26 | 27 | - Changed t-else syntax highlight from dynamic to static attribute 28 | 29 | ## [0.0.2] - 2023-2-11 30 | 31 | ### Added 32 | 33 | - Switch Below command 34 | - Basic syntax highlight for xpaths 35 | - Syntax builder scripts to make syntaxes easier to read and edit 36 | - Syntax highlight in single quote attributes 37 | - Syntax highlight for slot props 38 | 39 | ### Fixed 40 | 41 | - Added missing space in component's snippet indentation 42 | - Using `Switch Besides` or `Switch Below` does not open a new panel if one was already open 43 | 44 | ## [0.0.1] - 2023-03-10 45 | 46 | - Initial release 47 | 48 | ### Added 49 | 50 | - Owl templates syntax highlight in xml files 51 | - `Find Template` Command - Finds the template file of the currently selected element. 52 | - `Find Component` Command - Finds the selected component definition. 53 | - `Switch` Command - Finds the corresponding template or component file depending on the current file. 54 | - `Switch Besides` Command - Finds the corresponding template or component file depending on the current file and opens it besides. 55 | -------------------------------------------------------------------------------- /tools/owl-vision/README.md: -------------------------------------------------------------------------------- 1 | # 🦉 Owl Vision 🕶️ 2 | 3 | Owl Vision is an extension for the amazing [Owl framework](https://github.com/odoo/owl) that complements your templates with beautiful colors and allows you to more easily navigate between components and templates. 4 | 5 | ![Syntax highlight preview](https://raw.githubusercontent.com/odoo/owl/master/tools/owl-vision/assets/syntax_highlight.png) 6 | 7 | This extension also adds: 8 | - A basic component snippent. 9 | - "Go to definition" providers for component tags in xml or in inline templates. 10 | 11 | ## Commands 12 | 13 | * `Owl Vision: Find Template`: 14 | - If the cursor is on a template name, finds the corresponding template. 15 | - If the cursor is on a component, finds the template of the selected component. 16 | * `Owl Vision: Find Component`: Finds the selected component definition. 17 | * `Owl Vision: Switch`: Finds the corresponding template or component depending on the current file. 18 | * `Owl Vision: Switch (Besides)`: Finds the corresponding template or component depending on the current file and opens it besides. 19 | * `Owl Vision: Switch (Below)`: Finds the corresponding template or component depending on the current file and opens it below. 20 | 21 | ## Settings 22 | 23 | * `owl-vision.include`: Glob filter for files to include while searching. 24 | * `owl-vision.exclude`: Glob filter for files to exclude while searching. 25 | 26 | ## Troubleshooting 27 | 28 | ### *The extension cannot find my templates/components* 29 | 30 | The include and exclude settings have default values that may not work with your project structure, try adapting them. 31 | -------------------------------------------------------------------------------- /tools/owl-vision/assets/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/owl-vision/assets/icon128.png -------------------------------------------------------------------------------- /tools/owl-vision/assets/syntax_highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/owl/89cb00cc834cbd6070983bf58270f41a2030d78d/tools/owl-vision/assets/syntax_highlight.png -------------------------------------------------------------------------------- /tools/owl-vision/scripts/owl_template_syntax.mjs: -------------------------------------------------------------------------------- 1 | import { createTagPattern, exportPatterns } from "./syntax_builder_utils.mjs"; 2 | import { 3 | htmlAttributes, 4 | owlAttributesDynamic, 5 | owlAttributesFormattedString, 6 | owlAttributesStatic, 7 | propsAttributes 8 | } from "./syntax_parts/owl_attributes.mjs"; 9 | import { xpathAttributes } from "./syntax_parts/xpath.mjs"; 10 | 11 | const componentsTags = createTagPattern("component-tags", { 12 | match: "[A-Z][a-zA-Z0-9_]*", 13 | name: "entity.name.type.class owl.component", 14 | patterns: [ 15 | owlAttributesDynamic, 16 | owlAttributesDynamic, 17 | propsAttributes, 18 | ], 19 | }); 20 | 21 | const htmlTags = createTagPattern("html-tags", { 22 | match: "[a-z][a-zA-Z0-9_:.]+|[abiqsuw]", 23 | name: "entity.name.tag.localname.xml owl.xml.tag", 24 | patterns: [ 25 | owlAttributesFormattedString, 26 | xpathAttributes, 27 | owlAttributesDynamic, 28 | owlAttributesStatic, 29 | htmlAttributes, 30 | ], 31 | }); 32 | 33 | const tTag = createTagPattern("t-tag", { 34 | match: "t(?![a-zA-Z])", 35 | name: "entity.name.tag.localname.xml owl.tag", 36 | patterns: [ 37 | propsAttributes, 38 | owlAttributesFormattedString, 39 | owlAttributesDynamic, 40 | owlAttributesStatic, 41 | ], 42 | }); 43 | 44 | exportPatterns( 45 | "L:text.xml -comment", 46 | "owl.template", 47 | [componentsTags, htmlTags, tTag] 48 | ); 49 | -------------------------------------------------------------------------------- /tools/owl-vision/snippets/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "Basic OWL Component": { 3 | "prefix": "owlcomponent", 4 | "scope": "javascript,typescript", 5 | "body": [ 6 | "import { Component } from \"@odoo/owl\";", 7 | "", 8 | "class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}} extends ${2:Component} {", 9 | "", 10 | " static template = \"${3:${RELATIVE_FILEPATH/(.*[\\|\\/])??([a-zA-Z_]+)([\\|\\/]static[\\|\\/].*)/${2}/g}}.${4:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}}\";", 11 | " static components = {};", 12 | " static props = {};", 13 | "", 14 | " setup() {", 15 | " ${5:super.setup();}", 16 | " }", 17 | "", 18 | " ${6:// Do Something}", 19 | "}", 20 | "" 21 | 22 | ], 23 | "description": "The starting base for an owl component" 24 | }, 25 | 26 | "Basic OWL Template": { 27 | "prefix": "owltemplate", 28 | "scope": "xml", 29 | "body": [ 30 | "", 31 | "", 32 | "", 33 | "", 34 | " ", 35 | " ${3:

    Hello World

    }", 36 | "
    ", 37 | "", 38 | "
    ", 39 | "" 40 | ], 41 | "description": "Generate a basic OWL template XML file" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tools/owl-vision/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Search } from './search'; 3 | import { OpenDirection } from './utils'; 4 | import { OwlLanguageFeaturesProvider } from './language_features/language_features_provider'; 5 | 6 | export async function activate(context: vscode.ExtensionContext) { 7 | const search = new Search(); 8 | context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch', () => search.switch())); 9 | context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-besides', () => search.switch(OpenDirection.Besides))); 10 | context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-below', () => search.switch(OpenDirection.Below))); 11 | context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-component', () => search.findComponentCommand())); 12 | context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-template', () => search.findTemplateCommand())); 13 | 14 | const languageFeaturesProvider = new OwlLanguageFeaturesProvider(search); 15 | context.subscriptions.push(vscode.languages.registerCompletionItemProvider({ language: 'xml', scheme: 'file' }, languageFeaturesProvider, '.', '<')); 16 | context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'xml', scheme: 'file' }, languageFeaturesProvider)); 17 | } 18 | 19 | export function deactivate() { } 20 | -------------------------------------------------------------------------------- /tools/owl-vision/syntaxes/owl.markup.inline.json: -------------------------------------------------------------------------------- 1 | { 2 | "injectionSelector": "L:source.js -comment", 3 | "scopeName": "owl.markup.inline", 4 | "patterns": [ 5 | { 6 | "include": "#markup" 7 | } 8 | ], 9 | "repository": { 10 | "markup": { 11 | "begin": "\\s+(markup)(`)", 12 | "contentName": "meta.embedded.block.html", 13 | "beginCaptures": { 14 | "1": { 15 | "name": "entity.name.function.tagged-template.js owl-markup" 16 | }, 17 | "2": { 18 | "name": "punctuation.definition.string.template.begin.js string.template.js" 19 | } 20 | }, 21 | "end": "(`)", 22 | "endCaptures": { 23 | "1": { 24 | "name": "punctuation.definition.string.template.end.js string.template.js" 25 | } 26 | }, 27 | "patterns": [ 28 | { 29 | "include": "text.html.basic" 30 | } 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tools/owl-vision/syntaxes/owl.template.inline.json: -------------------------------------------------------------------------------- 1 | { 2 | "injectionSelector": "L:source.js -comment -(string -meta.embedded)", 3 | "scopeName": "owl.template.inline", 4 | "patterns": [ 5 | { 6 | "include": "#xml-tag" 7 | } 8 | ], 9 | "repository": { 10 | "xml-tag": { 11 | "begin": "\\s+(xml)(`)", 12 | "contentName": "meta.embedded.block.xml", 13 | "beginCaptures": { 14 | "1": { 15 | "name": "entity.name.function.tagged-template.js" 16 | }, 17 | "2": { 18 | "name": "punctuation.definition.string.template.begin.js" 19 | } 20 | }, 21 | "end": "(`)", 22 | "endCaptures": { 23 | "0": { 24 | "name": "string.js" 25 | }, 26 | "1": { 27 | "name": "punctuation.definition.string.template.end.js" 28 | } 29 | }, 30 | "patterns": [ 31 | { 32 | "include": "owl.template" 33 | }, 34 | { 35 | "include": "text.xml" 36 | } 37 | ] 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /tools/owl-vision/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true 12 | } 13 | } -------------------------------------------------------------------------------- /tools/playground_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import threading 4 | import time 5 | from http.server import SimpleHTTPRequestHandler, HTTPServer 6 | 7 | HOST = '127.0.0.1' 8 | PORT = 8000 9 | URL = 'http://{0}:{1}/docs/playground'.format(HOST, PORT) 10 | 11 | 12 | # We define our own handler here to remap owl.js GET requests to the Owl build 13 | # in dist/. This is useful for the benchmarks and playground applications. 14 | # With this, we can simply copy the playground folder as is in the gh-page when 15 | # we want to update the playground. 16 | class OWLHandler(SimpleHTTPRequestHandler): 17 | def do_GET(self): 18 | if self.path == '/docs/owl.js' or self.path == '/docs/playground/owl.js': 19 | self.path = '/dist/owl.es.js' 20 | return SimpleHTTPRequestHandler.do_GET(self) 21 | 22 | def end_headers(self): 23 | self.disable_cache_headers() 24 | SimpleHTTPRequestHandler.end_headers(self) 25 | 26 | def disable_cache_headers(self): 27 | self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") 28 | self.send_header("Pragma", "no-cache") 29 | self.send_header("Expires", "0") 30 | 31 | 32 | OWLHandler.extensions_map['.js'] = 'application/javascript' 33 | 34 | if __name__ == "__main__": 35 | print("Owl Tools") 36 | print("---------") 37 | print("Server running on: {}".format(URL)) 38 | httpd = HTTPServer((HOST, PORT), OWLHandler) 39 | threading.Thread(target=httpd.serve_forever, daemon=True).start() 40 | 41 | while True: 42 | try: 43 | time.sleep(1) 44 | except KeyboardInterrupt: 45 | httpd.server_close() 46 | quit(0) 47 | --------------------------------------------------------------------------------