Compare commits

...

55 Commits

Author SHA1 Message Date
Géry Debongnie 63128b7d29 [REL] v2.1.2
# v2.1.2

 - [FIX] t-out: does not crash when outputting null
 - [FIX] playground: fix name of playground component
 - [FIX] playground: use ES owl version instead of iife
 - [IMP] playground: give a name so it is more explicit in devtools
 - [REF] github page: move page to docs folder on master branch
 - [IMP/FIX] playground: fix and improve various things
2023-04-24 16:29:43 +02:00
Géry Debongnie f9d810a42f [FIX] t-out: does not crash when outputting null
Before this commit, doing `t-out="null"` would crash, since the
safeOutput function would assume that it is a block, thanks to the fact
that `typeof null === 'object'`.

We handle here the null value just as if it was undefined: it outputs an
empty string.
2023-04-24 16:23:27 +02:00
Samuel Degueldre 1784a98b2c [FIX] playground: fix name of playground component 2023-04-24 15:53:09 +02:00
Géry Debongnie 52be5ae85f [FIX] playground: use ES owl version instead of iife
Otherwise, it will try to import owl from the iife build, which will not
work
2023-04-24 15:44:41 +02:00
Géry Debongnie 0765b19367 [IMP] playground: give a name so it is more explicit in devtools 2023-04-24 15:44:41 +02:00
Samuel Degueldre b8b5190bc6 [REF] github page: move page to docs folder on master branch
Currently, some of the things on the playground must be changed directly
on the master branch while other things require checking out the
gh-pages branch and making the modifications there. Even when changes
need to be made on the master branch, all changes that are not direcly
in owl itself need to be copied by hand to the gh-pages branch.

This commit moves all files that exist on the gh-pages branch to the
master branch in the docs folder, this change will require configuring
the github page to use the docs folder instead of a separate branch, but
will make maintenance of the github page and playground easier going
forward.
2023-04-24 15:32:51 +02:00
Samuel Degueldre 9475de4d18 [IMP/FIX] playground: fix and improve various things
The playground hasn't been touched for a while and a few things were
broken before this commit:
- Exporting as a standalone web application was broken because it still
attempted to load the templates like it was done in owl 1
- Resizing the editor tabs still tried to use `this.trigger`

While fixing the export feature, the files needed by the app were
factored out of hardcoded strings and into real files, as this makes it
easier to maintain, since these files get syntax highlighting.

The samples received the same treatment: there is now a `samples` folder
containing all the samples, it also contains a jsconfig, giving
autocompletion on owl functions while editing the files, and allowing
the owl import to look how it would when using owl as a node_module.
In the browser, this import is translated to the relative path of the
owl module using an import map.
2023-04-24 15:32:51 +02:00
Géry Debongnie 4b9e6bad0b [REL] v2.1.1
# v2.1.1

 - [FIX] components: solve missing update (concurrency issue)
 - [DOC] remove old references to comp for useRef hook
 - [FIX] devtools: Fix build commands for windows users
 - [FIX] devtools: Fix devtools in detached window
 - [IMP] devtools: Add a button to navigate to the doc
 - [IMP] devtools: Add devtools documentation
 - [IMP] Allow app to be mounted in shadow DOM
 - [IMP] playground: allow sharing playground links
 - [IMP] runtime: allow validating object values using a type description
 - [FIX] devtools: Increase vertical padding of the search bar
 - [FIX] devtools: Hide the collapse all button
 - [FIX] devtools: Fix bad computation of highlight boxes on the page
 - [FIX] package: Auto-update package-lock.json
2023-04-17 11:08:57 +02:00
Géry Debongnie 9a5ff7a619 [FIX] components: solve missing update (concurrency issue)
Before this commit, in some specific situations, owl could skip updating
the DOM, even though it should. Here is what could happen:

- we have a parent component (A) and a child component (B)
- B depends on some reactive props from A
- we update some state that results in A being rendered, and B updated
- before this render is applied to the dom, we update some state again,
which causes A to be rendered again
- however, this new render is such that the B props are now identical,
so it will skip rendering B
- it will then apply the result of the render to the DOM => only A is
updated but B should also be updated!

The problem comes from the fact that Owl committed the props to the
component right after the rendering, to the new props were used in the
props comparison method to decide if B should be updated. This is
incorrect, we should always use the current props to decide what to do,
and only commit them to the component after it has been patched.
2023-04-17 11:06:31 +02:00
Géry Debongnie 5d40cc112d [DOC] remove old references to comp for useRef hook
Going from owl 1.0 to 2.0, we removed the possibility of having a
reference to a component. However, the documentation was not properly
updated to reflect that.
2023-04-13 10:39:20 +02:00
Julien Carion (juca) aa19897031 [FIX] devtools: Fix build commands for windows users
This commit adapts the commands used and the rollup file to be
compatible with windows console commands syntax.
2023-04-13 10:17:59 +02:00
Julien Carion (juca) afd7f957df [FIX] devtools: Fix devtools in detached window
This commit fixes the issues that were happening when using the
devtools in a detached window
2023-04-13 09:30:44 +02:00
Julien Carion (juca) cca4b4bb8e [IMP] devtools: Add a button to navigate to the doc
This commit adds a button to the devtools navbar which links
to the documentation.
2023-04-12 17:11:45 +02:00
Julien Carion (juca) b848a83777 [IMP] devtools: Add devtools documentation
This commit adds the documentation for the devtools extension.
2023-04-11 11:01:07 +02:00
tsm-odoo c0c40c0387 [IMP] Allow app to be mounted in shadow DOM
This commit allow owl Apps to be mounted into a shadow DOM.
2023-04-11 08:43:42 +02:00
Samuel Degueldre 606421776b [IMP] playground: allow sharing playground links
This is useful to share editable snippets of owl code, which can be used
to paste a reproduction for a given issue or give someone an interactive
example of some feature.
2023-04-06 11:48:36 +02:00
Samuel Degueldre 47009912df [IMP] runtime: allow validating object values using a type description
In JS, objects are often used as a mapping that gives fast lookup on the
keys, in those cases, the keys themselves are not known in advance, but
the values may have a shape that they're expected to follow. This commit
adds support for a `values` key in schema for objects, which will be
checked against all values in the object during validation.
2023-04-06 08:58:58 +02:00
Julien Carion (juca) fea8fcaf61 [FIX] devtools: Increase vertical padding of the search bar
This commit increases the vertical padding of the search bar in
order to be easier to click on.
2023-04-06 08:58:30 +02:00
Julien Carion (juca) e562f6a24f [FIX] devtools: Hide the collapse all button
This commit hides the collapse all button when the profiler is in
Events log mode since it doesn't do anything in this case
2023-04-06 08:58:30 +02:00
Julien Carion (juca) 23acf203e9 [FIX] devtools: Fix bad computation of highlight boxes on the page
This commit adds box-sizing: border-box to the highlight elements
created by the devtools on the page. This was making some of the
boxes overflow on some pages (like on POS sessions) because
the border-box box-sizing was not the default behavior on these
pages.
2023-04-06 08:58:30 +02:00
Julien Carion (juca) 94ae940e82 [FIX] package: Auto-update package-lock.json
This commit updates the version in package-lock.json and make it so
it is automatically and correctly updated at each new release.
2023-03-31 13:39:30 +02:00
Géry Debongnie 64de716b91 [REL] v2.1.0 2023-03-30 13:05:25 +02:00
Julien Carion (juca) 1b1597c49e [IMP] tools: Include the devtools into the release system
This commit adds the devtools as a zip file containing both chrome
and firefox versions of the extension in the release and also updates
the doc to install it easily.
2023-03-30 13:03:00 +02:00
Julien Carion ef5e4a0637 [ADD] tools: create owl-devtools extension
This commit adds the code of the owl devtools extension.
This extension can be added on chrome or on firefox for
developpers to be able to inspect the contents of all owl
applications that are present on any web page, see all the
components, interract with them and their content to some
extend and perform a bit of profiling with their renders.
2023-03-30 12:57:28 +02:00
Lucas Lefèvre a9323c3fcd [REF] hooks: avoid confusion for sync hooks
`onWillPatch`, `onWillUnmount`, `onWillDestroy` are synchronous hooks.
Owl will not wait any promise return by the callback.
Yet, the callback return type includes `Promise<void>`,
which is confusing as one might think it means the hooks is async.
2023-03-24 10:32:47 +01:00
Louis Wicket ce61e90135 [FIX] doc: add missing brackets 2023-03-22 15:19:00 +01:00
Géry Debongnie 8893e026d3 [REL] v2.0.9
# v2.0.9

 - [FIX] components: do not crash when binding anonymous function
2023-03-13 10:55:05 +01:00
Géry Debongnie 0024f33fa1 [FIX] components: do not crash when binding anonymous function
It is not really useful, but it is possible to bind an anonymous
function prop. However, with the recent change on how the bind feature
works, it now crashes.

This commit makes sure that we properly apply the `bind` operation to
the function, and not to the last term of the function.
2023-03-09 15:35:13 +01:00
Géry Debongnie 532ab7fae0 [REL] v2.0.8
# v2.0.8

 - [IMP] implement .alike suffix on props
 - [IMP] release: add version number on App
 - [IMP] app: add name as a config option
 - [FIX] runtime, compiler: fix refs getting set or unset incorrectly
 - [FIX] compiler: call translate function with correct string
 - [FIX] compiler: properly handle readonly attribute/readOnly property
 - [REF] blockdom,compiler: implement properties
 - [REF] tests: move properties tests in own file
 - [FIX] compiler: dynamic value on inputs doesn't turn 0 into empty string
2023-03-09 13:11:21 +01:00
Géry Debongnie a51b286671 [IMP] implement .alike suffix on props
It is common in Owl components to have anonymous function as props.
However, since each rendering create a different (but equivalent)
closure, Owl will consider the props different, so will update the child
component, but this is (often) not necessary.

This commit will help reduce the problem by introducing a new `.alike`
prop suffix, that will let Owl know that each version of that specific
prop should be considered the same, so, will be ignored by the props
comparison code.

closes #1360
2023-03-09 11:14:00 +01:00
Géry Debongnie aadc9eafa3 [IMP] release: add version number on App
Before this commit, the version number was added by the rollup
configuration script. It worked, but it meant that there was no robust
way to access the version number in many case. For example, what if I
have a page with two different version of owl? Or what if one of them
uses the ESM module system, and does not export owl as a global object?

This is not really a big deal, but accessing the version number is
useful for tooling purpose, so this commit makes it possible to read the
version number on each individual App, as a static property.
2023-03-08 10:18:35 +01:00
Géry Debongnie 1f60bc79c5 [IMP] app: add name as a config option
Useful for tooling purpose

closes #1342
2023-03-08 10:16:25 +01:00
Samuel Degueldre 975ed32f0b [FIX] runtime, compiler: fix refs getting set or unset incorrectly
Previously, refs could get set to null incorrectly, or could stay set
when they shouldn't. This was caused by the fact that singleRefSetter
and multiRefSetters are created on every render, meaning that any render
that did not affect the status of the ref (mounted or not), would
overwrite the previous ref setter, including its closure, causing the
captured `_el` or `count` to be lost. This means that on a subsequent
render that did affect the status of the ref, the singleRefSetter would
consider that it did not set the ref, and so shouldn't unset it, causing
it to incorrectly stay keep the element, while in multiRefSetter, the
opposite problem occured: the count would be 0 on removal, causing it to
believe that it was the first call in the corresponding patch that
affected the ref, when in fact, the closure is already stale, because it
was created from the previous render, and the fresh multiRefSetter from
the latest render may already have been called by an element with that
ref that came earlier in the template.

This commit changes the ref setting strategy to work around the issue of
stale closures: we define a setRef method on ComponentNode that is
always called, this method ignores calls with `null`, meaning that the
refs object on the ComponentNode never reverts to a null value for any
key. Instead, the useRef hook will check whether the element that the
ref points to is still mounted, and return null if not.
2023-03-06 15:17:14 +01:00
Géry Debongnie 7ac81ed5fe [FIX] compiler: call translate function with correct string
Before this commit, the parser would remove all consecutive white spaces
for text nodes. After that, the code generator would call the translate
function with the resulting string, which then is different than what we
would expect.

With this commit, we make sure we apply the translation before removing
the additional whitespace. To do that, we have to move the code
processing the string from the parser into the code generator, which
actually makes sense, as the parser should only collect all useful
information without applying too much logic.

closes #1351
2023-03-06 14:52:25 +01:00
Géry Debongnie cdad48d3a6 [FIX] compiler: properly handle readonly attribute/readOnly property
Before this commit, Owl template compiler would handle readonly
attribute as readonly properties. But this is incorrect, since the
property is actually named readOnly.

