mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e2202c02a |
@@ -120,7 +120,7 @@ npm install @odoo/owl
|
|||||||
|
|
||||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||||
|
|
||||||
- [owl-1.2.0](https://github.com/odoo/owl/releases/tag/v1.2.0)
|
- [owl-1.0.13](https://github.com/odoo/owl/releases/tag/v1.0.13)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ You will find here a complete reference of every feature, class or object
|
|||||||
provided by Owl.
|
provided by Owl.
|
||||||
|
|
||||||
- [Animations](reference/animations.md)
|
- [Animations](reference/animations.md)
|
||||||
- [Browser](reference/browser.md)
|
|
||||||
- [Component](reference/component.md)
|
- [Component](reference/component.md)
|
||||||
- [Content](reference/content.md)
|
- [Content](reference/content.md)
|
||||||
- [Concurrency Model](reference/concurrency_model.md)
|
- [Concurrency Model](reference/concurrency_model.md)
|
||||||
|
|||||||
@@ -52,20 +52,21 @@ sequence of events will happen:
|
|||||||
At node insertion:
|
At node insertion:
|
||||||
|
|
||||||
- the css classes `name-enter` and `name-enter-active` will be added directly
|
- the css classes `name-enter` and `name-enter-active` will be added directly
|
||||||
when the node is inserted into the DOM.
|
when the node is inserted into the DOM,
|
||||||
- on the next animation frame: the css class `name-enter` will be removed and the
|
- on the next animation frame: the css class `name-enter` will be removed and the
|
||||||
class `name-enter-to` will be added (so they can be used to trigger css
|
class `name-enter-to` will be added (so they can be used to trigger css
|
||||||
transition effects).
|
transition effects),
|
||||||
- at the end of the transition, `name-enter-to` and `name-enter-active` will be removed.
|
- the css class `name-enter-active` will be removed whenever a css transition
|
||||||
|
ends.
|
||||||
|
|
||||||
At node destruction:
|
At node destruction:
|
||||||
|
|
||||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||||
node is removed to the DOM.
|
node is removed to the DOM,
|
||||||
- on the next animation frame: the css class `name-leave` will be removed and the
|
- the css class `name-leave` will be removed on the next animation frame (so it
|
||||||
class `name-leave-to` will be added (so they can be used to trigger css
|
can be used to trigger css transition effects),
|
||||||
transition effects).
|
- the css class `name-leave-active` will be removed whenever a css transition
|
||||||
- at the end of the transition, `name-leave-to` and `name-leave-active` will be removed.
|
ends. Only then will the element be removed from the DOM.
|
||||||
|
|
||||||
For example, a simple fade in/out effect can be done with this:
|
For example, a simple fade in/out effect can be done with this:
|
||||||
|
|
||||||
@@ -92,36 +93,3 @@ Notes:
|
|||||||
|
|
||||||
Owl does not support more than one transition on a single node, so the
|
Owl does not support more than one transition on a single node, so the
|
||||||
`t-transition` expression must be a single value (i.e. no space allowed).
|
`t-transition` expression must be a single value (i.e. no space allowed).
|
||||||
|
|
||||||
## SCSS Mixins
|
|
||||||
|
|
||||||
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
|
|
||||||
|
|
||||||
```scss
|
|
||||||
@mixin animation-fade($time, $name) {
|
|
||||||
.#{$name}_fade-enter-active,
|
|
||||||
.#{$name}_fade-active {
|
|
||||||
transition: all $time;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$name}_fade-enter {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$name}_fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
```scss
|
|
||||||
@include animation-fade(0.5s, "o_notification");
|
|
||||||
```
|
|
||||||
|
|
||||||
You can now have in your template:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<SomeTag t-transition="o_notification_fade"/>
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
# 🦉 Browser 🦉
|
|
||||||
|
|
||||||
## Content
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Browser Content](#browser-content)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The browser object contains some browser native APIs, such as `setTimeout`, that
|
|
||||||
are used by Owl and its utility functions. They are exposed with the intent of
|
|
||||||
making them mockable if necessary.
|
|
||||||
|
|
||||||
```js
|
|
||||||
owl.browser.setTimeout === window.setTimeout; // return true
|
|
||||||
```
|
|
||||||
|
|
||||||
For now, this object contains some functions that are not used by Owl. They
|
|
||||||
will eventually be removed in Owl 2.0.
|
|
||||||
|
|
||||||
## Browser Content
|
|
||||||
|
|
||||||
More specifically, the `browser` object contains the following methods and objects:
|
|
||||||
|
|
||||||
- `setTimeout`
|
|
||||||
- `clearTimeout`
|
|
||||||
- `setInterval`
|
|
||||||
- `clearInterval`
|
|
||||||
- `requestAnimationFrame`
|
|
||||||
- `random`
|
|
||||||
- `Date`
|
|
||||||
- `fetch`
|
|
||||||
- `localStorage`
|
|
||||||
@@ -7,7 +7,6 @@ For example, `Component` is available at `owl.Component` and `EventBus` is
|
|||||||
exported as `owl.core.EventBus`.
|
exported as `owl.core.EventBus`.
|
||||||
|
|
||||||
```
|
```
|
||||||
browser
|
|
||||||
Component misc
|
Component misc
|
||||||
Context AsyncRoot
|
Context AsyncRoot
|
||||||
QWeb Portal
|
QWeb Portal
|
||||||
@@ -29,8 +28,6 @@ hooks utils
|
|||||||
useContext
|
useContext
|
||||||
useState
|
useState
|
||||||
useRef
|
useRef
|
||||||
useComponent
|
|
||||||
useEnv
|
|
||||||
useSubEnv
|
useSubEnv
|
||||||
useStore
|
useStore
|
||||||
useDispatch
|
useDispatch
|
||||||
|
|||||||
@@ -133,4 +133,17 @@ the `QWeb` instance and a `browser` object:
|
|||||||
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
|
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
|
||||||
for Owl to be able to render anything
|
for Owl to be able to render anything
|
||||||
- `browser`: this is an object that contains some common access points to the
|
- `browser`: this is an object that contains some common access points to the
|
||||||
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0.
|
browser methods with a side effect. This is particularly useful when one want
|
||||||
|
to test more advanced components, and be able to mock those methods.
|
||||||
|
|
||||||
|
More specifically, the `browser` object contains the following methods and objects:
|
||||||
|
|
||||||
|
- `setTimeout`
|
||||||
|
- `clearTimeout`
|
||||||
|
- `setInterval`
|
||||||
|
- `clearInterval`
|
||||||
|
- `requestAnimationFrame`
|
||||||
|
- `random`
|
||||||
|
- `Date`
|
||||||
|
- `fetch`
|
||||||
|
- `localStorage`
|
||||||
|
|||||||
+4
-14
@@ -21,8 +21,6 @@
|
|||||||
- [`useStore`](#usestore)
|
- [`useStore`](#usestore)
|
||||||
- [`useDispatch`](#usedispatch)
|
- [`useDispatch`](#usedispatch)
|
||||||
- [`useGetters`](#usegetters)
|
- [`useGetters`](#usegetters)
|
||||||
- [`useComponent`](#usecomponent)
|
|
||||||
- [`useEnv`](#useenv)
|
|
||||||
- [Making customized hooks](#making-customized-hooks)
|
- [Making customized hooks](#making-customized-hooks)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -383,16 +381,6 @@ The `useDispatch` hook is the way for components to get a reference to the store
|
|||||||
The `useGetters` hook is the way for components to get a reference to the store
|
The `useGetters` hook is the way for components to get a reference to the store
|
||||||
getters. See the [store documentation](store.md) for more information.
|
getters. See the [store documentation](store.md) for more information.
|
||||||
|
|
||||||
### `useComponent`
|
|
||||||
|
|
||||||
The `useComponent` hook is useful as a building block for some customized hooks,
|
|
||||||
that may need a reference to the component calling them.
|
|
||||||
|
|
||||||
### `useEnv`
|
|
||||||
|
|
||||||
The `useEnv` hook is useful as a building block for some customized hooks,
|
|
||||||
that may need a reference to the env of the component calling them.
|
|
||||||
|
|
||||||
### Making customized hooks
|
### Making customized hooks
|
||||||
|
|
||||||
Hooks are a wonderful way to organize the code of a complex component by feature
|
Hooks are a wonderful way to organize the code of a complex component by feature
|
||||||
@@ -447,11 +435,13 @@ not the solution to every problem.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
function useRouter() {
|
function useRouter() {
|
||||||
const env = useEnv();
|
return Component.current.env.router;
|
||||||
return env.router;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This means that we give control to the application developer to create the
|
This means that we give control to the application developer to create the
|
||||||
router, which is good, so they can set it up, subclass it, ... And then, to
|
router, which is good, so they can set it up, subclass it, ... And then, to
|
||||||
test our components, we can just add a mock router in the environment.
|
test our components, we can just add a mock router in the environment.
|
||||||
|
|
||||||
|
Note: the code above makes use of the `Component.current` property. This is the
|
||||||
|
way hooks are able to get a reference to the component currently being created.
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "1.2.0",
|
"version": "1.0.13",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"browser": "dist/owl.iife.js",
|
"browser": "dist/owl.iife.js",
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
"node": ">=10.15.3"
|
"node": ">=10.15.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:bundle": "rollup -c",
|
"dev": "rollup -c",
|
||||||
"build": "npm run build:bundle",
|
"build": "NODE_ENV=production rollup -c",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"tools:serve": "python3 tools/server.py || python tools/server.py",
|
"tools:serve": "python3 tools/server.py || python tools/server.py",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# 🦉 OWL Roadmap 🦉
|
# 🦉 OWL Roadmap 🦉
|
||||||
|
|
||||||
- Current version: 1.2.0
|
- Current version: 1.0.13
|
||||||
- Status: stable
|
- Status: stable
|
||||||
|
|
||||||
This roadmap is only an attempt at predicting Owl's future. Everything may
|
This roadmap is only an attempt at predicting Owl's future. Everything may
|
||||||
|
|||||||
+1
-8
@@ -10,8 +10,6 @@ export interface Browser {
|
|||||||
localStorage: Window["localStorage"];
|
localStorage: Window["localStorage"];
|
||||||
}
|
}
|
||||||
|
|
||||||
let localStorage: Window["localStorage"] | null = null;
|
|
||||||
|
|
||||||
export const browser: Browser = {
|
export const browser: Browser = {
|
||||||
setTimeout: window.setTimeout.bind(window),
|
setTimeout: window.setTimeout.bind(window),
|
||||||
clearTimeout: window.clearTimeout.bind(window),
|
clearTimeout: window.clearTimeout.bind(window),
|
||||||
@@ -21,10 +19,5 @@ export const browser: Browser = {
|
|||||||
random: Math.random,
|
random: Math.random,
|
||||||
Date: window.Date,
|
Date: window.Date,
|
||||||
fetch: (window.fetch || (() => {})).bind(window),
|
fetch: (window.fetch || (() => {})).bind(window),
|
||||||
get localStorage() {
|
localStorage: window.localStorage,
|
||||||
return localStorage || window.localStorage;
|
|
||||||
},
|
|
||||||
set localStorage(newLocalStorage: Window["localStorage"]) {
|
|
||||||
localStorage = newLocalStorage;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import "./props_validation";
|
|||||||
import { Scheduler, scheduler } from "./scheduler";
|
import { Scheduler, scheduler } from "./scheduler";
|
||||||
import { activateSheet } from "./styles";
|
import { activateSheet } from "./styles";
|
||||||
import { Browser, browser } from "../browser";
|
import { Browser, browser } from "../browser";
|
||||||
|
import { onMounted, onPatched } from "../hooks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl Component System
|
* Owl Component System
|
||||||
@@ -91,8 +92,24 @@ interface Internal<T extends Env> {
|
|||||||
refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null;
|
refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DevToolsAccess {
|
||||||
|
props?: any;
|
||||||
|
defaultProps?: any;
|
||||||
|
template?: string | null;
|
||||||
|
state: Observer;
|
||||||
|
tag: String;
|
||||||
|
depth: number,
|
||||||
|
}
|
||||||
|
|
||||||
export const portalSymbol = Symbol("portal"); // FIXME
|
export const portalSymbol = Symbol("portal"); // FIXME
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is required for the dev tools to have access to the __owl__ element.
|
||||||
|
*/
|
||||||
|
interface HTMLElementWithDevToolsAccess extends HTMLElement {
|
||||||
|
__owl_devtools__: DevToolsAccess
|
||||||
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Component
|
// Component
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -110,11 +127,13 @@ export class Component<Props extends {} = any, T extends Env = Env> {
|
|||||||
// expose scheduler s.t. it can be mocked for testing purposes
|
// expose scheduler s.t. it can be mocked for testing purposes
|
||||||
static scheduler: Scheduler = scheduler;
|
static scheduler: Scheduler = scheduler;
|
||||||
|
|
||||||
|
__devtools__: DevToolsAccess;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `el` is the root element of the component. Note that it could be null:
|
* The `el` is the root element of the component. Note that it could be null:
|
||||||
* this is the case if the component is not mounted yet, or is destroyed.
|
* this is the case if the component is not mounted yet, or is destroyed.
|
||||||
*/
|
*/
|
||||||
get el(): HTMLElement | null {
|
get el(): HTMLElementWithDevToolsAccess | null {
|
||||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +178,6 @@ export class Component<Props extends {} = any, T extends Env = Env> {
|
|||||||
if (!this.env.qweb) {
|
if (!this.env.qweb) {
|
||||||
this.env.qweb = new QWeb();
|
this.env.qweb = new QWeb();
|
||||||
}
|
}
|
||||||
// TODO: remove this in owl 2.0
|
|
||||||
if (!this.env.browser) {
|
if (!this.env.browser) {
|
||||||
this.env.browser = browser;
|
this.env.browser = browser;
|
||||||
}
|
}
|
||||||
@@ -209,6 +227,24 @@ export class Component<Props extends {} = any, T extends Env = Env> {
|
|||||||
if (constr.style) {
|
if (constr.style) {
|
||||||
this.__applyStyles(constr);
|
this.__applyStyles(constr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DevTools hooks
|
||||||
|
onMounted(() => {
|
||||||
|
this.__devtools__ = {
|
||||||
|
depth: this.__owl__.depth,
|
||||||
|
state: this.__owl__.observer,
|
||||||
|
tag: this.constructor.name
|
||||||
|
};
|
||||||
|
this.__devtools__.defaultProps = defaultProps;
|
||||||
|
this.__devtools__.props = this.props;
|
||||||
|
this.__devtools__.template = template;
|
||||||
|
this.el.__owl_devtools__ = this.__devtools__;
|
||||||
|
})
|
||||||
|
onPatched(() => {
|
||||||
|
this.__devtools__.depth = this.__owl__.depth,
|
||||||
|
this.el.__owl_devtools__ = this.__devtools__;
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -333,7 +369,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
|
|||||||
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
|
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
const fiber = new Fiber(null, this, true, target, position);
|
const fiber = new Fiber(null, this, false, target, position);
|
||||||
fiber.shouldPatch = false;
|
fiber.shouldPatch = false;
|
||||||
if (!__owl__.vnode) {
|
if (!__owl__.vnode) {
|
||||||
this.__prepareAndRender(fiber, () => {});
|
this.__prepareAndRender(fiber, () => {});
|
||||||
|
|||||||
@@ -115,6 +115,16 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
|
|||||||
__owl__.observer = new Observer();
|
__owl__.observer = new Observer();
|
||||||
__owl__.observer.notifyCB = component.render.bind(component);
|
__owl__.observer.notifyCB = component.render.bind(component);
|
||||||
}
|
}
|
||||||
|
const currentCB = __owl__.observer.notifyCB;
|
||||||
|
__owl__.observer.notifyCB = function () {
|
||||||
|
if (ctx.rev > mapping[id]) {
|
||||||
|
// in this case, the context has been updated since we were rendering
|
||||||
|
// last, and we do not need to render here with the observer. A
|
||||||
|
// rendering is coming anyway, with the correct props.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentCB();
|
||||||
|
};
|
||||||
|
|
||||||
mapping[id] = 0;
|
mapping[id] = 0;
|
||||||
const renderFn = __owl__.renderFn;
|
const renderFn = __owl__.renderFn;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export class Observer {
|
|||||||
rev: number = 1;
|
rev: number = 1;
|
||||||
allowMutations: boolean = true;
|
allowMutations: boolean = true;
|
||||||
weakMap: WeakMap<any, any> = new WeakMap();
|
weakMap: WeakMap<any, any> = new WeakMap();
|
||||||
|
|
||||||
notifyCB() {}
|
notifyCB() {}
|
||||||
|
|
||||||
observe<T>(value: T, parent?: any): T {
|
observe<T>(value: T, parent?: any): T {
|
||||||
|
|||||||
+1
-21
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Env } from "./component/component";
|
import { Component } from "./component/component";
|
||||||
import { Observer } from "./core/observer";
|
import { Observer } from "./core/observer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,26 +118,6 @@ export function useRef<C extends Component = Component>(name: string): Ref<C> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// "Builder" hooks
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This hook is useful as a building block for some customized hooks, that may
|
|
||||||
* need a reference to the component calling them.
|
|
||||||
*/
|
|
||||||
export function useComponent<P, E extends Env>(): Component<P, E> {
|
|
||||||
return Component.current as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This hook is useful as a building block for some customized hooks, that may
|
|
||||||
* need a reference to the env of the component calling them.
|
|
||||||
*/
|
|
||||||
export function useEnv<E extends Env>(): E {
|
|
||||||
return Component.current.env as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// useSubEnv
|
// useSubEnv
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { Router } from "./router/router";
|
|||||||
export { Component, mount } from "./component/component";
|
export { Component, mount } from "./component/component";
|
||||||
export { QWeb };
|
export { QWeb };
|
||||||
export { config };
|
export { config };
|
||||||
export { browser } from "./browser";
|
|
||||||
|
|
||||||
export const Context = _context.Context;
|
export const Context = _context.Context;
|
||||||
export const useState = _hooks.useState;
|
export const useState = _hooks.useState;
|
||||||
|
|||||||
+1
-3
@@ -70,7 +70,6 @@ const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
|||||||
|
|
||||||
const lineBreakRE = /[\r\n]/;
|
const lineBreakRE = /[\r\n]/;
|
||||||
const whitespaceRE = /\s+/g;
|
const whitespaceRE = /\s+/g;
|
||||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
|
||||||
|
|
||||||
const NODE_HOOKS_PARAMS = {
|
const NODE_HOOKS_PARAMS = {
|
||||||
create: "(_, n)",
|
create: "(_, n)",
|
||||||
@@ -497,8 +496,7 @@ export class QWeb extends EventBus {
|
|||||||
}
|
}
|
||||||
if (this.translateFn) {
|
if (this.translateFn) {
|
||||||
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
|
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
|
||||||
const match = translationRE.exec(text);
|
text = this.translateFn(text);
|
||||||
text = match[1] + this.translateFn(match[2]) + match[3];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ctx.parentNode) {
|
if (ctx.parentNode) {
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
import { Component, Env } from "./component/component";
|
import { Component } from "./component/component";
|
||||||
|
import { Env } from "./component/component";
|
||||||
import { Context, useContextWithCB } from "./context";
|
import { Context, useContextWithCB } from "./context";
|
||||||
import { onWillUpdateProps } from "./hooks";
|
import { onWillUpdateProps } from "./hooks";
|
||||||
|
|
||||||
|
|||||||
@@ -600,51 +600,4 @@ describe("unmounting and remounting", () => {
|
|||||||
await parent.render();
|
await parent.render();
|
||||||
expect(fixture.textContent).toBe("fixedsome text");
|
expect(fixture.textContent).toBe("fixedsome text");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("remounting component tree where a component implement shouldupdate", async () => {
|
|
||||||
let state: any;
|
|
||||||
const steps = [];
|
|
||||||
class Child extends Component {
|
|
||||||
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
|
|
||||||
|
|
||||||
state = useState({ word: "hello" });
|
|
||||||
|
|
||||||
constructor(parent, props) {
|
|
||||||
super(parent, props);
|
|
||||||
state = this.state;
|
|
||||||
}
|
|
||||||
patched() {
|
|
||||||
steps.push("patched");
|
|
||||||
}
|
|
||||||
mounted() {
|
|
||||||
steps.push("mounted");
|
|
||||||
}
|
|
||||||
willUnmount() {
|
|
||||||
steps.push("willUnmount");
|
|
||||||
}
|
|
||||||
shouldUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Parent extends Component {
|
|
||||||
static template = xml`<div><Child name="state.name"/></div>`;
|
|
||||||
static components = { Child };
|
|
||||||
state = useState({ name: "World" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent = await mount(Parent, { target: fixture });
|
|
||||||
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
|
|
||||||
|
|
||||||
parent.unmount();
|
|
||||||
expect(fixture.innerHTML).toBe("");
|
|
||||||
await parent.mount(fixture);
|
|
||||||
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
|
|
||||||
|
|
||||||
state.word = "test";
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
|
|
||||||
expect(steps).toEqual(["mounted", "willUnmount", "mounted", "patched"]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-20
@@ -289,26 +289,7 @@ describe("Context", () => {
|
|||||||
expect(testContext.subscriptions.update.length).toBe(0);
|
expect(testContext.subscriptions.update.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("concurrent renderings", async () => {
|
test("concurrent renderings", async () => {
|
||||||
/**
|
|
||||||
* Note: this test is interesting, but sadly just an incomplete attempt at
|
|
||||||
* protecting users against themselves. With the context API, it is not
|
|
||||||
* possible for the framework to protect completely against crashes. Maybe
|
|
||||||
* like in this case, when a component is in a simple hierarchy where all
|
|
||||||
* renderings come from the context changes, but in a real case, where some
|
|
||||||
* code can trigger a rendering independently, it is insufficient.
|
|
||||||
*
|
|
||||||
* The main problem is that the sub component depends on some external state,
|
|
||||||
* which may be modified, and then incompatible with the component actual
|
|
||||||
* state (for example, if the sub component has an id key related to some
|
|
||||||
* object that has been removed from the context).
|
|
||||||
*
|
|
||||||
* For now, sadly, the only solution is that components that depends on external
|
|
||||||
* state should guarantee their own integrity themselves. Then maybe this
|
|
||||||
* could be solved at the level of a state management solution that has a
|
|
||||||
* more advanced API, to let components determine if they should be updated
|
|
||||||
* or not (so, something slightly more advanced that the useStore hook).
|
|
||||||
*/
|
|
||||||
const testContext = new Context({ x: { n: 1 }, key: "x" });
|
const testContext = new Context({ x: { n: 1 }, key: "x" });
|
||||||
const def = makeDeferred();
|
const def = makeDeferred();
|
||||||
let stateC;
|
let stateC;
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ import {
|
|||||||
onWillPatch,
|
onWillPatch,
|
||||||
onWillStart,
|
onWillStart,
|
||||||
onWillUpdateProps,
|
onWillUpdateProps,
|
||||||
useEnv,
|
|
||||||
useSubEnv,
|
useSubEnv,
|
||||||
useExternalListener,
|
useExternalListener,
|
||||||
useComponent,
|
|
||||||
} from "../src/hooks";
|
} from "../src/hooks";
|
||||||
import { xml } from "../src/tags";
|
import { xml } from "../src/tags";
|
||||||
|
|
||||||
@@ -522,19 +520,6 @@ describe("hooks", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can use useEnv", async () => {
|
|
||||||
expect.assertions(1);
|
|
||||||
class TestComponent extends Component {
|
|
||||||
static template = xml`<div><t t-esc="env.val"/></div>`;
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
expect(useEnv()).toBe(env);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const component = new TestComponent();
|
|
||||||
await component.mount(fixture);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("can use sub env", async () => {
|
test("can use sub env", async () => {
|
||||||
class TestComponent extends Component {
|
class TestComponent extends Component {
|
||||||
static template = xml`<div><t t-esc="env.val"/></div>`;
|
static template = xml`<div><t t-esc="env.val"/></div>`;
|
||||||
@@ -550,19 +535,6 @@ describe("hooks", () => {
|
|||||||
expect(component.env).toHaveProperty("val");
|
expect(component.env).toHaveProperty("val");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can use useComponent", async () => {
|
|
||||||
expect.assertions(1);
|
|
||||||
class TestComponent extends Component {
|
|
||||||
static template = xml`<div></div>`;
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
expect(useComponent()).toBe(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const component = new TestComponent();
|
|
||||||
await component.mount(fixture);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parent and child env", async () => {
|
test("parent and child env", async () => {
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
static template = xml`<div><t t-esc="env.val"/></div>`;
|
static template = xml`<div><t t-esc="env.val"/></div>`;
|
||||||
|
|||||||
@@ -3924,18 +3924,6 @@ exports[`translation support some attributes are translated 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
|
|
||||||
"function anonymous(context, extra
|
|
||||||
) {
|
|
||||||
// Template name: \\"test\\"
|
|
||||||
let h = this.h;
|
|
||||||
let c1 = [], p1 = {key:1};
|
|
||||||
let vn1 = h('div', p1, c1);
|
|
||||||
c1.push({text: \` mot \`});
|
|
||||||
return vn1;
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
|
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
|
||||||
"function anonymous(context, extra
|
"function anonymous(context, extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -2154,17 +2154,6 @@ describe("translation support", () => {
|
|||||||
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
|
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("translation is done on the trimmed text, with extra spaces readded after", () => {
|
|
||||||
const translations = {
|
|
||||||
word: "mot",
|
|
||||||
};
|
|
||||||
const translateFn = jest.fn((expr) => translations[expr] || expr);
|
|
||||||
const qweb = new QWeb({ translateFn });
|
|
||||||
qweb.addTemplate("test", "<div> word </div>");
|
|
||||||
expect(renderToString(qweb, "test")).toBe("<div> mot </div>");
|
|
||||||
expect(translateFn).toHaveBeenCalledWith("word");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("t-key tests", () => {
|
describe("t-key tests", () => {
|
||||||
|
|||||||
+1
-105
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Env, mount } from "../src/component/component";
|
import { Component, Env } from "../src/component/component";
|
||||||
import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
|
import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
|
||||||
import { useState } from "../src/hooks";
|
import { useState } from "../src/hooks";
|
||||||
import { xml } from "../src/tags";
|
import { xml } from "../src/tags";
|
||||||
@@ -1241,108 +1241,4 @@ describe("various scenarios", () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("component with store, useState and shouldUpdate=false", async () => {
|
|
||||||
let state: any;
|
|
||||||
|
|
||||||
const store = new Store({ state: { rev: 0 } });
|
|
||||||
|
|
||||||
class Child extends Component {
|
|
||||||
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
|
|
||||||
|
|
||||||
state = useState({ word: "hello" });
|
|
||||||
|
|
||||||
constructor(parent, props) {
|
|
||||||
super(parent, props);
|
|
||||||
state = this.state;
|
|
||||||
useStore((props) => {
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Parent extends Component {
|
|
||||||
static template = xml`<div><Child name="state.name"/></div>`;
|
|
||||||
static components = { Child };
|
|
||||||
|
|
||||||
state = useState({ name: "World" });
|
|
||||||
|
|
||||||
constructor(parent, props) {
|
|
||||||
super(parent, props);
|
|
||||||
useStore((props) => store.state.rev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(env as any).store = store;
|
|
||||||
|
|
||||||
await mount(Parent, { target: fixture, env });
|
|
||||||
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
|
|
||||||
|
|
||||||
store.state.rev++;
|
|
||||||
// this is the key to the bug, it makes Parent be in "render" state but not
|
|
||||||
// yet rendered while the change of state happens
|
|
||||||
await Promise.resolve();
|
|
||||||
state.word = "test";
|
|
||||||
await nextTick();
|
|
||||||
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("component with store, useState, shouldUpdate=false and child with shouldupdate false", async () => {
|
|
||||||
let state: any;
|
|
||||||
|
|
||||||
const store = new Store({ state: { rev: 0 } });
|
|
||||||
|
|
||||||
class ChildChild extends Component {
|
|
||||||
static template = xml`<div><t t-esc="props.value"/></div>`;
|
|
||||||
|
|
||||||
shouldUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Child extends Component {
|
|
||||||
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/><ChildChild value="state.value"/></div>`;
|
|
||||||
static components = { ChildChild };
|
|
||||||
state = useState({ word: "hello", value: 3 });
|
|
||||||
|
|
||||||
constructor(parent, props) {
|
|
||||||
super(parent, props);
|
|
||||||
state = this.state;
|
|
||||||
useStore((props) => {
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Parent extends Component {
|
|
||||||
static template = xml`<div><Child name="state.name"/></div>`;
|
|
||||||
static components = { Child };
|
|
||||||
|
|
||||||
state = useState({ name: "World" });
|
|
||||||
constructor(parent, props) {
|
|
||||||
super(parent, props);
|
|
||||||
useStore((props) => store.state.rev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(env as any).store = store;
|
|
||||||
|
|
||||||
await mount(Parent, { target: fixture, env });
|
|
||||||
expect(fixture.innerHTML).toBe("<div><div>helloWorld<div>3</div></div></div>");
|
|
||||||
|
|
||||||
store.state.rev++;
|
|
||||||
// this is the key to the bug, it makes Parent be in "render" state but not
|
|
||||||
// yet rendered while the change of state happens
|
|
||||||
await Promise.resolve();
|
|
||||||
state.word = "test";
|
|
||||||
state.value = 44;
|
|
||||||
await nextTick();
|
|
||||||
expect(fixture.innerHTML).toBe("<div><div>testWorld<div>3</div></div></div>");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user