Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66a801393f | |||
| e7f405cc97 | |||
| 55c48b2b12 | |||
| 2eb151c92d | |||
| 7952f31e63 | |||
| 11e4e67599 | |||
| 7b7a6de373 | |||
| b63d1e28b2 | |||
| c0667a11c6 | |||
| fddb1ec924 | |||
| e6c3b62ef0 | |||
| 97b69f164f | |||
| 33dfeb1b41 | |||
| dd292472b9 | |||
| 9b18b57fdf | |||
| 68f491cd32 | |||
| 7b3e39ba27 | |||
| 61fc3f4fdc | |||
| 7b454dae66 | |||
| 70101e4c66 | |||
| 5ef405293a | |||
| 9dcbbe54eb | |||
| e94428a186 | |||
| 941190dfa8 | |||
| a53e42518f | |||
| b7c37ca69a | |||
| 34489a2494 | |||
| 398df543fe | |||
| aa6a4a5d46 | |||
| db3499f5e5 | |||
| 166cada8ff | |||
| acbe316689 | |||
| fb013ccc72 | |||
| fee3eecd7f | |||
| 6037018dd2 | |||
| eec7cc4ea7 | |||
| 035895b043 | |||
| ff5ef82ea2 | |||
| 0048205636 | |||
| 0d341d9e7b | |||
| 4c7f572dbe | |||
| 610c272104 | |||
| 32e4565131 | |||
| 9b9c15e4a9 | |||
| b1690f19cc | |||
| 5dcee2564c | |||
| 752160fd85 | |||
| 3937966b74 | |||
| e7ebb92104 | |||
| c78e070636 | |||
| 610ed02373 | |||
| 4b23f51974 | |||
| b25e988628 | |||
| e0758c0e9e | |||
| ba34811f71 | |||
| 28672096a2 | |||
| 9823a4e7dd | |||
| 105ec7ced8 | |||
| 8f9ad987b9 | |||
| 6050827689 | |||
| 9b1ec5dc0e | |||
| 9b1c270501 | |||
| 9d99f8936b | |||
| 7bc9d34a64 | |||
| b1a3b322ee | |||
| ca688d8640 | |||
| ea9f533536 | |||
| 875ebdcfb0 | |||
| 2e07799250 | |||
| 0d9d21a5c1 | |||
| 836e12b1c5 | |||
| 7538aeae0e | |||
| 3e9ba9ca8e | |||
| e4c296a7d2 | |||
| 44748270da | |||
| 8b1dc4c43d | |||
| c105c6da38 | |||
| 3001420a1d | |||
| 432ff444a1 | |||
| aa3c88a6c4 | |||
| 601a98e649 | |||
| 23c7d19ef0 | |||
| 59c49b5833 | |||
| 2cca0bd819 | |||
| fbf4c4add2 | |||
| 78d6ff735e | |||
| a7f51fa666 | |||
| 5175f95289 | |||
| 3c9f4a8ae9 | |||
| 310730782c | |||
| 77a413d750 | |||
| fe31f93c94 | |||
| 9cc0f88e02 | |||
| 412fda10fd | |||
| aa441274c7 | |||
| ba20267151 | |||
| aabb7559b8 | |||
| 606d14a399 | |||
| c049022798 | |||
| cae32c5bd4 | |||
| 63128b7d29 | |||
| f9d810a42f | |||
| 1784a98b2c | |||
| 52be5ae85f | |||
| 0765b19367 | |||
| b8b5190bc6 | |||
| 9475de4d18 | |||
| 4b9e6bad0b | |||
| 9a5ff7a619 | |||
| 5d40cc112d | |||
| aa19897031 | |||
| afd7f957df | |||
| cca4b4bb8e | |||
| b848a83777 | |||
| c0c40c0387 | |||
| 606421776b | |||
| 47009912df | |||
| fea8fcaf61 | |||
| e562f6a24f | |||
| 23acf203e9 | |||
| 94ae940e82 | |||
| 64de716b91 | |||
| 1b1597c49e | |||
| ef5e4a0637 | |||
| a9323c3fcd | |||
| ce61e90135 |
@@ -15,17 +15,21 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
#ide's
|
||||
.vscode
|
||||
**/.vscode/*
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
|
||||
# Extras temp file
|
||||
/tools/owl.js
|
||||
|
||||
release-notes.md
|
||||
|
||||
.rpt2_cache
|
||||
|
||||
# useful in some cases
|
||||
/temp
|
||||
/temp
|
||||
|
||||
# owl-vision
|
||||
*/owl-vision/out/
|
||||
*/owl-vision/.vs/
|
||||
**/*.vsix
|
||||
!*/owl-vision/.vscode/launch.json
|
||||
!*/owl-vision/.vscode/tasks.json
|
||||
|
||||
@@ -113,6 +113,7 @@ Are you new to Owl? This is the place to start!
|
||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
||||
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
|
||||
- [Owl devtools extension](doc/tools/devtools.md)
|
||||
|
||||
## Installing Owl
|
||||
|
||||
@@ -121,8 +122,28 @@ Owl is available on `npm` and can be installed with the following command:
|
||||
```
|
||||
npm install @odoo/owl
|
||||
```
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl](https://github.com/odoo/owl/releases/latest)
|
||||
|
||||
## Installing Owl devtools
|
||||
|
||||
The Owl devtools browser extension is also available in the [release](https://github.com/odoo/owl/releases/latest):
|
||||
Unzip the owl-devtools.zip file and follow the instructions depending on your browser:
|
||||
|
||||
### Chrome
|
||||
|
||||
Go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
|
||||
Select the devtools-chrome folder and that's it, your extension is active!
|
||||
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
|
||||
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
|
||||
|
||||
### Firefox
|
||||
Go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
|
||||
Select any file in the devtools-firefox folder and that's it, your extension is active!
|
||||
Here, you can use the reload button to refresh the extension.
|
||||
|
||||
Note that you may have to open another window or reload your tab to see the extension working.
|
||||
Also note that the extension will only be active on pages that have a sufficient version of owl.
|
||||
|
||||
|
||||
|
||||
@@ -47,3 +47,4 @@ Utility/helpers:
|
||||
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
||||
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
||||
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
||||
- [`batched`](reference/utils.md#batched): utility function to batch function calls
|
||||
|
||||
@@ -61,6 +61,8 @@ The `config` object is an object with some of the following keys:
|
||||
templates (see [translations](translations.md))
|
||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||
the components created by the application.
|
||||
- **`getTemplate ((s: string) => Element | Function | string | void)`**: a function that will be called by owl when it
|
||||
needs a template. If undefined is returned, owl looks into the app templates.
|
||||
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
|
||||
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
||||
|
||||
|
||||
@@ -357,7 +357,7 @@ cleaning operation, since the component may be destroyed before it has even been
|
||||
mounted. The `willDestroy` hook is useful in that situation, since it is always
|
||||
called.
|
||||
|
||||
The `onWillUnmount` hook is used to register a function that will be executed at
|
||||
The `onWillDestroy` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
@@ -451,5 +451,6 @@ console.log(status(component));
|
||||
// logs either:
|
||||
// - 'new', if the component is new and has not been mounted yet
|
||||
// - 'mounted', if the component is currently mounted
|
||||
// - 'cancelled', if the component has not been mounted yet but will be destroyed soon
|
||||
// - 'destroyed' if the component is currently destroyed
|
||||
```
|
||||
|
||||
@@ -51,8 +51,8 @@ that render its content, and a fallback if an error happened.
|
||||
```js
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<t t-if="error" t-slot="fallback">An error occurred</t>
|
||||
<t t-else="" t-slot="content"`;
|
||||
<t t-if="state.error" t-slot="fallback">An error occurred</t>
|
||||
<t t-else="" t-slot="default"/>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ error: false });
|
||||
|
||||
@@ -112,17 +112,16 @@ of a component, rendered by Owl. It only work on a html element tagged by the
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-ref="someDiv"/>
|
||||
<input t-ref="someInput"/>
|
||||
<span>hello</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the component will be able to access the `div` and the component
|
||||
`SubComponent` with the `useRef` hook:
|
||||
In this example, the component will be able to access the `input` with the `useRef` hook:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
inputRef = useRef("someComponent");
|
||||
inputRef = useRef("someInput");
|
||||
|
||||
someMethod() {
|
||||
// here, if component is mounted, refs are active:
|
||||
@@ -139,18 +138,18 @@ The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
`t-component` directives). For example,
|
||||
|
||||
```xml
|
||||
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>
|
||||
```
|
||||
|
||||
Here, the references need to be set like this:
|
||||
|
||||
```js
|
||||
this.ref1 = useRef("component_1");
|
||||
this.ref2 = useRef("component_2");
|
||||
this.ref1 = useRef("div_1");
|
||||
this.ref2 = useRef("div_2");
|
||||
```
|
||||
|
||||
References are only guaranteed to be active while the parent component is mounted.
|
||||
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
||||
If this is not the case, accessing `el` on it will return `null`.
|
||||
|
||||
### `useSubEnv` and `useChildSubEnv`
|
||||
|
||||
@@ -191,12 +190,13 @@ will then be updated accordingly.
|
||||
### `useExternalListener`
|
||||
|
||||
The `useExternalListener` hook helps solve a very common problem: adding and removing
|
||||
a listener on some target whenever a component is mounted/unmounted. For example,
|
||||
a listener on some target whenever a component is mounted/unmounted. It takes a target
|
||||
as its first argument, forwards the other arguments to `addEventListener`. For example,
|
||||
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
|
||||
to be closed:
|
||||
|
||||
```js
|
||||
useExternalListener(window, "click", this.closeMenu);
|
||||
useExternalListener(window, "click", this.closeMenu, { capture: true });
|
||||
```
|
||||
|
||||
### `useComponent`
|
||||
|
||||
@@ -212,6 +212,7 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of
|
||||
- `type`: the main type of the prop being validated
|
||||
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
|
||||
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
|
||||
- `values`: if the type was `Object`, then the `values` key describes the interface of values in the object, this allows validating objects that are used as mappings, where keys are not known in advance but the shape of the values is.
|
||||
- `validate`: this is a function which should return a boolean to determine if
|
||||
the value is valid or not. Useful for custom validation logic.
|
||||
- `optional`: if true, the prop is not mandatory
|
||||
@@ -237,7 +238,7 @@ class ComponentB extends owl.Component {
|
||||
count: {type: Number},
|
||||
messages: {
|
||||
type: Array,
|
||||
element: {type: Object, shape: {id: Boolean, text: String }
|
||||
element: {type: Object, shape: {id: Boolean, text: String }}
|
||||
},
|
||||
date: Date,
|
||||
combinedVal: [Number, Boolean],
|
||||
@@ -275,7 +276,12 @@ class ComponentB extends owl.Component {
|
||||
id: Number,
|
||||
name: {type: String, optional: true},
|
||||
url: String
|
||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
}
|
||||
}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
someObj3: {
|
||||
type: Object,
|
||||
values: { type: Array, element: String },
|
||||
}, // object with arbitary keys where values are arrays of strings
|
||||
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||
someVal: [Boolean, Date], // either a boolean or a date
|
||||
otherValue: true, // indicates that it is a prop
|
||||
|
||||
@@ -152,7 +152,7 @@ This may seem counter-intuitive, but it makes perfect sense in the context of co
|
||||
```js
|
||||
class DoubleCounter extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="state.selected + ': ' + state[state.selected].value"/>
|
||||
<t t-esc="'selected: ' + state.selected + ', value: ' + state[state.selected]"/>
|
||||
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
||||
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
||||
<button t-on-click="changeCounter">Switch counter</button>
|
||||
@@ -193,7 +193,7 @@ to be able to opt out of creating them in the first place. This is the purpose o
|
||||
### `markRaw`
|
||||
|
||||
Marks an object so that it is ignored by the reactivity system, meaning that if this object is ever
|
||||
part of a of a reactive object, it will be returned as is, and no keys in that object will be
|
||||
part of a reactive object, it will be returned as is, and no keys in that object will be
|
||||
observed.
|
||||
|
||||
```js
|
||||
|
||||
@@ -133,7 +133,7 @@ Slots can define a default content, in case the parent did not define them:
|
||||
## Dynamic Slots
|
||||
|
||||
The `t-slot` directive is actually able to use any expressions, using string
|
||||
interplolation:
|
||||
interpolation:
|
||||
|
||||
```xml
|
||||
<t t-slot="{{current}}" />
|
||||
@@ -175,7 +175,7 @@ class Notebook extends Component {
|
||||
<div class="notebook">
|
||||
<div class="tabs">
|
||||
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
|
||||
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
|
||||
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab_index">
|
||||
<t t-esc="props.slots[tab].title"/>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
@@ -376,15 +376,16 @@ An important difference should be made with the usual `QWeb` behaviour: Owl
|
||||
requires the presence of a `t-key` directive, to be able to properly reconcile
|
||||
renderings.
|
||||
|
||||
`t-foreach` can iterate on an array (the current item will be the current value)
|
||||
or an object (the current item will be the current key).
|
||||
`t-foreach` can iterate on any iterable, and also has special support for objects
|
||||
and maps, it will expose the key of the current iteration as the contents of the
|
||||
`t-as`, and the corresponding value with the same name and the suffix `_value`.
|
||||
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||
variables for various data points (note: `$as` will be replaced with the name
|
||||
passed to `t-as`):
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other useful
|
||||
variables (note: `$as` will be replaced with the name passed to `t-as`):
|
||||
|
||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||
integers, but for objects, it provides the value (where `$as` provides the key)
|
||||
- `$as_value`: the current iteration value, identical to `$as` for arrays and
|
||||
other iterables, but for objects and maps, it provides the value (where `$as`
|
||||
provides the key)
|
||||
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
|
||||
- `$as_first`: whether the current item is the first of the iteration
|
||||
(equivalent to `$as_index == 0`)
|
||||
@@ -477,7 +478,7 @@ not work with other iterables, such as `Set`. However, it is only a matter of
|
||||
using the `...` javascript operator. For example:
|
||||
|
||||
```xml
|
||||
<t t-foreach="...items" t-as="item">...</t>
|
||||
<t t-foreach="[...items]" t-as="item">...</t>
|
||||
```
|
||||
|
||||
The `...` operator will convert the `Set` (or any other iterables) into a list,
|
||||
|
||||
@@ -9,6 +9,7 @@ functions are all available in the `owl.utils` namespace.
|
||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||
- [`EventBus`](#eventbus): a simple EventBus
|
||||
- [`validate`](#validate): a validation function
|
||||
- [`batched`](#batched): batch function calls
|
||||
|
||||
## `whenReady`
|
||||
|
||||
@@ -78,3 +79,22 @@ validate(
|
||||
// - 'id' is missing (should be a number),
|
||||
// - 'url' is missing (should be a boolean or list of numbers),
|
||||
```
|
||||
|
||||
## `batched`
|
||||
|
||||
The `batched` function creates a batched version of a callback so that multiple calls to it within the same microtick will only result in a single invocation of the original callback.
|
||||
|
||||
```js
|
||||
function hello() {
|
||||
console.log("hello");
|
||||
}
|
||||
|
||||
const batchedHello = batched(hello);
|
||||
batchedHello();
|
||||
// Nothing is logged
|
||||
batchedHello();
|
||||
// Still not logged
|
||||
|
||||
await Promise.resolve(); // Await the next microtick
|
||||
// "hello" is logged only once
|
||||
```
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Owl Devtools Browser extension
|
||||
|
||||
The owl devtools browser extension is an extension available on chrome or firefox which adds an owl tab
|
||||
to the browser devtools in order to inspect all owl apps that are present on any web page, their components
|
||||
and allows to interract with their data to a certain extend. There is also a profiler available to visualize
|
||||
the components' lifecycle and be able to trace their origin.
|
||||
|
||||
See the [`devtools doc`](devtools_guide.md) for more information.
|
||||
|
||||
## Install the extension manually (for devs)
|
||||
|
||||
In the owl root folder:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
For chrome:
|
||||
|
||||
```bash
|
||||
npm run build:devtools-chrome
|
||||
```
|
||||
|
||||
For firefox:
|
||||
|
||||
```bash
|
||||
npm run build:devtools-firefox
|
||||
```
|
||||
|
||||
You can also run:
|
||||
|
||||
```bash
|
||||
npm run dev:devtools-chrome
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
npm run dev:devtools-firefox
|
||||
```
|
||||
|
||||
to avoid recompiling owl and gain time if it has already been done.
|
||||
|
||||
To run the extension:
|
||||
|
||||
In google chrome: go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
|
||||
Select the output folder (dist/devtools) and that's it, your extension is active!
|
||||
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
|
||||
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
|
||||
|
||||
In firefox: go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
|
||||
Select any file of the output folder (dist/devtools) and that's it, your extension is active!
|
||||
Here, you can use the reload button to refresh the extension.
|
||||
|
||||
Note that you may have to open another window or reload your tab to see the extension working.
|
||||
Also note that the extension will only be active on pages that have a sufficient version of owl.
|
||||
@@ -0,0 +1,155 @@
|
||||
# Owl Devtools Guide
|
||||
|
||||
## Information popup
|
||||
|
||||
After having installed the extension, a new icon will be added to your extension bar.
|
||||
If you don't see it, you can pin the extension using the extensions popup.
|
||||
|
||||
<img src="screenshots/extensions.png"/>
|
||||
|
||||
Clicking on the owl icon will open the information popup. This popup is useful
|
||||
to know in advance whether owl is loaded in the tab or not. This is also indicated
|
||||
by the icon itself: if it is flipped upside-down, it means that owl is not loaded in the
|
||||
active tab. Do note that old versions of owl are not supported by the extension and will
|
||||
therefore be indicated either as obsolete or absent by the extension popup.
|
||||
|
||||
<img src="screenshots/popup.png"/>
|
||||
|
||||
## First steps
|
||||
|
||||
When you are on a page where owl is detected, you can open your devtools either with
|
||||
right-click -> Inspect or using F12. In the devtools menu, you can search for the Owl
|
||||
tab which is added by the extension. It will be present by default at the end of the list but
|
||||
you can drag and drop it at the position you want for easier navigation in the future.
|
||||
|
||||
<img src="screenshots/find_owl_tab.png"/>
|
||||
|
||||
When you open the tab, you arrive on the Components view by default which is one of the
|
||||
two available tabs at the top. Here is an example of the devtools on the Odoo CRM app:
|
||||
|
||||
<img src="screenshots/crm.png"/>
|
||||
|
||||
## Components tab
|
||||
|
||||
The components tab is separated into two sub windows: the components tree in the left and
|
||||
the component details in the right. The components tree will display all the different
|
||||
components that are present in the tab in the form of a tree. The root of this tree is
|
||||
actually the app which is not a component but can still be inspected by the devtools like
|
||||
one. There can also be multiple apps loaded in the page like in website:
|
||||
|
||||
<img src="screenshots/multi_apps.png"/>
|
||||
|
||||
There is a convenient search bar at the top of the components tree which will help finding
|
||||
the components tou want in the tree and also, an element picker can be used to directly select
|
||||
the component you want to focus on in the page which is especially useful when 1trying to find
|
||||
what you want. Just click on the elements picker icon and click on the element you want to focus
|
||||
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
|
||||
page in this mode will highlight it and the same happens anytime in the components tree.
|
||||
|
||||
<img src="screenshots/picker.png"/>
|
||||
|
||||
In the tree itself, the navigation is quite simple and is similar to the one in the Elements tab
|
||||
of the browser's devtools. It is possible to navigate with the keyboard using the arrow keys and
|
||||
multiple shortcuts are available in a custom menu when right-clicking on a component. This menu
|
||||
allows to expand/fold all the children nodes of a component, fold its direct children only, inspect
|
||||
the source code of the component, send it as a global variable in the console, go to the Elements tab
|
||||
and focus on its content, force a rerender of the component, send its observed states to the console
|
||||
as a global variable, inspect its compiled template in the Sources tab or send its raw template
|
||||
to the console.
|
||||
|
||||
<img src="screenshots/menu.png"/>
|
||||
|
||||
The component details window in the right will show the component that is currently selected as well
|
||||
as its env, props, observed states and all the other variables that are present on its instance.
|
||||
While the props and the env are already present on the actual instance of the component and are
|
||||
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
|
||||
|
||||
The observed state is actually information about which variables are being observed by the component:
|
||||
when any property of a reactive object is being read by the component, the component will subscribe
|
||||
to this property which means it will listen to any change that can occur on the property and render
|
||||
when such a change occurs. This can be visualized easily within the devtools inside of the observed
|
||||
state section: observed properties of the reactive object(s) are displayed in bold while the others
|
||||
are greyed out. Do keep in mind that a greyed out property in the observed state of one component
|
||||
may be observed by another and the other way around is also possible. Here is an example for some
|
||||
user Field component:
|
||||
|
||||
<img src="screenshots/states.png"/>
|
||||
|
||||
Navigation inside the properties is also similar to the one in console variables: properties have
|
||||
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
|
||||
send any property to the console using the right-click context menu on it and functions can be inspected
|
||||
in the sources tab as well.
|
||||
|
||||
<img src="screenshots/function_menu.png"/>
|
||||
|
||||
There are several icons available to perform several of the actions described before in the components
|
||||
tree context menu and all these actions are also available by opening the menu by right-clicking on the
|
||||
component's name. Using the left click on the component's name will focus it in the components tree.
|
||||
|
||||
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
|
||||
property's value and modify it using the freshly created input then press enter to apply the changes.
|
||||
Do note that the modified values should be written in JSON format in order to be valid (examples:
|
||||
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Editing any value will produce a
|
||||
manual render of the component (or the root component of the application in the case of env values).
|
||||
Whether the edition has an impact on the component or not and whether it produces an error is the
|
||||
responsability of the user.
|
||||
|
||||
<img src="screenshots/edit.png"/>
|
||||
|
||||
## Profiler
|
||||
|
||||
The profiler tab is the other tab of the owl devtools. It consists in an actions bar at the top and
|
||||
a tree/list of events related to the owl components' renders. Here is an example of the events launched
|
||||
when entering the Odoo Crm app.
|
||||
|
||||
<img src="screenshots/profiler.png"/>
|
||||
|
||||
In the initial state, no event is displayed. You need to activate the recording of events before they
|
||||
are intercepted by the devtools using the record button.
|
||||
|
||||
<img src="screenshots/record.png"/>
|
||||
|
||||
The second button is used to clear all the events that have been recorded. The select can be used to
|
||||
switch between the tree view (which shows the causality between renders) and the events log view which
|
||||
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
|
||||
update and destroy events which reveals the component that initiated the event. Also, a transition line will
|
||||
appear each time a new animation frame has been loaded between events.
|
||||
|
||||
<img src="screenshots/events_log.png"/>
|
||||
|
||||
The third button is only visible in tree view and allows to fold all the render events that were recorded.
|
||||
Some actions are also available when using the right-click on any event of the tree view for navigation
|
||||
purpose in a similar fashion as in the components tree.
|
||||
|
||||
<img src="screenshots/tree_actions.png"/>
|
||||
|
||||
There is also the Trace Renderings and Trace Subscriptions features. These features are independant of the
|
||||
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
|
||||
the console all the render events and allows to show their traceback information. Similarly, the Trace
|
||||
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
|
||||
of the modification.
|
||||
|
||||
<img src="screenshots/trace_rendering.png"/>
|
||||
<img src="screenshots/trace_subscriptions.png"/>
|
||||
|
||||
The Owl Devtools also allow to inspect iframes coded in Owl: when an Owl iframe is detected in the page,
|
||||
the iframe selector will appear next to the tabs. This allows to switch from an iframe to another easily.
|
||||
Be aware that switching iframes will clear all record events from the profiler tab. Iframes detection is
|
||||
currently not working in the firefox version, we are aware of this issue and will try to address it in the
|
||||
future.
|
||||
|
||||
<img src="screenshots/iframes.png"/>
|
||||
|
||||
## Options
|
||||
|
||||
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
|
||||
be toggled using the sun/moon icon at the top-right corner of the tab. There is also a refresh button to
|
||||
completely reset the owl devtools.
|
||||
|
||||
<img src="screenshots/darkmode.png"/>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the feedback from the page to the devtools seems to be cut, you can first try to use the refresh
|
||||
button mentioned above but if it still doesn't seem to work, just close the devtools and refresh the page.
|
||||
This will eventually happen any time a tab stays opened for too long without being refreshed.
|
||||
|
After Width: | Height: | Size: 545 KiB |
|
After Width: | Height: | Size: 387 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 329 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 536 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 488 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 182 KiB |
@@ -0,0 +1,72 @@
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
|
||||
/* Tomorrow Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #8e908c;
|
||||
}
|
||||
|
||||
/* Tomorrow Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-deletion {
|
||||
color: #c82829;
|
||||
}
|
||||
|
||||
/* Tomorrow Orange */
|
||||
.hljs-number,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params,
|
||||
.hljs-meta,
|
||||
.hljs-link {
|
||||
color: #f5871f;
|
||||
}
|
||||
|
||||
/* Tomorrow Yellow */
|
||||
.hljs-attribute {
|
||||
color: #eab700;
|
||||
}
|
||||
|
||||
/* Tomorrow Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
color: #718c00;
|
||||
}
|
||||
|
||||
/* Tomorrow Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #4271ae;
|
||||
}
|
||||
|
||||
/* Tomorrow Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #8959a8;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
background: white;
|
||||
color: #4d4d4c;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
html, body {
|
||||
background-color: #fefefe;
|
||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-gap: 3rem;
|
||||
grid-template-areas: "header" "nav" "main" "footer";
|
||||
grid-template-columns: 1fr;
|
||||
height: 100%;
|
||||
grid-template-rows: 1fr 3rem 1fr 3rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
body {
|
||||
grid-template-rows: 1fr 3rem 1fr 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00A09D;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:visited {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
header {
|
||||
grid-area: header;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
text-align: center;
|
||||
padding-top: 3rem !important;
|
||||
}
|
||||
|
||||
hgroup > p {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
nav {
|
||||
grid-area: nav;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-area: main;
|
||||
/*justify-self: center;*/
|
||||
grid-column: 1 / -1;
|
||||
/*max-width: 80rem !important;*/
|
||||
/*margin: 3rem 0;*/
|
||||
}
|
||||
|
||||
footer {
|
||||
grid-area: footer;
|
||||
padding-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
header, nav, footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
article h3 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
article.brand {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
article.design {
|
||||
background-color: #875A7B;
|
||||
color: #f0eeee;
|
||||
}
|
||||
|
||||
header .homepage-logo {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
margin-left: initial;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row .column {
|
||||
margin-bottom: initial;
|
||||
padding: initial;
|
||||
}
|
||||
}
|
||||
@media (min-width: 60rem) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
margin-left: -1.0rem;
|
||||
width: calc(100% + 2.0rem);
|
||||
}
|
||||
.row .column {
|
||||
margin-bottom: inherit;
|
||||
padding: 0 1.0rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
/*!
|
||||
* Milligram v1.3.0
|
||||
* https://milligram.github.io
|
||||
*
|
||||
* Copyright (c) 2017 CJ Patoilo
|
||||
* Licensed under the MIT license
|
||||
*/
|
||||
|
||||
*,
|
||||
*:after,
|
||||
*:before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #606c76;
|
||||
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 1.6em;
|
||||
font-weight: 300;
|
||||
letter-spacing: .01em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 0.3rem solid #d1d1d1;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
blockquote *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.button,
|
||||
button,
|
||||
input[type='button'],
|
||||
input[type='reset'],
|
||||
input[type='submit'] {
|
||||
background-color: #9b4dca;
|
||||
border: 0.1rem solid #9b4dca;
|
||||
border-radius: .4rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
height: 3.8rem;
|
||||
letter-spacing: .1rem;
|
||||
line-height: 3.8rem;
|
||||
padding: 0 3.0rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button:focus, .button:hover,
|
||||
button:focus,
|
||||
button:hover,
|
||||
input[type='button']:focus,
|
||||
input[type='button']:hover,
|
||||
input[type='reset']:focus,
|
||||
input[type='reset']:hover,
|
||||
input[type='submit']:focus,
|
||||
input[type='submit']:hover {
|
||||
background-color: #606c76;
|
||||
border-color: #606c76;
|
||||
color: #fff;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.button[disabled],
|
||||
button[disabled],
|
||||
input[type='button'][disabled],
|
||||
input[type='reset'][disabled],
|
||||
input[type='submit'][disabled] {
|
||||
cursor: default;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.button[disabled]:focus, .button[disabled]:hover,
|
||||
button[disabled]:focus,
|
||||
button[disabled]:hover,
|
||||
input[type='button'][disabled]:focus,
|
||||
input[type='button'][disabled]:hover,
|
||||
input[type='reset'][disabled]:focus,
|
||||
input[type='reset'][disabled]:hover,
|
||||
input[type='submit'][disabled]:focus,
|
||||
input[type='submit'][disabled]:hover {
|
||||
background-color: #9b4dca;
|
||||
border-color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-outline,
|
||||
button.button-outline,
|
||||
input[type='button'].button-outline,
|
||||
input[type='reset'].button-outline,
|
||||
input[type='submit'].button-outline {
|
||||
background-color: transparent;
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-outline:focus, .button.button-outline:hover,
|
||||
button.button-outline:focus,
|
||||
button.button-outline:hover,
|
||||
input[type='button'].button-outline:focus,
|
||||
input[type='button'].button-outline:hover,
|
||||
input[type='reset'].button-outline:focus,
|
||||
input[type='reset'].button-outline:hover,
|
||||
input[type='submit'].button-outline:focus,
|
||||
input[type='submit'].button-outline:hover {
|
||||
background-color: transparent;
|
||||
border-color: #606c76;
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
|
||||
button.button-outline[disabled]:focus,
|
||||
button.button-outline[disabled]:hover,
|
||||
input[type='button'].button-outline[disabled]:focus,
|
||||
input[type='button'].button-outline[disabled]:hover,
|
||||
input[type='reset'].button-outline[disabled]:focus,
|
||||
input[type='reset'].button-outline[disabled]:hover,
|
||||
input[type='submit'].button-outline[disabled]:focus,
|
||||
input[type='submit'].button-outline[disabled]:hover {
|
||||
border-color: inherit;
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-clear,
|
||||
button.button-clear,
|
||||
input[type='button'].button-clear,
|
||||
input[type='reset'].button-clear,
|
||||
input[type='submit'].button-clear {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-clear:focus, .button.button-clear:hover,
|
||||
button.button-clear:focus,
|
||||
button.button-clear:hover,
|
||||
input[type='button'].button-clear:focus,
|
||||
input[type='button'].button-clear:hover,
|
||||
input[type='reset'].button-clear:focus,
|
||||
input[type='reset'].button-clear:hover,
|
||||
input[type='submit'].button-clear:focus,
|
||||
input[type='submit'].button-clear:hover {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
|
||||
button.button-clear[disabled]:focus,
|
||||
button.button-clear[disabled]:hover,
|
||||
input[type='button'].button-clear[disabled]:focus,
|
||||
input[type='button'].button-clear[disabled]:hover,
|
||||
input[type='reset'].button-clear[disabled]:focus,
|
||||
input[type='reset'].button-clear[disabled]:hover,
|
||||
input[type='submit'].button-clear[disabled]:focus,
|
||||
input[type='submit'].button-clear[disabled]:hover {
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f4f5f6;
|
||||
border-radius: .4rem;
|
||||
font-size: 86%;
|
||||
margin: 0 .2rem;
|
||||
padding: .2rem .5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f4f5f6;
|
||||
border-left: 0.3rem solid #9b4dca;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 0.1rem solid #f4f5f6;
|
||||
margin: 3.0rem 0;
|
||||
}
|
||||
|
||||
input[type='email'],
|
||||
input[type='number'],
|
||||
input[type='password'],
|
||||
input[type='search'],
|
||||
input[type='tel'],
|
||||
input[type='text'],
|
||||
input[type='url'],
|
||||
textarea,
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 0.1rem solid #d1d1d1;
|
||||
border-radius: .4rem;
|
||||
box-shadow: none;
|
||||
box-sizing: inherit;
|
||||
height: 3.8rem;
|
||||
padding: .6rem 1.0rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type='email']:focus,
|
||||
input[type='number']:focus,
|
||||
input[type='password']:focus,
|
||||
input[type='search']:focus,
|
||||
input[type='tel']:focus,
|
||||
input[type='text']:focus,
|
||||
input[type='url']:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: #9b4dca;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
|
||||
padding-right: 3.0rem;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 6.5rem;
|
||||
}
|
||||
|
||||
label,
|
||||
legend {
|
||||
display: block;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.label-inline {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 112.0rem;
|
||||
padding: 0 2.0rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row.row-no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.row.row-no-padding > .column {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.row.row-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row.row-top {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.row.row-bottom {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.row.row-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row.row-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.row.row-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.row .column {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
margin-left: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-10 {
|
||||
margin-left: 10%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-20 {
|
||||
margin-left: 20%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-25 {
|
||||
margin-left: 25%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-33, .row .column.column-offset-34 {
|
||||
margin-left: 33.3333%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-50 {
|
||||
margin-left: 50%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-66, .row .column.column-offset-67 {
|
||||
margin-left: 66.6666%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-75 {
|
||||
margin-left: 75%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-80 {
|
||||
margin-left: 80%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-90 {
|
||||
margin-left: 90%;
|
||||
}
|
||||
|
||||
.row .column.column-10 {
|
||||
flex: 0 0 10%;
|
||||
max-width: 10%;
|
||||
}
|
||||
|
||||
.row .column.column-20 {
|
||||
flex: 0 0 20%;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.row .column.column-25 {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.row .column.column-33, .row .column.column-34 {
|
||||
flex: 0 0 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
}
|
||||
|
||||
.row .column.column-40 {
|
||||
flex: 0 0 40%;
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
.row .column.column-50 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.row .column.column-60 {
|
||||
flex: 0 0 60%;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.row .column.column-66, .row .column.column-67 {
|
||||
flex: 0 0 66.6666%;
|
||||
max-width: 66.6666%;
|
||||
}
|
||||
|
||||
.row .column.column-75 {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.row .column.column-80 {
|
||||
flex: 0 0 80%;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.row .column.column-90 {
|
||||
flex: 0 0 90%;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.row .column .column-top {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.row .column .column-bottom {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.row .column .column-center {
|
||||
-ms-grid-row-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
margin-left: -1.0rem;
|
||||
width: calc(100% + 2.0rem);
|
||||
}
|
||||
.row .column {
|
||||
margin-bottom: inherit;
|
||||
padding: 0 1.0rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #9b4dca;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:focus, a:hover {
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
dl,
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
dl dl,
|
||||
dl ol,
|
||||
dl ul,
|
||||
ol dl,
|
||||
ol ol,
|
||||
ol ul,
|
||||
ul dl,
|
||||
ul ol,
|
||||
ul ul {
|
||||
font-size: 90%;
|
||||
margin: 1.5rem 0 1.5rem 3.0rem;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal inside;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: circle inside;
|
||||
}
|
||||
|
||||
.button,
|
||||
button,
|
||||
dd,
|
||||
dt,
|
||||
li {
|
||||
margin-bottom: 1.0rem;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
form,
|
||||
ol,
|
||||
p,
|
||||
pre,
|
||||
table,
|
||||
ul {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 0.1rem solid #e1e1e1;
|
||||
padding: 1.2rem 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td:first-child,
|
||||
th:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
td:last-child,
|
||||
th:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 300;
|
||||
letter-spacing: -.1rem;
|
||||
margin-bottom: 2.0rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4.6rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3.6rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2.8rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: -.08rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.8rem;
|
||||
letter-spacing: -.05rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=milligram.css.map */
|
||||
@@ -0,0 +1,349 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,15 @@
|
||||
import { mount, Component, useState, loadFile } from "@odoo/owl";
|
||||
|
||||
class Counter extends Component {
|
||||
static template = "Counter";
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
const templates = await loadFile("./counter.xml");
|
||||
|
||||
mount(Counter, document.getElementById("app-container"), { templates });
|
||||
@@ -0,0 +1,7 @@
|
||||
<templates>
|
||||
<t t-name="Counter">
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { loadFile } from "@odoo/owl";
|
||||
|
||||
for (const [className, type] of [["xml", "xml"], ["javascript", "js"]]) {
|
||||
loadFile(`./counter.${type}`).then(code => {
|
||||
const el = document.querySelector(`code.${className}`);
|
||||
el.textContent = code;
|
||||
hljs.highlightBlock(el);
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OWL Framework</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
|
||||
<link rel="stylesheet" href="assets/normalize.css">
|
||||
<link rel="stylesheet" href="assets/milligram.css">
|
||||
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
|
||||
<link rel="stylesheet" href="assets/main.css">
|
||||
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="container">
|
||||
<img class="homepage-logo" src="assets/owl_1f989.png" alt="OWL Logo">
|
||||
<hgroup>
|
||||
<h1>OWL Framework</h1>
|
||||
<p>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
<nav class="container">
|
||||
<p>
|
||||
<a href="https://github.com/odoo/owl" title="https://github.com/odoo/owl">Github</a>
|
||||
•
|
||||
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Documentation</a>
|
||||
•
|
||||
<a href="playground/">Playground</a>
|
||||
</p>
|
||||
</nav>
|
||||
<main>
|
||||
<article class="design">
|
||||
<div class="container">
|
||||
<h2>Why OWL?</h2>
|
||||
<div class="row">
|
||||
<section class="column">
|
||||
<h3>XML based</h3>
|
||||
<p>Templates are based on the XML format, which allows interesting applications. For example, they could be stored in a database and modified dynamically with <tt>xpaths</tt>.</p>
|
||||
</section>
|
||||
<section class="column">
|
||||
<h3>Templates compiled in the browser</h3>
|
||||
<p>This may not be a good fit for all applications, but if you need to generate dynamically user interfaces in the browser, this is very powerful. For example, a generic form view component could generate a specific form user interface for each various models, from a XML view.</p>
|
||||
</section>
|
||||
<section class="column">
|
||||
<h3>No toolchain required</h3>
|
||||
<p>This is extremely useful for some applications, if, for various reasons (security/deployment/dynamic modules/specific assets tools), it is not ok to use standard web tools based on <tt>npm</tt>.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="example">
|
||||
<div class="container">
|
||||
<h2>Example</h2>
|
||||
<div class="row">
|
||||
<section class="column">
|
||||
<pre><code class="javascript"></code></pre>
|
||||
</section>
|
||||
<section class="column">
|
||||
<pre><code class="xml"></code></pre>
|
||||
<div id="app-container"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
<footer>
|
||||
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
|
||||
</footer>
|
||||
<script src="assets/highlight.pack.js"></script>
|
||||
<script type="module" src="display_code.js"></script>
|
||||
<script type="module" src="counter.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OWL Playground</title>
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
|
||||
|
||||
<!-- Application JS/CSS -->
|
||||
<link rel="stylesheet" href="playground.css">
|
||||
<script type="module" src="playground.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -20,7 +20,7 @@ body {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
grid-template-columns: auto 8px 100%;
|
||||
grid-template-columns: auto 8px 1fr;
|
||||
}
|
||||
|
||||
/* LEFT BAR ****************************************/
|
||||
@@ -79,6 +79,19 @@ body {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.share-code {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copied-notification {
|
||||
font-size: x-small;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
white-space: nowrap;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* TABBED EDITOR ****************************************/
|
||||
|
||||
.tabbed-editor {
|
||||
@@ -1,7 +1,19 @@
|
||||
import { SAMPLES } from "./samples.js";
|
||||
import { debounce, loadJS } from "./utils.js";
|
||||
import { exportStandaloneApp } from "./export_snippet.js";
|
||||
const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl;
|
||||
import {
|
||||
App,
|
||||
Component,
|
||||
useState,
|
||||
useRef,
|
||||
onMounted,
|
||||
onWillUnmount,
|
||||
onPatched,
|
||||
onWillUpdateProps,
|
||||
loadFile as _loadFile,
|
||||
whenReady,
|
||||
__info__,
|
||||
useEffect,
|
||||
onWillStart,
|
||||
} from "../owl.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants, helpers, utils
|
||||
@@ -16,40 +28,37 @@ const MODES = {
|
||||
const DEFAULT_XML = `<templates>
|
||||
</templates>`;
|
||||
|
||||
// Memoize loadFile so that samples aren't reloaded whenever a sample is selected
|
||||
const fileCache = {};
|
||||
const loadFile = (path) => {
|
||||
if (!(path in fileCache)) {
|
||||
fileCache[path] = _loadFile(path);
|
||||
}
|
||||
return fileCache[path];
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an iframe, with all the js, css and xml properly injected.
|
||||
*/
|
||||
function makeCodeIframe(js, css, xml) {
|
||||
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`');
|
||||
|
||||
// create iframe
|
||||
const iframe = document.createElement("iframe");
|
||||
|
||||
iframe.onload = () => {
|
||||
const doc = iframe.contentDocument;
|
||||
const utilsMod = doc.createElement("script");
|
||||
utilsMod.setAttribute("type", "module");
|
||||
utilsMod.setAttribute("src", "utils.js");
|
||||
doc.head.appendChild(utilsMod);
|
||||
// inject js
|
||||
const owlScript = doc.createElement("script");
|
||||
owlScript.type = "text/javascript";
|
||||
owlScript.src = "../owl.js";
|
||||
owlScript.addEventListener("load", () => {
|
||||
const script = doc.createElement("script");
|
||||
script.type = "module";
|
||||
const content = `
|
||||
(async function(TEMPLATES) {
|
||||
${js}
|
||||
})(\`${sanitizedXML}\`)`;
|
||||
script.innerHTML = content;
|
||||
doc.body.appendChild(script);
|
||||
});
|
||||
doc.head.appendChild(owlScript);
|
||||
|
||||
// inject css
|
||||
const importMap = doc.createElement("script");
|
||||
importMap.type = "importmap";
|
||||
importMap.textContent = `{ "imports": { "@odoo/owl": "../owl.js" } }`;
|
||||
doc.head.appendChild(importMap);
|
||||
|
||||
const script = doc.createElement("script");
|
||||
script.type = "module";
|
||||
// escape characters with special meaning in template literals
|
||||
const escapedXml = xml.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${");
|
||||
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
|
||||
doc.body.appendChild(script);
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = css;
|
||||
style.innerText = css;
|
||||
doc.head.appendChild(style);
|
||||
};
|
||||
return iframe;
|
||||
@@ -58,53 +67,84 @@ function makeCodeIframe(js, css, xml) {
|
||||
//------------------------------------------------------------------------------
|
||||
// SAMPLES
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const SAMPLES = [
|
||||
{
|
||||
description: "Components",
|
||||
folder: "components",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
{
|
||||
description: "Form Input Bindings",
|
||||
folder: "form",
|
||||
code: ["js", "xml"],
|
||||
},
|
||||
{
|
||||
description: "Inline templates",
|
||||
folder: "single_file_component",
|
||||
code: ["js"],
|
||||
},
|
||||
{
|
||||
description: "Lifecycle demo",
|
||||
folder: "lifecycle_demo",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
{
|
||||
description: "Customized hook",
|
||||
folder: "custom_hooks",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
{
|
||||
description: "Todo List App (with reactivity)",
|
||||
folder: "todo_app",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
{
|
||||
description: "Responsive app",
|
||||
folder: "responsive_app",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
{
|
||||
description: "Slots And Generic Components",
|
||||
folder: "slots",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
{
|
||||
description: "Window Management System",
|
||||
folder: "window_manager",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
{
|
||||
description: "Benchmark example",
|
||||
folder: "benchmark",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
]
|
||||
|
||||
function loadSamples() {
|
||||
let result = SAMPLES.slice();
|
||||
const result = SAMPLES.map(({ description, folder, code }) => ({
|
||||
description,
|
||||
code: async () => Object.fromEntries(
|
||||
await Promise.all(
|
||||
code.map(async (type) => [type, await loadFile(`./samples/${folder}/${folder}.${type}`)])
|
||||
)
|
||||
),
|
||||
}));
|
||||
const localSample = localStorage.getItem("owl-playground-local-sample");
|
||||
if (localSample) {
|
||||
const { js, css, xml } = JSON.parse(localSample);
|
||||
result.unshift({
|
||||
description: "Local Storage Code",
|
||||
code: js,
|
||||
xml,
|
||||
css
|
||||
code: () => Promise.resolve({ js, xml, css }),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function saveLocalSample(js, css, xml) {
|
||||
const str = JSON.stringify({ js, css, xml });
|
||||
localStorage.setItem("owl-playground-local-sample", str);
|
||||
}
|
||||
|
||||
function deleteLocalSample() {
|
||||
localStorage.removeItem("owl-playground-local-sample");
|
||||
}
|
||||
|
||||
function useSamples() {
|
||||
const samples = loadSamples();
|
||||
const component = owl.useComponent();
|
||||
let interval;
|
||||
|
||||
onMounted(() => {
|
||||
const state = component.state;
|
||||
interval = setInterval(() => {
|
||||
if (component.isDirty) {
|
||||
saveLocalSample(state.js, state.css, state.xml);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
return samples;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tabbed editor
|
||||
//------------------------------------------------------------------------------
|
||||
class TabbedEditor extends owl.Component {
|
||||
class TabbedEditor extends Component {
|
||||
setup() {
|
||||
const props = this.props;
|
||||
this.state = useState({
|
||||
@@ -168,7 +208,7 @@ class TabbedEditor extends owl.Component {
|
||||
const resizer = ev => {
|
||||
const delta = ev.clientY - y;
|
||||
y = ev.clientY;
|
||||
this.trigger("updatePanelHeight", { delta });
|
||||
this.props.updatePanelHeight({ delta });
|
||||
};
|
||||
document.body.addEventListener("mousemove", resizer);
|
||||
document.body.addEventListener("mouseup", () => {
|
||||
@@ -205,25 +245,51 @@ TabbedEditor.template = "TabbedEditor";
|
||||
//------------------------------------------------------------------------------
|
||||
// MAIN APP
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
class Playground extends Component {
|
||||
static template = "Playground";
|
||||
static components = { TabbedEditor };
|
||||
setup() {
|
||||
this.version = owl.__info__.version;
|
||||
this.SAMPLES = useSamples();
|
||||
this.isDirty = false;
|
||||
this.version = __info__.version;
|
||||
|
||||
this.isDirty = false;
|
||||
this.state = useState({
|
||||
js: this.SAMPLES[0].code,
|
||||
css: this.SAMPLES[0].css || "",
|
||||
xml: this.SAMPLES[0].xml || DEFAULT_XML,
|
||||
js: "",
|
||||
css: "",
|
||||
xml: DEFAULT_XML,
|
||||
displayWelcome: true,
|
||||
splitLayout: true,
|
||||
leftPaneWidth: Math.ceil(window.innerWidth / 2),
|
||||
topPanelHeight: null
|
||||
});
|
||||
|
||||
this.samples = loadSamples();
|
||||
if (window.location.hash) {
|
||||
try {
|
||||
const { js, css, xml } = JSON.parse(atob(decodeURIComponent(window.location.hash.slice(1))));
|
||||
if ([js, css, xml].every(item => typeof item === "string")) {
|
||||
Object.assign(this.state, { js, css, xml });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
onWillStart(async () => {
|
||||
if (!this.state.js) {
|
||||
this.setSample(await this.samples[0].code());
|
||||
}
|
||||
})
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (this.isDirty) {
|
||||
const { js, css, xml } = this.state;
|
||||
const str = JSON.stringify({ js, css, xml });
|
||||
localStorage.setItem("owl-playground-local-sample", str);
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, () => []);
|
||||
|
||||
this.toggleLayout = debounce(this.toggleLayout, 250, true);
|
||||
this.runCode = debounce(this.runCode, 250, true);
|
||||
this.downloadCode = debounce(this.downloadCode, 250, true);
|
||||
this.exportStandaloneApp = debounce(this.exportStandaloneApp, 250, true);
|
||||
this.content = useRef("content");
|
||||
this.updateCode = this.updateCode.bind(this);
|
||||
}
|
||||
@@ -237,12 +303,23 @@ class App extends owl.Component {
|
||||
this.content.el.appendChild(subiframe);
|
||||
}
|
||||
|
||||
setSample(ev) {
|
||||
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
|
||||
this.state.js = sample.code;
|
||||
shareCode() {
|
||||
const state = btoa(JSON.stringify({ js: this.state.js, css: this.state.css, xml: this.state.xml }));
|
||||
const link = new URL(window.location.href);
|
||||
link.hash = state;
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(link.href);
|
||||
clearTimeout(this.state.copied)
|
||||
this.state.copied = setTimeout(() => this.state.copied = null, 2000);
|
||||
}
|
||||
window.location.href = link.href;
|
||||
}
|
||||
|
||||
setSample(sample) {
|
||||
this.state.js = sample.js;
|
||||
this.state.css = sample.css || "";
|
||||
this.state.xml = sample.xml || DEFAULT_XML;
|
||||
deleteLocalSample();
|
||||
localStorage.removeItem("owl-playground-local-sample");
|
||||
this.isDirty = false;
|
||||
}
|
||||
|
||||
@@ -250,14 +327,14 @@ class App extends owl.Component {
|
||||
return `width:${this.state.leftPaneWidth}px`;
|
||||
}
|
||||
|
||||
get rightPaneStyle() {
|
||||
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
|
||||
}
|
||||
|
||||
get topEditorStyle() {
|
||||
return `flex: 0 0 ${this.state.topPanelHeight}px`;
|
||||
}
|
||||
|
||||
async onSampleChange(ev) {
|
||||
this.setSample(await this.samples.find(s => s.description === ev.target.value).code());
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
const resizer = ev => {
|
||||
this.state.leftPaneWidth = ev.clientX;
|
||||
@@ -284,39 +361,40 @@ class App extends owl.Component {
|
||||
toggleLayout() {
|
||||
this.state.splitLayout = !this.state.splitLayout;
|
||||
}
|
||||
updatePanelHeight(ev) {
|
||||
if (!ev.detail.delta) {
|
||||
return;
|
||||
}
|
||||
let height = this.state.topPanelHeight;
|
||||
if (!height) {
|
||||
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
|
||||
}
|
||||
this.state.topPanelHeight = height + ev.detail.delta;
|
||||
updatePanelHeight({ delta }) {
|
||||
let height = this.state.topPanelHeight || document.querySelector(".tabbed-editor").clientHeight;
|
||||
this.state.topPanelHeight = height + delta;
|
||||
}
|
||||
|
||||
async downloadCode() {
|
||||
async exportStandaloneApp() {
|
||||
const { js, css, xml } = this.state;
|
||||
const content = await exportStandaloneApp(js, css, xml);
|
||||
|
||||
await loadJS("libs/jszip.min.js");
|
||||
const zip = new JSZip();
|
||||
zip.file("app.py", await loadFile("./standalone_app/app.py"));
|
||||
zip.file("index.html", await loadFile("./standalone_app/index.html"));
|
||||
zip.file("owl.js", await loadFile("../owl.js"));
|
||||
zip.file("app.js", `const TEMPLATES = await (await fetch('app.xml')).text();\n${js}`);
|
||||
zip.file("app.css", css);
|
||||
zip.file("app.xml", xml);
|
||||
|
||||
await loadJS("libs/FileSaver.min.js");
|
||||
saveAs(content, "app.zip");
|
||||
saveAs(await zip.generateAsync({ type: "blob" }), "app.zip");
|
||||
}
|
||||
}
|
||||
App.components = { TabbedEditor };
|
||||
App.template = "App";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
document.title = `${document.title} (v${owl.__info__.version})`;
|
||||
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
|
||||
document.title = `${document.title} (v${__info__.version})`;
|
||||
const commit = `https://github.com/odoo/owl/commit/${__info__.hash}`;
|
||||
console.info(`This application is using Owl built with the following commit:`, commit);
|
||||
const [templates] = await Promise.all([
|
||||
owl.loadFile("templates.xml"),
|
||||
owl.whenReady()
|
||||
loadFile("templates.xml"),
|
||||
whenReady()
|
||||
]);
|
||||
const rootApp = new owl.App(App);
|
||||
const rootApp = new App(Playground, { name: "Owl Playground" });
|
||||
rootApp.addTemplates(templates);
|
||||
|
||||
await rootApp.mount(document.body);
|
||||
@@ -0,0 +1,11 @@
|
||||
tr.danger {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
.remove {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Component, mount, useState, onPatched} from "@odoo/owl";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Data generation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let idCounter = 1;
|
||||
const adjectives = [
|
||||
"pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
|
||||
"quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
|
||||
"unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
|
||||
const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
|
||||
const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
|
||||
|
||||
function _random (max) { return Math.round(Math.random() * 1000) % max; };
|
||||
|
||||
function buildData(count) {
|
||||
const data = new Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const label = `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`;
|
||||
data[i] = {
|
||||
id: idCounter++,
|
||||
label,
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Components
|
||||
// -----------------------------------------------------------------------------
|
||||
class Button extends Component {
|
||||
static template = "Button";
|
||||
}
|
||||
|
||||
|
||||
class Row extends Component {
|
||||
static template = "Row";
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = "Root";
|
||||
static components = { Button, Row };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
rows: [],
|
||||
selectedRowId: null
|
||||
});
|
||||
this.benchmarking = false;
|
||||
onPatched(() => {
|
||||
if (this.benchmarking) {
|
||||
this.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start(descr) {
|
||||
this.benchmarking = `[${descr}]`;
|
||||
console.time(this.benchmarking);
|
||||
}
|
||||
stop() {
|
||||
console.timeEnd(this.benchmarking);
|
||||
this.benchmarking = false;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.start('add1000');
|
||||
this.state.rows = buildData(1000);
|
||||
this.state.selectedRowId = null;
|
||||
}
|
||||
|
||||
runLots() {
|
||||
this.start('add10_000');
|
||||
this.state.rows = buildData(10_000);
|
||||
this.state.selectedRowId = null;
|
||||
}
|
||||
|
||||
add() {
|
||||
this.start('append1000');
|
||||
this.state.rows = this.state.rows.concat(buildData(1000));
|
||||
}
|
||||
|
||||
update() {
|
||||
this.start('update1/10th');
|
||||
let index = 0;
|
||||
const rows = this.state.rows;
|
||||
while (index < rows.length) {
|
||||
rows[index].label = rows[index].label + " !!!";
|
||||
index += 10;
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.start('clear');
|
||||
this.state.rows = [];
|
||||
this.state.selectedRowId = null;
|
||||
}
|
||||
|
||||
swapRows() {
|
||||
this.start('swap');
|
||||
const rows = this.state.rows;
|
||||
if (rows.length > 998) {
|
||||
let tmp = rows[1];
|
||||
rows[1] = rows[998];
|
||||
rows[998] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
selectRow(id) {
|
||||
this.start('select');
|
||||
this.state.selectedRowId = id;
|
||||
}
|
||||
|
||||
removeRow(id) {
|
||||
this.start('remove1');
|
||||
const rows = this.state.rows;
|
||||
rows.splice(rows.findIndex(row => row.id === id), 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// dev=false for benchmarking. we don't want to benchmark dev code!
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: false });
|
||||
@@ -0,0 +1,52 @@
|
||||
<templates>
|
||||
<t t-name="Button">
|
||||
<div class='col-sm-6 smallpad'>
|
||||
<button t-att-id="props.id" class='btn btn-primary btn-block' type='button' t-on-click="props.onClick">
|
||||
<t t-esc="props.text"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="Row">
|
||||
<tr t-att-class="props.isSelected ? 'danger' : ''">
|
||||
<td class="col-md-1" t-esc="props.row.id" />
|
||||
<td class="col-md-4">
|
||||
<a t-on-click="() => props.onSelect(props.row.id)" t-esc="props.row.label" />
|
||||
</td>
|
||||
<td class="col-md-1">
|
||||
<a t-on-click="() => props.onRemove(props.row.id)" class="remove">[x]
|
||||
<span class='glyphicon glyphicon-remove' aria-hidden="true" />
|
||||
</a>
|
||||
</td>
|
||||
<td class='col-md-6'/>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-name="Root">
|
||||
<div class='container'>
|
||||
<div class='jumbotron'>
|
||||
<div class='row'>
|
||||
<div class='col-md-6'>
|
||||
<h1>Owl Keyed</h1>
|
||||
</div>
|
||||
<div class='col-md-6'>
|
||||
<div class='row'>
|
||||
<Button id="'run'" onClick.bind="run" text="'Create 1,000 rows'" />
|
||||
<Button id="'runlots'" onClick.bind="runLots" text="'Create 10,000 rows'" />
|
||||
<Button id="'add'" onClick.bind="add" text="'Append 1,000 rows'" />
|
||||
<Button id="'update'" onClick.bind="update" text="'Update every 10th row'" />
|
||||
<Button id="'clear'" onClick.bind="clear" text="'Clear'" />
|
||||
<Button id="'swaprows'" onClick.bind="swapRows" text="'Swap Rows'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class='table table-hover table-striped test-data'>
|
||||
<tbody>
|
||||
<t t-foreach="state.rows" t-as="row" t-key="row.id">
|
||||
<Row row="row" isSelected="row.id === state.selectedRowId" onSelect.bind="selectRow" onRemove.bind="removeRow"/>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,10 @@
|
||||
.greeter {
|
||||
font-size: 20px;
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
margin: 5px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
background-color: #eeeeee;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// In this example, we show how components can be defined and created.
|
||||
import { Component, useState, mount } from "@odoo/owl";
|
||||
|
||||
class Greeter extends Component {
|
||||
static template = "Greeter";
|
||||
|
||||
setup() {
|
||||
this.state = useState({ word: 'Hello' });
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
|
||||
}
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class Root extends Component {
|
||||
static components = { Greeter };
|
||||
static template = "Root"
|
||||
|
||||
setup() {
|
||||
this.state = useState({ name: 'World'});
|
||||
}
|
||||
}
|
||||
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||
@@ -0,0 +1,9 @@
|
||||
<templates>
|
||||
<div t-name="Greeter" class="greeter" t-on-click="toggle">
|
||||
<t t-esc="state.word"/>, <t t-esc="props.name"/>
|
||||
</div>
|
||||
|
||||
<t t-name="Root">
|
||||
<Greeter name="state.name"/>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
button {
|
||||
width: 120px;
|
||||
height: 35px;
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// In this example, we show how hooks can be used or defined.
|
||||
import { Component, mount, useState, onWillDestroy } from "@odoo/owl";
|
||||
|
||||
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||
// position
|
||||
function useMouse() {
|
||||
const position = useState({x:0, y: 0});
|
||||
|
||||
function update(e) {
|
||||
position.x = e.clientX;
|
||||
position.y = e.clientY;
|
||||
}
|
||||
window.addEventListener('mousemove', update);
|
||||
onWillDestroy(() => {
|
||||
window.removeEventListener('mousemove', update);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
|
||||
// Main root component
|
||||
class Root extends Component {
|
||||
static template = "Root";
|
||||
|
||||
setup() {
|
||||
// simple state hook (reactive object)
|
||||
this.counter = useState({ value: 0 });
|
||||
|
||||
// this hooks is bound to the 'mouse' property.
|
||||
this.mouse = useMouse();
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.counter.value++;
|
||||
}
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
<templates>
|
||||
<div t-name="Root">
|
||||
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
|
||||
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -0,0 +1,22 @@
|
||||
// This example illustrate how the t-model directive can be used to synchronize
|
||||
// data between html inputs (and select/textareas) and the state of a component.
|
||||
// Note that there are two controls with t-model="color": they are totally
|
||||
// synchronized.
|
||||
import { Component, useState, mount } from "@odoo/owl";
|
||||
|
||||
class Form extends Component {
|
||||
static template = "Form";
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
text: "",
|
||||
othertext: "",
|
||||
number: 11,
|
||||
color: "",
|
||||
bool: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Form, document.body, { templates: TEMPLATES, dev: true });
|
||||
@@ -0,0 +1,37 @@
|
||||
<templates>
|
||||
<div t-name="Form">
|
||||
<h1>Form</h1>
|
||||
<div>
|
||||
Text (immediate): <input t-model="state.text"/>
|
||||
</div>
|
||||
<div>
|
||||
Other text (lazy): <input t-model.lazy="state.othertext"/>
|
||||
</div>
|
||||
<div>
|
||||
Number: <input t-model.number="state.number"/>
|
||||
</div>
|
||||
<div>
|
||||
Boolean: <input type="checkbox" t-model="state.bool"/>
|
||||
</div>
|
||||
<div>
|
||||
Color, with a select:
|
||||
<select t-model="state.color">
|
||||
<option value="">Select a color</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
Color, with radio buttons:
|
||||
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
|
||||
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
|
||||
</div>
|
||||
<hr/>
|
||||
<h1>State</h1>
|
||||
<div>Text: <t t-esc="state.text"/></div>
|
||||
<div>Other Text: <t t-esc="state.othertext"/></div>
|
||||
<div>Number: <t t-esc="state.number"/></div>
|
||||
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
|
||||
<div>Color: <t t-esc="state.color"/></div>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"owl": ["../../../src/index.ts"],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
button {
|
||||
font-size: 18px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.demo {
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
background-color: #dddddd;
|
||||
width: 250px;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// This example shows all the possible lifecycle hooks
|
||||
//
|
||||
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
|
||||
// methods in the console. Try modifying its state by clicking on it, or by
|
||||
// clicking on the two main buttons, and look into the console to see what
|
||||
// happens.
|
||||
import {
|
||||
Component,
|
||||
useState,
|
||||
mount,
|
||||
useComponent,
|
||||
onWillStart,
|
||||
onMounted,
|
||||
onWillUnmount,
|
||||
onWillUpdateProps,
|
||||
onPatched,
|
||||
onWillPatch,
|
||||
onWillRender,
|
||||
onRendered,
|
||||
onWillDestroy,
|
||||
} from "@odoo/owl";
|
||||
|
||||
function useLogLifecycle() {
|
||||
const component = useComponent();
|
||||
const name = component.constructor.name;
|
||||
onWillStart(() => console.log(`${name}:willStart`));
|
||||
onMounted(() => console.log(`${name}:mounted`));
|
||||
onWillUpdateProps(() => console.log(`${name}:willUpdateProps`));
|
||||
onWillRender(() => console.log(`${name}:willRender`));
|
||||
onRendered(() => console.log(`${name}:rendered`));
|
||||
onWillPatch(() => console.log(`${name}:willPatch`));
|
||||
onPatched(() => console.log(`${name}:patched`));
|
||||
onWillUnmount(() => console.log(`${name}:willUnmount`));
|
||||
onWillDestroy(() => console.log(`${name}:willDestroy`));
|
||||
}
|
||||
|
||||
class DemoComponent extends Component {
|
||||
static template = "DemoComponent";
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
this.state = useState({ n: 0 });
|
||||
}
|
||||
increment() {
|
||||
this.state.n++;
|
||||
}
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = "Root";
|
||||
static components = { DemoComponent };
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
this.state = useState({ n: 0, flag: true });
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.state.n++;
|
||||
}
|
||||
|
||||
toggleSubComponent() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
}
|
||||
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<templates>
|
||||
<div t-name="DemoComponent" t-on-click="increment" class="demo">
|
||||
<div>Demo Sub Component</div>
|
||||
<div>(click on me to update me)</div>
|
||||
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
|
||||
</div>
|
||||
|
||||
<div t-name="Root">
|
||||
<button t-on-click="increment">Increment Parent State</button>
|
||||
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
|
||||
<div t-if="state.flag">
|
||||
<DemoComponent n="state.n"/>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -0,0 +1,56 @@
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app {
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app.desktop {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
flex: 0 0 30px;
|
||||
height: 30px;
|
||||
background-color: cadetblue;
|
||||
color: white;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.controlpanel {
|
||||
flex: 0 0 100px;
|
||||
height: 100px;
|
||||
background-color: #dddddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.formview {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 60%;
|
||||
min-height: 200px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.chatter {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 40%;
|
||||
background-color: #eeeeee;
|
||||
color: #333333;
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// In this example, we show how one can design an application that is responsive:
|
||||
// its UI is different in mobile mode or in desktop mode.
|
||||
//
|
||||
// The main idea is to have a "isMobile" key in the environment, then listen
|
||||
// to resize events and update the env if needed. Then, the whole interface
|
||||
// will be updated, creating and destroying components as needed.
|
||||
//
|
||||
// To see this in action, try resizing the window. The application will switch
|
||||
// to mobile mode whenever it has less than 768px.
|
||||
import { Component, useState, mount, reactive, useEnv } from "@odoo/owl";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function debounce(func, wait, immediate) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
function later() {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
}
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Responsive hook
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function createUI() {
|
||||
const getIsMobile = () => window.innerWidth <= 768;
|
||||
|
||||
const ui = reactive({ isMobile: getIsMobile() });
|
||||
|
||||
const updateEnv = debounce(() => {
|
||||
const isMobile = getIsMobile();
|
||||
if (ui.isMobile !== isMobile) {
|
||||
ui.isMobile = isMobile;
|
||||
}
|
||||
}, 15);
|
||||
window.addEventListener("resize", updateEnv);
|
||||
return ui;
|
||||
}
|
||||
|
||||
function useUI() {
|
||||
const env = useEnv();
|
||||
return useState(env.ui);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Components
|
||||
//------------------------------------------------------------------------------
|
||||
class Navbar extends Component {
|
||||
static template = "Navbar";
|
||||
}
|
||||
|
||||
class MobileSearchView extends Component {
|
||||
static template = "MobileSearchView";
|
||||
}
|
||||
|
||||
class ControlPanel extends Component {
|
||||
static template = "ControlPanel";
|
||||
static components = { MobileSearchView };
|
||||
setup() {
|
||||
this.ui = useUI();
|
||||
}
|
||||
}
|
||||
|
||||
class AdvancedComponent extends Component {
|
||||
static template = "AdvancedComponent";
|
||||
}
|
||||
|
||||
class FormView extends Component {
|
||||
static template = "FormView";
|
||||
static components = { AdvancedComponent };
|
||||
setup() {
|
||||
this.ui = useUI();
|
||||
}
|
||||
}
|
||||
|
||||
class Chatter extends Component {
|
||||
static template = "Chatter";
|
||||
|
||||
setup() {
|
||||
this.messages = Array.from(Array(100).keys());
|
||||
}
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = "Root";
|
||||
static components = { Navbar, ControlPanel, FormView, Chatter };
|
||||
|
||||
setup() {
|
||||
this.ui = useUI();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application Startup
|
||||
//------------------------------------------------------------------------------
|
||||
const env = {
|
||||
ui: createUI()
|
||||
};
|
||||
|
||||
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
|
||||
@@ -0,0 +1,42 @@
|
||||
<templates>
|
||||
<div t-name="Navbar" class="navbar">Navbar</div>
|
||||
|
||||
<div t-name="ControlPanel" class="controlpanel">
|
||||
<h2>Control Panel</h2>
|
||||
<MobileSearchView t-if="ui.isMobile" />
|
||||
</div>
|
||||
|
||||
<div t-name="FormView" class="formview">
|
||||
<h2>Form View</h2>
|
||||
<AdvancedComponent t-if="!ui.isMobile" />
|
||||
</div>
|
||||
|
||||
<div t-name="Chatter" class="chatter">
|
||||
<h2>Chatter</h2>
|
||||
<t t-foreach="messages" t-as="item" t-key="item"><div>Message <t t-esc="item"/></div></t>
|
||||
</div>
|
||||
|
||||
<div t-name="MobileSearchView">Mobile searchview</div>
|
||||
|
||||
<div t-name="AdvancedComponent">
|
||||
This component is only created in desktop mode.
|
||||
<button>Button!</button>
|
||||
</div>
|
||||
|
||||
<t t-name="maincontent">
|
||||
<FormView />
|
||||
<Chatter />
|
||||
</t>
|
||||
<div t-name="Root" class="app" t-att-class="{mobile: ui.isMobile, desktop: !ui.isMobile}">
|
||||
<Navbar/>
|
||||
<ControlPanel/>
|
||||
<div class="content-wrapper" t-if="!ui.isMobile">
|
||||
<div class="content">
|
||||
<t t-call="maincontent"/>
|
||||
</div>
|
||||
</div>
|
||||
<t t-else="">
|
||||
<t t-call="maincontent"/>
|
||||
</t>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -0,0 +1,28 @@
|
||||
// This example illustrates how one can write Owl components with
|
||||
// inline templates.
|
||||
|
||||
import { Component, useState, xml, mount } from "@odoo/owl";
|
||||
|
||||
// Counter component
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="() => state.value++">
|
||||
Click! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 })
|
||||
}
|
||||
|
||||
// Root
|
||||
class Root extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Counter/>
|
||||
<Counter/>
|
||||
</div>`;
|
||||
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true});
|
||||
@@ -0,0 +1,43 @@
|
||||
.main {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #eeeeee;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
margin: 10px;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
.card.full {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.card.small {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
flex: 0 0 25px;
|
||||
font-weight: bold;
|
||||
background-color: darkcyan;
|
||||
color: white;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.card-title button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1 1 auto;
|
||||
padding: 5px;
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// We show here how slots can be used to create generic components.
|
||||
// In this example, the Card component is basically only a container. It is not
|
||||
// aware of its content. It just knows where it should be (with t-slot).
|
||||
// The parent component define the content with t-set-slot.
|
||||
//
|
||||
// Note that the t-on-click event, defined in the Root template, is executed in
|
||||
// the context of the Root component, even though it is inside the Card component
|
||||
import { Component, useState, mount } from "@odoo/owl";
|
||||
|
||||
class Card extends Component {
|
||||
static template = "Card";
|
||||
|
||||
setup() {
|
||||
this.state = useState({ showContent: true });
|
||||
}
|
||||
|
||||
toggleDisplay() {
|
||||
this.state.showContent = !this.state.showContent;
|
||||
}
|
||||
}
|
||||
|
||||
class Counter extends Component {
|
||||
static template = "Counter";
|
||||
|
||||
setup() {
|
||||
this.state = useState({val: 1});
|
||||
}
|
||||
|
||||
inc() {
|
||||
this.state.val++;
|
||||
}
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class Root extends Component {
|
||||
static template = "Root"
|
||||
static components = { Card, Counter };
|
||||
|
||||
setup() {
|
||||
this.state = useState({a: 1, b: 3});
|
||||
}
|
||||
|
||||
inc(key, delta) {
|
||||
this.state[key] += delta;
|
||||
}
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true});
|
||||
@@ -0,0 +1,33 @@
|
||||
<templates>
|
||||
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
|
||||
<div class="card-title">
|
||||
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
|
||||
</div>
|
||||
<t t-if="state.showContent">
|
||||
<div class="card-content" >
|
||||
<t t-slot="content"/>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<t t-slot="footer"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<t t-esc="state.val"/><button t-on-click="inc">Inc</button>
|
||||
</div>
|
||||
|
||||
<div t-name="Root" class="main">
|
||||
<Card title="'Title card A'">
|
||||
<t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
|
||||
<t t-set-slot="footer"><button t-on-click="() => this.inc('a', 1)">Increment A</button></t>
|
||||
</Card>
|
||||
<Card title="'Title card B'">
|
||||
<t t-set-slot="content">
|
||||
<div>Card 2... [<t t-esc="state.b"/>]</div>
|
||||
<Counter />
|
||||
</t>
|
||||
<t t-set-slot="footer"><button t-on-click="() => this.inc('b', -1)">Decrement B</button></t>
|
||||
</Card>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -0,0 +1,379 @@
|
||||
html,body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #4d4d4d;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -155px;
|
||||
width: 100%;
|
||||
font-size: 100px;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
color: rgba(175, 47, 47, 0.15);
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none; /* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.toggle-all + label {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -52px;
|
||||
left: -13px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all + label:before {
|
||||
content: "❯";
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked + label:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: calc(100% - 43px);
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle + label {
|
||||
/*
|
||||
Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked + label {
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #d9d9d9;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: "×";
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: #777;
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #bfbfbf;
|
||||
font-size: 10px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.filters a {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// This example is an implementation of the TodoList application, from the
|
||||
// www.todomvc.com project. This is a non trivial application with some
|
||||
// interesting user interactions. It uses the local storage for persistence.
|
||||
//
|
||||
// In this implementation, we use the owl reactivity mechanism.
|
||||
import { Component, useState, mount, useRef, reactive, useEnv, useEffect } from "@odoo/owl";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants, helpers
|
||||
//------------------------------------------------------------------------------
|
||||
const ENTER_KEY = 13;
|
||||
const ESC_KEY = 27;
|
||||
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
useEffect(el => el && el.focus(), () => [ref.el]);
|
||||
}
|
||||
|
||||
function useStore() {
|
||||
const env = useEnv();
|
||||
return useState(env.store);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Task store
|
||||
//------------------------------------------------------------------------------
|
||||
class TaskList {
|
||||
constructor(tasks) {
|
||||
this.tasks = tasks || [];
|
||||
const taskIds = this.tasks.map((t) => t.id);
|
||||
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
|
||||
}
|
||||
|
||||
addTask(text) {
|
||||
text = text.trim();
|
||||
if (text) {
|
||||
const task = {
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
this.tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
toggleTask(id) {
|
||||
const task = this.tasks.find(t => t.id === id);
|
||||
task.isCompleted = !task.isCompleted;
|
||||
}
|
||||
|
||||
toggleAll(value) {
|
||||
for (let task of this.tasks) {
|
||||
task.isCompleted = value;
|
||||
}
|
||||
}
|
||||
|
||||
clearCompleted() {
|
||||
const tasks = this.tasks.filter(t => t.isCompleted);
|
||||
for (let task of tasks) {
|
||||
this.deleteTask(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
deleteTask(id) {
|
||||
const index = this.tasks.findIndex((t) => t.id === id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
|
||||
updateTask(id, text) {
|
||||
const value = text.trim();
|
||||
if (!value) {
|
||||
this.deleteTask(id);
|
||||
} else {
|
||||
const task = this.tasks.find(t => t.id === id);
|
||||
task.text = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
|
||||
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
|
||||
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
|
||||
saveTasks();
|
||||
return taskStore;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Todo
|
||||
//------------------------------------------------------------------------------
|
||||
class Todo extends Component {
|
||||
static template = "Todo";
|
||||
|
||||
setup() {
|
||||
useAutofocus("input");
|
||||
this.store = useStore();
|
||||
this.state = useState({
|
||||
isEditing: false
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyup(ev) {
|
||||
if (ev.keyCode === ENTER_KEY) {
|
||||
this.updateText(ev.target.value);
|
||||
}
|
||||
if (ev.keyCode === ESC_KEY) {
|
||||
ev.target.value = this.props.text;
|
||||
this.state.isEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur(ev) {
|
||||
this.updateText(ev.target.value);
|
||||
}
|
||||
|
||||
updateText(text) {
|
||||
this.store.updateTask(this.props.id, text);
|
||||
this.state.isEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TodoList
|
||||
//------------------------------------------------------------------------------
|
||||
class TodoList extends Component {
|
||||
static template = "TodoList";
|
||||
static components = { Todo };
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.state = useState({ filter: "all" });
|
||||
}
|
||||
|
||||
get displayedTasks() {
|
||||
const tasks = this.store.tasks;
|
||||
switch (this.state.filter) {
|
||||
case "active":
|
||||
return tasks.filter((t) => !t.isCompleted);
|
||||
case "completed":
|
||||
return tasks.filter((t) => t.isCompleted);
|
||||
case "all":
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
get allChecked() {
|
||||
return this.store.tasks.every(todo => todo.isCompleted);
|
||||
}
|
||||
|
||||
get remaining() {
|
||||
return this.store.tasks.filter(todo => !todo.isCompleted).length;
|
||||
}
|
||||
|
||||
get remainingText() {
|
||||
const items = this.remaining < 2 ? "item" : "items";
|
||||
return ` ${items} left`;
|
||||
}
|
||||
|
||||
addTodo(ev) {
|
||||
if (ev.keyCode === ENTER_KEY) {
|
||||
const text = ev.target.value;
|
||||
if (text.trim()) {
|
||||
this.store.addTask(text);
|
||||
}
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.state.filter = filter;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// App Initialization
|
||||
//------------------------------------------------------------------------------
|
||||
const env = { store: createTaskStore() };
|
||||
mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
|
||||
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<section t-name="TodoList" class="todoapp">
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
|
||||
</header>
|
||||
<section class="main" t-if="store.tasks.length">
|
||||
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="() => store.toggleAll(!allChecked)"/>
|
||||
<label for="toggle-all"></label>
|
||||
<ul class="todo-list">
|
||||
<t t-foreach="displayedTasks" t-as="todo" t-key="todo.id">
|
||||
<Todo id="todo.id" isCompleted="todo.isCompleted" text="todo.text"/>
|
||||
</t>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" t-if="store.tasks.length">
|
||||
<span class="todo-count">
|
||||
<strong>
|
||||
<t t-esc="remaining"/>
|
||||
</strong>
|
||||
<t t-esc="remainingText"/>
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a t-on-click="() => this.setFilter('all')" t-att-class="{selected: state.filter === 'all'}">All</a>
|
||||
</li>
|
||||
<li>
|
||||
<a t-on-click="() => this.setFilter('active')" t-att-class="{selected: state.filter === 'active'}">Active</a>
|
||||
</li>
|
||||
<li>
|
||||
<a t-on-click="() => this.setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="clear-completed" t-if="store.tasks.length gt remaining" t-on-click="() => store.clearCompleted()">
|
||||
Clear completed
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<li t-name="Todo" class="todo" t-att-class="{completed: props.isCompleted, editing: state.isEditing}">
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox" t-on-change="() => store.toggleTask(props.id)" t-att-checked="props.completed"/>
|
||||
<label t-on-dblclick="() => state.isEditing = true">
|
||||
<t t-esc="props.text"/>
|
||||
</label>
|
||||
<button class="destroy" t-on-click="() => store.deleteTask(props.id)"></button>
|
||||
</div>
|
||||
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.text" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
|
||||
</li>
|
||||
</templates>
|
||||
@@ -0,0 +1,70 @@
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto 50px;
|
||||
}
|
||||
|
||||
.window-manager {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #eeeeee;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menubar {
|
||||
background-color: #875a7b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menubar button {
|
||||
height: 40px;
|
||||
font-size: 18px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.window {
|
||||
display: grid;
|
||||
grid-template-rows: 30px auto;
|
||||
border: 1px solid gray;
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
box-shadow: 1px 1px 2px 1px grey;
|
||||
}
|
||||
|
||||
.window.dragging {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.window .header {
|
||||
background-color: #875a7b;
|
||||
display: grid;
|
||||
grid-template-columns: auto 24px;
|
||||
color: white;
|
||||
line-height: 30px;
|
||||
padding-left: 5px;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.window .header .close {
|
||||
cursor: pointer;
|
||||
font-size: 22px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: 20px;
|
||||
}
|
||||
.counter button {
|
||||
width: 80px;
|
||||
height:40px;
|
||||
font-size: 20px;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// This example is slightly more complex than usual. We demonstrate
|
||||
// here a way to manage sub windows in Owl, declaratively. This is still just a
|
||||
// demonstration. Managing windows can be as complex as we want. For example,
|
||||
// we could implement the following features:
|
||||
// - resizing windows
|
||||
// - minimizing windows
|
||||
// - configuration options for windows to make a window non resizeable
|
||||
// - minimal width/height
|
||||
// - better heuristic for initial window position
|
||||
// - ...
|
||||
import { Component, useState, mount, useRef, reactive, useEnv, onMounted } from "@odoo/owl";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Window manager code
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class WindowManager {
|
||||
// contains all components with metadata
|
||||
static Windows = {};
|
||||
windows = {}; // mapping id => info
|
||||
nextId = 1;
|
||||
|
||||
add(type) {
|
||||
const Comp = WindowManager.Windows[type];
|
||||
const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth));
|
||||
const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight));
|
||||
const id = this.nextId++;
|
||||
this.windows[id] = {
|
||||
id,
|
||||
title: Comp.defaultTitle,
|
||||
width: Comp.defaultWidth,
|
||||
height: Comp.defaultHeight,
|
||||
left,
|
||||
top,
|
||||
Component: Comp,
|
||||
};
|
||||
}
|
||||
|
||||
close(id) {
|
||||
delete this.windows[id];
|
||||
}
|
||||
|
||||
updatePosition(id, left, top) {
|
||||
const w = this.windows[id];
|
||||
w.left = left;
|
||||
w.top = top;
|
||||
}
|
||||
|
||||
getWindows() {
|
||||
return Object.values(this.windows);
|
||||
}
|
||||
}
|
||||
|
||||
function createWindowService() {
|
||||
return reactive(new WindowManager());
|
||||
}
|
||||
|
||||
function useWindowService() {
|
||||
const env = useEnv();
|
||||
return useState(env.windowService);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Generic Window Component
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class Window extends Component {
|
||||
static template = "Window";
|
||||
static nextZIndex = 1;
|
||||
zIndex = 0;
|
||||
|
||||
setup() {
|
||||
this.windowService = useWindowService();
|
||||
this.root = useRef('root');
|
||||
onMounted(this.updateZIndex);
|
||||
}
|
||||
|
||||
get style() {
|
||||
let { width, height, top, left } = this.props.info;
|
||||
return `width: ${width}px;height: ${height}px;top:${top}px;left:${left}px;z-index:${this.zIndex}`;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.windowService.close(this.props.info.id);
|
||||
}
|
||||
|
||||
startDragAndDrop(ev) {
|
||||
this.updateZIndex();
|
||||
const self = this;
|
||||
const root = this.root;
|
||||
|
||||
const el = root.el;
|
||||
el.classList.add('dragging');
|
||||
|
||||
const current = this.props.info;
|
||||
const offsetX = current.left - ev.pageX;
|
||||
const offsetY = current.top - ev.pageY;
|
||||
let left, top;
|
||||
|
||||
window.addEventListener("mousemove", moveWindow);
|
||||
window.addEventListener("mouseup", stopDnD, { once: true });
|
||||
|
||||
function moveWindow(ev) {
|
||||
left = Math.max(offsetX + ev.pageX, 0);
|
||||
top = Math.max(offsetY + ev.pageY, 0);
|
||||
el.style.left = `${left}px`;
|
||||
el.style.top = `${top}px`;
|
||||
}
|
||||
function stopDnD() {
|
||||
window.removeEventListener("mousemove", moveWindow);
|
||||
el.classList.remove('dragging');
|
||||
|
||||
if (top !== undefined && left !== undefined) {
|
||||
self.windowService.updatePosition(current.id, left, top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateZIndex() {
|
||||
this.zIndex = Window.nextZIndex++;
|
||||
this.root.el.style['z-index'] = this.zIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Two concrete Window type implementations
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class HelloWorld extends Component {
|
||||
static template = "HelloWorld";
|
||||
static defaultTitle = "Hello Owl!";
|
||||
static defaultWidth = 200;
|
||||
static defaultHeight = 100;
|
||||
}
|
||||
|
||||
|
||||
class Counter extends Component {
|
||||
static template = "Counter";
|
||||
static defaultTitle = "Click Counter";
|
||||
static defaultWidth = 300;
|
||||
static defaultHeight = 120;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
inc() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
// register window components
|
||||
WindowManager.Windows.Hello = HelloWorld;
|
||||
WindowManager.Windows.Counter = Counter;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Window Container
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class WindowContainer extends Component {
|
||||
static template = "WindowContainer";
|
||||
static components = { Window };
|
||||
|
||||
setup() {
|
||||
this.windowService = useWindowService();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Root Component
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class Root extends Component {
|
||||
static template = "Root";
|
||||
static components = { WindowContainer };
|
||||
|
||||
setup() {
|
||||
this.windowService = useWindowService();
|
||||
}
|
||||
|
||||
addWindow(type) {
|
||||
this.windowService.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Setup
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const env = {
|
||||
windowService: createWindowService(),
|
||||
};
|
||||
|
||||
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
|
||||
@@ -0,0 +1,32 @@
|
||||
<templates>
|
||||
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex" t-ref="root">
|
||||
<div class="header">
|
||||
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
|
||||
<span class="close" t-on-click.stop="close">×</span>
|
||||
</div>
|
||||
<t t-slot="default"/>
|
||||
</div>
|
||||
|
||||
<div t-name="WindowContainer" class="window-manager">
|
||||
<Window t-foreach="windowService.getWindows()" t-as="w" t-key="w.id" info="w">
|
||||
<t t-component="w.Component"/>
|
||||
</Window>
|
||||
</div>
|
||||
|
||||
<div t-name="Root" class="app">
|
||||
<WindowContainer/>
|
||||
<div class="menubar">
|
||||
<button t-on-click="() => this.addWindow('Hello')">Say Hello</button>
|
||||
<button t-on-click="() => this.addWindow('Counter')">Counter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="HelloWorld">
|
||||
Some content here...
|
||||
</div>
|
||||
|
||||
<div t-name="Counter" class="counter">
|
||||
<button t-on-click="inc">Click</button>
|
||||
<span><t t-esc="state.value"/></span>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
|
||||
httpd = None
|
||||
def start_server():
|
||||
global httpd
|
||||
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
|
||||
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
|
||||
httpd.serve_forever()
|
||||
|
||||
url = 'http://127.0.0.1:3600'
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Owl Application")
|
||||
print("---------------")
|
||||
print("Server running on: {}".format(url))
|
||||
threading.Thread(target=start_server, daemon=True).start()
|
||||
|
||||
while True:
|
||||
try:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
httpd.server_close()
|
||||
quit(0)
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OWL App</title>
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
|
||||
<script type="module" src="app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<pre>
|
||||
This app requires a web server, simply opening the html file using your browser will not work.
|
||||
You can run a simple web server with one of the following:
|
||||
- if you have python3 installed, run app.py with python3 or run 'python3 -m http.server' in the app folder
|
||||
- if you have node/npm installed, run 'npx serve' in the app folder
|
||||
- if you have php installed, run 'php -S localhost:8080 -t .' in the app folder
|
||||
</pre>
|
||||
<script>if (window.location.protocol !== "file:") document.querySelector("pre").remove()</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
<templates>
|
||||
<div t-name="TabbedEditor" class="tabbed-editor">
|
||||
<div t-name="TabbedEditor" class="tabbed-editor" t-att-style="props.style">
|
||||
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
|
||||
<t t-foreach="['js', 'xml', 'css']" t-as="tab" t-key="tab">
|
||||
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="() => setTab(tab)">
|
||||
@@ -10,18 +10,19 @@
|
||||
<div class="code-editor" t-ref="editor"></div>
|
||||
</div>
|
||||
|
||||
<div t-name="App" class="playground">
|
||||
<div t-name="Playground" class="playground">
|
||||
<div class="left-bar" t-att-class="{split: state.splitLayout}"
|
||||
t-att-style="leftPaneStyle">
|
||||
<div class="menubar">
|
||||
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
|
||||
<select t-on-change="setSample">
|
||||
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index">
|
||||
<select t-on-change="onSampleChange">
|
||||
<option t-foreach="samples" t-as="sample" t-key="sample_index">
|
||||
<t t-esc="sample.description"/>
|
||||
</option>
|
||||
</select>
|
||||
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
|
||||
<a class="btn flash" t-on-click="exportStandaloneApp" title="Download zip that bundles this app with a tiny python web server"><i class="fas fa-download"></i></a>
|
||||
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
|
||||
<a class="btn flash share-code" t-on-click="shareCode" title="Copy a link to this playground"><span>Share</span><span class="copied-notification" t-if="state.copied">Copied link to clipboard!</span></a>
|
||||
</div>
|
||||
<TabbedEditor
|
||||
js="state.js"
|
||||
@@ -36,19 +37,19 @@
|
||||
css="state.css"
|
||||
xml="state.xml"
|
||||
resizeable="true"
|
||||
onUpdatePanelHeight="updatePanelHeight"
|
||||
updatePanelHeight.bind="updatePanelHeight"
|
||||
updateCode="updateCode"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
|
||||
<div class="right-pane" t-att-style="rightPaneStyle">
|
||||
<div class="right-pane">
|
||||
<div class="welcome" t-if="state.displayWelcome">
|
||||
<div>🦉 Odoo Web Library 🦉</div>
|
||||
<div>v<t t-esc="version"/></div>
|
||||
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
|
||||
<div class="note">
|
||||
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
|
||||
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,12 +18,6 @@ export function debounce(func, wait, immediate) {
|
||||
};
|
||||
}
|
||||
|
||||
export function useBus(bus, evName, cb) {
|
||||
bus.addEventListener(evName, cb);
|
||||
owl.onWillUnmount(() => {
|
||||
bus.removeEventListener(evName, cb);
|
||||
});
|
||||
}
|
||||
|
||||
const loadedScripts = {};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Github page and playground
|
||||
|
||||
This folder contains the code for owl's [github page](https://odoo.github.io/owl)
|
||||
and the owl [playground](https://odoo.github.io/owl/playground/). If you're
|
||||
looking for the owl documentation, click [here](/doc)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.0.9",
|
||||
"version": "2.2.11",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
@@ -16,17 +16,21 @@
|
||||
"build:runtime": "rollup -c --failAfterWarnings runtime",
|
||||
"build:compiler": "rollup -c --failAfterWarnings compiler",
|
||||
"build": "npm run build:bundle",
|
||||
"build:devtools": "rollup -c ./tools/devtools/rollup.config.js",
|
||||
"dev:devtools-chrome": "npm run build:devtools -- --config-browser=chrome",
|
||||
"dev:devtools-firefox": "npm run build:devtools -- --config-browser=firefox",
|
||||
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
|
||||
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
|
||||
"test": "jest",
|
||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
||||
"test:watch": "jest --watch",
|
||||
"playground:serve": "python3 tools/server.py || python tools/server.py",
|
||||
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
|
||||
"playground": "npm run build && npm run playground:serve",
|
||||
"preplayground:watch": "npm run build",
|
||||
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
|
||||
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
|
||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
|
||||
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
|
||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
||||
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
||||
"publish": "npm run build && npm publish",
|
||||
"release": "node tools/release.js",
|
||||
"compile_templates": "node tools/compile_xml.js"
|
||||
},
|
||||
@@ -56,7 +60,11 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "2.4.1",
|
||||
"rollup": "^2.56.3",
|
||||
"rollup-plugin-copy": "^3.3.0",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-dts": "^4.2.2",
|
||||
"rollup-plugin-execute": "^1.1.1",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.31.1",
|
||||
"source-map-support": "^0.5.10",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Custom error class that wraps error that happen in the owl lifecycle
|
||||
export class OwlError extends Error {
|
||||
cause?: any;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { OwlError } from "./owl_error";
|
||||
|
||||
/**
|
||||
* Parses an XML string into an XML document, throwing errors on parser errors
|
||||
* instead of returning an XML document containing the parseerror.
|
||||
*
|
||||
* @param xml the string to parse
|
||||
* @returns an XML document corresponding to the content of the string
|
||||
*/
|
||||
export function parseXML(xml: string): XMLDocument {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new OwlError(msg);
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
Attrs,
|
||||
EventHandlers,
|
||||
} from "./parser";
|
||||
import { OwlError } from "../runtime/error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
|
||||
type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
|
||||
const whitespaceRE = /\s+/g;
|
||||
@@ -82,6 +82,14 @@ function isProp(tag: string, key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a template literal that evaluates to str. You can add interpolation
|
||||
* sigils into the string if required
|
||||
*/
|
||||
function toStringExpression(str: string) {
|
||||
return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// BlockDescription
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -160,7 +168,6 @@ interface Context {
|
||||
block: BlockDescription | null;
|
||||
index: number | string;
|
||||
forceNewBlock: boolean;
|
||||
preventRoot?: boolean;
|
||||
isLast?: boolean;
|
||||
translate: boolean;
|
||||
tKeyExpr: string | null;
|
||||
@@ -312,14 +319,13 @@ export class CodeGenerator {
|
||||
mainCode.push(``);
|
||||
for (let block of this.blocks) {
|
||||
if (block.dom) {
|
||||
let xmlString = block.asXmlString();
|
||||
xmlString = xmlString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
||||
let xmlString = toStringExpression(block.asXmlString());
|
||||
if (block.dynamicTagName) {
|
||||
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
|
||||
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
|
||||
mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
|
||||
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
||||
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
||||
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
||||
} else {
|
||||
mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
|
||||
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,7 +383,7 @@ export class CodeGenerator {
|
||||
): BlockDescription {
|
||||
const hasRoot = this.target.hasRoot;
|
||||
const block = new BlockDescription(this.target, type);
|
||||
if (!hasRoot && !ctx.preventRoot) {
|
||||
if (!hasRoot) {
|
||||
this.target.hasRoot = true;
|
||||
block.isRoot = true;
|
||||
}
|
||||
@@ -403,7 +409,7 @@ export class CodeGenerator {
|
||||
blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
|
||||
}
|
||||
|
||||
if (block.isRoot && !ctx.preventRoot) {
|
||||
if (block.isRoot) {
|
||||
if (this.target.on) {
|
||||
blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
|
||||
}
|
||||
@@ -447,6 +453,11 @@ export class CodeGenerator {
|
||||
.join("");
|
||||
}
|
||||
|
||||
translate(str: string): string {
|
||||
const match = translationRE.exec(str) as any;
|
||||
return match[1] + this.translateFn(match[2]) + match[3];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the newly created block name, if any
|
||||
*/
|
||||
@@ -511,7 +522,7 @@ export class CodeGenerator {
|
||||
const isNewBlock = !block || forceNewBlock;
|
||||
if (isNewBlock) {
|
||||
block = this.createBlock(block, "comment", ctx);
|
||||
this.insertBlock(`comment(\`${ast.value}\`)`, block, {
|
||||
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
@@ -527,8 +538,7 @@ export class CodeGenerator {
|
||||
|
||||
let value = ast.value;
|
||||
if (value && ctx.translate !== false) {
|
||||
const match = translationRE.exec(value) as any;
|
||||
value = match[1] + this.translateFn(match[2]) + match[3];
|
||||
value = this.translate(value);
|
||||
}
|
||||
if (!ctx.inPreTag) {
|
||||
value = value.replace(whitespaceRE, " ");
|
||||
@@ -536,7 +546,7 @@ export class CodeGenerator {
|
||||
|
||||
if (!block || forceNewBlock) {
|
||||
block = this.createBlock(block, "text", ctx);
|
||||
this.insertBlock(`text(\`${value}\`)`, block, {
|
||||
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
@@ -582,11 +592,6 @@ export class CodeGenerator {
|
||||
}
|
||||
// attributes
|
||||
const attrs: Attrs = {};
|
||||
const nameSpace = ast.ns || ctx.nameSpace;
|
||||
if (nameSpace && isNewBlock) {
|
||||
// specific namespace uri
|
||||
attrs["block-ns"] = nameSpace;
|
||||
}
|
||||
|
||||
for (let key in ast.attrs) {
|
||||
let expr, attrName;
|
||||
@@ -715,7 +720,10 @@ export class CodeGenerator {
|
||||
attrs["block-ref"] = String(idx);
|
||||
}
|
||||
|
||||
const dom = xmlDoc.createElement(ast.tag);
|
||||
const nameSpace = ast.ns || ctx.nameSpace;
|
||||
const dom = nameSpace
|
||||
? xmlDoc.createElementNS(nameSpace, ast.tag)
|
||||
: xmlDoc.createElement(ast.tag);
|
||||
for (const [attr, val] of Object.entries(attrs)) {
|
||||
if (!(attr === "class" && val === "")) {
|
||||
dom.setAttribute(attr, val);
|
||||
@@ -757,7 +765,7 @@ export class CodeGenerator {
|
||||
if (!current) break;
|
||||
}
|
||||
}
|
||||
this.addLine(`let ${block!.children.map((c) => c.varName)};`, codeIdx);
|
||||
this.addLine(`let ${block!.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
||||
}
|
||||
}
|
||||
return block!.varName;
|
||||
@@ -773,7 +781,8 @@ export class CodeGenerator {
|
||||
expr = compileExpr(ast.expr);
|
||||
if (ast.defaultValue) {
|
||||
this.helpers.add("withDefault");
|
||||
expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
// FIXME: defaultValue is not translated
|
||||
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
||||
}
|
||||
}
|
||||
if (!block || forceNewBlock) {
|
||||
@@ -859,7 +868,7 @@ export class CodeGenerator {
|
||||
if (!current) break;
|
||||
}
|
||||
}
|
||||
this.addLine(`let ${block!.children.map((c) => c.varName)};`, codeIdx);
|
||||
this.addLine(`let ${block!.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
||||
}
|
||||
|
||||
// note: this part is duplicated from end of compilemulti:
|
||||
@@ -890,18 +899,18 @@ export class CodeGenerator {
|
||||
}
|
||||
this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
|
||||
this.target.indentLevel++;
|
||||
this.addLine(`ctx[\`${ast.elem}\`] = ${vals}[${loopVar}];`);
|
||||
this.addLine(`ctx[\`${ast.elem}\`] = ${keys}[${loopVar}];`);
|
||||
if (!ast.hasNoFirst) {
|
||||
this.addLine(`ctx[\`${ast.elem}_first\`] = ${loopVar} === 0;`);
|
||||
}
|
||||
if (!ast.hasNoLast) {
|
||||
this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${vals}.length - 1;`);
|
||||
this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${keys}.length - 1;`);
|
||||
}
|
||||
if (!ast.hasNoIndex) {
|
||||
this.addLine(`ctx[\`${ast.elem}_index\`] = ${loopVar};`);
|
||||
}
|
||||
if (!ast.hasNoValue) {
|
||||
this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`);
|
||||
this.addLine(`ctx[\`${ast.elem}_value\`] = ${vals}[${loopVar}];`);
|
||||
}
|
||||
this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
|
||||
if (this.dev) {
|
||||
@@ -985,7 +994,6 @@ export class CodeGenerator {
|
||||
block,
|
||||
index,
|
||||
forceNewBlock: !isTSet,
|
||||
preventRoot: ctx.preventRoot,
|
||||
isLast: ctx.isLast && i === l - 1,
|
||||
});
|
||||
this.compileAST(child, subCtx);
|
||||
@@ -994,20 +1002,18 @@ export class CodeGenerator {
|
||||
}
|
||||
}
|
||||
if (isNewBlock) {
|
||||
if (block!.hasDynamicChildren) {
|
||||
if (block!.children.length) {
|
||||
const code = this.target.code;
|
||||
const children = block!.children.slice();
|
||||
let current = children.shift();
|
||||
for (let i = codeIdx; i < code.length; i++) {
|
||||
if (code[i].trimStart().startsWith(`const ${current!.varName} `)) {
|
||||
code[i] = code[i].replace(`const ${current!.varName}`, current!.varName);
|
||||
current = children.shift();
|
||||
if (!current) break;
|
||||
}
|
||||
if (block!.hasDynamicChildren && block!.children.length) {
|
||||
const code = this.target.code;
|
||||
const children = block!.children.slice();
|
||||
let current = children.shift();
|
||||
for (let i = codeIdx; i < code.length; i++) {
|
||||
if (code[i].trimStart().startsWith(`const ${current!.varName} `)) {
|
||||
code[i] = code[i].replace(`const ${current!.varName}`, current!.varName);
|
||||
current = children.shift();
|
||||
if (!current) break;
|
||||
}
|
||||
this.addLine(`let ${block!.children.map((c) => c.varName)};`, codeIdx);
|
||||
}
|
||||
this.addLine(`let ${block!.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
||||
}
|
||||
|
||||
const args = block!.children.map((c) => c.varName).join(", ");
|
||||
@@ -1023,32 +1029,31 @@ export class CodeGenerator {
|
||||
ctxVar = generateId("ctx");
|
||||
this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);
|
||||
}
|
||||
const isDynamic = INTERP_REGEXP.test(ast.name);
|
||||
const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
|
||||
if (block && !forceNewBlock) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
if (ast.body) {
|
||||
this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
|
||||
this.addLine(`${ctxVar}[isBoundary] = 1;`);
|
||||
this.helpers.add("isBoundary");
|
||||
const subCtx = createContext(ctx, { preventRoot: true, ctxVar });
|
||||
const subCtx = createContext(ctx, { ctxVar });
|
||||
const bl = this.compileMulti({ type: ASTType.Multi, content: ast.body }, subCtx);
|
||||
if (bl) {
|
||||
this.helpers.add("zero");
|
||||
this.addLine(`${ctxVar}[zero] = ${bl};`);
|
||||
}
|
||||
}
|
||||
const isDynamic = INTERP_REGEXP.test(ast.name);
|
||||
const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
|
||||
if (block) {
|
||||
if (!forceNewBlock) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
}
|
||||
const key = `key + \`${this.generateComponentKey()}\``;
|
||||
|
||||
const key = this.generateComponentKey();
|
||||
if (isDynamic) {
|
||||
const templateVar = generateId("template");
|
||||
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||||
this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
|
||||
}
|
||||
this.define(templateVar, subTemplate);
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block!, {
|
||||
...ctx,
|
||||
forceNewBlock: !block,
|
||||
@@ -1056,7 +1061,6 @@ export class CodeGenerator {
|
||||
} else {
|
||||
const id = generateId(`callTemplate_`);
|
||||
this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block!, {
|
||||
...ctx,
|
||||
forceNewBlock: !block,
|
||||
@@ -1095,10 +1099,13 @@ export class CodeGenerator {
|
||||
} else {
|
||||
let value: string;
|
||||
if (ast.defaultValue) {
|
||||
const defaultValue = toStringExpression(
|
||||
ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue
|
||||
);
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
value = `withDefault(${expr}, ${defaultValue})`;
|
||||
} else {
|
||||
value = `\`${ast.defaultValue}\``;
|
||||
value = defaultValue;
|
||||
}
|
||||
} else {
|
||||
value = expr;
|
||||
@@ -1109,12 +1116,12 @@ export class CodeGenerator {
|
||||
return null;
|
||||
}
|
||||
|
||||
generateComponentKey() {
|
||||
generateComponentKey(currentKey: string = "key") {
|
||||
const parts = [generateId("__")];
|
||||
for (let i = 0; i < this.target.loopLevel; i++) {
|
||||
parts.push(`\${key${i + 1}}`);
|
||||
}
|
||||
return parts.join("__");
|
||||
return `${currentKey} + \`${parts.join("__")}\``;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1217,7 +1224,6 @@ export class CodeGenerator {
|
||||
}
|
||||
|
||||
// cmap key
|
||||
const key = this.generateComponentKey();
|
||||
let expr: string;
|
||||
if (ast.isDynamic) {
|
||||
expr = generateId("Comp");
|
||||
@@ -1235,7 +1241,7 @@ export class CodeGenerator {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
|
||||
let keyArg = `key + \`${key}\``;
|
||||
let keyArg = this.generateComponentKey();
|
||||
if (ctx.tKeyExpr) {
|
||||
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||||
}
|
||||
@@ -1314,7 +1320,7 @@ export class CodeGenerator {
|
||||
}
|
||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||
if (isMultiple) {
|
||||
key = `${key} + \`${this.generateComponentKey()}\``;
|
||||
key = this.generateComponentKey(key);
|
||||
}
|
||||
|
||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
||||
@@ -1357,7 +1363,6 @@ export class CodeGenerator {
|
||||
|
||||
let { block } = ctx;
|
||||
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
||||
const key = this.generateComponentKey();
|
||||
let ctxStr = "ctx";
|
||||
if (this.target.loopLevel || !this.hasSafeContext) {
|
||||
ctxStr = generateId("ctx");
|
||||
@@ -1371,7 +1376,8 @@ export class CodeGenerator {
|
||||
});
|
||||
|
||||
const target = compileExpr(ast.target);
|
||||
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
|
||||
const key = this.generateComponentKey();
|
||||
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;
|
||||
if (block) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TemplateSet } from "../runtime/template_set";
|
||||
import type { BDom } from "../runtime/blockdom";
|
||||
import { CodeGenerator, Config } from "./code_generator";
|
||||
import { parse } from "./parser";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
|
||||
export type Template = (context: any, vnode: any, key?: string) => BDom;
|
||||
|
||||
@@ -27,5 +28,15 @@ export function compile(
|
||||
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
|
||||
const code = codeGenerator.generateCode();
|
||||
// template function
|
||||
return new Function("app, bdom, helpers", code) as TemplateFunction;
|
||||
try {
|
||||
return new Function("app, bdom, helpers", code) as TemplateFunction;
|
||||
} catch (originalError: any) {
|
||||
const { name } = options;
|
||||
const nameStr = name ? `template "${name}"` : "anonymous template";
|
||||
const err = new OwlError(
|
||||
`Failed to compile ${nameStr}: ${originalError.message}\n\ngenerated code:\nfunction(app, bdom, helpers) {\n${code}\n}`
|
||||
);
|
||||
err.cause = originalError;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OwlError } from "../runtime/error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
|
||||
/**
|
||||
* Owl QWeb Expression Parser
|
||||
@@ -268,7 +268,7 @@ export function compileExprToArray(expr: string): Token[] {
|
||||
const localVars = new Set<string>();
|
||||
const tokens = tokenize(expr);
|
||||
let i = 0;
|
||||
let stack = []; // to track last opening [ or {
|
||||
let stack = []; // to track last opening (, [ or {
|
||||
|
||||
while (i < tokens.length) {
|
||||
let token = tokens[i];
|
||||
@@ -279,10 +279,12 @@ export function compileExprToArray(expr: string): Token[] {
|
||||
switch (token.type) {
|
||||
case "LEFT_BRACE":
|
||||
case "LEFT_BRACKET":
|
||||
case "LEFT_PAREN":
|
||||
stack.push(token.type);
|
||||
break;
|
||||
case "RIGHT_BRACE":
|
||||
case "RIGHT_BRACKET":
|
||||
case "RIGHT_PAREN":
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OwlError } from "../runtime/error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { parseXML } from "../common/utils";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// AST Type definition
|
||||
@@ -213,14 +214,14 @@ export function parse(xml: string | Element): AST {
|
||||
|
||||
function _parse(xml: Element): AST {
|
||||
normalizeXML(xml);
|
||||
const ctx = { inPreTag: false, inSVG: false };
|
||||
const ctx = { inPreTag: false };
|
||||
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
|
||||
}
|
||||
|
||||
interface ParsingContext {
|
||||
tModelInfo?: TModelInfo | null;
|
||||
nameSpace?: string;
|
||||
inPreTag: boolean;
|
||||
inSVG: boolean;
|
||||
}
|
||||
|
||||
function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||
@@ -235,10 +236,10 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
parseTSetNode(node, ctx) ||
|
||||
@@ -323,9 +324,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (tagName === "pre") {
|
||||
ctx.inPreTag = true;
|
||||
}
|
||||
const shouldAddSVGNS = ROOT_SVG_TAGS.has(tagName) && !ctx.inSVG;
|
||||
ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
|
||||
const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
|
||||
|
||||
let ns = !ctx.nameSpace && ROOT_SVG_TAGS.has(tagName) ? "http://www.w3.org/2000/svg" : null;
|
||||
const ref = node.getAttribute("t-ref");
|
||||
node.removeAttribute("t-ref");
|
||||
|
||||
@@ -336,10 +336,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
|
||||
for (let attr of nodeAttrsNames) {
|
||||
const value = node.getAttribute(attr)!;
|
||||
if (attr.startsWith("t-on")) {
|
||||
if (attr === "t-on") {
|
||||
throw new OwlError("Missing event name with t-on directive");
|
||||
}
|
||||
if (attr === "t-on" || attr === "t-on-") {
|
||||
throw new OwlError("Missing event name with t-on directive");
|
||||
}
|
||||
if (attr.startsWith("t-on-")) {
|
||||
on = on || {};
|
||||
on[attr.slice(5)] = value;
|
||||
} else if (attr.startsWith("t-model")) {
|
||||
@@ -365,13 +365,11 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
const typeAttr = node.getAttribute("type");
|
||||
const isInput = tagName === "input";
|
||||
const isSelect = tagName === "select";
|
||||
const isTextarea = tagName === "textarea";
|
||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||
const isRadioInput = isInput && typeAttr === "radio";
|
||||
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
|
||||
const hasLazyMod = attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const hasTrimMod = attr.includes(".trim");
|
||||
const hasLazyMod = hasTrimMod || attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||||
|
||||
model = {
|
||||
@@ -381,8 +379,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
||||
eventType,
|
||||
hasDynamicChildren: false,
|
||||
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
|
||||
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
|
||||
shouldTrim: hasTrimMod,
|
||||
shouldNumberize: hasNumberMod,
|
||||
};
|
||||
if (isSelect) {
|
||||
// don't pollute the original ctx
|
||||
@@ -391,6 +389,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
}
|
||||
} else if (attr.startsWith("block-")) {
|
||||
throw new OwlError(`Invalid attribute: '${attr}'`);
|
||||
} else if (attr === "xmlns") {
|
||||
ns = value;
|
||||
} else if (attr !== "t-name") {
|
||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
||||
@@ -403,6 +403,9 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
attrs[attr] = value;
|
||||
}
|
||||
}
|
||||
if (ns) {
|
||||
ctx.nameSpace = ns;
|
||||
}
|
||||
|
||||
const children = parseChildren(node, ctx);
|
||||
return {
|
||||
@@ -446,9 +449,6 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
content: [tesc],
|
||||
};
|
||||
}
|
||||
if (ast.type === ASTType.TComponent) {
|
||||
throw new OwlError("t-esc is not supported on Component nodes");
|
||||
}
|
||||
return tesc;
|
||||
}
|
||||
|
||||
@@ -741,14 +741,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
// be ignored)
|
||||
let el = slotNode.parentElement!;
|
||||
let isInSubComponent = false;
|
||||
while (el !== clone) {
|
||||
while (el && el !== clone) {
|
||||
if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) {
|
||||
isInSubComponent = true;
|
||||
break;
|
||||
}
|
||||
el = el.parentElement!;
|
||||
}
|
||||
if (isInSubComponent) {
|
||||
if (isInSubComponent || !el) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -943,21 +943,23 @@ function normalizeTIf(el: Element) {
|
||||
*
|
||||
* @param el the element containing the tree that should be normalized
|
||||
*/
|
||||
function normalizeTEsc(el: Element) {
|
||||
const elements = [...el.querySelectorAll("[t-esc]")].filter(
|
||||
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
|
||||
);
|
||||
for (const el of elements) {
|
||||
if (el.childNodes.length) {
|
||||
throw new OwlError("Cannot have t-esc on a component that already has content");
|
||||
function normalizeTEscTOut(el: Element) {
|
||||
for (const d of ["t-esc", "t-out"]) {
|
||||
const elements = [...el.querySelectorAll(`[${d}]`)].filter(
|
||||
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
|
||||
);
|
||||
for (const el of elements) {
|
||||
if (el.childNodes.length) {
|
||||
throw new OwlError(`Cannot have ${d} on a component that already has content`);
|
||||
}
|
||||
const value = el.getAttribute(d);
|
||||
el.removeAttribute(d);
|
||||
const t = el.ownerDocument.createElement("t");
|
||||
if (value != null) {
|
||||
t.setAttribute(d, value);
|
||||
}
|
||||
el.appendChild(t);
|
||||
}
|
||||
const value = el.getAttribute("t-esc");
|
||||
el.removeAttribute("t-esc");
|
||||
const t = el.ownerDocument.createElement("t");
|
||||
if (value != null) {
|
||||
t.setAttribute("t-esc", value);
|
||||
}
|
||||
el.appendChild(t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -969,42 +971,5 @@ function normalizeTEsc(el: Element) {
|
||||
*/
|
||||
function normalizeXML(el: Element) {
|
||||
normalizeTIf(el);
|
||||
normalizeTEsc(el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an XML string into an XML document, throwing errors on parser errors
|
||||
* instead of returning an XML document containing the parseerror.
|
||||
*
|
||||
* @param xml the string to parse
|
||||
* @returns an XML document corresponding to the content of the string
|
||||
*/
|
||||
function parseXML(xml: string): XMLDocument {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new OwlError(msg);
|
||||
}
|
||||
|
||||
return doc;
|
||||
normalizeTEscTOut(el);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { version } from "../version";
|
||||
import { Component, ComponentConstructor, Props } from "./component";
|
||||
import { ComponentNode } from "./component_node";
|
||||
import { nodeErrorHandlers, OwlError, handleError } from "./error_handling";
|
||||
import { nodeErrorHandlers, handleError } from "./error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { Fiber, RootFiber, MountOptions } from "./fibers";
|
||||
import { Scheduler } from "./scheduler";
|
||||
import { validateProps } from "./template_helpers";
|
||||
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
||||
import { validateTarget } from "./utils";
|
||||
import { toRaw, reactive } from "./reactivity";
|
||||
|
||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||
|
||||
@@ -33,21 +35,21 @@ This is not suitable for production use.
|
||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
||||
};
|
||||
|
||||
const apps = new Set<App>();
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__OWL_DEVTOOLS__: {
|
||||
apps: Set<App>;
|
||||
Fiber: typeof Fiber;
|
||||
RootFiber: typeof RootFiber;
|
||||
toRaw: typeof toRaw;
|
||||
reactive: typeof reactive;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.__OWL_DEVTOOLS__ ||= {
|
||||
apps: new Set<App>(),
|
||||
Fiber: Fiber,
|
||||
RootFiber: RootFiber,
|
||||
};
|
||||
window.__OWL_DEVTOOLS__ ||= { apps, Fiber, RootFiber, toRaw, reactive };
|
||||
|
||||
export class App<
|
||||
T extends abstract new (...args: any) => any = any,
|
||||
@@ -55,6 +57,7 @@ export class App<
|
||||
E = any
|
||||
> extends TemplateSet {
|
||||
static validateTarget = validateTarget;
|
||||
static apps = apps;
|
||||
static version = version;
|
||||
|
||||
name: string;
|
||||
@@ -69,7 +72,7 @@ export class App<
|
||||
super(config);
|
||||
this.name = config.name || "";
|
||||
this.Root = Root;
|
||||
window.__OWL_DEVTOOLS__.apps.add(this);
|
||||
apps.add(this);
|
||||
if (config.test) {
|
||||
this.dev = true;
|
||||
}
|
||||
@@ -84,7 +87,10 @@ export class App<
|
||||
this.props = config.props || ({} as P);
|
||||
}
|
||||
|
||||
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
|
||||
mount(
|
||||
target: HTMLElement | ShadowRoot,
|
||||
options?: MountOptions
|
||||
): Promise<Component<P, E> & InstanceType<T>> {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
||||
@@ -99,7 +105,7 @@ export class App<
|
||||
return new ComponentNode(Component, props, this, null, null);
|
||||
}
|
||||
|
||||
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
|
||||
mountNode(node: ComponentNode, target: HTMLElement | ShadowRoot, options?: MountOptions) {
|
||||
const promise: any = new Promise((resolve, reject) => {
|
||||
let isResolved = false;
|
||||
// manually set a onMounted callback.
|
||||
@@ -128,10 +134,10 @@ export class App<
|
||||
|
||||
destroy() {
|
||||
if (this.root) {
|
||||
this.scheduler.flush();
|
||||
this.root.destroy();
|
||||
this.scheduler.processTasks();
|
||||
}
|
||||
window.__OWL_DEVTOOLS__.apps.delete(this);
|
||||
apps.delete(this);
|
||||
}
|
||||
|
||||
createComponent<P extends Props>(
|
||||
|
||||
@@ -36,10 +36,18 @@ export function createAttrUpdater(attr: string): Setter<HTMLElement> {
|
||||
|
||||
export function attrsSetter(this: HTMLElement, attrs: any) {
|
||||
if (isArray(attrs)) {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
if (attrs[0] === "class") {
|
||||
setClass.call(this, attrs[1]);
|
||||
} else {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
}
|
||||
} else {
|
||||
for (let k in attrs) {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
if (k === "class") {
|
||||
setClass.call(this, attrs[k]);
|
||||
} else {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +60,11 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
||||
if (val === oldAttrs[1]) {
|
||||
return;
|
||||
}
|
||||
setAttribute.call(this, name, val);
|
||||
if (name === "class") {
|
||||
updateClass.call(this, val, oldAttrs[1]);
|
||||
} else {
|
||||
setAttribute.call(this, name, val);
|
||||
}
|
||||
} else {
|
||||
removeAttribute.call(this, oldAttrs[0]);
|
||||
setAttribute.call(this, name, val);
|
||||
@@ -60,13 +72,21 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
||||
} else {
|
||||
for (let k in oldAttrs) {
|
||||
if (!(k in attrs)) {
|
||||
removeAttribute.call(this, k);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, "", oldAttrs[k]);
|
||||
} else {
|
||||
removeAttribute.call(this, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let k in attrs) {
|
||||
const val = attrs[k];
|
||||
if (val !== oldAttrs[k]) {
|
||||
setAttribute.call(this, k, val);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, val, oldAttrs[k]);
|
||||
} else {
|
||||
setAttribute.call(this, k, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OwlError } from "../error_handling";
|
||||
import { OwlError } from "../../common/owl_error";
|
||||
import { attrsSetter, attrsUpdater, createAttrUpdater, setClass, updateClass } from "./attributes";
|
||||
import { config } from "./config";
|
||||
import { createEventHandler } from "./events";
|
||||
@@ -144,12 +144,7 @@ function buildTree(
|
||||
info.push({ type: "child", idx: index });
|
||||
el = document.createTextNode("");
|
||||
}
|
||||
const attrs = (node as Element).attributes;
|
||||
const ns = attrs.getNamedItem("block-ns");
|
||||
if (ns) {
|
||||
attrs.removeNamedItem("block-ns");
|
||||
currentNS = ns.value;
|
||||
}
|
||||
currentNS ||= (node as Element).namespaceURI;
|
||||
if (!el) {
|
||||
el = currentNS
|
||||
? document.createElementNS(currentNS, tagName)
|
||||
@@ -165,6 +160,7 @@ function buildTree(
|
||||
const fragment = document.createElement("template").content;
|
||||
fragment.appendChild(el);
|
||||
}
|
||||
const attrs = (node as Element).attributes;
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
const attrName = attrs[i].name;
|
||||
const attrValue = attrs[i].value;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { inOwnerDocument } from "../utils";
|
||||
import { config } from "./config";
|
||||
|
||||
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
||||
@@ -28,7 +29,7 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
|
||||
|
||||
function listener(ev: Event) {
|
||||
const currentTarget = ev.currentTarget as HTMLElement;
|
||||
if (!currentTarget || !currentTarget.ownerDocument.contains(currentTarget)) return;
|
||||
if (!currentTarget || !inOwnerDocument(currentTarget)) return;
|
||||
const data = (currentTarget as any)[eventKey];
|
||||
if (!data) return;
|
||||
config.mainEventHandler(data, ev, currentTarget);
|
||||
|
||||
@@ -23,7 +23,7 @@ export type ComponentConstructor<P extends Props = any, E = any> = (new (
|
||||
|
||||
export class Component<Props = any, Env = any> {
|
||||
static template: string = "";
|
||||
static props?: any;
|
||||
static props?: Schema;
|
||||
static defaultProps?: any;
|
||||
|
||||
props: Props;
|
||||
|
||||