With this commit, we make sure that readonly AND readOnly are both
interpreted as the `readOnly` property. This is due to the fact that
QWeb does not discriminate between attribute and properties, so we have
to infer which is which.

closes #1362
2023-03-01 12:53:37 +01:00
Géry Debongnie f892929c80 [REF] blockdom,compiler: implement properties
Before this commit, properties were just handled as a special case of
attributes. But it does not make that much sense, since they are
different. For example, the `readOnly` property does not have the same
name as the `readonly` attribute.  The confusion probably comes from the
fact that QWeb does not distinguish between property and attributes, so
Owl has to infer which one is which.

With this commit, we introduce a `block-property` directive in blockdom,
and change the compiler to use it in emitted code. It has the additional
benefits of slightly shrinking the runtime code, since the `isProp`
function can now be located in the compiler.
2023-03-01 12:53:37 +01:00
Géry Debongnie f0fb3ab64e [REF] tests: move properties tests in own file 2023-03-01 12:53:37 +01:00
Samuel Degueldre a35b9814c0 [FIX] compiler: dynamic value on inputs doesn't turn 0 into empty string
In #1161, we fixed blockdom treating 0 as falsy when patching a value,
which would result in an empty attribute. It turns out that there is
another place where we had the same fallback, which is when we compute a
dynamic attribute value, so while blockdom had the ability to set the
value to 0, when using a dynamic attribute value we would never give it
0 as a value. This commit changes the fallback strategy for dynamic
attribute values to be the same as the one in blockdom.

closes #1358
2023-02-24 16:22:24 +01:00
Géry Debongnie 276c8a0295 [REL] v2.0.7
# v2.0.7

 - [FIX] compiler: t-key and t-ref together
2023-02-20 09:44:36 +01:00
Lucas Perais 0717fe241d [FIX] compiler: t-key and t-ref together
Have a t-ref on a DOM node with a t-key.
Change the t-key.

Before this commit, the old block removed its element from the component's refs *after*
the new block had been mounted, meaning that in effect, the resulting
ref at the end of the whole patch was null.

After this commit, we only remove an element from the component's ref if that very same
element was indeed the ref. (otherwise it means someone else has changed the ref.)
2023-02-20 09:42:16 +01:00
Géry Debongnie 1291f1f175 [REL] v2.0.6
# v2.0.6

 - [IMP] devtools: provide access to Fiber and RootFiber
 - Bump shelljs and git-rev-sync
 - [FIX] props validation: do not subscribe to props keys
 - [FIX] reactivity: only show key in subscription if observed by callback
 - [FIX] components: stop rendering stale t-component when delayed
