Compare commits

..

1 Commits

Author SHA1 Message Date
Géry Debongnie cf02319c34 [REF] app: move app specific error handling code
It was before in the generic error handling file. However, since it is
there, it is not possible to modify this behaviour by subclassing App.
Another usecase would be for the devtools extension: with this commit,
it is now easily able to listen to app destruction events.
2023-03-16 15:33:59 +01:00
183 changed files with 2527 additions and 18791 deletions
+4 -1
View File
@@ -20,9 +20,12 @@ yarn-error.log*
node_modules
# Extras temp file
/tools/owl.js
release-notes.md
.rpt2_cache
# useful in some cases
/temp
/temp
+1 -22
View File
@@ -113,7 +113,6 @@ 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
@@ -122,28 +121,8 @@ Owl is available on `npm` and can be installed with the following command:
```
npm install @odoo/owl
```
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl](https://github.com/odoo/owl/releases/latest)
## Installing Owl devtools
The Owl devtools browser extension is also available in the [release](https://github.com/odoo/owl/releases/latest):
Unzip the owl-devtools.zip file and follow the instructions depending on your browser:
### Chrome
Go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
Select the devtools-chrome folder and that's it, your extension is active!
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
### Firefox
Go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
Select any file in the devtools-firefox folder and that's it, your extension is active!
Here, you can use the reload button to refresh the extension.
Note that you may have to open another window or reload your tab to see the extension working.
Also note that the extension will only be active on pages that have a sufficient version of owl.
-1
View File
@@ -451,6 +451,5 @@ 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
```
+8 -7
View File
@@ -112,16 +112,17 @@ of a component, rendered by Owl. It only work on a html element tagged by the
```xml
<div>
<input t-ref="someInput"/>
<input t-ref="someDiv"/>
<span>hello</span>
</div>
```
In this example, the component will be able to access the `input` with the `useRef` hook:
In this example, the component will be able to access the `div` and the component
`SubComponent` with the `useRef` hook:
```js
class Parent extends Component {
inputRef = useRef("someInput");
inputRef = useRef("someComponent");
someMethod() {
// here, if component is mounted, refs are active:
@@ -138,18 +139,18 @@ The `t-ref` directive also accepts dynamic values with string interpolation
`t-component` directives). For example,
```xml
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
```
Here, the references need to be set like this:
```js
this.ref1 = useRef("div_1");
this.ref2 = useRef("div_2");
this.ref1 = useRef("component_1");
this.ref2 = useRef("component_2");
```
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` on it will return `null`.
If this is not the case, accessing `el` or `comp` on it will return `null`.
### `useSubEnv` and `useChildSubEnv`
-5
View File
@@ -212,7 +212,6 @@ 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
@@ -277,10 +276,6 @@ class ComponentB extends owl.Component {
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someObj3: {
type: Object,
values: { type: Array, element: String },
}, // object with arbitary keys where values are arrays of strings
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
+1 -1
View File
@@ -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_index">
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
<t t-esc="props.slots[tab].title"/>
</span>
</t>
+8 -9
View File
@@ -376,16 +376,15 @@ An important difference should be made with the usual `QWeb` behaviour: Owl
requires the presence of a `t-key` directive, to be able to properly reconcile
renderings.
`t-foreach` can iterate on any iterable, and also has special support for objects
and maps, it will expose the key of the current iteration as the contents of the
`t-as`, and the corresponding value with the same name and the suffix `_value`.
`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).
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`):
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`):
- `$as_value`: the current iteration value, identical to `$as` for arrays and
other iterables, but for objects and maps, it provides the value (where `$as`
provides the key)
- `$as_value`: the current iteration value, identical to `$as` for lists and
integers, but for objects, it provides the value (where `$as` provides the key)
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
- `$as_first`: whether the current item is the first of the iteration
(equivalent to `$as_index == 0`)
@@ -478,7 +477,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,
-56
View File
@@ -1,56 +0,0 @@
# Owl Devtools Browser extension
The owl devtools browser extension is an extension available on chrome or firefox which adds an owl tab
to the browser devtools in order to inspect all owl apps that are present on any web page, their components
and allows to interract with their data to a certain extend. There is also a profiler available to visualize
the components' lifecycle and be able to trace their origin.
See the [`devtools doc`](devtools_guide.md) for more information.
## Install the extension manually (for devs)
In the owl root folder:
```bash
npm install
```
For chrome:
```bash
npm run build:devtools-chrome
```
For firefox:
```bash
npm run build:devtools-firefox
```
You can also run:
```bash
npm run dev:devtools-chrome
```
or
```bash
npm run dev:devtools-firefox
```
to avoid recompiling owl and gain time if it has already been done.
To run the extension:
In google chrome: go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
Select the output folder (dist/devtools) and that's it, your extension is active!
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
In firefox: go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
Select any file of the output folder (dist/devtools) and that's it, your extension is active!
Here, you can use the reload button to refresh the extension.
Note that you may have to open another window or reload your tab to see the extension working.
Also note that the extension will only be active on pages that have a sufficient version of owl.
-150
View File
@@ -1,150 +0,0 @@
# Owl Devtools Guide
## Information popup
After having installed the extension, a new icon will be added to your extension bar.
If you don't see it, you can pin the extension using the extensions popup.
<img src="screenshots/extensions.png"/>
Clicking on the owl icon will open the information popup. This popup is useful
to know in advance whether owl is loaded in the tab or not. This is also indicated
by the icon itself: if it is flipped upside-down, it means that owl is not loaded in the
active tab. Do note that old versions of owl are not supported by the extension and will
therefore be indicated either as obsolete or absent by the extension popup.
<img src="screenshots/popup.png"/>
## First steps
When you are on a page where owl is detected, you can open your devtools either with
right-click -> Inspect or using F12. In the devtools menu, you can search for the Owl
tab which is added by the extension. It will be present by default at the end of the list but
you can drag and drop it at the position you want for easier navigation in the future.
<img src="screenshots/find_owl_tab.png"/>
When you open the tab, you arrive on the Components view by default which is one of the
two available tabs at the top. Here is an example of the devtools on the Odoo CRM app:
<img src="screenshots/crm.png"/>
## Components tab
The components tab is separated into two sub windows: the components tree in the left and
the component details in the right. The components tree will display all the different
components that are present in the tab in the form of a tree. The root of this tree is
actually the app which is not a component but can still be inspected by the devtools like
one. There can also be multiple apps loaded in the page like in the following:
<img src="screenshots/multi_apps.png"/>
There is a convenient search bar at the top of the components tree which will help finding
the components tou want in the tree and also, an element picker can be used to directly select
the component you want to focus on in the page which is especially useful when trying to find
what you want. Just click on the elements picker icon and click on the element you want to focus
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
page in this mode will highlight it and the same happens anytime in the components tree.
<img src="screenshots/picker.png"/>
In the tree itself, the navigation is quite simple and is similar to the one in the Elements tab
of the browser's devtools. It is possible to navigate with the keyboard using the arrow keys and
multiple shortcuts are available in a custom menu when right-clicking on a component. This menu
allows to expand/fold all the children nodes of a component, fold its direct children only, inspect
the source code of the component, send it as a global variable in the console, go to the Elements tab
and focus on its content, force a rerender of the component, send its observed states to the console
as a global variable, inspect its compiled template in the Sources tab or send its raw template
to the console.
<img src="screenshots/menu.png"/>
The component details window in the right will show the component that is currently selected as well
as its env, props, observed states and all the other variables that are present on its instance.
While the props and the env are already present on the actual instance of the component and are
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
The observed state is actually information about which variables are observed by the component
which will trigger a rerender of the component when it is modified. The keys represent which part of the
variable is actually observed and the target is the actual variable. For simplicity, the properties
that are not observed by the component are greyed out while the others are in bold. This means that
editing bold ones will trigger a rerender while the greyed out ones will not.
<img src="screenshots/states.png"/>
In the given example, we have two keys/target pairs for two different variables. The first one indicates
that adding or removing an element to the array will trigger a rerender since the length will have changed.
Replacing the element at index 0, 1 or 2 will also have the same effect as implied by the keys. It doesn't
mean that editing the properties of element at index 0, 1 or 2 will rerender the component though. It may
be the case for some but this will be described in another keys/target pair. The second keys/target pair
is actually the element at index 0 of the first pair. It only has id in the keys meaning that only the
id property will actually trigger a rerender the component when modified. Be aware however that the other
properties may be in the observed state of another component like a child one in this case. A greyed out
property only implies it is not reactive for the selected component and not for the others.
The navigation inside the properties is also similar to the one in console variables: properties have
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
send any property to the console using the right-click context menu on it and functions can be inspected
in the sources tab as well.
<img src="screenshots/function_menu.png"/>
There are several icons available to perform several of the actions described before in the components
tree context menu and all these actions are also available by opening the menu by right-clicking on the
component's name. Using the left click on the component's name will focus it in the components tree.
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
property's value and modify it using the freshly created input then press enter to apply the changes.
Do note that the modified values should be written in JSON format in order to be valid (examples:
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Whether it has an impact on the
component or not and whether it produces an error is the responsability of the user.
<img src="screenshots/edit.png"/>
## Profiler
The profiler tab is the other tab of the owl devtools. It consists in an actions bar at the top and
a tree/list of events related to the owl components' renders. Here is an example of the events launched
when entering the Odoo Crm app.
<img src="screenshots/profiler.png"/>
In the initial state, no event is displayed. You need to activate the recording of events before they
are intercepted by the devtools using the record button.
<img src="screenshots/record.png"/>
The second button is used to clear all the events that have been recorded. The select can be used to
switch between the tree view (which shows the causality between renders) and the events log view which
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
update and destroy events which reveals the component that initiated the event.
<img src="screenshots/events_log.png"/>
The third button is only visible in tree view and allows to fold all the render events that were recorded.
Some actions are also available when using the right-click on any event of the tree view for navigation
purpose in a similar fashion as in the components tree.
<img src="screenshots/tree_actions.png"/>
There is also the Trace Renderings and Trace Subscriptions features. These features are independant of the
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
the console all the render events and allows to show their traceback information. Similarly, the Trace
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
of the modification
<img src="screenshots/trace_rendering.png"/>
<img src="screenshots/trace_subscriptions.png"/>
## Options
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
be toggled using the sun/moon icon at the top-right corner of the tab. All the examples above were created
with the dark mode enabled. There is also a refresh button to completely reset the owl devtools.
<img src="screenshots/darkmode.png"/>
## Troubleshooting
If the feedback from the page to the devtools seems to be cut, just close the devtools and refresh the page.
This will eventually happen any time a tab stays opened for too long without being refreshed.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

File diff suppressed because one or more lines are too long
-72
View File
@@ -1,72 +0,0 @@
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #8e908c;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #c82829;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5871f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #eab700;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #718c00;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #4271ae;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #8959a8;
}
.hljs {
display: block;
overflow-x: auto;
background: white;
color: #4d4d4c;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
-116
View File
@@ -1,116 +0,0 @@
html, body {
background-color: #fefefe;
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
height: 100%;
margin: 0;
text-rendering: optimizeLegibility;
}
body {
display: grid;
grid-gap: 3rem;
grid-template-areas: "header" "nav" "main" "footer";
grid-template-columns: 1fr;
height: 100%;
grid-template-rows: 1fr 3rem 1fr 3rem;
}
@media screen and (max-width: 800px) {
body {
grid-template-rows: 1fr 3rem 1fr 5rem;
}
}
a {
color: #00A09D;
text-decoration: none;
}
a:hover, a:active, a:visited {
text-decoration: underline;
}
header {
grid-area: header;
display: flex;
align-items: center;
flex-direction: column;
justify-content: flex-end;
text-align: center;
padding-top: 3rem !important;
}
hgroup > p {
margin-bottom: auto;
}
nav {
grid-area: nav;
}
main {
grid-area: main;
/*justify-self: center;*/
grid-column: 1 / -1;
/*max-width: 80rem !important;*/
/*margin: 3rem 0;*/
}
footer {
grid-area: footer;
padding-bottom: 3rem !important;
}
header, nav, footer {
text-align: center;
}
article {
padding: 3rem 0;
}
article h3 {
font-size: 2rem;
font-weight: 700;
}
article.brand {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
article.design {
background-color: #875A7B;
color: #f0eeee;
}
header .homepage-logo {
margin-bottom: 2rem;
}
@media (min-width: 40rem) {
.row {
flex-direction: column;
margin-left: initial;
width: 100%;
}
.row .column {
margin-bottom: initial;
padding: initial;
}
}
@media (min-width: 60rem) {
.row {
flex-direction: row;
margin-left: -1.0rem;
width: calc(100% + 2.0rem);
}
.row .column {
margin-bottom: inherit;
padding: 0 1.0rem;
}
}
-602
View File
@@ -1,602 +0,0 @@
/*!
* Milligram v1.3.0
* https://milligram.github.io
*
* Copyright (c) 2017 CJ Patoilo
* Licensed under the MIT license
*/
*,
*:after,
*:before {
box-sizing: inherit;
}
html {
box-sizing: border-box;
font-size: 62.5%;
}
body {
color: #606c76;
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
font-size: 1.6em;
font-weight: 300;
letter-spacing: .01em;
line-height: 1.6;
}
blockquote {
border-left: 0.3rem solid #d1d1d1;
margin-left: 0;
margin-right: 0;
padding: 1rem 1.5rem;
}
blockquote *:last-child {
margin-bottom: 0;
}
.button,
button,
input[type='button'],
input[type='reset'],
input[type='submit'] {
background-color: #9b4dca;
border: 0.1rem solid #9b4dca;
border-radius: .4rem;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 1.1rem;
font-weight: 700;
height: 3.8rem;
letter-spacing: .1rem;
line-height: 3.8rem;
padding: 0 3.0rem;
text-align: center;
text-decoration: none;
text-transform: uppercase;
white-space: nowrap;
}
.button:focus, .button:hover,
button:focus,
button:hover,
input[type='button']:focus,
input[type='button']:hover,
input[type='reset']:focus,
input[type='reset']:hover,
input[type='submit']:focus,
input[type='submit']:hover {
background-color: #606c76;
border-color: #606c76;
color: #fff;
outline: 0;
}
.button[disabled],
button[disabled],
input[type='button'][disabled],
input[type='reset'][disabled],
input[type='submit'][disabled] {
cursor: default;
opacity: .5;
}
.button[disabled]:focus, .button[disabled]:hover,
button[disabled]:focus,
button[disabled]:hover,
input[type='button'][disabled]:focus,
input[type='button'][disabled]:hover,
input[type='reset'][disabled]:focus,
input[type='reset'][disabled]:hover,
input[type='submit'][disabled]:focus,
input[type='submit'][disabled]:hover {
background-color: #9b4dca;
border-color: #9b4dca;
}
.button.button-outline,
button.button-outline,
input[type='button'].button-outline,
input[type='reset'].button-outline,
input[type='submit'].button-outline {
background-color: transparent;
color: #9b4dca;
}
.button.button-outline:focus, .button.button-outline:hover,
button.button-outline:focus,
button.button-outline:hover,
input[type='button'].button-outline:focus,
input[type='button'].button-outline:hover,
input[type='reset'].button-outline:focus,
input[type='reset'].button-outline:hover,
input[type='submit'].button-outline:focus,
input[type='submit'].button-outline:hover {
background-color: transparent;
border-color: #606c76;
color: #606c76;
}
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
button.button-outline[disabled]:focus,
button.button-outline[disabled]:hover,
input[type='button'].button-outline[disabled]:focus,
input[type='button'].button-outline[disabled]:hover,
input[type='reset'].button-outline[disabled]:focus,
input[type='reset'].button-outline[disabled]:hover,
input[type='submit'].button-outline[disabled]:focus,
input[type='submit'].button-outline[disabled]:hover {
border-color: inherit;
color: #9b4dca;
}
.button.button-clear,
button.button-clear,
input[type='button'].button-clear,
input[type='reset'].button-clear,
input[type='submit'].button-clear {
background-color: transparent;
border-color: transparent;
color: #9b4dca;
}
.button.button-clear:focus, .button.button-clear:hover,
button.button-clear:focus,
button.button-clear:hover,
input[type='button'].button-clear:focus,
input[type='button'].button-clear:hover,
input[type='reset'].button-clear:focus,
input[type='reset'].button-clear:hover,
input[type='submit'].button-clear:focus,
input[type='submit'].button-clear:hover {
background-color: transparent;
border-color: transparent;
color: #606c76;
}
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
button.button-clear[disabled]:focus,
button.button-clear[disabled]:hover,
input[type='button'].button-clear[disabled]:focus,
input[type='button'].button-clear[disabled]:hover,
input[type='reset'].button-clear[disabled]:focus,
input[type='reset'].button-clear[disabled]:hover,
input[type='submit'].button-clear[disabled]:focus,
input[type='submit'].button-clear[disabled]:hover {
color: #9b4dca;
}
code {
background: #f4f5f6;
border-radius: .4rem;
font-size: 86%;
margin: 0 .2rem;
padding: .2rem .5rem;
white-space: nowrap;
}
pre {
background: #f4f5f6;
border-left: 0.3rem solid #9b4dca;
overflow-y: hidden;
}
pre > code {
border-radius: 0;
display: block;
padding: 1rem 1.5rem;
white-space: pre;
}
hr {
border: 0;
border-top: 0.1rem solid #f4f5f6;
margin: 3.0rem 0;
}
input[type='email'],
input[type='number'],
input[type='password'],
input[type='search'],
input[type='tel'],
input[type='text'],
input[type='url'],
textarea,
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
border: 0.1rem solid #d1d1d1;
border-radius: .4rem;
box-shadow: none;
box-sizing: inherit;
height: 3.8rem;
padding: .6rem 1.0rem;
width: 100%;
}
input[type='email']:focus,
input[type='number']:focus,
input[type='password']:focus,
input[type='search']:focus,
input[type='tel']:focus,
input[type='text']:focus,
input[type='url']:focus,
textarea:focus,
select:focus {
border-color: #9b4dca;
outline: 0;
}
select {
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
padding-right: 3.0rem;
}
select:focus {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
}
textarea {
min-height: 6.5rem;
}
label,
legend {
display: block;
font-size: 1.6rem;
font-weight: 700;
margin-bottom: .5rem;
}
fieldset {
border-width: 0;
padding: 0;
}
input[type='checkbox'],
input[type='radio'] {
display: inline;
}
.label-inline {
display: inline-block;
font-weight: normal;
margin-left: .5rem;
}
.container {
margin: 0 auto;
max-width: 112.0rem;
padding: 0 2.0rem;
position: relative;
width: 100%;
}
.row {
display: flex;
flex-direction: column;
padding: 0;
width: 100%;
}
.row.row-no-padding {
padding: 0;
}
.row.row-no-padding > .column {
padding: 0;
}
.row.row-wrap {
flex-wrap: wrap;
}
.row.row-top {
align-items: flex-start;
}
.row.row-bottom {
align-items: flex-end;
}
.row.row-center {
align-items: center;
}
.row.row-stretch {
align-items: stretch;
}
.row.row-baseline {
align-items: baseline;
}
.row .column {
display: block;
flex: 1 1 auto;
margin-left: 0;
max-width: 100%;
width: 100%;
}
.row .column.column-offset-10 {
margin-left: 10%;
}
.row .column.column-offset-20 {
margin-left: 20%;
}
.row .column.column-offset-25 {
margin-left: 25%;
}
.row .column.column-offset-33, .row .column.column-offset-34 {
margin-left: 33.3333%;
}
.row .column.column-offset-50 {
margin-left: 50%;
}
.row .column.column-offset-66, .row .column.column-offset-67 {
margin-left: 66.6666%;
}
.row .column.column-offset-75 {
margin-left: 75%;
}
.row .column.column-offset-80 {
margin-left: 80%;
}
.row .column.column-offset-90 {
margin-left: 90%;
}
.row .column.column-10 {
flex: 0 0 10%;
max-width: 10%;
}
.row .column.column-20 {
flex: 0 0 20%;
max-width: 20%;
}
.row .column.column-25 {
flex: 0 0 25%;
max-width: 25%;
}
.row .column.column-33, .row .column.column-34 {
flex: 0 0 33.3333%;
max-width: 33.3333%;
}
.row .column.column-40 {
flex: 0 0 40%;
max-width: 40%;
}
.row .column.column-50 {
flex: 0 0 50%;
max-width: 50%;
}
.row .column.column-60 {
flex: 0 0 60%;
max-width: 60%;
}
.row .column.column-66, .row .column.column-67 {
flex: 0 0 66.6666%;
max-width: 66.6666%;
}
.row .column.column-75 {
flex: 0 0 75%;
max-width: 75%;
}
.row .column.column-80 {
flex: 0 0 80%;
max-width: 80%;
}
.row .column.column-90 {
flex: 0 0 90%;
max-width: 90%;
}
.row .column .column-top {
align-self: flex-start;
}
.row .column .column-bottom {
align-self: flex-end;
}
.row .column .column-center {
-ms-grid-row-align: center;
align-self: center;
}
@media (min-width: 40rem) {
.row {
flex-direction: row;
margin-left: -1.0rem;
width: calc(100% + 2.0rem);
}
.row .column {
margin-bottom: inherit;
padding: 0 1.0rem;
}
}
a {
color: #9b4dca;
text-decoration: none;
}
a:focus, a:hover {
color: #606c76;
}
dl,
ol,
ul {
list-style: none;
margin-top: 0;
padding-left: 0;
}
dl dl,
dl ol,
dl ul,
ol dl,
ol ol,
ol ul,
ul dl,
ul ol,
ul ul {
font-size: 90%;
margin: 1.5rem 0 1.5rem 3.0rem;
}
ol {
list-style: decimal inside;
}
ul {
list-style: circle inside;
}
.button,
button,
dd,
dt,
li {
margin-bottom: 1.0rem;
}
fieldset,
input,
select,
textarea {
margin-bottom: 1.5rem;
}
blockquote,
dl,
figure,
form,
ol,
p,
pre,
table,
ul {
margin-bottom: 2.5rem;
}
table {
border-spacing: 0;
width: 100%;
}
td,
th {
border-bottom: 0.1rem solid #e1e1e1;
padding: 1.2rem 1.5rem;
text-align: left;
}
td:first-child,
th:first-child {
padding-left: 0;
}
td:last-child,
th:last-child {
padding-right: 0;
}
b,
strong {
font-weight: bold;
}
p {
margin-top: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 300;
letter-spacing: -.1rem;
margin-bottom: 2.0rem;
margin-top: 0;
}
h1 {
font-size: 4.6rem;
line-height: 1.2;
}
h2 {
font-size: 3.6rem;
line-height: 1.25;
}
h3 {
font-size: 2.8rem;
line-height: 1.3;
}
h4 {
font-size: 2.2rem;
letter-spacing: -.08rem;
line-height: 1.35;
}
h5 {
font-size: 1.8rem;
letter-spacing: -.05rem;
line-height: 1.5;
}
h6 {
font-size: 1.6rem;
letter-spacing: 0;
line-height: 1.4;
}
img {
max-width: 100%;
}
.clearfix:after {
clear: both;
content: ' ';
display: table;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
/*# sourceMappingURL=milligram.css.map */
-349
View File
@@ -1,349 +0,0 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

-15
View File
@@ -1,15 +0,0 @@
import { mount, Component, useState, loadFile } from "@odoo/owl";
class Counter extends Component {
static template = "Counter";
setup() {
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
const templates = await loadFile("./counter.xml");
mount(Counter, document.getElementById("app-container"), { templates });
-7
View File
@@ -1,7 +0,0 @@
<templates>
<t t-name="Counter">
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</t>
</templates>
-9
View File
@@ -1,9 +0,0 @@
import { loadFile } from "@odoo/owl";
for (const [className, type] of [["xml", "xml"], ["javascript", "js"]]) {
loadFile(`./counter.${type}`).then(code => {
const el = document.querySelector(`code.${className}`);
el.textContent = code;
hljs.highlightBlock(el);
})
}
-74
View File
@@ -1,74 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OWL Framework</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="assets/milligram.css">
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
<link rel="stylesheet" href="assets/main.css">
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
</head>
<body>
<header class="container">
<img class="homepage-logo" src="assets/owl_1f989.png" alt="OWL Logo">
<hgroup>
<h1>OWL Framework</h1>
<p>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</p>
</hgroup>
</header>
<nav class="container">
<p>
<a href="https://github.com/odoo/owl" title="https://github.com/odoo/owl">Github</a>
&bull;
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Documentation</a>
&bull;
<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>
-5990
View File
File diff suppressed because it is too large Load Diff
-18
View File
@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL Playground</title>
<link rel="icon" href="data:,">
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="playground.js"></script>
</head>
<body>
</body>
</html>
@@ -1,11 +0,0 @@
tr.danger {
font-weight: bold;
}
.remove:hover {
font-weight: bold;
}
.remove {
cursor: pointer;
}
@@ -1,124 +0,0 @@
import { Component, mount, useState, onPatched} from "@odoo/owl";
// -----------------------------------------------------------------------------
// Data generation
// -----------------------------------------------------------------------------
let idCounter = 1;
const adjectives = [
"pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
"quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
"unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
function _random (max) { return Math.round(Math.random() * 1000) % max; };
function buildData(count) {
const data = new Array(count);
for (let i = 0; i < count; i++) {
const label = `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`;
data[i] = {
id: idCounter++,
label,
};
}
return data;
}
// -----------------------------------------------------------------------------
// Components
// -----------------------------------------------------------------------------
class Button extends Component {
static template = "Button";
}
class Row extends Component {
static template = "Row";
}
class Root extends Component {
static template = "Root";
static components = { Button, Row };
setup() {
this.state = useState({
rows: [],
selectedRowId: null
});
this.benchmarking = false;
onPatched(() => {
if (this.benchmarking) {
this.stop();
}
});
}
start(descr) {
this.benchmarking = `[${descr}]`;
console.time(this.benchmarking);
}
stop() {
console.timeEnd(this.benchmarking);
this.benchmarking = false;
}
run() {
this.start('add1000');
this.state.rows = buildData(1000);
this.state.selectedRowId = null;
}
runLots() {
this.start('add10_000');
this.state.rows = buildData(10_000);
this.state.selectedRowId = null;
}
add() {
this.start('append1000');
this.state.rows = this.state.rows.concat(buildData(1000));
}
update() {
this.start('update1/10th');
let index = 0;
const rows = this.state.rows;
while (index < rows.length) {
rows[index].label = rows[index].label + " !!!";
index += 10;
}
}
clear() {
this.start('clear');
this.state.rows = [];
this.state.selectedRowId = null;
}
swapRows() {
this.start('swap');
const rows = this.state.rows;
if (rows.length > 998) {
let tmp = rows[1];
rows[1] = rows[998];
rows[998] = tmp;
}
}
selectRow(id) {
this.start('select');
this.state.selectedRowId = id;
}
removeRow(id) {
this.start('remove1');
const rows = this.state.rows;
rows.splice(rows.findIndex(row => row.id === id), 1);
}
}
// dev=false for benchmarking. we don't want to benchmark dev code!
mount(Root, document.body, { templates: TEMPLATES, dev: false });
@@ -1,52 +0,0 @@
<templates>
<t t-name="Button">
<div class='col-sm-6 smallpad'>
<button t-att-id="props.id" class='btn btn-primary btn-block' type='button' t-on-click="props.onClick">
<t t-esc="props.text"/>
</button>
</div>
</t>
<t t-name="Row">
<tr t-att-class="props.isSelected ? 'danger' : ''">
<td class="col-md-1" t-esc="props.row.id" />
<td class="col-md-4">
<a t-on-click="() => props.onSelect(props.row.id)" t-esc="props.row.label" />
</td>
<td class="col-md-1">
<a t-on-click="() => props.onRemove(props.row.id)" class="remove">[x]
<span class='glyphicon glyphicon-remove' aria-hidden="true" />
</a>
</td>
<td class='col-md-6'/>
</tr>
</t>
<t t-name="Root">
<div class='container'>
<div class='jumbotron'>
<div class='row'>
<div class='col-md-6'>
<h1>Owl Keyed</h1>
</div>
<div class='col-md-6'>
<div class='row'>
<Button id="'run'" onClick.bind="run" text="'Create 1,000 rows'" />
<Button id="'runlots'" onClick.bind="runLots" text="'Create 10,000 rows'" />
<Button id="'add'" onClick.bind="add" text="'Append 1,000 rows'" />
<Button id="'update'" onClick.bind="update" text="'Update every 10th row'" />
<Button id="'clear'" onClick.bind="clear" text="'Clear'" />
<Button id="'swaprows'" onClick.bind="swapRows" text="'Swap Rows'" />
</div>
</div>
</div>
</div>
<table class='table table-hover table-striped test-data'>
<tbody>
<t t-foreach="state.rows" t-as="row" t-key="row.id">
<Row row="row" isSelected="row.id === state.selectedRowId" onSelect.bind="selectRow" onRemove.bind="removeRow"/>
</t>
</tbody>
</table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
</div>
</t>
</templates>
@@ -1,10 +0,0 @@
.greeter {
font-size: 20px;
width: 300px;
height: 100px;
margin: 5px;
text-align: center;
line-height: 100px;
background-color: #eeeeee;
user-select: none;
}
@@ -1,26 +0,0 @@
// In this example, we show how components can be defined and created.
import { Component, useState, mount } from "@odoo/owl";
class Greeter extends Component {
static template = "Greeter";
setup() {
this.state = useState({ word: 'Hello' });
}
toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
}
}
// Main root component
class Root extends Component {
static components = { Greeter };
static template = "Root"
setup() {
this.state = useState({ name: 'World'});
}
}
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -1,9 +0,0 @@
<templates>
<div t-name="Greeter" class="greeter" t-on-click="toggle">
<t t-esc="state.word"/>, <t t-esc="props.name"/>
</div>
<t t-name="Root">
<Greeter name="state.name"/>
</t>
</templates>
@@ -1,6 +0,0 @@
button {
width: 120px;
height: 35px;
font-size: 16px;
}
@@ -1,40 +0,0 @@
// In this example, we show how hooks can be used or defined.
import { Component, mount, useState, onWillDestroy } from "@odoo/owl";
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
window.addEventListener('mousemove', update);
onWillDestroy(() => {
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
class Root extends Component {
static template = "Root";
setup() {
// simple state hook (reactive object)
this.counter = useState({ value: 0 });
// this hooks is bound to the 'mouse' property.
this.mouse = useMouse();
}
increment() {
this.counter.value++;
}
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -1,7 +0,0 @@
<templates>
<div t-name="Root">
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>
</templates>
-22
View File
@@ -1,22 +0,0 @@
// This example illustrate how the t-model directive can be used to synchronize
// data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally
// synchronized.
import { Component, useState, mount } from "@odoo/owl";
class Form extends Component {
static template = "Form";
setup() {
this.state = useState({
text: "",
othertext: "",
number: 11,
color: "",
bool: false
});
}
}
// Application setup
mount(Form, document.body, { templates: TEMPLATES, dev: true });
-37
View File
@@ -1,37 +0,0 @@
<templates>
<div t-name="Form">
<h1>Form</h1>
<div>
Text (immediate): <input t-model="state.text"/>
</div>
<div>
Other text (lazy): <input t-model.lazy="state.othertext"/>
</div>
<div>
Number: <input t-model.number="state.number"/>
</div>
<div>
Boolean: <input type="checkbox" t-model="state.bool"/>
</div>
<div>
Color, with a select:
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Color, with radio buttons:
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
</div>
<hr/>
<h1>State</h1>
<div>Text: <t t-esc="state.text"/></div>
<div>Other Text: <t t-esc="state.othertext"/></div>
<div>Number: <t t-esc="state.number"/></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
<div>Color: <t t-esc="state.color"/></div>
</div>
</templates>
-7
View File
@@ -1,7 +0,0 @@
{
"compilerOptions": {
"paths": {
"owl": ["../../../src/index.ts"],
},
}
}
@@ -1,11 +0,0 @@
button {
font-size: 18px;
margin: 5px;
}
.demo {
margin: 10px;
padding: 10px;
background-color: #dddddd;
width: 250px;
}
@@ -1,68 +0,0 @@
// This example shows all the possible lifecycle hooks
//
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
// methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what
// happens.
import {
Component,
useState,
mount,
useComponent,
onWillStart,
onMounted,
onWillUnmount,
onWillUpdateProps,
onPatched,
onWillPatch,
onWillRender,
onRendered,
onWillDestroy,
} from "@odoo/owl";
function useLogLifecycle() {
const component = useComponent();
const name = component.constructor.name;
onWillStart(() => console.log(`${name}:willStart`));
onMounted(() => console.log(`${name}:mounted`));
onWillUpdateProps(() => console.log(`${name}:willUpdateProps`));
onWillRender(() => console.log(`${name}:willRender`));
onRendered(() => console.log(`${name}:rendered`));
onWillPatch(() => console.log(`${name}:willPatch`));
onPatched(() => console.log(`${name}:patched`));
onWillUnmount(() => console.log(`${name}:willUnmount`));
onWillDestroy(() => console.log(`${name}:willDestroy`));
}
class DemoComponent extends Component {
static template = "DemoComponent";
setup() {
useLogLifecycle();
this.state = useState({ n: 0 });
}
increment() {
this.state.n++;
}
}
class Root extends Component {
static template = "Root";
static components = { DemoComponent };
setup() {
useLogLifecycle();
this.state = useState({ n: 0, flag: true });
}
increment() {
this.state.n++;
}
toggleSubComponent() {
this.state.flag = !this.state.flag;
}
}
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -1,15 +0,0 @@
<templates>
<div t-name="DemoComponent" t-on-click="increment" class="demo">
<div>Demo Sub Component</div>
<div>(click on me to update me)</div>
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
</div>
<div t-name="Root">
<button t-on-click="increment">Increment Parent State</button>
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
<div t-if="state.flag">
<DemoComponent n="state.n"/>
</div>
</div>
</templates>
@@ -1,56 +0,0 @@
body {
margin: 0;
}
.app {
height: 100%;
flex-direction: column;
}
.app.desktop {
display: flex;
}
.navbar {
flex: 0 0 30px;
height: 30px;
background-color: cadetblue;
color: white;
line-height: 30px;
}
.controlpanel {
flex: 0 0 100px;
height: 100px;
background-color: #dddddd;
padding: 8px;
}
.content-wrapper {
flex: 1 1 auto;
position: relative;
}
.content {
display: flex;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.formview {
overflow-y: auto;
flex: 1 1 60%;
min-height: 200px;
padding: 8px;
}
.chatter {
overflow-y: auto;
flex: 1 1 40%;
background-color: #eeeeee;
color: #333333;
padding: 8px;
}
@@ -1,118 +0,0 @@
// In this example, we show how one can design an application that is responsive:
// its UI is different in mobile mode or in desktop mode.
//
// The main idea is to have a "isMobile" key in the environment, then listen
// to resize events and update the env if needed. Then, the whole interface
// will be updated, creating and destroying components as needed.
//
// To see this in action, try resizing the window. The application will switch
// to mobile mode whenever it has less than 768px.
import { Component, useState, mount, reactive, useEnv } from "@odoo/owl";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
function debounce(func, wait, immediate) {
let timeout;
return function () {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
//------------------------------------------------------------------------------
// Responsive hook
//------------------------------------------------------------------------------
function createUI() {
const getIsMobile = () => window.innerWidth <= 768;
const ui = reactive({ isMobile: getIsMobile() });
const updateEnv = debounce(() => {
const isMobile = getIsMobile();
if (ui.isMobile !== isMobile) {
ui.isMobile = isMobile;
}
}, 15);
window.addEventListener("resize", updateEnv);
return ui;
}
function useUI() {
const env = useEnv();
return useState(env.ui);
}
//------------------------------------------------------------------------------
// Components
//------------------------------------------------------------------------------
class Navbar extends Component {
static template = "Navbar";
}
class MobileSearchView extends Component {
static template = "MobileSearchView";
}
class ControlPanel extends Component {
static template = "ControlPanel";
static components = { MobileSearchView };
setup() {
this.ui = useUI();
}
}
class AdvancedComponent extends Component {
static template = "AdvancedComponent";
}
class FormView extends Component {
static template = "FormView";
static components = { AdvancedComponent };
setup() {
this.ui = useUI();
}
}
class Chatter extends Component {
static template = "Chatter";
setup() {
this.messages = Array.from(Array(100).keys());
}
}
class Root extends Component {
static template = "Root";
static components = { Navbar, ControlPanel, FormView, Chatter };
setup() {
this.ui = useUI();
}
}
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
const env = {
ui: createUI()
};
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
@@ -1,42 +0,0 @@
<templates>
<div t-name="Navbar" class="navbar">Navbar</div>
<div t-name="ControlPanel" class="controlpanel">
<h2>Control Panel</h2>
<MobileSearchView t-if="ui.isMobile" />
</div>
<div t-name="FormView" class="formview">
<h2>Form View</h2>
<AdvancedComponent t-if="!ui.isMobile" />
</div>
<div t-name="Chatter" class="chatter">
<h2>Chatter</h2>
<t t-foreach="messages" t-as="item" t-key="item"><div>Message <t t-esc="item"/></div></t>
</div>
<div t-name="MobileSearchView">Mobile searchview</div>
<div t-name="AdvancedComponent">
This component is only created in desktop mode.
<button>Button!</button>
</div>
<t t-name="maincontent">
<FormView />
<Chatter />
</t>
<div t-name="Root" class="app" t-att-class="{mobile: ui.isMobile, desktop: !ui.isMobile}">
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!ui.isMobile">
<div class="content">
<t t-call="maincontent"/>
</div>
</div>
<t t-else="">
<t t-call="maincontent"/>
</t>
</div>
</templates>
@@ -1,28 +0,0 @@
// This example illustrates how one can write Owl components with
// inline templates.
import { Component, useState, xml, mount } from "@odoo/owl";
// Counter component
class Counter extends Component {
static template = xml`
<button t-on-click="() => state.value++">
Click! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 })
}
// Root
class Root extends Component {
static template = xml`
<div>
<Counter/>
<Counter/>
</div>`;
static components = { Counter };
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true});
-43
View File
@@ -1,43 +0,0 @@
.main {
display: flex;
}
.card {
display: flex;
flex-direction: column;
background-color: #eeeeee;
width: 200px;
height: 100px;
margin: 10px;
border: 1px solid gray;
}
.card.full {
height: 100px;
}
.card.small {
height: 25px;
}
.card-title {
flex: 0 0 25px;
font-weight: bold;
background-color: darkcyan;
color: white;
padding: 2px;
}
.card-title button {
float: right;
}
.card-content {
flex: 1 1 auto;
padding: 5px;
border-top: 1px solid white;
}
.card-footer {
border-top: 1px solid white;
}
-49
View File
@@ -1,49 +0,0 @@
// We show here how slots can be used to create generic components.
// In this example, the Card component is basically only a container. It is not
// aware of its content. It just knows where it should be (with t-slot).
// The parent component define the content with t-set-slot.
//
// Note that the t-on-click event, defined in the Root template, is executed in
// the context of the Root component, even though it is inside the Card component
import { Component, useState, mount } from "@odoo/owl";
class Card extends Component {
static template = "Card";
setup() {
this.state = useState({ showContent: true });
}
toggleDisplay() {
this.state.showContent = !this.state.showContent;
}
}
class Counter extends Component {
static template = "Counter";
setup() {
this.state = useState({val: 1});
}
inc() {
this.state.val++;
}
}
// Main root component
class Root extends Component {
static template = "Root"
static components = { Card, Counter };
setup() {
this.state = useState({a: 1, b: 3});
}
inc(key, delta) {
this.state[key] += delta;
}
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true});
-33
View File
@@ -1,33 +0,0 @@
<templates>
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
<div class="card-title">
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
</div>
<t t-if="state.showContent">
<div class="card-content" >
<t t-slot="content"/>
</div>
<div class="card-footer">
<t t-slot="footer"/>
</div>
</t>
</div>
<div t-name="Counter">
<t t-esc="state.val"/><button t-on-click="inc">Inc</button>
</div>
<div t-name="Root" class="main">
<Card title="'Title card A'">
<t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set-slot="footer"><button t-on-click="() => this.inc('a', 1)">Increment A</button></t>
</Card>
<Card title="'Title card B'">
<t t-set-slot="content">
<div>Card 2... [<t t-esc="state.b"/>]</div>
<Counter />
</t>
<t t-set-slot="footer"><button t-on-click="() => this.inc('b', -1)">Decrement B</button></t>
</Card>
</div>
</templates>
@@ -1,379 +0,0 @@
html,body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all + label:before {
content: "";
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked + label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: "×";
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
.filters a {
cursor: pointer;
}
@@ -1,182 +0,0 @@
// This example is an implementation of the TodoList application, from the
// www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence.
//
// In this implementation, we use the owl reactivity mechanism.
import { Component, useState, mount, useRef, reactive, useEnv, useEffect } from "@odoo/owl";
//------------------------------------------------------------------------------
// Constants, helpers
//------------------------------------------------------------------------------
const ENTER_KEY = 13;
const ESC_KEY = 27;
function useAutofocus(name) {
let ref = useRef(name);
useEffect(el => el && el.focus(), () => [ref.el]);
}
function useStore() {
const env = useEnv();
return useState(env.store);
}
//------------------------------------------------------------------------------
// Task store
//------------------------------------------------------------------------------
class TaskList {
constructor(tasks) {
this.tasks = tasks || [];
const taskIds = this.tasks.map((t) => t.id);
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
}
addTask(text) {
text = text.trim();
if (text) {
const task = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(task) {
task.isCompleted = !task.isCompleted;
}
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);
}
}
deleteTask(id) {
const index = this.tasks.findIndex((t) => t.id === id);
this.tasks.splice(index, 1);
}
updateTask(id, text) {
const value = text.trim();
if (!value) {
this.deleteTask(id);
} else {
const task = this.tasks.find(t => t.id === id);
task.text = value;
}
}
}
function createTaskStore() {
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
saveTasks();
return taskStore;
}
//------------------------------------------------------------------------------
// Todo
//------------------------------------------------------------------------------
class Todo extends Component {
static template = "Todo";
setup() {
useAutofocus("input");
this.store = useStore();
this.state = useState({
isEditing: false
});
}
handleKeyup(ev) {
if (ev.keyCode === ENTER_KEY) {
this.updateText(ev.target.value);
}
if (ev.keyCode === ESC_KEY) {
ev.target.value = this.props.text;
this.state.isEditing = false;
}
}
handleBlur(ev) {
this.updateText(ev.target.value);
}
updateText(text) {
this.store.updateTask(this.props.id, text);
this.state.isEditing = false;
}
}
//------------------------------------------------------------------------------
// TodoList
//------------------------------------------------------------------------------
class TodoList extends Component {
static template = "TodoList";
static components = { Todo };
setup() {
this.store = useStore();
this.state = useState({ filter: "all" });
}
get displayedTasks() {
const tasks = this.store.tasks;
switch (this.state.filter) {
case "active":
return tasks.filter((t) => !t.isCompleted);
case "completed":
return tasks.filter((t) => t.isCompleted);
case "all":
return tasks;
}
}
get allChecked() {
return this.store.tasks.every(todo => todo.isCompleted);
}
get remaining() {
return this.store.tasks.filter(todo => !todo.isCompleted).length;
}
get remainingText() {
const items = this.remaining < 2 ? "item" : "items";
return ` ${items} left`;
}
addTodo(ev) {
if (ev.keyCode === ENTER_KEY) {
const text = ev.target.value;
if (text.trim()) {
this.store.addTask(text);
}
ev.target.value = "";
}
}
setFilter(filter) {
this.state.filter = filter;
}
}
//------------------------------------------------------------------------------
// App Initialization
//------------------------------------------------------------------------------
const env = { store: createTaskStore() };
mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
@@ -1,50 +0,0 @@
<templates>
<section t-name="TodoList" class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header>
<section class="main" t-if="store.tasks.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="() => store.toggleAll(!allChecked)"/>
<label for="toggle-all"></label>
<ul class="todo-list">
<t t-foreach="displayedTasks" t-as="todo" t-key="todo.id">
<Todo id="todo.id" isCompleted="todo.isCompleted" text="todo.text"/>
</t>
</ul>
</section>
<footer class="footer" t-if="store.tasks.length">
<span class="todo-count">
<strong>
<t t-esc="remaining"/>
</strong>
<t t-esc="remainingText"/>
</span>
<ul class="filters">
<li>
<a t-on-click="() => this.setFilter('all')" t-att-class="{selected: state.filter === 'all'}">All</a>
</li>
<li>
<a t-on-click="() => this.setFilter('active')" t-att-class="{selected: state.filter === 'active'}">Active</a>
</li>
<li>
<a t-on-click="() => this.setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li>
</ul>
<button class="clear-completed" t-if="store.tasks.length gt remaining" t-on-click="() => store.clearCompleted()">
Clear completed
</button>
</footer>
</section>
<li t-name="Todo" class="todo" t-att-class="{completed: props.isCompleted, editing: state.isEditing}">
<div class="view">
<input class="toggle" type="checkbox" t-on-change="() => store.toggleTask(props.id)" t-att-checked="props.completed"/>
<label t-on-dblclick="() => state.isEditing = true">
<t t-esc="props.text"/>
</label>
<button class="destroy" t-on-click="() => store.deleteTask(props.id)"></button>
</div>
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.text" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
</li>
</templates>
@@ -1,70 +0,0 @@
body {
margin: 0;
}
.app {
width: 100%;
height: 100%;
display: grid;
grid-template-rows: auto 50px;
}
.window-manager {
position: relative;
width: 100%;
height: 100%;
background-color: #eeeeee;
overflow: hidden;
}
.menubar {
background-color: #875a7b;
color: white;
}
.menubar button {
height: 40px;
font-size: 18px;
margin: 5px;
}
.window {
display: grid;
grid-template-rows: 30px auto;
border: 1px solid gray;
background-color: white;
position: absolute;
box-shadow: 1px 1px 2px 1px grey;
}
.window.dragging {
opacity: 0.75;
}
.window .header {
background-color: #875a7b;
display: grid;
grid-template-columns: auto 24px;
color: white;
line-height: 30px;
padding-left: 5px;
cursor: default;
user-select: none;
}
.window .header .close {
cursor: pointer;
font-size: 22px;
padding-left: 4px;
padding-right: 4px;
font-weight: bold;
}
.counter {
font-size: 20px;
}
.counter button {
width: 80px;
height:40px;
font-size: 20px;
}
@@ -1,192 +0,0 @@
// This example is slightly more complex than usual. We demonstrate
// here a way to manage sub windows in Owl, declaratively. This is still just a
// demonstration. Managing windows can be as complex as we want. For example,
// we could implement the following features:
// - resizing windows
// - minimizing windows
// - configuration options for windows to make a window non resizeable
// - minimal width/height
// - better heuristic for initial window position
// - ...
import { Component, useState, mount, useRef, reactive, useEnv, onMounted } from "@odoo/owl";
// -----------------------------------------------------------------------------
// Window manager code
// -----------------------------------------------------------------------------
class WindowManager {
// contains all components with metadata
static Windows = {};
windows = {}; // mapping id => info
nextId = 1;
add(type) {
const Comp = WindowManager.Windows[type];
const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth));
const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight));
const id = this.nextId++;
this.windows[id] = {
id,
title: Comp.defaultTitle,
width: Comp.defaultWidth,
height: Comp.defaultHeight,
left,
top,
Component: Comp,
};
}
close(id) {
delete this.windows[id];
}
updatePosition(id, left, top) {
const w = this.windows[id];
w.left = left;
w.top = top;
}
getWindows() {
return Object.values(this.windows);
}
}
function createWindowService() {
return reactive(new WindowManager());
}
function useWindowService() {
const env = useEnv();
return useState(env.windowService);
}
// -----------------------------------------------------------------------------
// Generic Window Component
// -----------------------------------------------------------------------------
class Window extends Component {
static template = "Window";
static nextZIndex = 1;
zIndex = 0;
setup() {
this.windowService = useWindowService();
this.root = useRef('root');
onMounted(this.updateZIndex);
}
get style() {
let { width, height, top, left } = this.props.info;
return `width: ${width}px;height: ${height}px;top:${top}px;left:${left}px;z-index:${this.zIndex}`;
}
close() {
this.windowService.close(this.props.info.id);
}
startDragAndDrop(ev) {
this.updateZIndex();
const self = this;
const root = this.root;
const el = root.el;
el.classList.add('dragging');
const current = this.props.info;
const offsetX = current.left - ev.pageX;
const offsetY = current.top - ev.pageY;
let left, top;
window.addEventListener("mousemove", moveWindow);
window.addEventListener("mouseup", stopDnD, { once: true });
function moveWindow(ev) {
left = Math.max(offsetX + ev.pageX, 0);
top = Math.max(offsetY + ev.pageY, 0);
el.style.left = `${left}px`;
el.style.top = `${top}px`;
}
function stopDnD() {
window.removeEventListener("mousemove", moveWindow);
el.classList.remove('dragging');
if (top !== undefined && left !== undefined) {
self.windowService.updatePosition(current.id, left, top);
}
}
}
updateZIndex() {
this.zIndex = Window.nextZIndex++;
this.root.el.style['z-index'] = this.zIndex;
}
}
// -----------------------------------------------------------------------------
// Two concrete Window type implementations
// -----------------------------------------------------------------------------
class HelloWorld extends Component {
static template = "HelloWorld";
static defaultTitle = "Hello Owl!";
static defaultWidth = 200;
static defaultHeight = 100;
}
class Counter extends Component {
static template = "Counter";
static defaultTitle = "Click Counter";
static defaultWidth = 300;
static defaultHeight = 120;
state = useState({ value: 0 });
inc() {
this.state.value++;
}
}
// register window components
WindowManager.Windows.Hello = HelloWorld;
WindowManager.Windows.Counter = Counter;
// -----------------------------------------------------------------------------
// Window Container
// -----------------------------------------------------------------------------
class WindowContainer extends Component {
static template = "WindowContainer";
static components = { Window };
setup() {
this.windowService = useWindowService();
}
}
// -----------------------------------------------------------------------------
// Root Component
// -----------------------------------------------------------------------------
class Root extends Component {
static template = "Root";
static components = { WindowContainer };
setup() {
this.windowService = useWindowService();
}
addWindow(type) {
this.windowService.add(type);
}
}
// -----------------------------------------------------------------------------
// Setup
// -----------------------------------------------------------------------------
const env = {
windowService: createWindowService(),
};
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
@@ -1,32 +0,0 @@
<templates>
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex" t-ref="root">
<div class="header">
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
<span class="close" t-on-click.stop="close">×</span>
</div>
<t t-slot="default"/>
</div>
<div t-name="WindowContainer" class="window-manager">
<Window t-foreach="windowService.getWindows()" t-as="w" t-key="w.id" info="w">
<t t-component="w.Component"/>
</Window>
</div>
<div t-name="Root" class="app">
<WindowContainer/>
<div class="menubar">
<button t-on-click="() => this.addWindow('Hello')">Say Hello</button>
<button t-on-click="() => this.addWindow('Counter')">Counter</button>
</div>
</div>
<div t-name="HelloWorld">
Some content here...
</div>
<div t-name="Counter" class="counter">
<button t-on-click="inc">Click</button>
<span><t t-esc="state.value"/></span>
</div>
</templates>
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
httpd = None
def start_server():
global httpd
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="app.css">
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
<script type="module" src="app.js"></script>
</head>
<body>
<pre>
This app requires a web server, simply opening the html file using your browser will not work.
You can run a simple web server with one of the following:
- if you have python3 installed, run app.py with python3 or run 'python3 -m http.server' in the app folder
- if you have node/npm installed, run 'npx serve' in the app folder
- if you have php installed, run 'php -S localhost:8080 -t .' in the app folder
</pre>
<script>if (window.location.protocol !== "file:") document.querySelector("pre").remove()</script>
</body>
</html>
-5
View File
@@ -1,5 +0,0 @@
# Github page and playground
This folder contains the code for owl's [github page](https://odoo.github.io/owl)
and the owl [playground](https://odoo.github.io/owl/playground/). If you're
looking for the owl documentation, click [here](/doc)
+216 -490
View File
File diff suppressed because it is too large Load Diff
+5 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.2",
"version": "2.0.9",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
@@ -16,21 +16,17 @@
"build:runtime": "rollup -c --failAfterWarnings runtime",
"build:compiler": "rollup -c --failAfterWarnings compiler",
"build": "npm run build:bundle",
"build:devtools": "rollup -c ./tools/devtools/rollup.config.js",
"dev:devtools-chrome": "npm run build:devtools -- --config-browser=chrome",
"dev:devtools-firefox": "npm run build:devtools -- --config-browser=firefox",
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch",
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
"playground:serve": "python3 tools/server.py || python tools/server.py",
"playground": "npm run build && npm run playground:serve",
"preplayground:watch": "npm run build",
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
"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",
"lint": "eslint src/**/*.ts tests/**/*.ts",
"publish": "npm run build && npm publish",
"release": "node tools/release.js",
"compile_templates": "node tools/compile_xml.js"
},
@@ -60,11 +56,7 @@
"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",
+4 -9
View File
@@ -447,11 +447,6 @@ 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
*/
@@ -532,7 +527,8 @@ export class CodeGenerator {
let value = ast.value;
if (value && ctx.translate !== false) {
value = this.translate(value);
const match = translationRE.exec(value) as any;
value = match[1] + this.translateFn(match[2]) + match[3];
}
if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " ");
@@ -1099,11 +1095,10 @@ export class CodeGenerator {
} else {
let value: string;
if (ast.defaultValue) {
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
if (ast.value) {
value = `withDefault(${expr}, \`${defaultValue}\`)`;
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
} else {
value = `\`${defaultValue}\``;
value = `\`${ast.defaultValue}\``;
}
} else {
value = expr;
+8 -6
View File
@@ -336,10 +336,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
for (let attr of nodeAttrsNames) {
const value = node.getAttribute(attr)!;
if (attr === "t-on" || attr === "t-on-") {
throw new OwlError("Missing event name with t-on directive");
}
if (attr.startsWith("t-on-")) {
if (attr.startsWith("t-on")) {
if (attr === "t-on") {
throw new OwlError("Missing event name with t-on directive");
}
on = on || {};
on[attr.slice(5)] = value;
} else if (attr.startsWith("t-model")) {
@@ -365,8 +365,10 @@ 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");
@@ -379,8 +381,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
specialInitTargetAttr: isRadioInput ? "checked" : null,
eventType,
hasDynamicChildren: false,
shouldTrim: hasTrimMod,
shouldNumberize: hasNumberMod,
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
};
if (isSelect) {
// don't pollute the original ctx
+12 -10
View File
@@ -7,7 +7,6 @@ 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
@@ -40,8 +39,6 @@ declare global {
apps: Set<App>;
Fiber: typeof Fiber;
RootFiber: typeof RootFiber;
toRaw: typeof toRaw;
reactive: typeof reactive;
};
}
}
@@ -50,8 +47,6 @@ window.__OWL_DEVTOOLS__ ||= {
apps: new Set<App>(),
Fiber: Fiber,
RootFiber: RootFiber,
toRaw: toRaw,
reactive: reactive,
};
export class App<
@@ -89,10 +84,7 @@ export class App<
this.props = config.props || ({} as P);
}
mount(
target: HTMLElement | ShadowRoot,
options?: MountOptions
): Promise<Component<P, E> & InstanceType<T>> {
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
App.validateTarget(target);
if (this.dev) {
validateProps(this.Root, this.props, { __owl__: { app: this } });
@@ -107,7 +99,7 @@ export class App<
return new ComponentNode(Component, props, this, null, null);
}
mountNode(node: ComponentNode, target: HTMLElement | ShadowRoot, options?: MountOptions) {
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
const promise: any = new Promise((resolve, reject) => {
let isResolved = false;
// manually set a onMounted callback.
@@ -221,6 +213,16 @@ export class App<
handleError(...args: Parameters<typeof handleError>) {
return handleError(...args);
}
onUnhandledError(error: Error) {
console.warn(`[Owl] Unhandled error. Destroying the root component`);
try {
this.destroy();
} catch (e) {
console.error(e);
}
throw error;
}
}
export async function mount<
+5 -25
View File
@@ -36,18 +36,10 @@ export function createAttrUpdater(attr: string): Setter<HTMLElement> {
export function attrsSetter(this: HTMLElement, attrs: any) {
if (isArray(attrs)) {
if (attrs[0] === "class") {
setClass.call(this, attrs[1]);
} else {
setAttribute.call(this, attrs[0], attrs[1]);
}
setAttribute.call(this, attrs[0], attrs[1]);
} else {
for (let k in attrs) {
if (k === "class") {
setClass.call(this, attrs[k]);
} else {
setAttribute.call(this, k, attrs[k]);
}
setAttribute.call(this, k, attrs[k]);
}
}
}
@@ -60,11 +52,7 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
if (val === oldAttrs[1]) {
return;
}
if (name === "class") {
updateClass.call(this, val, oldAttrs[1]);
} else {
setAttribute.call(this, name, val);
}
setAttribute.call(this, name, val);
} else {
removeAttribute.call(this, oldAttrs[0]);
setAttribute.call(this, name, val);
@@ -72,21 +60,13 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
} else {
for (let k in oldAttrs) {
if (!(k in attrs)) {
if (k === "class") {
updateClass.call(this, "", oldAttrs[k]);
} else {
removeAttribute.call(this, k);
}
removeAttribute.call(this, k);
}
}
for (let k in attrs) {
const val = attrs[k];
if (val !== oldAttrs[k]) {
if (k === "class") {
updateClass.call(this, val, oldAttrs[k]);
} else {
setAttribute.call(this, k, val);
}
setAttribute.call(this, k, val);
}
}
}
+1 -2
View File
@@ -1,4 +1,3 @@
import { inOwnerDocument } from "../utils";
import { config } from "./config";
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
@@ -29,7 +28,7 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
function listener(ev: Event) {
const currentTarget = ev.currentTarget as HTMLElement;
if (!currentTarget || !inOwnerDocument(currentTarget)) return;
if (!currentTarget || !currentTarget.ownerDocument.contains(currentTarget)) return;
const data = (currentTarget as any)[eventKey];
if (!data) return;
config.mainEventHandler(data, ev, currentTarget);
+3 -21
View File
@@ -73,7 +73,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
forceNextRender: boolean = false;
parentKey: string | null;
props: P;
nextProps: P | null = null;
renderFn: Function;
parent: ComponentNode | null;
@@ -145,9 +144,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
async render(deep: boolean) {
if (this.status >= STATUS.CANCELLED) {
return;
}
let current = this.fiber;
if (current && (current.root!.locked || (current as any).bdom === true)) {
await Promise.resolve();
@@ -174,7 +170,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
if (this.status >= STATUS.CANCELLED) {
if (this.status === STATUS.DESTROYED) {
return;
}
// We only want to actually render the component if the following two
@@ -193,20 +189,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
}
cancel() {
this._cancel();
delete this.parent!.children[this.parentKey!];
this.app.scheduler.scheduleDestroy(this);
}
_cancel() {
this.status = STATUS.CANCELLED;
const children = this.children;
for (let childKey in children) {
children[childKey]._cancel();
}
}
destroy() {
let shouldRemove = this.status === STATUS.MOUNTED;
this._destroy();
@@ -238,7 +220,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
async updateAndRender(props: P, parentFiber: Fiber) {
this.nextProps = props;
const rawProps = props;
props = Object.assign({}, props);
// update
const fiber = makeChildFiber(this, parentFiber);
@@ -263,6 +245,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
return;
}
component.props = props;
this.props = rawProps;
fiber.render();
const parentRoot = parentFiber.root!;
if (this.willPatch.length) {
@@ -344,7 +327,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
// by the component will be patched independently in the appropriate
// fiber.complete
this._patch();
this.props = this.nextProps!;
}
}
_patch() {
+10 -18
View File
@@ -51,28 +51,20 @@ export function handleError(params: ErrorParams) {
);
}
const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber;
const fiber = "fiber" in params ? params.fiber : node.fiber!;
if (fiber) {
// resets the fibers on components if possible. This is important so that
// new renderings can be properly included in the initial one, if any.
let current: Fiber | null = fiber;
do {
current.node.fiber = current;
current = current.parent;
} while (current);
// resets the fibers on components if possible. This is important so that
// new renderings can be properly included in the initial one, if any.
let current: Fiber | null = fiber;
do {
current.node.fiber = current;
current = current.parent;
} while (current);
fibersInError.set(fiber.root!, error);
}
fibersInError.set(fiber.root!, error);
const handled = _handleError(node, error);
if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`);
try {
node.app.destroy();
} catch (e) {
console.error(e);
}
throw error;
node.app.onUnhandledError(error);
}
}
+2 -1
View File
@@ -55,7 +55,8 @@ function cancelFibers(fibers: Fiber[]): number {
let node = fiber.node;
fiber.render = throwOnRender;
if (node.status === STATUS.NEW) {
node.cancel();
node.destroy();
delete node.parent!.children[node.parentKey!];
}
node.fiber = null;
if (fiber.bdom) {
+7 -15
View File
@@ -1,7 +1,6 @@
import type { Env } from "./app";
import { getCurrent } from "./component_node";
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
import { inOwnerDocument } from "./utils";
// -----------------------------------------------------------------------------
// useRef
@@ -17,7 +16,7 @@ export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el:
return {
get el(): T | null {
const el = refs[name];
return inOwnerDocument(el) ? el : null;
return el?.ownerDocument.contains(el) ? el : null;
},
};
}
@@ -59,35 +58,28 @@ export function useChildSubEnv(envExtension: Env) {
// useEffect
// -----------------------------------------------------------------------------
type EffectDeps<T extends any[]> = T | (T extends [...infer H, never] ? EffectDeps<H> : never);
/**
* @template T
* @param {...T} dependencies the dependencies computed by computeDependencies
* @param {...any} dependencies the dependencies computed by computeDependencies
* @returns {void|(()=>void)} a cleanup function that reverses the side
* effects of the effect callback.
*/
type Effect<T extends [...T]> = (...dependencies: EffectDeps<T>) => void | (() => void);
type Effect = (...dependencies: any[]) => void | (() => void);
/**
* This hook will run a callback when a component is mounted and patched, and
* will run a cleanup function before patching and before unmounting the
* the component.
*
* @template T
* @param {Effect<T>} effect the effect to run on component mount and/or patch
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
* @param {Effect} effect the effect to run on component mount and/or patch
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
* dependencies that will decide if the effect needs to be cleaned up and
* run again. If the dependencies did not change, the effect will not run
* again. The default value returns an array containing only NaN because
* NaN !== NaN, which will cause the effect to rerun on every patch.
*/
export function useEffect<T extends [...T]>(
effect: Effect<T>,
computeDependencies: () => T = () => [NaN] as never
) {
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
let cleanup: (() => void) | void;
let dependencies: T;
let dependencies: any[];
onMounted(() => {
dependencies = computeDependencies();
cleanup = effect(...dependencies);
+1 -1
View File
@@ -54,7 +54,7 @@ export {
onWillDestroy,
onError,
} from "./lifecycle_hooks";
export { validate, validateType } from "./validation";
export { validate } from "./validation";
export { OwlError } from "./error_handling";
export const __info__ = {
+3 -3
View File
@@ -63,7 +63,7 @@ export function onMounted(fn: () => void | any) {
node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
}
export function onWillPatch(fn: () => any | void) {
export function onWillPatch(fn: () => Promise<void> | any | void) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
@@ -75,13 +75,13 @@ export function onPatched(fn: () => void | any) {
node.patched.push(decorate(fn.bind(node.component), "onPatched"));
}
export function onWillUnmount(fn: () => void | any) {
export function onWillUnmount(fn: () => Promise<void> | void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
}
export function onWillDestroy(fn: () => void | any) {
export function onWillDestroy(fn: () => Promise<void> | void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
+9 -26
View File
@@ -1,4 +1,3 @@
import type { ComponentNode } from "./component_node";
import { fibersInError } from "./error_handling";
import { Fiber, RootFiber } from "./fibers";
import { STATUS } from "./status";
@@ -15,7 +14,6 @@ export class Scheduler {
requestAnimationFrame: Window["requestAnimationFrame"];
frame: number = 0;
delayedRenders: Fiber[] = [];
cancelledNodes: Set<ComponentNode> = new Set();
constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
@@ -25,13 +23,6 @@ export class Scheduler {
this.tasks.add(fiber.root!);
}
scheduleDestroy(node: ComponentNode) {
this.cancelledNodes.add(node);
if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => this.processTasks());
}
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged.
@@ -48,23 +39,15 @@ export class Scheduler {
}
if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => this.processTasks());
}
}
processTasks() {
this.frame = 0;
for (let node of this.cancelledNodes) {
node._destroy();
}
this.cancelledNodes.clear();
for (let task of this.tasks) {
this.processFiber(task);
}
for (let task of this.tasks) {
if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task);
}
this.frame = this.requestAnimationFrame(() => {
this.frame = 0;
this.tasks.forEach((fiber) => this.processFiber(fiber));
for (let task of this.tasks) {
if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task);
}
}
});
}
}
+1 -6
View File
@@ -7,20 +7,15 @@ import type { Component } from "./component";
export const enum STATUS {
NEW,
MOUNTED, // is ready, and in DOM. It has a valid el
// component has been created, but has been replaced by a newer component before being mounted
// it is cancelled until the next animation frame where it will be destroyed
CANCELLED,
DESTROYED,
}
type STATUS_DESCR = "new" | "mounted" | "cancelled" | "destroyed";
type STATUS_DESCR = "new" | "mounted" | "destroyed";
export function status(component: Component): STATUS_DESCR {
switch (component.__owl__.status) {
case STATUS.NEW:
return "new";
case STATUS.CANCELLED:
return "cancelled";
case STATUS.MOUNTED:
return "mounted";
case STATUS.DESTROYED:
+8 -16
View File
@@ -60,26 +60,18 @@ function withKey(elem: any, k: string) {
return elem;
}
function prepareList(collection: unknown): [unknown[], unknown[], number, undefined[]] {
let keys: unknown[];
let values: unknown[];
function prepareList(collection: any): [any[], any[], number, any[]] {
let keys: any[];
let values: any[];
if (Array.isArray(collection)) {
keys = collection;
values = collection;
} else if (collection instanceof Map) {
keys = [...collection.keys()];
values = [...collection.values()];
} else if (collection && typeof collection === "object") {
if (Symbol.iterator in collection) {
keys = [...(<Iterable<unknown>>collection)];
values = keys;
} else {
values = Object.keys(collection);
keys = Object.values(collection);
}
} else if (collection) {
values = Object.keys(collection);
keys = Object.values(collection);
} else {
throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`);
throw new OwlError("Invalid loop expression");
}
const n = values.length;
return [keys, values, n, new Array(n)];
@@ -142,7 +134,7 @@ class LazyValue {
* Safely outputs `value` as a block depending on the nature of `value`
*/
export function safeOutput(value: any, defaultValue?: any): ReturnType<typeof toggler> {
if (value === undefined || value === null) {
if (value === undefined) {
return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
}
let safeKey;
+1 -1
View File
@@ -125,7 +125,7 @@ export class TemplateSet {
callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent, key + subTemplate));
return toggler(subTemplate, template.call(owner, ctx, parent, key));
}
}
+17 -25
View File
@@ -9,39 +9,31 @@ export type Callback = () => void;
* @returns a batched version of the original callback
*/
export function batched(callback: Callback): Callback {
let scheduled = false;
return async (...args) => {
if (!scheduled) {
scheduled = true;
await Promise.resolve();
scheduled = false;
callback(...args);
let called = false;
return async () => {
// This await blocks all calls to the callback here, then releases them sequentially
// in the next microtick. This line decides the granularity of the batch.
await Promise.resolve();
if (!called) {
called = true;
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback.
// Schedule this before calling the callback so that calls to the batched function
// within the callback will proceed only after resetting called to false, and have
// a chance to execute the callback again
Promise.resolve().then(() => (called = false));
callback();
}
};
}
/**
* Determine whether the given element is contained in its ownerDocument:
* either directly or with a shadow root in between.
*/
export function inOwnerDocument(el?: HTMLElement) {
if (!el) {
return false;
}
if (el.ownerDocument.contains(el)) {
return true;
}
const rootNode = el.getRootNode();
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
}
export function validateTarget(target: HTMLElement | ShadowRoot) {
export function validateTarget(target: HTMLElement) {
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
const document = target && target.ownerDocument;
if (document) {
const HTMLElement = document.defaultView!.HTMLElement;
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
if (target instanceof HTMLElement) {
if (!document.body.contains(target)) {
throw new OwlError("Cannot mount a component on a detached dom node");
}
return;
+3 -15
View File
@@ -17,7 +17,6 @@ interface TypeInfo {
validate?: Function;
shape?: Schema;
element?: TypeDescription;
values?: TypeDescription;
}
type ValueType = { value: any };
@@ -138,7 +137,7 @@ function validateArrayType(key: string, value: any, descr: TypeDescription): str
return null;
}
export function validateType(key: string, value: any, descr: TypeDescription): string | null {
function validateType(key: string, value: any, descr: TypeDescription): string | null {
if (value === undefined) {
return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
} else if (isBaseType(descr)) {
@@ -152,24 +151,13 @@ export function validateType(key: string, value: any, descr: TypeDescription): s
let result: string | null = null;
if ("element" in descr) {
result = validateArrayType(key, value, descr.element!);
} else if ("shape" in descr) {
} else if ("shape" in descr && !result) {
if (typeof value !== "object" || Array.isArray(value)) {
result = `'${key}' is not an object`;
} else {
const errors = validateSchema(value, descr.shape!);
if (errors.length) {
result = `'${key}' doesn't have the correct shape (${errors.join(", ")})`;
}
}
} else if ("values" in descr) {
if (typeof value !== "object" || Array.isArray(value)) {
result = `'${key}' is not an object`;
} else {
const errors = Object.entries(value)
.map(([key, value]) => validateType(key, value, descr.values!))
.filter(Boolean);
if (errors.length) {
result = `some of the values in '${key}' are invalid (${errors.join(", ")})`;
result = `'${key}' has not the correct shape (${errors.join(", ")})`;
}
}
}
+2 -2
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.2";
// do not modify manually. This value is updated by the release script.
export const version = "2.0.9";
-31
View File
@@ -145,34 +145,3 @@ test("class attribute (with a preexisting value", async () => {
patch(tree, block([""]));
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
});
test("block-class attributes with preexisting class attribute", async () => {
const block = createBlock('<div block-attributes="0" class="owl"></div>');
const tree = block([{ class: "eagle" }]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
patch(tree, block([{ class: "falcon" }]));
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
patch(tree, block([{}]));
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
});
test("block-class attributes (array syntax) with preexisting class attribute", async () => {
const block = createBlock('<div block-attributes="0" class="owl"></div>');
const tree = block([["class", "eagle"]]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
patch(tree, block([["class", "falcon"]]));
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
patch(tree, block([["class", ""]]));
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
patch(tree, block([["class", "buzzard"]]));
expect(fixture.innerHTML).toBe(`<div class="owl buzzard"></div>`);
});
@@ -707,123 +707,6 @@ exports[`attributes updating classes (with obj notation) 1`] = `
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
return block1([attr1]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\" class=\\"c\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\" block-attribute-1=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\" block-attributes=\\"1\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {'b':true};
let attr2 = {class:'a'};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 6`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {'b':true};
return block1([attr1]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 7`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = ('b');
return block1([attr1]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 8`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various escapes 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -77,62 +77,6 @@ exports[`t-foreach iterate on items 1`] = `
}"
`;
exports[`t-foreach iterate, Map param 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['value']);;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
ctx[\`item_index\`] = i1;
ctx[\`item_value\`] = k_block1[i1];
const key1 = ctx['item_index'];
const b3 = text(\` [\`);
const b4 = text(ctx['item_index']);
const b5 = text(\`: \`);
const b6 = text(ctx['item']);
const b7 = text(\` \`);
const b8 = text(ctx['item_value']);
const b9 = text(\`] \`);
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-foreach iterate, Set param 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['value']);;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
ctx[\`item_index\`] = i1;
ctx[\`item_value\`] = k_block1[i1];
const key1 = ctx['item_index'];
const b3 = text(\` [\`);
const b4 = text(ctx['item_index']);
const b5 = text(\`: \`);
const b6 = text(ctx['item']);
const b7 = text(\` \`);
const b8 = text(ctx['item_value']);
const b9 = text(\`] \`);
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-foreach iterate, dict param 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -164,62 +108,6 @@ exports[`t-foreach iterate, dict param 1`] = `
}"
`;
exports[`t-foreach iterate, generator param 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['gen']());;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
ctx[\`item_index\`] = i1;
ctx[\`item_value\`] = k_block1[i1];
const key1 = ctx['item_index'];
const b3 = text(\` [\`);
const b4 = text(ctx['item_index']);
const b5 = text(\`: \`);
const b6 = text(ctx['item']);
const b7 = text(\` \`);
const b8 = text(ctx['item_value']);
const b9 = text(\`] \`);
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-foreach iterate, iterable param 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['map'].values());;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
ctx[\`item_index\`] = i1;
ctx[\`item_value\`] = k_block1[i1];
const key1 = ctx['item_index'];
const b3 = text(\` [\`);
const b4 = text(ctx['item_index']);
const b5 = text(\`: \`);
const b6 = text(ctx['item']);
const b7 = text(\` \`);
const b8 = text(ctx['item_value']);
const b9 = text(\`] \`);
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-foreach iterate, position 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -434,18 +434,6 @@ exports[`t-out t-out with the 0 number, in a p 1`] = `
}"
`;
exports[`t-out top level t-out with null 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return safeOutput(ctx['var']);
}
}"
`;
exports[`t-out top level t-out with undefined 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1,79 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`translation support body of t-sets are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", \`translated\`);
return text(ctx['label']);
}
}"
`;
exports[`translation support body of t-sets inside translation=off are not translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", \`untranslated\`);
return text(ctx['label']);
}
}"
`;
exports[`translation support body of t-sets with html content are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
let block1 = createBlock(\`<div>translated</div>\`);
function value1(ctx, node, key = \\"\\") {
return block1();
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx[\`label\`] = new LazyValue(value1, ctx, this, node, key);
return safeOutput(ctx['label']);
}
}"
`;
exports[`translation support body of t-sets with text and html content are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
let block3 = createBlock(\`<div>translated</div>\`);
function value1(ctx, node, key = \\"\\") {
const b2 = text(\` translated \`);
const b3 = block3();
return multi([b2, b3]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx[\`label\`] = new LazyValue(value1, ctx, this, node, key);
return safeOutput(ctx['label']);
}
}"
`;
exports[`translation support can set and remove translatable attributes 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -126,21 +52,6 @@ exports[`translation support some attributes are translated 1`] = `
}"
`;
exports[`translation support t-set and falsy t-value: t-body are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", withDefault(false, \`translated\`));
return text(ctx['label']);
}
}"
`;
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
"function anonymous(app, bdom, helpers
) {
-29
View File
@@ -371,33 +371,4 @@ describe("attributes", () => {
// not sure about this. maybe we want to remove the attribute?
expect(fixture.innerHTML).toBe('<div class="hoy a b"></div>');
});
test("various combinations of class, t-att-class, and t-att", () => {
const template1 = `<div t-att="{ class: 'a' }" class="c">content</div>`;
expect(renderToString(template1)).toBe('<div class="c a">content</div>');
const template2 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}" class="c">content</div>`;
expect(renderToString(template2)).toBe('<div class="c a b">content</div>');
const template3 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template3)).toBe('<div class="c a b">content</div>');
const template4 = `<div class="c" t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template4)).toBe('<div class="c a b">content</div>');
const template5 = `<div class="c" t-att-class="{'b': true}" t-att="{ class: 'a' }">content</div>`;
expect(renderToString(template5)).toBe('<div class="c b a">content</div>');
const template6 = `<div class="c" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template6)).toBe('<div class="c b">content</div>');
const template7 = `<div class="c" t-attf-class="{{'b'}}">content</div>`;
expect(renderToString(template7)).toBe('<div class="c b">content</div>');
const template8 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template8)).toBe('<div class="c a b">content</div>');
const template9 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template9)).toBe('<div class="a b">content</div>');
});
});
+2 -26
View File
@@ -1147,24 +1147,6 @@ describe("qweb parser", () => {
});
});
test("t-onclick without dash", async () => {
expect(() => parse(`<button t-onclick="add">Click</button>`)).toThrowError(
"Unknown QWeb directive: 't-onclick'"
);
});
test("t-on without event", async () => {
expect(() => parse(`<button t-on="add">Click</button>`)).toThrowError(
"Missing event name with t-on directive"
);
});
test("t-on- without event", async () => {
expect(() => parse(`<button t-on-="add">Click</button>`)).toThrowError(
"Missing event name with t-on directive"
);
});
// ---------------------------------------------------------------------------
// t-model
// ---------------------------------------------------------------------------
@@ -1293,12 +1275,6 @@ describe("qweb parser", () => {
});
});
test("component with event handler", async () => {
expect(() => parse(`<MyComponent t-onclick="someMethod"/>`)).toThrowError(
"unsupported directive on Component: t-onclick"
);
});
test("component with t-ref", async () => {
expect(() => parse(`<MyComponent t-ref="something"/>`)).toThrow(
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop."
@@ -1991,8 +1967,8 @@ describe("qweb parser", () => {
baseExpr: "state",
expr: "'stuff'",
eventType: "click",
shouldNumberize: true,
shouldTrim: true,
shouldNumberize: false,
shouldTrim: false,
targetAttr: "value",
hasDynamicChildren: false,
specialInitTargetAttr: "checked",
+1 -61
View File
@@ -105,64 +105,6 @@ describe("t-foreach", () => {
expect(renderToString(template, context)).toBe(expected);
});
test("iterate, Map param", () => {
const template = `
<t t-foreach="value" t-as="item" t-key="item_index">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>`;
const expected = ` [0: 1 a] [1: 2 b] [2: 3 c] `;
const context = {
value: new Map([
["a", 1],
["b", 2],
["c", 3],
]),
};
expect(renderToString(template, context)).toBe(expected);
});
test("iterate, Set param", () => {
const template = `
<t t-foreach="value" t-as="item" t-key="item_index">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>`;
const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `;
const context = { value: new Set([1, 2, 3]) };
expect(renderToString(template, context)).toBe(expected);
});
test("iterate, iterable param", () => {
const template = `
<t t-foreach="map.values()" t-as="item" t-key="item_index">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>`;
const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `;
const context = {
map: new Map([
["a", 1],
["b", 2],
["c", 3],
]),
};
expect(renderToString(template, context)).toBe(expected);
});
test("iterate, generator param", () => {
const template = `
<t t-foreach="gen()" t-as="item" t-key="item_index">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>`;
const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `;
const context = {
*gen() {
yield 1;
yield 2;
yield 3;
},
};
expect(renderToString(template, context)).toBe(expected);
});
test("does not pollute the rendering context", () => {
const template = `
<div>
@@ -251,9 +193,7 @@ describe("t-foreach", () => {
test("throws error if invalid loop expression", () => {
const test = `<div><t t-foreach="abc" t-as="item" t-key="item"><span t-key="item_index"/></t></div>`;
expect(() => renderToString(test)).toThrow(
'Invalid loop expression: "undefined" is not iterable'
);
expect(() => renderToString(test)).toThrow("Invalid loop expression");
});
test("t-foreach with t-if inside", () => {
-5
View File
@@ -62,11 +62,6 @@ describe("t-out", () => {
expect(renderToString(template, { var: undefined })).toBe("");
});
test("top level t-out with null", () => {
const template = `<t t-out="var"/>`;
expect(renderToString(template, { var: null })).toBe("");
});
test("with an extended String class", () => {
class LoveString extends String {
valueOf(): string {
-70
View File
@@ -100,74 +100,4 @@ describe("translation support", () => {
expect(translateFn).toHaveBeenCalledWith("some word");
expect(fixture.innerHTML).toBe("<div>un mot</div>");
});
test("body of t-sets are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label">untranslated</t>
<t t-esc="label"/>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("translated");
});
test("body of t-sets inside translation=off are not translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-translation="off">
<t t-set="label">untranslated</t>
<t t-esc="label"/>
</t>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("untranslated");
});
test("body of t-sets with html content are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label"><div>untranslated</div></t>
<t t-out="label"/>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div>translated</div>");
});
test("body of t-sets with text and html content are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label">
some text
<div>untranslated</div>
</t>
<t t-out="label"/>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe(" translated <div>translated</div>");
});
test("t-set and falsy t-value: t-body are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label" t-value="false">untranslated</t>
<t t-esc="label"/>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("translated");
});
});
@@ -212,73 +212,6 @@ exports[`changing state before first render does not trigger a render 1`] = `
}"
`;
exports[`component destroyed just after render 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`B\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`component destroyed just after render 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`B\`);
const b3 = text(ctx['state'].value);
return multi([b2, b3]);
}
}"
`;
exports[`components are not destroyed between animation frame 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`B\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`A\`);
if (ctx['state'].flag) {
b3 = comp1({}, key + \`__1\`, node, this, null);
}
return multi([b2, b3]);
}
}"
`;
exports[`components are not destroyed between animation frame 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`C\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`B\`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
}"
`;
exports[`components are not destroyed between animation frame 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`C\`);
}
}"
`;
exports[`concurrent renderings scenario 1 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1319,31 +1252,6 @@ exports[`delayed render does not go through when t-component value changed 3`] =
}"
`;
exports[`delayed render is not cancelled by upcoming render 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`B\`, true, false, false, [\\"state\\",\\"isEmpty\\"]);
return function template(ctx, node, key = \\"\\") {
return comp1({state: ctx['state'],isEmpty: ctx['state'].groups.length===0}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`delayed render is not cancelled by upcoming render 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['props'].state.groups.length);
const b3 = text(ctx['props'].state.config.test);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, but then initial rendering is cancelled by yet another render 1`] = `
"function anonymous(app, bdom, helpers
) {

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