2023-02-17 14:31:18 +01:00
Géry Debongnie 6c0a3525c8 [IMP] devtools: provide access to Fiber and RootFiber
The devtools extension needs to hook itself into some internal functions of
owl, and to do that, it needs a reference to Fiber and RootFiber
classes.
2023-02-17 14:20:03 +01:00
dependabot[bot] 13241422e9 Bump shelljs and git-rev-sync
Bumps [shelljs](https://github.com/shelljs/shelljs) to 0.8.5 and updates ancestor dependency [git-rev-sync](https://github.com/kurttheviking/git-rev-sync-js). These dependencies need to be updated together.


Updates `shelljs` from 0.7.7 to 0.8.5
- [Release notes](https://github.com/shelljs/shelljs/releases)
- [Changelog](https://github.com/shelljs/shelljs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/shelljs/shelljs/compare/v0.7.7...v0.8.5)

Updates `git-rev-sync` from 1.12.0 to 3.0.2
- [Release notes](https://github.com/kurttheviking/git-rev-sync-js/releases)
- [Commits](https://github.com/kurttheviking/git-rev-sync-js/compare/v1.12.0...v3.0.2)

---
updated-dependencies:
- dependency-name: shelljs
  dependency-type: indirect
- dependency-name: git-rev-sync
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-17 11:42:38 +01:00
Géry Debongnie 8f2c7f24d7 [FIX] props validation: do not subscribe to props keys
In dev mode, owl inserts an additional props validation step. Before
this commit, the validation would iterate on all key/value pairs of each
props. If the props is reactive, this has the unfortunate side effect of
subscribing the component to each of the keys, even though it should not
be.

This commit avoids the issue by only validating the raw object.
2023-02-09 13:34:07 +01:00
Samuel Degueldre fed9cc467f [FIX] reactivity: only show key in subscription if observed by callback
Previously, the getSubscriptions debugging utility returned all observed
keys for a given target, instead of only the keys observed by the
callback it received as argument. This commit fixes that issues.
2023-02-09 10:13:41 +01:00
Samuel Degueldre 33174b301b [FIX] components: stop rendering stale t-component when delayed
Previously, if the value of a t-component directive changed, but there
was already a scheduled render for the component before that change, the
rendering of the existing component would get delayed (as it should),
but after the parent's rendering was complete, the old component which
will be destroyed during the patch would still get rendered, despite
being stale. This can cause crashes if that component relies on state
that no longer exists.

This was caused by the fact that when rendering, we check the parent
chain for an active render, and also check that the component still
exists in the parent's fiber childrenMap (ie, we know that the upcoming
patch is not about to destroy the component). To do this, we use the
component's parentKey, but in the case of t-component, the parentKey for
both possible components is the same, causing the stale component to
incorrectly believe that it's not about to be destroyed, by finding the
next component in the childrenMap under the same key.

This commit fixes that by prepending the component's name to the
parentKey, meaning that if the value of the t-component changes, so will
the key, and the render will be further delayed until the new state is
patched, at which point the stale component will be destroyed and the
following attempt to render will be cancelled as expected.

The choice to use the component's name is to simplify the
implementation, this means that if using t-component and switching
between components that have the same class name, this protection will
not work. The alternative would be to generate a unique id for the
component class, eg with a WeakMap, but this both complicates the
implementation and adds runtime overhead, for a case that is likely
extremely rare. We are open to refine the implementation should this
problem occur in practice.
2023-02-06 11:11:42 +01:00
Samuel Degueldre ea5d2be502 [REL] v2.0.5
# v2.0.5

 - [FIX] reactivity: improve performance for long-lived reactives
2023-01-27 15:29:07 +01:00
Samuel Degueldre 4a761a7403 [FIX] reactivity: improve performance for long-lived reactives
Previously, when having a long lived reactive object and writing a lot
of keys to it, clearing the reactive subscriptions on that object would
slow down as time went on. This is because when clearing a target's
subsctiptions for a callback, we look at all the keys that are observed, and for all
the observed keys, we remove the callback from the set of callbacks
observing that key. The problem arises from the fact that after doing
that, even if the set of callbacks observing that key is now empty, we
don't remove the key from the observed keys, meaning that any further
clearing of subscriptions will have to iterate over that key to attempt
to clear the callbacks even if there are none.

This commit fixes this problem by simply removing the key from the
observedKeys if there are no longer any callbacks observing it.

This commit also improves performance for bare reactives (reactives
created with no callback). The initial implementation simply creates a
default empty callback when creating a reactive, and treats it like any
other, meaning it can observe keys and be notified of changes even
though we know in advance that it does nothing. This can compound with
the previous issue when you're doing a lot of manipulations on a bare
reactive internally for the sole purpose of notifying outside observers,
as this creates a lot of useless work in the reactivity system.
2023-01-27 09:34:55 +01:00
Lucas Lefèvre 8702db03fb [FIX] tools: allow to pre-compile templates with - in their name
Compiling a template with a dash ("-") in its name generates an
invalid function name in the compiled code.

A template named `"my-component"` leads to the function
`function my-component(app, bdom, helpers) { ... }` which has an
invalid name.

closes #1333
2023-01-25 07:56:30 +01:00
Samuel Degueldre b2685b6709 [IMP] tools: pull playground before trying to publish
Previously, if you were not the last person to publish to the
playground, your playground branch would be behind the remote and trying
to push to it would fail.

This commit attempts to pull the changes before updating owl so that
even if you were not the last person to push to the playground, as long
as the pull is a fast-forward, publishing to the playground won't fail.

This commit also adapts some of the status number manipulation to use
bitwise or instead of addition, since return status code can be both
positive or negative and may cancel one another. Using bitwise or
ensures than any non-zero code will make the status non-zero and stay
that way.
2023-01-23 12:39:50 +01:00
Samuel Degueldre c30678f3ea [REL] v2.0.4
# v2.0.4

 - [IMP] app: expose live apps for the devtools
 - [FIX] compiler: support t-model radio group in t-foreach
 - [IMP] runtime: improve useExternalListener typing
2023-01-23 12:21:13 +01:00
Samuel Degueldre 6ca6717965 [IMP] tools: release scripts creates a template for release notes
This template contains the release version as a markdown title followed
by a markdown list with all the commit titles since the previous
release.
2023-01-23 12:19:14 +01:00
Julien Carion ad4adb930e [IMP] app: expose live apps for the devtools
This commit exposes owl apps in a global variable so that the owl
devtools can hook themselves on these apps. While the devtools are not
yet ready to be merged, exposing the apps will allow us to test the
devtools in production scenarios while polishing the development of
them.
2023-01-23 11:40:02 +01:00
Lucas Perais cea82e945d [FIX] compiler: t-model supports radio group in t-foreach
Have a radio group defined inside a t-foreach:
```xml
  <t t-foreach="values" t-as="val" t-key="val">
    <input name="radiogroup" t-att-value="val" t-model="state.radioGroup" />
  </t>
```

Before this commit the algorithm that set the "checked" attribute on the current active
radio button according to the state did not support having a dynamic value (`t-att-value`)

After this commit, this use case works as we go look in the dynamic attributes too.
2023-01-20 09:26:54 +01:00
Samuel Degueldre a69f8a39e7 [IMP] runtime: improve useExternalListener typing
Previously the type of the target for useExternalListener was
HTMLElement or Window, this makes document an invalid target. Here there
is no reason to use the EventTarget interface instead, as
useExternalListener only uses methods from that interface and should
work with any event target.

Closes odoo/owl#1323
2023-01-18 13:03:23 +01:00
209 changed files with 19916 additions and 3700 deletions
+1 -4
View File
@@ -20,12 +20,9 @@ yarn-error.log*
node_modules
# Extras temp file
/tools/owl.js
release-notes.md
.rpt2_cache
# useful in some cases
/temp
/temp
+22 -1
View File
@@ -113,6 +113,7 @@ Are you new to Owl? This is the place to start!
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
- [Owl devtools extension](doc/tools/devtools.md)
## Installing Owl
@@ -121,8 +122,28 @@ Owl is available on `npm` and can be installed with the following command:
```
npm install @odoo/owl
```
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl](https://github.com/odoo/owl/releases/latest)
## Installing Owl devtools
The Owl devtools browser extension is also available in the [release](https://github.com/odoo/owl/releases/latest):
Unzip the owl-devtools.zip file and follow the instructions depending on your browser:
### Chrome
Go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
Select the devtools-chrome folder and that's it, your extension is active!
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
### Firefox
Go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
Select any file in the devtools-firefox folder and that's it, your extension is active!
Here, you can use the reload button to refresh the extension.
Note that you may have to open another window or reload your tab to see the extension working.
Also note that the extension will only be active on pages that have a sufficient version of owl.
+7 -8
View File
@@ -112,17 +112,16 @@ of a component, rendered by Owl. It only work on a html element tagged by the
```xml
<div>
<input t-ref="someDiv"/>
<input t-ref="someInput"/>
<span>hello</span>
</div>
```
In this example, the component will be able to access the `div` and the component
`SubComponent` with the `useRef` hook:
In this example, the component will be able to access the `input` with the `useRef` hook:
```js
class Parent extends Component {
inputRef = useRef("someComponent");
inputRef = useRef("someInput");
someMethod() {
// here, if component is mounted, refs are active:
@@ -139,18 +138,18 @@ The `t-ref` directive also accepts dynamic values with string interpolation
`t-component` directives). For example,
```xml
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>
```
Here, the references need to be set like this:
```js
this.ref1 = useRef("component_1");
this.ref2 = useRef("component_2");
this.ref1 = useRef("div_1");
this.ref2 = useRef("div_2");
```
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` or `comp` on it will return `null`.
If this is not the case, accessing `el` on it will return `null`.
### `useSubEnv` and `useChildSubEnv`
+50
View File
@@ -4,6 +4,7 @@
- [Overview](#overview)
- [Definition](#definition)
- [Props comparison](#props-comparison)
- [Binding function props](#binding-function-props)
- [Dynamic Props](#dynamic-props)
- [Default Props](#default-props)
@@ -56,6 +57,47 @@ the `props` object contains the following keys:
- for `ComponentA`: `a` and `b`,
- for `ComponentB`: `model`,
## Props comparison
Whenever Owl encounters a subcomponent in a template, it performs a shallow
comparison of all props. If they are all referentially equal, then the subcomponent
will not even be updated. Otherwise, if at least one props has changed, then
Owl will update it.
However, in some cases, we know that two values are different, but they have the
same effect, and should not be considered different by Owl. For example, anonymous
functions in a template are always different, but most of them should not be
considered different:
```xml
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<Todo todo="todo" onDelete="() => deleteTodo(todo.id)" />
</t>
```
In that case, one can use the `.alike` suffix:
```xml
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<Todo todo="todo" onDelete.alike="() => deleteTodo(todo.id)" />
</t>
```
This tells Owl that this specific prop should always be considered equivalent
(or, in other words, should be removed from the list of comparable props).
Note that even if most anonymous functions should probably be considered `alike`,
it is not necessarily true in all cases. It depends on what values are captured
by the anonymous function. The following example shows a case where it is probably
wrong to use `.alike`.
```xml
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<!-- Probably wrong! todo.isCompleted may change -->
<Todo todo="todo" toggle.alike="() => toggleTodo(todo.isCompleted)" />
</t>
```
## Binding function props
It is common to have the need to pass a callback as a prop. Since Owl components
@@ -95,6 +137,9 @@ class SomeComponent extends Component {
}
```
The `.bind` suffix also implies `.alike`, so these props will not cause additional
renderings.
## Dynamic Props
The `t-props` directive can be used to specify totally dynamic props:
@@ -167,6 +212,7 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of
- `type`: the main type of the prop being validated
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
- `values`: if the type was `Object`, then the `values` key describes the interface of values in the object, this allows validating objects that are used as mappings, where keys are not known in advance but the shape of the values is.
- `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic.
- `optional`: if true, the prop is not mandatory
@@ -231,6 +277,10 @@ 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
@@ -477,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
@@ -0,0 +1,56 @@
# Owl Devtools Browser extension
The owl devtools browser extension is an extension available on chrome or firefox which adds an owl tab
to the browser devtools in order to inspect all owl apps that are present on any web page, their components
and allows to interract with their data to a certain extend. There is also a profiler available to visualize
the components' lifecycle and be able to trace their origin.
See the [`devtools doc`](devtools_guide.md) for more information.
## Install the extension manually (for devs)
In the owl root folder:
```bash
npm install
```
For chrome:
```bash
npm run build:devtools-chrome
```
For firefox:
```bash
npm run build:devtools-firefox
```
You can also run:
```bash
npm run dev:devtools-chrome
```
or
```bash
npm run dev:devtools-firefox
```
to avoid recompiling owl and gain time if it has already been done.
To run the extension:
In google chrome: go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
Select the output folder (dist/devtools) and that's it, your extension is active!
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
In firefox: go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
Select any file of the output folder (dist/devtools) and that's it, your extension is active!
Here, you can use the reload button to refresh the extension.
Note that you may have to open another window or reload your tab to see the extension working.
Also note that the extension will only be active on pages that have a sufficient version of owl.
+150
View File
@@ -0,0 +1,150 @@
# Owl Devtools Guide
## Information popup
After having installed the extension, a new icon will be added to your extension bar.
If you don't see it, you can pin the extension using the extensions popup.
<img src="screenshots/extensions.png"/>
Clicking on the owl icon will open the information popup. This popup is useful
to know in advance whether owl is loaded in the tab or not. This is also indicated
by the icon itself: if it is flipped upside-down, it means that owl is not loaded in the
active tab. Do note that old versions of owl are not supported by the extension and will
therefore be indicated either as obsolete or absent by the extension popup.
<img src="screenshots/popup.png"/>
## First steps
When you are on a page where owl is detected, you can open your devtools either with
right-click -> Inspect or using F12. In the devtools menu, you can search for the Owl
tab which is added by the extension. It will be present by default at the end of the list but
you can drag and drop it at the position you want for easier navigation in the future.
<img src="screenshots/find_owl_tab.png"/>
When you open the tab, you arrive on the Components view by default which is one of the
two available tabs at the top. Here is an example of the devtools on the Odoo CRM app:
<img src="screenshots/crm.png"/>
## Components tab
The components tab is separated into two sub windows: the components tree in the left and
the component details in the right. The components tree will display all the different
components that are present in the tab in the form of a tree. The root of this tree is
actually the app which is not a component but can still be inspected by the devtools like
one. There can also be multiple apps loaded in the page like in the following:
<img src="screenshots/multi_apps.png"/>
There is a convenient search bar at the top of the components tree which will help finding
the components tou want in the tree and also, an element picker can be used to directly select
the component you want to focus on in the page which is especially useful when trying to find
what you want. Just click on the elements picker icon and click on the element you want to focus
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
page in this mode will highlight it and the same happens anytime in the components tree.
<img src="screenshots/picker.png"/>
In the tree itself, the navigation is quite simple and is similar to the one in the Elements tab
of the browser's devtools. It is possible to navigate with the keyboard using the arrow keys and
multiple shortcuts are available in a custom menu when right-clicking on a component. This menu
allows to expand/fold all the children nodes of a component, fold its direct children only, inspect
the source code of the component, send it as a global variable in the console, go to the Elements tab
and focus on its content, force a rerender of the component, send its observed states to the console
as a global variable, inspect its compiled template in the Sources tab or send its raw template
to the console.
<img src="screenshots/menu.png"/>
The component details window in the right will show the component that is currently selected as well
as its env, props, observed states and all the other variables that are present on its instance.
While the props and the env are already present on the actual instance of the component and are
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
The observed state is actually information about which variables are observed by the component
which will trigger a rerender of the component when it is modified. The keys represent which part of the
variable is actually observed and the target is the actual variable. For simplicity, the properties
that are not observed by the component are greyed out while the others are in bold. This means that
editing bold ones will trigger a rerender while the greyed out ones will not.
<img src="screenshots/states.png"/>
In the given example, we have two keys/target pairs for two different variables. The first one indicates
that adding or removing an element to the array will trigger a rerender since the length will have changed.
Replacing the element at index 0, 1 or 2 will also have the same effect as implied by the keys. It doesn't
mean that editing the properties of element at index 0, 1 or 2 will rerender the component though. It may
be the case for some but this will be described in another keys/target pair. The second keys/target pair
is actually the element at index 0 of the first pair. It only has id in the keys meaning that only the
id property will actually trigger a rerender the component when modified. Be aware however that the other
properties may be in the observed state of another component like a child one in this case. A greyed out
property only implies it is not reactive for the selected component and not for the others.
The navigation inside the properties is also similar to the one in console variables: properties have
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
send any property to the console using the right-click context menu on it and functions can be inspected
in the sources tab as well.
<img src="screenshots/function_menu.png"/>
There are several icons available to perform several of the actions described before in the components
tree context menu and all these actions are also available by opening the menu by right-clicking on the
component's name. Using the left click on the component's name will focus it in the components tree.
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
property's value and modify it using the freshly created input then press enter to apply the changes.
Do note that the modified values should be written in JSON format in order to be valid (examples:
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Whether it has an impact on the
component or not and whether it produces an error is the responsability of the user.
<img src="screenshots/edit.png"/>
## Profiler
The profiler tab is the other tab of the owl devtools. It consists in an actions bar at the top and
a tree/list of events related to the owl components' renders. Here is an example of the events launched
when entering the Odoo Crm app.
<img src="screenshots/profiler.png"/>
In the initial state, no event is displayed. You need to activate the recording of events before they
are intercepted by the devtools using the record button.
<img src="screenshots/record.png"/>
The second button is used to clear all the events that have been recorded. The select can be used to
switch between the tree view (which shows the causality between renders) and the events log view which
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
update and destroy events which reveals the component that initiated the event.
<img src="screenshots/events_log.png"/>
The third button is only visible in tree view and allows to fold all the render events that were recorded.
Some actions are also available when using the right-click on any event of the tree view for navigation
purpose in a similar fashion as in the components tree.
<img src="screenshots/tree_actions.png"/>
There is also the Trace Renderings and Trace Subscriptions features. These features are independant of the
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
the console all the render events and allows to show their traceback information. Similarly, the Trace
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
of the modification
<img src="screenshots/trace_rendering.png"/>
<img src="screenshots/trace_subscriptions.png"/>
## Options
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
be toggled using the sun/moon icon at the top-right corner of the tab. All the examples above were created
with the dark mode enabled. There is also a refresh button to completely reset the owl devtools.
<img src="screenshots/darkmode.png"/>
## Troubleshooting
If the feedback from the page to the devtools seems to be cut, just close the devtools and refresh the page.
This will eventually happen any time a tab stays opened for too long without being refreshed.
Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

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

After

Width:  |  Height:  |  Size: 12 KiB

+15
View File
@@ -0,0 +1,15 @@
import { mount, Component, useState, loadFile } from "@odoo/owl";
class Counter extends Component {
static template = "Counter";
setup() {
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
const templates = await loadFile("./counter.xml");
mount(Counter, document.getElementById("app-container"), { templates });
+7
View File
@@ -0,0 +1,7 @@
<templates>
<t t-name="Counter">
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</t>
</templates>
+9
View File
@@ -0,0 +1,9 @@
import { loadFile } from "@odoo/owl";
for (const [className, type] of [["xml", "xml"], ["javascript", "js"]]) {
loadFile(`./counter.${type}`).then(code => {
const el = document.querySelector(`code.${className}`);
el.textContent = code;
hljs.highlightBlock(el);
})
}
+75
View File
@@ -0,0 +1,75 @@
<!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 src="./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="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
<script type="module" src="display_code.js"></script>
<script type="module" src="counter.js"></script>
</body>
</html>
+5925
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL Playground</title>
<link rel="icon" href="data:,">
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="playground.js"></script>
</head>
<body>
</body>
</html>
@@ -20,7 +20,7 @@ body {
display: grid;
height: 100%;
width: 100%;
grid-template-columns: auto 8px 100%;
grid-template-columns: auto 8px 1fr;
}
/* LEFT BAR ****************************************/
@@ -79,6 +79,19 @@ body {
margin: 5px;
}
.share-code {
position: relative;
}
.copied-notification {
font-size: x-small;
position: absolute;
bottom: -5px;
white-space: nowrap;
left: 50%;
transform: translateX(-50%);
}
/* TABBED EDITOR ****************************************/
.tabbed-editor {
@@ -1,7 +1,19 @@
import { SAMPLES } from "./samples.js";
import { debounce, loadJS } from "./utils.js";
import { exportStandaloneApp } from "./export_snippet.js";
const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl;
import {
App,
Component,
useState,
useRef,
onMounted,
onWillUnmount,
onPatched,
onWillUpdateProps,
loadFile as _loadFile,
whenReady,
__info__,
useEffect,
onWillStart,
} from "../owl.js";
//------------------------------------------------------------------------------
// Constants, helpers, utils
@@ -16,40 +28,38 @@ const MODES = {
const DEFAULT_XML = `<templates>
</templates>`;
// Memoize loadFile so that samples aren't reloaded whenever a sample is selected
const fileCache = {};
const loadFile = (path) => {
if (!(path in fileCache)) {
fileCache[path] = _loadFile(path);
}
return fileCache[path];
}
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`');
// escape backticks in the xml so they don't close the template string
const escapedXml = xml.replace(/`/g, '\\\`');
// create iframe
const iframe = document.createElement("iframe");
iframe.onload = () => {
const doc = iframe.contentDocument;
const utilsMod = doc.createElement("script");
utilsMod.setAttribute("type", "module");
utilsMod.setAttribute("src", "utils.js");
doc.head.appendChild(utilsMod);
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "module";
const content = `
(async function(TEMPLATES) {
${js}
})(\`${sanitizedXML}\`)`;
script.innerHTML = content;
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
// inject css
const importMap = doc.createElement("script");
importMap.type = "importmap";
importMap.textContent = `{ "imports": { "@odoo/owl": "../owl.js" } }`;
doc.head.appendChild(importMap);
const script = doc.createElement("script");
script.type = "module";
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
doc.body.appendChild(script);
const style = document.createElement("style");
style.innerHTML = css;
style.innerText = css;
doc.head.appendChild(style);
};
return iframe;
@@ -58,53 +68,84 @@ function makeCodeIframe(js, css, xml) {
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
const SAMPLES = [
{
description: "Components",
folder: "components",
code: ["js", "xml", "css"],
},
{
description: "Form Input Bindings",
folder: "form",
code: ["js", "xml"],
},
{
description: "Inline templates",
folder: "single_file_component",
code: ["js"],
},
{
description: "Lifecycle demo",
folder: "lifecycle_demo",
code: ["js", "xml", "css"],
},
{
description: "Customized hook",
folder: "custom_hooks",
code: ["js", "xml", "css"],
},
{
description: "Todo List App (with reactivity)",
folder: "todo_app",
code: ["js", "xml", "css"],
},
{
description: "Responsive app",
folder: "responsive_app",
code: ["js", "xml", "css"],
},
{
description: "Slots And Generic Components",
folder: "slots",
code: ["js", "xml", "css"],
},
{
description: "Window Management System",
folder: "window_manager",
code: ["js", "xml", "css"],
},
{
description: "Benchmark example",
folder: "benchmark",
code: ["js", "xml", "css"],
},
]
function loadSamples() {
let result = SAMPLES.slice();
const result = SAMPLES.map(({ description, folder, code }) => ({
description,
code: async () => Object.fromEntries(
await Promise.all(
code.map(async (type) => [type, await loadFile(`./samples/${folder}/${folder}.${type}`)])
)
),
}));
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: js,
xml,
css
code: () => Promise.resolve({ js, xml, css }),
});
}
return result;
}
function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample");
}
function useSamples() {
const samples = loadSamples();
const component = owl.useComponent();
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
class TabbedEditor extends Component {
setup() {
const props = this.props;
this.state = useState({
@@ -168,7 +209,7 @@ class TabbedEditor extends owl.Component {
const resizer = ev => {
const delta = ev.clientY - y;
y = ev.clientY;
this.trigger("updatePanelHeight", { delta });
this.props.updatePanelHeight({ delta });
};
document.body.addEventListener("mousemove", resizer);
document.body.addEventListener("mouseup", () => {
@@ -205,25 +246,51 @@ TabbedEditor.template = "TabbedEditor";
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
class Playground extends Component {
static template = "Playground";
static components = { TabbedEditor };
setup() {
this.version = owl.__info__.version;
this.SAMPLES = useSamples();
this.isDirty = false;
this.version = __info__.version;
this.isDirty = false;
this.state = useState({
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
js: "",
css: "",
xml: DEFAULT_XML,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
});
this.samples = loadSamples();
if (window.location.hash) {
try {
const { js, css, xml } = JSON.parse(atob(decodeURIComponent(window.location.hash.slice(1))));
if ([js, css, xml].every(item => typeof item === "string")) {
Object.assign(this.state, { js, css, xml });
}
} catch {}
}
onWillStart(async () => {
if (!this.state.js) {
this.setSample(await this.samples[0].code());
}
})
useEffect(() => {
const interval = setInterval(() => {
if (this.isDirty) {
const { js, css, xml } = this.state;
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
}, 1000);
return () => clearInterval(interval);
}, () => []);
this.toggleLayout = debounce(this.toggleLayout, 250, true);
this.runCode = debounce(this.runCode, 250, true);
this.downloadCode = debounce(this.downloadCode, 250, true);
this.exportStandaloneApp = debounce(this.exportStandaloneApp, 250, true);
this.content = useRef("content");
this.updateCode = this.updateCode.bind(this);
}
@@ -237,12 +304,23 @@ class App extends owl.Component {
this.content.el.appendChild(subiframe);
}
setSample(ev) {
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
shareCode() {
const state = btoa(JSON.stringify({ js: this.state.js, css: this.state.css, xml: this.state.xml }));
const link = new URL(window.location.href);
link.hash = state;
if (navigator.clipboard) {
navigator.clipboard.writeText(link.href);
clearTimeout(this.state.copied)
this.state.copied = setTimeout(() => this.state.copied = null, 2000);
}
window.location.href = link.href;
}
setSample(sample) {
this.state.js = sample.js;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
localStorage.removeItem("owl-playground-local-sample");
this.isDirty = false;
}
@@ -250,14 +328,14 @@ class App extends owl.Component {
return `width:${this.state.leftPaneWidth}px`;
}
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
async onSampleChange(ev) {
this.setSample(await this.samples.find(s => s.description === ev.target.value).code());
}
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
@@ -284,39 +362,40 @@ class App extends owl.Component {
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.detail.delta;
updatePanelHeight({ delta }) {
let height = this.state.topPanelHeight || document.querySelector(".tabbed-editor").clientHeight;
this.state.topPanelHeight = height + delta;
}
async downloadCode() {
async exportStandaloneApp() {
const { js, css, xml } = this.state;
const content = await exportStandaloneApp(js, css, xml);
await loadJS("libs/jszip.min.js");
const zip = new JSZip();
zip.file("app.py", await loadFile("./standalone_app/app.py"));
zip.file("index.html", await loadFile("./standalone_app/index.html"));
zip.file("owl.js", await loadFile("../owl.js"));
zip.file("app.js", `const TEMPLATES = await (await fetch('app.xml')).text();\n${js}`);
zip.file("app.css", css);
zip.file("app.xml", xml);
await loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip");
saveAs(await zip.generateAsync({ type: "blob" }), "app.zip");
}
}
App.components = { TabbedEditor };
App.template = "App";
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
document.title = `${document.title} (v${__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
owl.loadFile("templates.xml"),
owl.whenReady()
loadFile("templates.xml"),
whenReady()
]);
const rootApp = new owl.App(App);
const rootApp = new App(Playground, { name: "Owl Playground" });
rootApp.addTemplates(templates);
await rootApp.mount(document.body);
@@ -0,0 +1,11 @@
tr.danger {
font-weight: bold;
}
.remove:hover {
font-weight: bold;
}
.remove {
cursor: pointer;
}
@@ -0,0 +1,124 @@
import { Component, mount, useState, onPatched} from "@odoo/owl";
// -----------------------------------------------------------------------------
// Data generation
// -----------------------------------------------------------------------------
let idCounter = 1;
const adjectives = [
"pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
"quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
"unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
function _random (max) { return Math.round(Math.random() * 1000) % max; };
function buildData(count) {
const data = new Array(count);
for (let i = 0; i < count; i++) {
const label = `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`;
data[i] = {
id: idCounter++,
label,
};
}
return data;
}
// -----------------------------------------------------------------------------
// Components
// -----------------------------------------------------------------------------
class Button extends Component {
static template = "Button";
}
class Row extends Component {
static template = "Row";
}
class Root extends Component {
static template = "Root";
static components = { Button, Row };
setup() {
this.state = useState({
rows: [],
selectedRowId: null
});
this.benchmarking = false;
onPatched(() => {
if (this.benchmarking) {
this.stop();
}
});
}
start(descr) {
this.benchmarking = `[${descr}]`;
console.time(this.benchmarking);
}
stop() {
console.timeEnd(this.benchmarking);
this.benchmarking = false;
}
run() {
this.start('add1000');
this.state.rows = buildData(1000);
this.state.selectedRowId = null;
}
runLots() {
this.start('add10_000');
this.state.rows = buildData(10_000);
this.state.selectedRowId = null;
}
add() {
this.start('append1000');
this.state.rows = this.state.rows.concat(buildData(1000));
}
update() {
this.start('update1/10th');
let index = 0;
const rows = this.state.rows;
while (index < rows.length) {
rows[index].label = rows[index].label + " !!!";
index += 10;
}
}
clear() {
this.start('clear');
this.state.rows = [];
this.state.selectedRowId = null;
}
swapRows() {
this.start('swap');
const rows = this.state.rows;
if (rows.length > 998) {
let tmp = rows[1];
rows[1] = rows[998];
rows[998] = tmp;
}
}
selectRow(id) {
this.start('select');
this.state.selectedRowId = id;
}
removeRow(id) {
this.start('remove1');
const rows = this.state.rows;
rows.splice(rows.findIndex(row => row.id === id), 1);
}
}
// dev=false for benchmarking. we don't want to benchmark dev code!
mount(Root, document.body, { templates: TEMPLATES, dev: false });
@@ -0,0 +1,52 @@
<templates>
<t t-name="Button">
<div class='col-sm-6 smallpad'>
<button t-att-id="props.id" class='btn btn-primary btn-block' type='button' t-on-click="props.onClick">
<t t-esc="props.text"/>
</button>
</div>
</t>
<t t-name="Row">
<tr t-att-class="props.isSelected ? 'danger' : ''">
<td class="col-md-1" t-esc="props.row.id" />
<td class="col-md-4">
<a t-on-click="() => props.onSelect(props.row.id)" t-esc="props.row.label" />
</td>
<td class="col-md-1">
<a t-on-click="() => props.onRemove(props.row.id)" class="remove">[x]
<span class='glyphicon glyphicon-remove' aria-hidden="true" />
</a>
</td>
<td class='col-md-6'/>
</tr>
</t>
<t t-name="Root">
<div class='container'>
<div class='jumbotron'>
<div class='row'>
<div class='col-md-6'>
<h1>Owl Keyed</h1>
</div>
<div class='col-md-6'>
<div class='row'>
<Button id="'run'" onClick.bind="run" text="'Create 1,000 rows'" />
<Button id="'runlots'" onClick.bind="runLots" text="'Create 10,000 rows'" />
<Button id="'add'" onClick.bind="add" text="'Append 1,000 rows'" />
<Button id="'update'" onClick.bind="update" text="'Update every 10th row'" />
<Button id="'clear'" onClick.bind="clear" text="'Clear'" />
<Button id="'swaprows'" onClick.bind="swapRows" text="'Swap Rows'" />
</div>
</div>
</div>
</div>
<table class='table table-hover table-striped test-data'>
<tbody>
<t t-foreach="state.rows" t-as="row" t-key="row.id">
<Row row="row" isSelected="row.id === state.selectedRowId" onSelect.bind="selectRow" onRemove.bind="removeRow"/>
</t>
</tbody>
</table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
</div>
</t>
</templates>
@@ -0,0 +1,10 @@
.greeter {
font-size: 20px;
width: 300px;
height: 100px;
margin: 5px;
text-align: center;
line-height: 100px;
background-color: #eeeeee;
user-select: none;
}
@@ -0,0 +1,26 @@
// In this example, we show how components can be defined and created.
import { Component, useState, mount } from "@odoo/owl";
class Greeter extends Component {
static template = "Greeter";
setup() {
this.state = useState({ word: 'Hello' });
}
toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
}
}
// Main root component
class Root extends Component {
static components = { Greeter };
static template = "Root"
setup() {
this.state = useState({ name: 'World'});
}
}
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -0,0 +1,9 @@
<templates>
<div t-name="Greeter" class="greeter" t-on-click="toggle">
<t t-esc="state.word"/>, <t t-esc="props.name"/>
</div>
<t t-name="Root">
<Greeter name="state.name"/>
</t>
</templates>
@@ -0,0 +1,6 @@
button {
width: 120px;
height: 35px;
font-size: 16px;
}
@@ -0,0 +1,40 @@
// In this example, we show how hooks can be used or defined.
import { Component, mount, useState, onWillDestroy } from "@odoo/owl";
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
window.addEventListener('mousemove', update);
onWillDestroy(() => {
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
class Root extends Component {
static template = "Root";
setup() {
// simple state hook (reactive object)
this.counter = useState({ value: 0 });
// this hooks is bound to the 'mouse' property.
this.mouse = useMouse();
}
increment() {
this.counter.value++;
}
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -0,0 +1,7 @@
<templates>
<div t-name="Root">
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>
</templates>
+22
View File
@@ -0,0 +1,22 @@
// This example illustrate how the t-model directive can be used to synchronize
// data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally
// synchronized.
import { Component, useState, mount } from "@odoo/owl";
class Form extends Component {
static template = "Form";
setup() {
this.state = useState({
text: "",
othertext: "",
number: 11,
color: "",
bool: false
});
}
}
// Application setup
mount(Form, document.body, { templates: TEMPLATES, dev: true });
+37
View File
@@ -0,0 +1,37 @@
<templates>
<div t-name="Form">
<h1>Form</h1>
<div>
Text (immediate): <input t-model="state.text"/>
</div>
<div>
Other text (lazy): <input t-model.lazy="state.othertext"/>
</div>
<div>
Number: <input t-model.number="state.number"/>
</div>
<div>
Boolean: <input type="checkbox" t-model="state.bool"/>
</div>
<div>
Color, with a select:
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Color, with radio buttons:
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
</div>
<hr/>
<h1>State</h1>
<div>Text: <t t-esc="state.text"/></div>
<div>Other Text: <t t-esc="state.othertext"/></div>
<div>Number: <t t-esc="state.number"/></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
<div>Color: <t t-esc="state.color"/></div>
</div>
</templates>
+7
View File
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"owl": ["../../../src/index.ts"],
},
}
}
@@ -0,0 +1,11 @@
button {
font-size: 18px;
margin: 5px;
}
.demo {
margin: 10px;
padding: 10px;
background-color: #dddddd;
width: 250px;
}
@@ -0,0 +1,68 @@
// This example shows all the possible lifecycle hooks
//
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
// methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what
// happens.
import {
Component,
useState,
mount,
useComponent,
onWillStart,
onMounted,
onWillUnmount,
onWillUpdateProps,
onPatched,
onWillPatch,
onWillRender,
onRendered,
onWillDestroy,
} from "@odoo/owl";
function useLogLifecycle() {
const component = useComponent();
const name = component.constructor.name;
onWillStart(() => console.log(`${name}:willStart`));
onMounted(() => console.log(`${name}:mounted`));
onWillUpdateProps(() => console.log(`${name}:willUpdateProps`));
onWillRender(() => console.log(`${name}:willRender`));
onRendered(() => console.log(`${name}:rendered`));
onWillPatch(() => console.log(`${name}:willPatch`));
onPatched(() => console.log(`${name}:patched`));
onWillUnmount(() => console.log(`${name}:willUnmount`));
onWillDestroy(() => console.log(`${name}:willDestroy`));
}
class DemoComponent extends Component {
static template = "DemoComponent";
setup() {
useLogLifecycle();
this.state = useState({ n: 0 });
}
increment() {
this.state.n++;
}
}
class Root extends Component {
static template = "Root";
static components = { DemoComponent };
setup() {
useLogLifecycle();
this.state = useState({ n: 0, flag: true });
}
increment() {
this.state.n++;
}
toggleSubComponent() {
this.state.flag = !this.state.flag;
}
}
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -0,0 +1,15 @@
<templates>
<div t-name="DemoComponent" t-on-click="increment" class="demo">
<div>Demo Sub Component</div>
<div>(click on me to update me)</div>
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
</div>
<div t-name="Root">
<button t-on-click="increment">Increment Parent State</button>
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
<div t-if="state.flag">
<DemoComponent n="state.n"/>
</div>
</div>
</templates>
@@ -0,0 +1,56 @@
body {
margin: 0;
}
.app {
height: 100%;
flex-direction: column;
}
.app.desktop {
display: flex;
}
.navbar {
flex: 0 0 30px;
height: 30px;
background-color: cadetblue;
color: white;
line-height: 30px;
}
.controlpanel {
flex: 0 0 100px;
height: 100px;
background-color: #dddddd;
padding: 8px;
}
.content-wrapper {
flex: 1 1 auto;
position: relative;
}
.content {
display: flex;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.formview {
overflow-y: auto;
flex: 1 1 60%;
min-height: 200px;
padding: 8px;
}
.chatter {
overflow-y: auto;
flex: 1 1 40%;
background-color: #eeeeee;
color: #333333;
padding: 8px;
}
@@ -0,0 +1,118 @@
// In this example, we show how one can design an application that is responsive:
// its UI is different in mobile mode or in desktop mode.
//
// The main idea is to have a "isMobile" key in the environment, then listen
// to resize events and update the env if needed. Then, the whole interface
// will be updated, creating and destroying components as needed.
//
// To see this in action, try resizing the window. The application will switch
// to mobile mode whenever it has less than 768px.
import { Component, useState, mount, reactive, useEnv } from "@odoo/owl";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
function debounce(func, wait, immediate) {
let timeout;
return function () {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
//------------------------------------------------------------------------------
// Responsive hook
//------------------------------------------------------------------------------
function createUI() {
const getIsMobile = () => window.innerWidth <= 768;
const ui = reactive({ isMobile: getIsMobile() });
const updateEnv = debounce(() => {
const isMobile = getIsMobile();
if (ui.isMobile !== isMobile) {
ui.isMobile = isMobile;
}
}, 15);
window.addEventListener("resize", updateEnv);
return ui;
}
function useUI() {
const env = useEnv();
return useState(env.ui);
}
//------------------------------------------------------------------------------
// Components
//------------------------------------------------------------------------------
class Navbar extends Component {
static template = "Navbar";
}
class MobileSearchView extends Component {
static template = "MobileSearchView";
}
class ControlPanel extends Component {
static template = "ControlPanel";
static components = { MobileSearchView };
setup() {
this.ui = useUI();
}
}
class AdvancedComponent extends Component {
static template = "AdvancedComponent";
}
class FormView extends Component {
static template = "FormView";
static components = { AdvancedComponent };
setup() {
this.ui = useUI();
}
}
class Chatter extends Component {
static template = "Chatter";
setup() {
this.messages = Array.from(Array(100).keys());
}
}
class Root extends Component {
static template = "Root";
static components = { Navbar, ControlPanel, FormView, Chatter };
setup() {
this.ui = useUI();
}
}
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
const env = {
ui: createUI()
};
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
@@ -0,0 +1,42 @@
<templates>
<div t-name="Navbar" class="navbar">Navbar</div>
<div t-name="ControlPanel" class="controlpanel">
<h2>Control Panel</h2>
<MobileSearchView t-if="ui.isMobile" />
</div>
<div t-name="FormView" class="formview">
<h2>Form View</h2>
<AdvancedComponent t-if="!ui.isMobile" />
</div>
<div t-name="Chatter" class="chatter">
<h2>Chatter</h2>
<t t-foreach="messages" t-as="item" t-key="item"><div>Message <t t-esc="item"/></div></t>
</div>
<div t-name="MobileSearchView">Mobile searchview</div>
<div t-name="AdvancedComponent">
This component is only created in desktop mode.
<button>Button!</button>
</div>
<t t-name="maincontent">
<FormView />
<Chatter />
</t>
<div t-name="Root" class="app" t-att-class="{mobile: ui.isMobile, desktop: !ui.isMobile}">
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!ui.isMobile">
<div class="content">
<t t-call="maincontent"/>
</div>
</div>
<t t-else="">
<t t-call="maincontent"/>
</t>
</div>
</templates>
@@ -0,0 +1,28 @@
// This example illustrates how one can write Owl components with
// inline templates.
import { Component, useState, xml, mount } from "@odoo/owl";
// Counter component
class Counter extends Component {
static template = xml`
<button t-on-click="() => state.value++">
Click! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 })
}
// Root
class Root extends Component {
static template = xml`
<div>
<Counter/>
<Counter/>
</div>`;
static components = { Counter };
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true});
+43
View File
@@ -0,0 +1,43 @@
.main {
display: flex;
}
.card {
display: flex;
flex-direction: column;
background-color: #eeeeee;
width: 200px;
height: 100px;
margin: 10px;
border: 1px solid gray;
}
.card.full {
height: 100px;
}
.card.small {
height: 25px;
}
.card-title {
flex: 0 0 25px;
font-weight: bold;
background-color: darkcyan;
color: white;
padding: 2px;
}
.card-title button {
float: right;
}
.card-content {
flex: 1 1 auto;
padding: 5px;
border-top: 1px solid white;
}
.card-footer {
border-top: 1px solid white;
}
+49
View File
@@ -0,0 +1,49 @@
// We show here how slots can be used to create generic components.
// In this example, the Card component is basically only a container. It is not
// aware of its content. It just knows where it should be (with t-slot).
// The parent component define the content with t-set-slot.
//
// Note that the t-on-click event, defined in the Root template, is executed in
// the context of the Root component, even though it is inside the Card component
import { Component, useState, mount } from "@odoo/owl";
class Card extends Component {
static template = "Card";
setup() {
this.state = useState({ showContent: true });
}
toggleDisplay() {
this.state.showContent = !this.state.showContent;
}
}
class Counter extends Component {
static template = "Counter";
setup() {
this.state = useState({val: 1});
}
inc() {
this.state.val++;
}
}
// Main root component
class Root extends Component {
static template = "Root"
static components = { Card, Counter };
setup() {
this.state = useState({a: 1, b: 3});
}
inc(key, delta) {
this.state[key] += delta;
}
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true});
+33
View File
@@ -0,0 +1,33 @@
<templates>
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
<div class="card-title">
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
</div>
<t t-if="state.showContent">
<div class="card-content" >
<t t-slot="content"/>
</div>
<div class="card-footer">
<t t-slot="footer"/>
</div>
</t>
</div>
<div t-name="Counter">
<t t-esc="state.val"/><button t-on-click="inc">Inc</button>
</div>
<div t-name="Root" class="main">
<Card title="'Title card A'">
<t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set-slot="footer"><button t-on-click="() => this.inc('a', 1)">Increment A</button></t>
</Card>
<Card title="'Title card B'">
<t t-set-slot="content">
<div>Card 2... [<t t-esc="state.b"/>]</div>
<Counter />
</t>
<t t-set-slot="footer"><button t-on-click="() => this.inc('b', -1)">Decrement B</button></t>
</Card>
</div>
</templates>
@@ -0,0 +1,379 @@
html,body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all + label:before {
content: "";
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked + label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: "×";
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
.filters a {
cursor: pointer;
}
@@ -0,0 +1,182 @@
// 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 });
@@ -0,0 +1,50 @@
<templates>
<section t-name="TodoList" class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header>
<section class="main" t-if="store.tasks.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="() => store.toggleAll(!allChecked)"/>
<label for="toggle-all"></label>
<ul class="todo-list">
<t t-foreach="displayedTasks" t-as="todo" t-key="todo.id">
<Todo id="todo.id" isCompleted="todo.isCompleted" text="todo.text"/>
</t>
</ul>
</section>
<footer class="footer" t-if="store.tasks.length">
<span class="todo-count">
<strong>
<t t-esc="remaining"/>
</strong>
<t t-esc="remainingText"/>
</span>
<ul class="filters">
<li>
<a t-on-click="() => this.setFilter('all')" t-att-class="{selected: state.filter === 'all'}">All</a>
</li>
<li>
<a t-on-click="() => this.setFilter('active')" t-att-class="{selected: state.filter === 'active'}">Active</a>
</li>
<li>
<a t-on-click="() => this.setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li>
</ul>
<button class="clear-completed" t-if="store.tasks.length gt remaining" t-on-click="() => store.clearCompleted()">
Clear completed
</button>
</footer>
</section>
<li t-name="Todo" class="todo" t-att-class="{completed: props.isCompleted, editing: state.isEditing}">
<div class="view">
<input class="toggle" type="checkbox" t-on-change="() => store.toggleTask(props.id)" t-att-checked="props.completed"/>
<label t-on-dblclick="() => state.isEditing = true">
<t t-esc="props.text"/>
</label>
<button class="destroy" t-on-click="() => store.deleteTask(props.id)"></button>
</div>
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.text" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
</li>
</templates>
@@ -0,0 +1,70 @@
body {
margin: 0;
}
.app {
width: 100%;
height: 100%;
display: grid;
grid-template-rows: auto 50px;
}
.window-manager {
position: relative;
width: 100%;
height: 100%;
background-color: #eeeeee;
overflow: hidden;
}
.menubar {
background-color: #875a7b;
color: white;
}
.menubar button {
height: 40px;
font-size: 18px;
margin: 5px;
}
.window {
display: grid;
grid-template-rows: 30px auto;
border: 1px solid gray;
background-color: white;
position: absolute;
box-shadow: 1px 1px 2px 1px grey;
}
.window.dragging {
opacity: 0.75;
}
.window .header {
background-color: #875a7b;
display: grid;
grid-template-columns: auto 24px;
color: white;
line-height: 30px;
padding-left: 5px;
cursor: default;
user-select: none;
}
.window .header .close {
cursor: pointer;
font-size: 22px;
padding-left: 4px;
padding-right: 4px;
font-weight: bold;
}
.counter {
font-size: 20px;
}
.counter button {
width: 80px;
height:40px;
font-size: 20px;
}
@@ -0,0 +1,192 @@
// This example is slightly more complex than usual. We demonstrate
// here a way to manage sub windows in Owl, declaratively. This is still just a
// demonstration. Managing windows can be as complex as we want. For example,
// we could implement the following features:
// - resizing windows
// - minimizing windows
// - configuration options for windows to make a window non resizeable
// - minimal width/height
// - better heuristic for initial window position
// - ...
import { Component, useState, mount, useRef, reactive, useEnv, onMounted } from "@odoo/owl";
// -----------------------------------------------------------------------------
// Window manager code
// -----------------------------------------------------------------------------
class WindowManager {
// contains all components with metadata
static Windows = {};
windows = {}; // mapping id => info
nextId = 1;
add(type) {
const Comp = WindowManager.Windows[type];
const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth));
const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight));
const id = this.nextId++;
this.windows[id] = {
id,
title: Comp.defaultTitle,
width: Comp.defaultWidth,
height: Comp.defaultHeight,
left,
top,
Component: Comp,
};
}
close(id) {
delete this.windows[id];
}
updatePosition(id, left, top) {
const w = this.windows[id];
w.left = left;
w.top = top;
}
getWindows() {
return Object.values(this.windows);
}
}
function createWindowService() {
return reactive(new WindowManager());
}
function useWindowService() {
const env = useEnv();
return useState(env.windowService);
}
// -----------------------------------------------------------------------------
// Generic Window Component
// -----------------------------------------------------------------------------
class Window extends Component {
static template = "Window";
static nextZIndex = 1;
zIndex = 0;
setup() {
this.windowService = useWindowService();
this.root = useRef('root');
onMounted(this.updateZIndex);
}
get style() {
let { width, height, top, left } = this.props.info;
return `width: ${width}px;height: ${height}px;top:${top}px;left:${left}px;z-index:${this.zIndex}`;
}
close() {
this.windowService.close(this.props.info.id);
}
startDragAndDrop(ev) {
this.updateZIndex();
const self = this;
const root = this.root;
const el = root.el;
el.classList.add('dragging');
const current = this.props.info;
const offsetX = current.left - ev.pageX;
const offsetY = current.top - ev.pageY;
let left, top;
window.addEventListener("mousemove", moveWindow);
window.addEventListener("mouseup", stopDnD, { once: true });
function moveWindow(ev) {
left = Math.max(offsetX + ev.pageX, 0);
top = Math.max(offsetY + ev.pageY, 0);
el.style.left = `${left}px`;
el.style.top = `${top}px`;
}
function stopDnD() {
window.removeEventListener("mousemove", moveWindow);
el.classList.remove('dragging');
if (top !== undefined && left !== undefined) {
self.windowService.updatePosition(current.id, left, top);
}
}
}
updateZIndex() {
this.zIndex = Window.nextZIndex++;
this.root.el.style['z-index'] = this.zIndex;
}
}
// -----------------------------------------------------------------------------
// Two concrete Window type implementations
// -----------------------------------------------------------------------------
class HelloWorld extends Component {
static template = "HelloWorld";
static defaultTitle = "Hello Owl!";
static defaultWidth = 200;
static defaultHeight = 100;
}
class Counter extends Component {
static template = "Counter";
static defaultTitle = "Click Counter";
static defaultWidth = 300;
static defaultHeight = 120;
state = useState({ value: 0 });
inc() {
this.state.value++;
}
}
// register window components
WindowManager.Windows.Hello = HelloWorld;
WindowManager.Windows.Counter = Counter;
// -----------------------------------------------------------------------------
// Window Container
// -----------------------------------------------------------------------------
class WindowContainer extends Component {
static template = "WindowContainer";
static components = { Window };
setup() {
this.windowService = useWindowService();
}
}
// -----------------------------------------------------------------------------
// Root Component
// -----------------------------------------------------------------------------
class Root extends Component {
static template = "Root";
static components = { WindowContainer };
setup() {
this.windowService = useWindowService();
}
addWindow(type) {
this.windowService.add(type);
}
}
// -----------------------------------------------------------------------------
// Setup
// -----------------------------------------------------------------------------
const env = {
windowService: createWindowService(),
};
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
@@ -0,0 +1,32 @@
<templates>
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex" t-ref="root">
<div class="header">
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
<span class="close" t-on-click.stop="close">×</span>
</div>
<t t-slot="default"/>
</div>
<div t-name="WindowContainer" class="window-manager">
<Window t-foreach="windowService.getWindows()" t-as="w" t-key="w.id" info="w">
<t t-component="w.Component"/>
</Window>
</div>
<div t-name="Root" class="app">
<WindowContainer/>
<div class="menubar">
<button t-on-click="() => this.addWindow('Hello')">Say Hello</button>
<button t-on-click="() => this.addWindow('Counter')">Counter</button>
</div>
</div>
<div t-name="HelloWorld">
Some content here...
</div>
<div t-name="Counter" class="counter">
<button t-on-click="inc">Click</button>
<span><t t-esc="state.value"/></span>
</div>
</templates>
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
httpd = None
def start_server():
global httpd
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="app.css">
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
<script type="module" src="app.js"></script>
</head>
<body>
<pre>
This app requires a web server, simply opening the html file using your browser will not work.
You can run a simple web server with one of the following:
- if you have python3 installed, run app.py with python3 or run 'python3 -m http.server' in the app folder
- if you have node/npm installed, run 'npx serve' in the app folder
- if you have php installed, run 'php -S localhost:8080 -t .' in the app folder
</pre>
<script>if (window.location.protocol !== "file:") document.querySelector("pre").remove()</script>
</body>
</html>
@@ -1,5 +1,5 @@
<templates>
<div t-name="TabbedEditor" class="tabbed-editor">
<div t-name="TabbedEditor" class="tabbed-editor" t-att-style="props.style">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab" t-key="tab">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="() => setTab(tab)">
@@ -10,18 +10,19 @@
<div class="code-editor" t-ref="editor"></div>
</div>
<div t-name="App" class="playground">
<div t-name="Playground" class="playground">
<div class="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample">
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index">
<select t-on-change="onSampleChange">
<option t-foreach="samples" t-as="sample" t-key="sample_index">
<t t-esc="sample.description"/>
</option>
</select>
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="btn flash" t-on-click="exportStandaloneApp" title="Download zip that bundles this app with a tiny python web server"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
<a class="btn flash share-code" t-on-click="shareCode" title="Copy a link to this playground"><span>Share</span><span class="copied-notification" t-if="state.copied">Copied link to clipboard!</span></a>
</div>
<TabbedEditor
js="state.js"
@@ -36,19 +37,19 @@
css="state.css"
xml="state.xml"
resizeable="true"
onUpdatePanelHeight="updatePanelHeight"
updatePanelHeight.bind="updatePanelHeight"
updateCode="updateCode"/>
</t>
</div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
<div class="right-pane" t-att-style="rightPaneStyle">
<div class="right-pane">
<div class="welcome" t-if="state.displayWelcome">
<div>🦉 Odoo Web Library 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
<div class="note">
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
<p>
Note: these examples make use of recent features of Javascript, and require a recent browser to work without a transpilation step! Among other things, it uses class fields and import maps. If you encounter issues, make sure your browser is up to date.
</p>
</div>
</div>
@@ -18,12 +18,6 @@ export function debounce(func, wait, immediate) {
};
}
export function useBus(bus, evName, cb) {
bus.addEventListener(evName, cb);
owl.onWillUnmount(() => {
bus.removeEventListener(evName, cb);
});
}
const loadedScripts = {};
+5
View File
@@ -0,0 +1,5 @@
# Github page and playground
This folder contains the code for owl's [github page](https://odoo.github.io/owl)
and the owl [playground](https://odoo.github.io/owl/playground/). If you're
looking for the owl documentation, click [here](/doc)
+498 -224
View File
File diff suppressed because it is too large Load Diff
+14 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.0.3",
"version": "2.1.2",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
@@ -16,17 +16,21 @@
"build:runtime": "rollup -c --failAfterWarnings runtime",
"build:compiler": "rollup -c --failAfterWarnings compiler",
"build": "npm run build:bundle",
"build:devtools": "rollup -c ./tools/devtools/rollup.config.js",
"dev:devtools-chrome": "npm run build:devtools -- --config-browser=chrome",
"dev:devtools-firefox": "npm run build:devtools -- --config-browser=firefox",
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch",
"playground:serve": "python3 tools/server.py || python tools/server.py",
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
"playground": "npm run build && npm run playground:serve",
"preplayground:watch": "npm run build",
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
"lint": "eslint src/**/*.ts tests/**/*.ts",
"publish": "npm run build && npm publish",
"release": "node tools/release.js",
"compile_templates": "node tools/compile_xml.js"
},
@@ -48,7 +52,7 @@
"chalk": "^3.0.0",
"current-git-branch": "^1.1.0",
"eslint": "8.31.0",
"git-rev-sync": "^1.12.0",
"git-rev-sync": "^3.0.2",
"github-api": "^3.3.0",
"jest": "^27.1.0",
"jest-diff": "^27.3.1",
@@ -56,7 +60,11 @@
"npm-run-all": "^4.1.5",
"prettier": "2.4.1",
"rollup": "^2.56.3",
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-dts": "^4.2.2",
"rollup-plugin-execute": "^1.1.1",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.31.1",
"source-map-support": "^0.5.10",
-1
View File
@@ -15,7 +15,6 @@ if (pkg.module !== ES_FILENAME || pkg.main !== CJS_FILENAME) {
}
const outro = `
__info__.version = '${pkg.version}';
__info__.date = '${new Date().toISOString()}';
__info__.hash = '${git.short()}';
__info__.url = 'https://github.com/odoo/owl';
+98 -52
View File
@@ -1,4 +1,3 @@
import { isProp } from "../runtime/blockdom/attributes";
import {
compileExpr,
compileExprToArray,
@@ -33,6 +32,7 @@ import {
import { OwlError } from "../runtime/error_handling";
type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
const whitespaceRE = /\s+/g;
export interface Config {
translateFn?: (s: string) => string;
@@ -58,6 +58,30 @@ function generateId(prefix: string = "") {
return prefix + nextDataIds[prefix];
}
function isProp(tag: string, key: string): boolean {
switch (tag) {
case "input":
return (
key === "checked" ||
key === "indeterminate" ||
key === "value" ||
key === "readonly" ||
key === "readOnly" ||
key === "disabled"
);
case "option":
return key === "selected" || key === "disabled";
case "textarea":
return key === "value" || key === "readonly" || key === "readOnly" || key === "disabled";
case "select":
return key === "value" || key === "disabled";
case "button":
case "optgroup":
return key === "disabled";
}
return false;
}
// -----------------------------------------------------------------------------
// BlockDescription
// -----------------------------------------------------------------------------
@@ -143,6 +167,7 @@ interface Context {
nameSpace?: string;
tModelSelectedExpr?: string;
ctxVar?: string;
inPreTag?: boolean;
}
function createContext(parentCtx: Context, params?: Partial<Context>): Context {
@@ -167,11 +192,9 @@ class CodeTarget {
code: string[] = [];
hasRoot = false;
hasCache = false;
hasRef: boolean = false;
// maps ref name to [id, expr]
refInfo: { [name: string]: [string, string] } = {};
shouldProtectScope: boolean = false;
on: EventHandlers | null;
hasRefWrapper: boolean = false;
constructor(name: string, on?: EventHandlers | null) {
this.name = name;
@@ -190,17 +213,13 @@ class CodeTarget {
generateCode(): string {
let result: string[] = [];
result.push(`function ${this.name}(ctx, node, key = "") {`);
if (this.hasRef) {
result.push(` const refs = this.__owl__.refs;`);
for (let name in this.refInfo) {
const [id, expr] = this.refInfo[name];
result.push(` const ${id} = ${expr};`);
}
}
if (this.shouldProtectScope) {
result.push(` ctx = Object.create(ctx);`);
result.push(` ctx[isBoundary] = 1`);
}
if (this.hasRefWrapper) {
result.push(` let refWrapper = makeRefWrapper(this.__owl__);`);
}
if (this.hasCache) {
result.push(` let cache = ctx.cache || {};`);
result.push(` let nextCache = ctx.cache = {};`);
@@ -511,6 +530,9 @@ export class CodeGenerator {
const match = translationRE.exec(value) as any;
value = match[1] + this.translateFn(match[2]) + match[3];
}
if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " ");
}
if (!block || forceNewBlock) {
block = this.createBlock(block, "text", ctx);
@@ -578,19 +600,26 @@ export class CodeGenerator {
attrName = key === "t-att" ? null : key.slice(6);
expr = compileExpr(ast.attrs[key]);
if (attrName && isProp(ast.tag, attrName)) {
if (attrName === "readonly") {
// the property has a different name than the attribute
attrName = "readOnly";
}
// we force a new string or new boolean to bypass the equality check in blockdom when patching same value
if (attrName === "value") {
// When the expression is falsy, fall back to an empty string
expr = `new String((${expr}) || "")`;
// When the expression is falsy (except 0), fall back to an empty string
expr = `new String((${expr}) === 0 ? 0 : ((${expr}) || ""))`;
} else {
expr = `new Boolean(${expr})`;
}
}
const idx = block!.insertData(expr, "attr");
if (key === "t-att") {
attrs[`block-attributes`] = String(idx);
const idx = block!.insertData(expr, "prop");
attrs[`block-property-${idx}`] = attrName!;
} else {
attrs[`block-attribute-${idx}`] = attrName!;
const idx = block!.insertData(expr, "attr");
if (key === "t-att") {
attrs[`block-attributes`] = String(idx);
} else {
attrs[`block-attribute-${idx}`] = attrName!;
}
}
} else if (this.translatableAttributes.includes(key)) {
attrs[key] = this.translateFn(ast.attrs[key]);
@@ -632,15 +661,23 @@ export class CodeGenerator {
let idx: number;
if (specialInitTargetAttr) {
idx = block!.insertData(`${fullExpression} === '${attrs[targetAttr]}'`, "attr");
attrs[`block-attribute-${idx}`] = specialInitTargetAttr;
let targetExpr = targetAttr in attrs && `'${attrs[targetAttr]}'`;
if (!targetExpr && ast.attrs) {
// look at the dynamic attribute counterpart
const dynamicTgExpr = ast.attrs[`t-att-${targetAttr}`];
if (dynamicTgExpr) {
targetExpr = compileExpr(dynamicTgExpr);
}
}
idx = block!.insertData(`${fullExpression} === ${targetExpr}`, "prop");
attrs[`block-property-${idx}`] = specialInitTargetAttr;
} else if (hasDynamicChildren) {
const bValueId = generateId("bValue");
tModelSelectedExpr = `${bValueId}`;
this.define(tModelSelectedExpr, fullExpression);
} else {
idx = block!.insertData(`${fullExpression}`, "attr");
attrs[`block-attribute-${idx}`] = targetAttr;
idx = block!.insertData(`${fullExpression}`, "prop");
attrs[`block-property-${idx}`] = targetAttr;
}
this.helpers.add("toNumber");
let valueCode = `ev.target.${targetAttr}`;
@@ -661,28 +698,21 @@ export class CodeGenerator {
// t-ref
if (ast.ref) {
this.target.hasRef = true;
const isDynamic = INTERP_REGEXP.test(ast.ref);
if (isDynamic) {
const str = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
const idx = block!.insertData(`(el) => refs[${str}] = el`, "ref");
attrs["block-ref"] = String(idx);
} else {
let name = ast.ref;
if (name in this.target.refInfo) {
// ref has already been defined
this.helpers.add("multiRefSetter");
const info = this.target.refInfo[name];
const index = block!.data.push(info[0]) - 1;
attrs["block-ref"] = String(index);
info[1] = `multiRefSetter(refs, \`${name}\`)`;
} else {
let id = generateId("ref");
this.target.refInfo[name] = [id, `(el) => refs[\`${name}\`] = el`];
const index = block!.data.push(id) - 1;
attrs["block-ref"] = String(index);
}
if (this.dev) {
this.helpers.add("makeRefWrapper");
this.target.hasRefWrapper = true;
}
const isDynamic = INTERP_REGEXP.test(ast.ref);
let name = `\`${ast.ref}\``;
if (isDynamic) {
name = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
}
let setRefStr = `(el) => this.__owl__.setRef((${name}), el)`;
if (this.dev) {
setRefStr = `refWrapper(${name}, ${setRefStr})`;
}
const idx = block!.insertData(setRefStr, "ref");
attrs["block-ref"] = String(idx);
}
const dom = xmlDoc.createElement(ast.tag);
@@ -706,6 +736,7 @@ export class CodeGenerator {
tKeyExpr: ctx.tKeyExpr,
nameSpace,
tModelSelectedExpr,
inPreTag: ctx.inPreTag || ast.tag === "pre",
});
this.compileAST(child, subCtx);
}
@@ -1101,12 +1132,15 @@ export class CodeGenerator {
value = this.captureExpression(value);
if (name.includes(".")) {
let [_name, suffix] = name.split(".");
if (suffix === "bind") {
this.helpers.add("bind");
name = _name;
value = `bind(this, ${value || undefined})`;
} else {
throw new OwlError("Invalid prop suffix");
name = _name;
switch (suffix) {
case "bind":
value = `(${value}).bind(this)`;
break;
case "alike":
break;
default:
throw new OwlError("Invalid prop suffix");
}
}
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
@@ -1206,15 +1240,27 @@ export class CodeGenerator {
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
}
let id = generateId("comp");
const propList: string[] = [];
for (let p in ast.props || {}) {
let [name, suffix] = p.split(".");
if (!suffix) {
propList.push(`"${name}"`);
}
}
this.staticDefs.push({
id,
expr: `app.createComponent(${
ast.isDynamic ? null : expr
}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, ${
!ast.props && !ast.dynamicProps
})`,
}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`,
});
if (ast.isDynamic) {
// If the component class changes, this can cause delayed renders to go
// through if the key doesn't change. Use the component name for now.
// This means that two component classes with the same name isn't supported
// in t-component. We can generate a unique id per class later if needed.
keyArg = `(${expr}).name + ${keyArg}`;
}
let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;
if (ast.isDynamic) {
blockExpr = `toggler(${expr}, ${blockExpr})`;
+2 -6
View File
@@ -261,16 +261,12 @@ function parseTNode(node: Element, ctx: ParsingContext): AST | null {
// Text and Comment Nodes
// -----------------------------------------------------------------------------
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
if (node.nodeType === Node.TEXT_NODE) {
let value = node.textContent || "";
if (!ctx.inPreTag) {
if (lineBreakRE.test(value) && !value.trim()) {
return null;
}
value = value.replace(whitespaceRE, " ");
if (!ctx.inPreTag && lineBreakRE.test(value) && !value.trim()) {
return null;
}
return { type: ASTType.Text, value };
+54 -15
View File
@@ -1,7 +1,8 @@
import { version } from "../version";
import { Component, ComponentConstructor, Props } from "./component";
import { ComponentNode } from "./component_node";
import { nodeErrorHandlers, OwlError, handleError } from "./error_handling";
import { Fiber, MountOptions } from "./fibers";
import { Fiber, RootFiber, MountOptions } from "./fibers";
import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers";
import { TemplateSet, TemplateSetConfig } from "./template_set";
@@ -14,6 +15,7 @@ export interface Env {
}
export interface AppConfig<P, E> extends TemplateSetConfig {
name?: string;
props?: P;
env?: E;
test?: boolean;
@@ -31,13 +33,31 @@ This is not suitable for production use.
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
};
declare global {
interface Window {
__OWL_DEVTOOLS__: {
apps: Set<App>;
Fiber: typeof Fiber;
RootFiber: typeof RootFiber;
};
}
}
window.__OWL_DEVTOOLS__ ||= {
apps: new Set<App>(),
Fiber: Fiber,
RootFiber: RootFiber,
};
export class App<
T extends abstract new (...args: any) => any = any,
P extends object = any,
E = any
> extends TemplateSet {
static validateTarget = validateTarget;
static version = version;
name: string;
Root: ComponentConstructor<P, E>;
props: P;
env: E;
@@ -47,7 +67,9 @@ export class App<
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
super(config);
this.name = config.name || "";
this.Root = Root;
window.__OWL_DEVTOOLS__.apps.add(this);
if (config.test) {
this.dev = true;
}
@@ -62,7 +84,10 @@ export class App<
this.props = config.props || ({} as P);
}
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
mount(
target: HTMLElement | ShadowRoot,
options?: MountOptions
): Promise<Component<P, E> & InstanceType<T>> {
App.validateTarget(target);
if (this.dev) {
validateProps(this.Root, this.props, { __owl__: { app: this } });
@@ -77,7 +102,7 @@ export class App<
return new ComponentNode(Component, props, this, null, null);
}
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
mountNode(node: ComponentNode, target: HTMLElement | ShadowRoot, options?: MountOptions) {
const promise: any = new Promise((resolve, reject) => {
let isResolved = false;
// manually set a onMounted callback.
@@ -109,6 +134,7 @@ export class App<
this.scheduler.flush();
this.root.destroy();
}
window.__OWL_DEVTOOLS__.apps.delete(this);
}
createComponent<P extends Props>(
@@ -116,22 +142,35 @@ export class App<
isStatic: boolean,
hasSlotsProp: boolean,
hasDynamicPropList: boolean,
hasNoProp: boolean
propList: string[]
) {
const isDynamic = !isStatic;
function _arePropsDifferent(props1: Props, props2: Props): boolean {
for (let k in props1) {
if (props1[k] !== props2[k]) {
return true;
let arePropsDifferent: (p1: Object, p2: Object) => boolean;
const hasNoProp = propList.length === 0;
if (hasSlotsProp) {
arePropsDifferent = (_1, _2) => true;
} else if (hasDynamicPropList) {
arePropsDifferent = function (props1: Props, props2: Props) {
for (let k in props1) {
if (props1[k] !== props2[k]) {
return true;
}
}
}
return hasDynamicPropList && Object.keys(props1).length !== Object.keys(props2).length;
return Object.keys(props1).length !== Object.keys(props2).length;
};
} else if (hasNoProp) {
arePropsDifferent = (_1: any, _2: any) => false;
} else {
arePropsDifferent = function (props1: Props, props2: Props) {
for (let p of propList) {
if (props1[p] !== props2[p]) {
return true;
}
}
return false;
};
}
const arePropsDifferent = hasSlotsProp
? (_1: any, _2: any) => true
: hasNoProp
? (_1: any, _2: any) => false
: _arePropsDifferent;
const updateAndRender = ComponentNode.prototype.updateAndRender;
const initiateRender = ComponentNode.prototype.initiateRender;
-30
View File
@@ -140,33 +140,3 @@ export function updateClass(this: HTMLElement, val: any, oldVal: any) {
}
}
}
export function makePropSetter(name: string): Setter<HTMLElement> {
return function setProp(this: HTMLElement, value: any) {
// support 0, fallback to empty string for other falsy values
(this as any)[name] = value === 0 ? 0 : value ? value.valueOf() : "";
};
}
export function isProp(tag: string, key: string): boolean {
switch (tag) {
case "input":
return (
key === "checked" ||
key === "indeterminate" ||
key === "value" ||
key === "readonly" ||
key === "disabled"
);
case "option":
return key === "selected" || key === "disabled";
case "textarea":
return key === "value" || key === "readonly" || key === "disabled";
case "select":
return key === "value" || key === "disabled";
case "button":
case "optgroup":
return key === "disabled";
}
return false;
}
+29 -15
View File
@@ -1,13 +1,5 @@
import { OwlError } from "../error_handling";
import {
attrsSetter,
attrsUpdater,
createAttrUpdater,
isProp,
makePropSetter,
setClass,
updateClass,
} from "./attributes";
import { attrsSetter, attrsUpdater, createAttrUpdater, setClass, updateClass } from "./attributes";
import { config } from "./config";
import { createEventHandler } from "./events";
import type { VNode } from "./index";
@@ -25,6 +17,13 @@ const nodeGetNextSibling = getDescriptor(nodeProto, "nextSibling").get!;
const NO_OP = () => {};
function makePropSetter(name: string): Setter<HTMLElement> {
return function setProp(this: HTMLElement, value: any) {
// support 0, fallback to empty string for other falsy values
(this as any)[name] = value === 0 ? 0 : value ? value.valueOf() : "";
};
}
// -----------------------------------------------------------------------------
// Main compiler code
// -----------------------------------------------------------------------------
@@ -101,7 +100,7 @@ function normalizeNode(node: HTMLElement | Text) {
interface DynamicInfo {
idx: number;
refIdx?: number;
type: "text" | "child" | "handler" | "attribute" | "attributes" | "ref";
type: "text" | "child" | "handler" | "attribute" | "attributes" | "property" | "ref";
isOnlyChild?: boolean;
name?: string;
tag?: string;
@@ -184,6 +183,14 @@ function buildTree(
name: attrValue,
tag: tagName,
});
} else if (attrName.startsWith("block-property-")) {
const idx = parseInt(attrName.slice(15), 10);
info.push({
type: "property",
idx,
name: attrValue,
tag: tagName,
});
} else if (attrName === "block-attributes") {
info.push({
type: "attributes",
@@ -377,15 +384,22 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
};
}
break;
case "property": {
const refIdx = info.refIdx!;
const setProp = makePropSetter(info.name!);
ctx.locations.push({
idx: info.idx,
refIdx,
setData: setProp,
updateData: setProp,
});
break;
}
case "attribute": {
const refIdx = info.refIdx!;
let updater: any;
let setter: any;
if (isProp(info.tag!, info.name!)) {
const setProp = makePropSetter(info.name!);
setter = setProp;
updater = setProp;
} else if (info.name === "class") {
if (info.name === "class") {
setter = setClass;
updater = updateClass;
} else {
+2 -1
View File
@@ -1,3 +1,4 @@
import { inOwnerDocument } from "../utils";
import { config } from "./config";
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
@@ -28,7 +29,7 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
function listener(ev: Event) {
const currentTarget = ev.currentTarget as HTMLElement;
if (!currentTarget || !currentTarget.ownerDocument.contains(currentTarget)) return;
if (!currentTarget || !inOwnerDocument(currentTarget)) return;
const data = (currentTarget as any)[eventKey];
if (!data) return;
config.mainEventHandler(data, ev, currentTarget);
+16 -2
View File
@@ -73,6 +73,7 @@ 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;
@@ -220,7 +221,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
async updateAndRender(props: P, parentFiber: Fiber) {
const rawProps = props;
this.nextProps = props;
props = Object.assign({}, props);
// update
const fiber = makeChildFiber(this, parentFiber);
@@ -245,7 +246,6 @@ 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) {
@@ -281,6 +281,19 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
}
/**
* Sets a ref to a given HTMLElement.
*
* @param name the name of the ref to set
* @param el the HTMLElement to set the ref to. The ref is not set if the el
* is null, but useRef will not return elements that are not in the DOM
*/
setRef(name: string, el: HTMLElement | null) {
if (el) {
this.refs[name] = el;
}
}
// ---------------------------------------------------------------------------
// Block DOM methods
// ---------------------------------------------------------------------------
@@ -314,6 +327,7 @@ 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() {
+4 -2
View File
@@ -1,6 +1,7 @@
import type { Env } from "./app";
import { getCurrent } from "./component_node";
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
import { inOwnerDocument } from "./utils";
// -----------------------------------------------------------------------------
// useRef
@@ -15,7 +16,8 @@ export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el:
const refs = node.refs;
return {
get el(): T | null {
return refs[name] || null;
const el = refs[name];
return inOwnerDocument(el) ? el : null;
},
};
}
@@ -117,7 +119,7 @@ export function useEffect(effect: Effect, computeDependencies: () => any[] = ()
* `useExternalListener(window, 'click', this._doSomething);`
* */
export function useExternalListener(
target: HTMLElement | typeof window,
target: EventTarget,
eventName: string,
handler: EventListener,
eventParams?: AddEventListenerOptions
+5 -2
View File
@@ -1,3 +1,4 @@
import { App } from "./app";
import {
config,
createBlock,
@@ -53,7 +54,9 @@ export {
onWillDestroy,
onError,
} from "./lifecycle_hooks";
export { validate } from "./validation";
export { validate, validateType } from "./validation";
export { OwlError } from "./error_handling";
export const __info__ = {};
export const __info__ = {
version: App.version,
};
+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: () => Promise<void> | any | void) {
export function onWillPatch(fn: () => any | void) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
@@ -75,13 +75,13 @@ export function onPatched(fn: () => void | any) {
node.patched.push(decorate(fn.bind(node.component), "onPatched"));
}
export function onWillUnmount(fn: () => Promise<void> | void | any) {
export function onWillUnmount(fn: () => void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
}
export function onWillDestroy(fn: () => Promise<void> | void | any) {
export function onWillDestroy(fn: () => void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
+23 -7
View File
@@ -1,8 +1,13 @@
import { Callback } from "./utils";
import type { Callback } from "./utils";
import { OwlError } from "./error_handling";
// Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes");
// Used to specify the absence of a callback, can be used as WeakMap key but
// should only be used as a sentinel value and never called.
const NO_CALLBACK = () => {
throw new Error("Called NO_CALLBACK. Owl is broken, please report this to the maintainers.");
};
// The following types only exist to signify places where objects are expected
// to be reactive or not, they provide no type checking benefit over "object"
@@ -86,6 +91,9 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callbac
* @param callback the function to call when the key changes
*/
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
if (callback === NO_CALLBACK) {
return;
}
if (!targetToKeysToCallbacks.get(target)) {
targetToKeysToCallbacks.set(target, new Map());
}
@@ -140,8 +148,11 @@ export function clearReactivesForCallback(callback: Callback): void {
if (!observedKeys) {
continue;
}
for (const callbacks of observedKeys.values()) {
for (const [key, callbacks] of observedKeys.entries()) {
callbacks.delete(callback);
if (!callbacks.size) {
observedKeys.delete(key);
}
}
}
targetsToClear.clear();
@@ -151,10 +162,15 @@ export function getSubscriptions(callback: Callback) {
const targets = callbacksToTargets.get(callback) || [];
return [...targets].map((target) => {
const keysToCallbacks = targetToKeysToCallbacks.get(target);
return {
target,
keys: keysToCallbacks ? [...keysToCallbacks.keys()] : [],
};
let keys = [];
if (keysToCallbacks) {
for (const [key, cbs] of keysToCallbacks) {
if (cbs.has(callback)) {
keys.push(key);
}
}
}
return { target, keys };
});
}
// Maps reactive objects to the underlying target
@@ -187,7 +203,7 @@ const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>()
* reactive has changed
* @returns a proxy that tracks changes to it
*/
export function reactive<T extends Target>(target: T, callback: Callback = () => {}): T {
export function reactive<T extends Target>(target: T, callback: Callback = NO_CALLBACK): T {
if (!canBeMadeReactive(target)) {
throw new OwlError(`Cannot make the given value reactive`);
}
+16 -39
View File
@@ -5,6 +5,7 @@ import { isOptional, validateSchema } from "./validation";
import type { ComponentConstructor } from "./component";
import { markRaw } from "./reactivity";
import { OwlError } from "./error_handling";
import type { ComponentNode } from "./component_node";
const ObjectCreate = Object.create;
/**
@@ -133,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) {
if (value === undefined || value === null) {
return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
}
let safeKey;
@@ -166,42 +167,6 @@ export function safeOutput(value: any, defaultValue?: any): ReturnType<typeof to
return toggler(safeKey, block);
}
let boundFunctions = new WeakMap();
const WeakMapGet = WeakMap.prototype.get;
const WeakMapSet = WeakMap.prototype.set;
function bind(component: any, fn: Function): Function {
let boundFnMap = WeakMapGet.call(boundFunctions, component);
if (!boundFnMap) {
boundFnMap = new WeakMap();
WeakMapSet.call(boundFunctions, component, boundFnMap);
}
let boundFn = WeakMapGet.call(boundFnMap, fn);
if (!boundFn) {
boundFn = fn.bind(component);
WeakMapSet.call(boundFnMap, fn, boundFn);
}
return boundFn;
}
type RefMap = { [key: string]: HTMLElement | null };
type RefSetter = (el: HTMLElement | null) => void;
function multiRefSetter(refs: RefMap, name: string): RefSetter {
let count = 0;
return (el) => {
if (el) {
count++;
if (count > 1) {
throw new OwlError("Cannot have 2 elements with same ref name at the same time");
}
}
if (count === 0 || el) {
refs[name] = el;
}
};
}
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
@@ -250,6 +215,19 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
}
}
function makeRefWrapper(node: ComponentNode) {
let refNames: Set<String> = new Set();
return (name: string, fn: Function) => {
if (refNames.has(name)) {
throw new OwlError(
`Cannot set the same ref more than once in the same component, ref "${name}" was set multiple times in ${node.name}`
);
}
refNames.add(name);
return fn;
};
}
export const helpers = {
withDefault,
zero: Symbol("zero"),
@@ -259,14 +237,13 @@ export const helpers = {
withKey,
prepareList,
setContextValue,
multiRefSetter,
shallowEqual,
toNumber,
validateProps,
LazyValue,
safeOutput,
bind,
createCatcher,
markRaw,
OwlError,
makeRefWrapper,
};
+18 -3
View File
@@ -27,13 +27,28 @@ export function batched(callback: Callback): Callback {
};
}
export function validateTarget(target: HTMLElement) {
/**
* 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) {
// 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) {
if (!document.body.contains(target)) {
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
throw new OwlError("Cannot mount a component on a detached dom node");
}
return;
+17 -3
View File
@@ -1,4 +1,5 @@
import { OwlError } from "./error_handling";
import { toRaw } from "./reactivity";
type BaseType =
| typeof String
@@ -16,6 +17,7 @@ interface TypeInfo {
validate?: Function;
shape?: Schema;
element?: TypeDescription;
values?: TypeDescription;
}
type ValueType = { value: any };
@@ -84,6 +86,7 @@ export function validateSchema(obj: { [key: string]: any }, schema: Schema): str
if (Array.isArray(schema)) {
schema = toSchema(schema);
}
obj = toRaw(obj);
let errors = [];
// check if each value in obj has correct shape
for (let key in obj) {
@@ -135,7 +138,7 @@ function validateArrayType(key: string, value: any, descr: TypeDescription): str
return null;
}
function validateType(key: string, value: any, descr: TypeDescription): string | null {
export function validateType(key: string, value: any, descr: TypeDescription): string | null {
if (value === undefined) {
return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
} else if (isBaseType(descr)) {
@@ -149,13 +152,24 @@ function validateType(key: string, value: any, descr: TypeDescription): string |
let result: string | null = null;
if ("element" in descr) {
result = validateArrayType(key, value, descr.element!);
} else if ("shape" in descr && !result) {
} else if ("shape" in descr) {
if (typeof value !== "object" || Array.isArray(value)) {
result = `'${key}' is not an object`;
} else {
const errors = validateSchema(value, descr.shape!);
if (errors.length) {
result = `'${key}' has not the correct shape (${errors.join(", ")})`;
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(", ")})`;
}
}
}
+2
View File
@@ -0,0 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.1.2";
+11 -11
View File
@@ -50,7 +50,7 @@ exports[`Reactivity: useState destroyed component before being mounted is inacti
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
@@ -82,7 +82,7 @@ exports[`Reactivity: useState destroyed component is inactive 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
@@ -129,7 +129,7 @@ exports[`Reactivity: useState parent and children subscribed to same context 1`]
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
@@ -222,8 +222,8 @@ exports[`Reactivity: useState two components are updated in parallel 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp2 = app.createComponent(\`Child\`, true, false, false, true);
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
const comp2 = app.createComponent(\`Child\`, true, false, false, []);
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
@@ -253,8 +253,8 @@ exports[`Reactivity: useState two components can subscribe to same context 1`] =
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp2 = app.createComponent(\`Child\`, true, false, false, true);
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
const comp2 = app.createComponent(\`Child\`, true, false, false, []);
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
@@ -284,8 +284,8 @@ exports[`Reactivity: useState two independent components on different levels are
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp2 = app.createComponent(\`Parent\`, true, false, false, true);
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
const comp2 = app.createComponent(\`Parent\`, true, false, false, []);
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
@@ -315,7 +315,7 @@ exports[`Reactivity: useState two independent components on different levels are
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
@@ -345,7 +345,7 @@ exports[`Reactivity: useState useless atoms should be deleted 1`] = `
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
const comp1 = app.createComponent(\`Quantity\`, true, false, false, false);
const comp1 = app.createComponent(\`Quantity\`, true, false, false, [\\"id\\"]);
let block1 = createBlock(\`<div><block-child-0/> Total: <block-text-0/> Count: <block-text-1/></div>\`);
+1 -1
View File
@@ -69,7 +69,7 @@ describe("app", () => {
static template = xml`<div t-esc="message"/>`;
}
await mount(Root, fixture, { dev: true, props: { messge: "hey" }, warnIfNoStaticProps: true });
await mount(Root, fixture, { test: true, props: { messge: "hey" }, warnIfNoStaticProps: true });
console.warn = originalconsoleWarn;
expect(mockConsoleWarn).toBeCalledWith(
-56
View File
@@ -145,59 +145,3 @@ test("class attribute (with a preexisting value", async () => {
patch(tree, block([""]));
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
});
describe("properties", () => {
test("input with value attribute", () => {
// render input with initial value
const block = createBlock(`<input block-attribute-0="value"/>`);
const tree = block(["zucchini"]);
mount(tree, fixture);
// const bnode1 = renderToBdom(template, { v: "zucchini" });
// const fixture = makeTestFixture();
// mount(bnode1, fixture);
const input = fixture.querySelector("input")!;
expect(input.value).toBe("zucchini");
// change value manually in input, to simulate user input
input.value = "tomato";
expect(input.value).toBe("tomato");
// rerender with a different value, and patch actual dom, to check that
// input value was properly reset by owl
patch(tree, block(["potato"]));
expect(input.value).toBe("potato");
});
test("input with value attribute, and falsy value given", () => {
const block = createBlock(`<input block-attribute-0="value"/>`);
const tree = block([undefined]);
mount(tree, fixture);
const input = fixture.querySelector("input")!;
expect(input.value).toBe("");
patch(tree, block([null]));
expect(input.value).toBe("");
patch(tree, block([0]));
expect(input.value).toBe("0");
patch(tree, block([""]));
expect(input.value).toBe("");
patch(tree, block([false]));
expect(input.value).toBe("");
});
test("input type=checkbox with checked attribute", () => {
// render input with initial value
const block = createBlock(`<input type="checkbox" block-attribute-0="checked"/>`);
const tree = block([true]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<input type="checkbox">`);
const input = fixture.querySelector("input")!;
expect(input.checked).toBe(true);
});
});

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