Compare commits

...

43 Commits

Author SHA1 Message Date
Géry Debongnie 5a75e5ec19 bump to v0.5.0 2019-04-04 14:19:40 +02:00
Géry Debongnie 3e8682b741 ref: adapt qweb to output code closer to es5 2019-04-04 14:14:02 +02:00
Géry Debongnie fd26775a0f imp: adapt build scripts to allow ES5 output 2019-04-04 13:55:46 +02:00
Géry Debongnie a86d0b5b02 imp: store connect: mapStateToProps receive ownProps 2019-04-02 14:23:40 +02:00
Géry Debongnie 7ebea5a486 imp: better error message when evaluating/compiling templates
fixes #3
2019-04-02 11:03:04 +02:00
Géry Debongnie 56be87f26e fix: allow t-attf- attributes to combine with regular attrs
fixes #10
2019-04-02 09:22:37 +02:00
Géry Debongnie 990a08dc04 imp: add componentDidUpdate hook 2019-04-01 16:00:57 +02:00
Alexandre Kühn e7a4ad99de [FIX] connected child components with custom hooks 2019-04-01 14:44:53 +02:00
Géry Debongnie d6e61c2203 imp: qweb engine can be initialized with templates 2019-03-29 16:40:59 +01:00
Géry Debongnie e6b0d06e3e fix: make sure qweb throw error if invalid templates 2019-03-29 15:37:42 +01:00
Géry Debongnie 5cd45b59af fix: properly destroy/detach widgets in some cases
Sometimes, widgets were not properly destroyed/detached. This was caused
by the fact that we used the hook remove, when we sometimes need to use
the hook destroy as well.
2019-03-29 12:50:45 +01:00
Géry Debongnie 1e58c7b12b fix: update component snapshots 2019-03-29 12:39:00 +01:00
Géry Debongnie afc89e6f66 fix: if t-props is not defined, should fallback on {} 2019-03-28 17:35:08 +01:00
Géry Debongnie 04679a6d03 fix: hello world example was wrong :( 2019-03-28 14:03:41 +01:00
Géry Debongnie cb2e76498b fix: update build scripts: core.js -> owl.js 2019-03-28 10:57:12 +01:00
Géry Debongnie 7ac4e63e51 doc: improve readme.md 2019-03-28 10:52:07 +01:00
Géry Debongnie d804bcf2dd doc: improve main page style 2019-03-28 10:42:12 +01:00
Géry Debongnie c5951cc98e bump to v0.4.0
- rebranding to OWL
- much better documentation
- qweb compiler optimizations/cleanup
- many bug fixes
2019-03-28 09:51:40 +01:00
Géry Debongnie 68731a435b doc: improve the documentation, add a lot of information 2019-03-28 09:49:19 +01:00
Géry Debongnie 8e8d4a28c5 doc: add state management page, and update documentation 2019-03-27 17:02:35 +01:00
Géry Debongnie 8c5919cbc9 doc: fix error in main examples 2019-03-27 16:42:21 +01:00
Géry Debongnie 1efa65ff04 doc: add another example in readme.md 2019-03-27 15:40:24 +01:00
Géry Debongnie 7473d45700 doc: update readme to add a minimal hello world 2019-03-27 15:36:09 +01:00
Géry Debongnie 80e2124cbf imp: rename library into owl 2019-03-27 15:26:20 +01:00
Géry Debongnie e53287b52e ref: reorganize exports into core/extras 2019-03-27 15:18:25 +01:00
Géry Debongnie 2758269139 add patch/unpatch utility methods 2019-03-26 14:30:27 +01:00
Géry Debongnie 766d2f2301 ref: qweb context cleanup
Some code was uselessly duplicated.  We simplify in this commit the
handling of subcontexts.

closes #9
2019-03-22 15:02:07 +01:00
Géry Debongnie 597adb3981 ref: improve qweb implementation 2019-03-22 14:51:02 +01:00
Géry Debongnie 6ea9ccf56f fix: remove exprCache, as formatting is contextual
Formatting expressions now depends on the currently defined variables, so
it cannot be put in a cache.
2019-03-22 14:46:47 +01:00
Géry Debongnie a444e3dcac imp: export utils in main bundle 2019-03-22 12:08:55 +01:00
Géry Debongnie a7d2edd0c9 imp: qweb compiler now condenses whitespaces
This is a pretty big breaking change: the html output by qweb is now
different.

The main goal is to optimize the compiled templates:
- condense all consecutive whitespaces into a single spaces
- if possible, drop completely some text nodes, based on the following
  heuristic: if a text node is only composed of whitespaces, and
contains at least one linebreak, then it can be dropped.

This leads in my benchmark test to an improvement of about 10%, in
rendering speed and in memory consuption.

Note: whitespace here means anything that matches the \s regexp: newlines,
tabs, ...

close #8
2019-03-22 12:04:50 +01:00
Géry Debongnie d81e5d8f4f imp: add debug mode to store 2019-03-22 10:56:34 +01:00
Géry Debongnie d48cb5aae4 imp: work on store, refactor, improve todoapp 2019-03-22 10:47:59 +01:00
Géry Debongnie a236ae396a fix: make sure props are always defined
fix #7
2019-03-22 09:28:56 +01:00
Géry Debongnie add380f59b ref: move PureWidget back to core, use updateEnv 2019-03-21 16:33:08 +01:00
Géry Debongnie ad9fb77458 fix: force children to rerender when env changes 2019-03-21 16:06:06 +01:00
Géry Debongnie 4326d18961 imp: update build scripts 2019-03-21 14:51:54 +01:00
Géry Debongnie 88a3b6d25e add some tests to store class 2019-03-21 12:09:17 +01:00
Géry Debongnie a34fc39100 imp: allow object descriptors for attributes
close #6
2019-03-21 11:15:06 +01:00
Géry Debongnie a8b3ec7726 support using object variables in t-esc 2019-03-21 10:47:08 +01:00
Géry Debongnie d799e3dd47 prevent useless rerenders when updatingEnv on unmounted component
fix #4
2019-03-21 10:39:09 +01:00
Géry Debongnie b446d5d338 add src/ folder to jest config 2019-03-21 10:37:24 +01:00
Géry Debongnie 71c1469124 update examples doc 2019-03-21 10:23:44 +01:00
39 changed files with 1685 additions and 1135 deletions
+95 -43
View File
@@ -1,68 +1,120 @@
**Warning!**
<h1 align="center">🦉 Odoo Web Lab 🦉</h1>
This is currently a proof of concept, definitely not a production-ready codebase.
We hope to use it in the Odoo web client soon.
## Project Overview
---
Odoo Web Lab (OWL) is a project to collect some useful, reusable, (hopefully)
well designed building blocks for building web applications. However, since this is the basis for the Odoo web client, we will not hesitate
to design the code here to better match the Odoo architecture/design principles.
# Core Utility for Odoo Web Client
The most important element of this repository is certainly the component system.
It is designed to be:
## Overview
1. **declarative:** the user interface should be described in term of the state
of the application, not as a sequence of imperative steps.
This repository contains some useful building block for building web applications.
We made some efforts to make sure the code is reusable and generic. However, since this is the basis for the Odoo web client, we will not hesitate to design
the code here to better match the Odoo architecture/design principles.
2. **composable:** each widget can seamlessly be created in a parent widget by
a simple directive in its template.
Currently, this repository contains:
3. **asynchronous rendering:** the framework will transparently wait for each
subwidgets to be ready before applying the rendering. It uses native promises
under the hood.
- some utility functions/classes
- an implementation/extension of the QWeb template engine that outputs a virtual
dom (using the snabbdom library)
- a Component class, which uses the QWeb engine as its underlying rendering
mechanism. The component class is designed to be declarative, with
asynchronous rendering.
4. **uses QWeb as a template system:** the templates are described in XML
and follow the QWeb specification. This is a requirement for Odoo.
In the future, this repository may includes other features. Here are some possible
ideas:
- a (frontend) router could be included.
- a store base class (as in the flux/redux architecture)
5. **with an imperative escape hatch:** if necessary, sub widgets can easily be
manually created/destroyed.
Note: the code is written in typescript. This does not mean that the main web
client will ever be converted to typescript (even though I would really like it).
## Main scripts
## Installing/Building
To install every dependency needed to play with this code:
Some npm scripts are available:
```
npm install
```
To build a bundle of this as a library:
```
npm run build
```
To run tests:
```
npm run test
npm run test:watch
```
Note that the test scripts also run the example tests suites.
| Command | Description |
| ------------------ | --------------------------------------------------------- |
| npm install | install every dependency required for this project |
| npm run build | build a bundle of _owl_ in the _/dist/_ folder |
| npm run build:es5 | build a bundle of _owl_ in the _/dist/_ folder (ES5 code) |
| npm run minify | minify the prebuilt owl.js file |
| npm run test | run all tests |
| npm run test:watch | run all tests, and keep a watcher |
## Documentation
The complete documentation can be found [here](doc/readme.md). The most important sections are:
- [Quick Start](doc/quick_start.md)
- [Tutorial](doc/tutorial.md)
- [Component](doc/component.md)
- [QWeb](doc/qweb.md)
# Examples
There are two examples of how to work with this web framework:
Here is a minimal Hello World example:
- [Demo Application](examples/readme.md#demo)
```javascript
class HelloWorld extends owl.core.Component {
inlineTemplate = `<div>Hello <t t-esc="props.name"/></div>`;
}
const env = {
qweb: new owl.core.QWeb()
};
const hello = new HelloWorld(env, { name: "World" });
hello.mount(document.body);
```
The next example show how interactive widgets can be created and how widget
composition works:
```javascript
class Counter extends owl.core.Component {
inlineTemplate = `
<div>
<button t-on-click="increment(-1)">-</button>
<span style="font-weight:bold">Value: <t t-esc="state.value"/></span>
<button t-on-click="increment(1)">+</button>
</div>`;
constructor(parent, props) {
super(parent, props);
this.state = {
value: props.initialState || 0
};
}
increment(delta) {
this.updateState({ value: this.state.value + delta });
}
}
class App extends owl.core.Component {
inlineTemplate = `
<div>
<t t-widget="Counter" t-props="{initialState: 1}"/>
<t t-widget="Counter" t-props="{initialState: 42}"/>
</div>`;
widgets = { Counter };
}
const env = {
qweb: new owl.core.QWeb()
};
const app = new App(env);
app.mount(document.body);
```
More interesting examples on how to work with this web framework can be found in the _examples/_ folder:
- [Todo Application](examples/readme.md#todo-app)
- [Web Client](examples/readme.md#web-client-example)
- [Benchmarks](examples/readme.md#benchmarks)
## License
React is [GPL licensed](./LICENSE).
+8
View File
@@ -25,3 +25,11 @@ result = qweb.render("sometemplate", { name: "world" });
- t-on directive
- t-widget, t-props, t-key
- t-ref
## Note on white spaces
White spaces in a templates are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
+22
View File
@@ -0,0 +1,22 @@
# Odoo Web Lab Documentation
Currently, this repository contains:
1. **core**
- an implementation/extension of the QWeb template engine that outputs a virtual
dom (using the snabbdom library)
- a Component class, which uses the QWeb engine as its underlying rendering
mechanism. The component class is designed to be declarative, with
asynchronous rendering. Also, it uses snabbdom as the virtual dom library.
- some utility functions/classes
2. **extras**
- a Store class and a connect function, to help manage the state of an application (like react-redux)
- a Registry: this is a simple key/store mapping
- [Quick Start](quick_start.md)
- [Tutorial](tutorial.md)
- [Component](component.md)
- [QWeb](qweb.md)
- [State Management](state_management.md)
+8
View File
@@ -0,0 +1,8 @@
# OWL: State Management
Managing the state in an application is not an easy task. Many different
architectures/designs/systems/... have been created. We propose here to use
the idea of a central store.
- Store
- connect
+3
View File
@@ -0,0 +1,3 @@
# Tutorial
todo...
+1 -1
View File
@@ -25,7 +25,7 @@ const template = `
</div>
</div>`;
export class App extends odoo.core.Component {
export class App extends owl.core.Component {
constructor(parent, props) {
super(parent, props);
this.inlineTemplate = template;
+1 -1
View File
@@ -1,4 +1,4 @@
export class Counter extends odoo.core.Component {
export class Counter extends owl.core.Component {
inlineTemplate = `
<div>
<button t-on-click="increment(-1)">-</button>
+1 -1
View File
@@ -6,7 +6,7 @@
<link rel="icon" href="data:,">
<!-- Application JS/CSS -->
<script src="/core.js"></script>
<script src="/owl.js"></script>
<link rel="stylesheet" href="/app.css">
<script type="module" src="/main.js"></script>
+1 -1
View File
@@ -2,7 +2,7 @@ import { App } from "./app.js";
function createApp(el) {
const env = {
qweb: new odoo.core.QWeb()
qweb: new owl.core.QWeb()
};
const app = new App(env, { initialState: 13 });
app.mount(el);
+1 -1
View File
@@ -1,6 +1,6 @@
import { Counter } from "./counter.js";
export class Message extends odoo.core.Component {
export class Message extends owl.core.Component {
inlineTemplate = `
<div class="message">
<span class="author"><t t-esc="props.author"/></span>
+35 -13
View File
@@ -1,24 +1,46 @@
# Examples
To play with the examples:
This project features three example to illustrate how to work with the web-core framework:
- _benchmarks_ is a small application to test large number of widgets,
- _todoapp_ is the classical todo application (from the todomvc project),
- _web_ is a rethinking of how the odoo web client could be implemented.
## Benchmarks
This example is just a playground to experiment/showcase some features of the framework, with large number of widgets.
```
npm run example:build # make a build in dist/app/
npm run example:dev # make a build in dist/app/, and make a live server to access it
npm run example:benchmarks:build # make a build in dist/examples/
npm run example:benchmarks:dev # make a build in dist/examples/, and make a live server to access it
```
The _example_:dev\_ task will build the app (without minification), then start a live-server with hot-reloading, and watch the file system to make sure
that assets are properly rebuilt if necessary, and then reloaded.
The benchmarks application generates a large number of demo messages, and display them in a list, with a few buttons that can
be used to alter the number of visible widgets.
Note that each message is itself a widget, with a sub widget. This
example could be made faster (by not using subwidgets), but the point is to observe/measure the overhead of the Component class.
## Todo App
The Todo App is the classical todo application from _http://todomvc.com/_. It is a good mini application with non trivial data structures and interface updates.
```
npm run example:todoapp:build # make a build in dist/examples/
npm run example:todoapp:dev # make a build in dist/examples/, and make a live server to access it
```
It is implemented with the Store class (as in redux/vuex).
## Web Client Example
To test and play with the component class, there is a demo application located
in the _examples/web/_ folder. It is a rethink of what the web client could look like
if it is ever rebuilt from scratch. It is currently an experiment. But
obviously, we hope someday to be able to use this work and actually rewrite
completely the odoo web client.
The _web_ example,located in the _examples/web/_ folder is a rethink of what the
web client could look like if it is ever rebuilt from scratch. It is currently an experiment. But obviously, we hope someday to be able to use this work and
actually rewrite completely the odoo web client.
## Demo
```
npm run example:web:build # make a build in dist/examples/
npm run example:web:dev # make a build in dist/examples/, and make a live server to access it
```
This example is just a playground to experiment/showcase some features of
the framework.
Note that it is currently written in typescript.
+14 -10
View File
@@ -1,20 +1,21 @@
import { TodoItem } from "./TodoItem.js";
const { StoreMixin, Component } = odoo.core;
const { Component } = owl.core;
const { connect } = owl.extras;
const ENTER_KEY = 13;
export class TodoApp extends StoreMixin(Component) {
function mapStateToProps(state) {
return { todos: state.todos };
}
class TodoApp extends Component {
template = "todoapp";
widgets = { TodoItem };
state = { filter: "all" };
get todos() {
return this.env.store.state.todos;
}
get visibleTodos() {
let todos = this.todos;
let todos = this.props.todos;
if (this.state.filter === "active") {
todos = todos.filter(t => !t.completed);
}
@@ -25,15 +26,16 @@ export class TodoApp extends StoreMixin(Component) {
}
get allChecked() {
return this.todos.every(todo => todo.completed);
return this.props.todos.every(todo => todo.completed);
}
get remaining() {
return this.todos.filter(todo => !todo.completed).length;
return this.props.todos.filter(todo => !todo.completed).length;
}
get remainingText() {
return (this.remaining < 2 ? "item" : "items") + " left";
const items = this.remaining < 2 ? "item" : "items";
return ` ${items} left`;
}
addTodo(ev) {
@@ -54,3 +56,5 @@ export class TodoApp extends StoreMixin(Component) {
this.env.store.dispatch("toggleAll", !this.allChecked);
}
}
export default connect(mapStateToProps)(TodoApp);
+1 -1
View File
@@ -1,7 +1,7 @@
const ENTER_KEY = 13;
const ESC_KEY = 27;
export class TodoItem extends odoo.core.Component {
export class TodoItem extends owl.core.Component {
template = "todoitem";
state = { isEditing: false };
+1 -1
View File
@@ -6,7 +6,7 @@
<link rel="icon" href="data:,">
<!-- Application JS/CSS -->
<script src="/core.js"></script>
<script src="/owl.js"></script>
<link rel="stylesheet" href="/app.css">
<script type="module" src="/main.js"></script>
+2 -2
View File
@@ -1,4 +1,4 @@
import { TodoApp } from "./components/TodoApp.js";
import TodoApp from "./components/TodoApp.js";
import { makeStore } from "./store.js";
async function makeEnv() {
@@ -8,7 +8,7 @@ async function makeEnv() {
}
let templates = await result.text();
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
const qweb = new odoo.core.QWeb();
const qweb = new owl.core.QWeb();
qweb.loadTemplates(templates);
return {
qweb,
+1 -1
View File
@@ -66,7 +66,7 @@ export function makeStore() {
);
const nextId = Math.max(0, ...todos.map(t => t.id || 0)) + 1;
const state = { todos, nextId };
const store = new odoo.core.Store({ state, actions, mutations });
const store = new owl.extras.Store({ state, actions, mutations });
store.on("update", null, () => {
const state = JSON.stringify(store.state.todos);
window.localStorage.setItem(LOCALSTORAGE_KEY, state);
+7 -7
View File
@@ -8,7 +8,7 @@
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header>
<!-- main section -->
<section class="main" t-if="todos.length">
<section class="main" t-if="props.todos.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="toggleAll"/>
<label for="toggle-all"></label>
<ul class="todo-list">
@@ -18,7 +18,7 @@
</ul>
</section>
<!-- footer -->
<footer class="footer" t-if="todos.length">
<footer class="footer" t-if="props.todos.length">
<span class="todo-count">
<strong>
<t t-esc="remaining"/>
@@ -27,22 +27,22 @@
</span>
<ul class="filters">
<li>
<a href="#/all" t-on-click="updateState({filter:'all'})" t-att-class="state.filter === 'all' ? 'selected' : ''">All</a>
<a href="#/all" t-on-click="updateState({filter:'all'})" t-att-class="{selected: state.filter === 'all'}">All</a>
</li>
<li>
<a href="#/active" t-on-click="updateState({filter:'active'})" t-att-class="state.filter === 'active' ? 'selected' : ''">Active</a>
<a href="#/active" t-on-click="updateState({filter:'active'})" t-att-class="{selected: state.filter === 'active'}">Active</a>
</li>
<li>
<a href="#/completed" t-on-click="updateState({filter:'completed'})" t-att-class="state.filter === 'completed' ? 'selected' : ''">Completed</a>
<a href="#/completed" t-on-click="updateState({filter:'completed'})" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li>
</ul>
<button class="clear-completed" t-if="todos.length gt remaining" t-on-click="clearCompleted">
<button class="clear-completed" t-if="props.todos.length gt remaining" t-on-click="clearCompleted">
Clear completed
</button>
</footer>
</section>
<li t-name="todoitem" class="todo" t-att-class="(props.completed ? 'completed ' : ' ') + (state.isEditing ? 'editing' : '')">
<li t-name="todoitem" class="todo" t-att-class="{completed: props.completed, editing: state.isEditing}">
<div class="view">
<input class="toggle" type="checkbox" t-on-change="toggleTodo" t-att-checked="props.completed"/>
<label t-on-dblclick="editTodo">
+1 -2
View File
@@ -40,8 +40,7 @@ export class Root extends Widget<Store, State> {
applyMobileInterface() {
const isMobile = window.innerWidth <= 768;
if (isMobile !== this.env.isMobile) {
this.env.isMobile = isMobile;
this.render();
this.updateEnv({ isMobile });
}
}
+3 -31
View File
@@ -1,38 +1,10 @@
import { Component } from "../../../../../src/component";
import { Component, PureComponent } from "../../../../../src/component";
import { Env } from "./env";
//------------------------------------------------------------------------------
// Widget classes
//------------------------------------------------------------------------------
export class Widget<P, S> extends Component<Env, P, S> {
constructor(parent, props) {
super(parent, props);
(<any>this.__widget__).isMobile = this.env.isMobile;
}
async updateProps(nextProps: P): Promise<void> {
if ((<any>this.__widget__).isMobile !== this.env.isMobile) {
(<any>this.__widget__).isMobile = this.env.isMobile;
return this._updateProps(nextProps);
}
return super.updateProps(nextProps);
}
}
export class Widget<P, S> extends Component<Env, P, S> {}
export class PureWidget<P, S> extends Widget<P, S> {
shouldUpdate(nextProps: P): boolean {
for (let k in nextProps) {
if (nextProps[k] !== this.props[k]) {
return true;
}
}
return false;
}
async updateState(nextState: Partial<S>) {
for (let k in nextState) {
if (nextState[k] !== this.state[k]) {
return super.updateState(nextState);
}
}
}
}
export class PureWidget<P, S> extends PureComponent<Env, P, S> {}
@@ -1,24 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`can be rendered 1`] = `
"<div class=\\"o_home_menu\\">
<div class=\\"o_apps\\">
<a href=\\"#menu_id=96&amp;action_id=131\\" class=\\"o_app\\" data-menu=\\"96\\">
<i class=\\"fa fa-comment o_app_icon fa-3x fa-fw\\"></i>
<div class=\\"o_caption\\">
Discuss
</div>
</a><a href=\\"#menu_id=205&amp;action_id=250\\" class=\\"o_app\\" data-menu=\\"205\\">
<i class=\\"fa fa-pen o_app_icon fa-3x fa-fw\\"></i>
<div class=\\"o_caption\\">
Notes
</div>
</a><a href=\\"#menu_id=409&amp;action_id=597\\" class=\\"o_app\\" data-menu=\\"409\\">
<i class=\\"fa fa-handshake o_app_icon fa-3x fa-fw\\"></i>
<div class=\\"o_caption\\">
CRM
</div>
</a>
</div>
</div>"
`;
exports[`can be rendered 1`] = `"<div class=\\"o_home_menu\\"><div class=\\"o_apps\\"><a href=\\"#menu_id=96&amp;action_id=131\\" class=\\"o_app\\" data-menu=\\"96\\"><i class=\\"fa fa-comment o_app_icon fa-3x fa-fw\\"></i><div class=\\"o_caption\\">Discuss</div></a><a href=\\"#menu_id=205&amp;action_id=250\\" class=\\"o_app\\" data-menu=\\"205\\"><i class=\\"fa fa-pen o_app_icon fa-3x fa-fw\\"></i><div class=\\"o_caption\\">Notes</div></a><a href=\\"#menu_id=409&amp;action_id=597\\" class=\\"o_app\\" data-menu=\\"409\\"><i class=\\"fa fa-handshake o_app_icon fa-3x fa-fw\\"></i><div class=\\"o_caption\\">CRM</div></a></div></div>"`;
@@ -1,45 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`can be rendered (in home menu, no app) 1`] = `
"<div class=\\"o_navbar o_in_home\\">
</div>"
`;
exports[`can be rendered (in home menu, no app) 1`] = `"<div class=\\"o_navbar o_in_home\\"></div>"`;
exports[`can be rendered (in home menu, one active app) 1`] = `
"<div class=\\"o_navbar o_in_home\\">
<a aria-label=\\"Applications\\" class=\\"o_title fa fa-chevron-left\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a>
</div>"
`;
exports[`can be rendered (in home menu, one active app) 1`] = `"<div class=\\"o_navbar o_in_home\\"><a aria-label=\\"Applications\\" class=\\"o_title fa fa-chevron-left\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a></div>"`;
exports[`can render one menu item 1`] = `
"<div class=\\"o_navbar\\">
<a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a>
<a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">
Discuss
</a>
<ul class=\\"o_menu_sections\\">
</ul>
</div>"
`;
exports[`can render one menu item 1`] = `"<div class=\\"o_navbar\\"><a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a><a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">Discuss</a><ul class=\\"o_menu_sections\\"></ul></div>"`;
exports[`mobile mode: navbar is different 1`] = `
"<div class=\\"o_navbar\\">
<a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a>
<a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">
Notes
</a>
<ul class=\\"o_menu_sections\\">
<li>MOBILEMODE</li>
</ul>
</div>"
`;
exports[`mobile mode: navbar is different 1`] = `"<div class=\\"o_navbar\\"><a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a><a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">Notes</a><ul class=\\"o_menu_sections\\"><li>MOBILEMODE</li></ul></div>"`;
@@ -1,27 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`can be rendered (type = warning) 1`] = `
"<div class=\\"o_notification o_error\\">
<div class=\\"o_notification_title\\">
<span class=\\"o_icon fa fa-3x fa-exclamation\\" role=\\"img\\" aria-label=\\"Notification title\\" title=\\"Notification title\\"></span>
title
</div>
<div class=\\"o_notification_content\\">
message
</div>
</div>"
`;
exports[`can be rendered (type = warning) 1`] = `"<div class=\\"o_notification o_error\\"><div class=\\"o_notification_title\\"><span class=\\"o_icon fa fa-3x fa-exclamation\\" role=\\"img\\" aria-label=\\"Notification title\\" title=\\"Notification title\\"></span>title</div><div class=\\"o_notification_content\\">message</div></div>"`;
exports[`can be rendered 1`] = `
"<div class=\\"o_notification\\">
<div class=\\"o_notification_title\\">
<span class=\\"o_icon fa fa-3x fa-lightbulb\\" role=\\"img\\" aria-label=\\"Notification title\\" title=\\"Notification title\\"></span>
title
</div>
<div class=\\"o_notification_content\\">
message
</div>
</div>"
`;
exports[`can be rendered 1`] = `"<div class=\\"o_notification\\"><div class=\\"o_notification_title\\"><span class=\\"o_icon fa fa-3x fa-lightbulb\\" role=\\"img\\" aria-label=\\"Notification title\\" title=\\"Notification title\\"></span>title</div><div class=\\"o_notification_content\\">message</div></div>"`;
@@ -1,257 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`can be rendered (in home menu) 1`] = `
"<div class=\\"o_web_client\\">
<div class=\\"o_navbar o_in_home\\">
</div>
<div class=\\"o_home_menu\\">
<div class=\\"o_apps\\">
<a href=\\"#menu_id=96&amp;action_id=131\\" class=\\"o_app\\" data-menu=\\"96\\">
<i class=\\"fa fa-comment o_app_icon fa-3x fa-fw\\"></i>
<div class=\\"o_caption\\">
Discuss
</div>
</a><a href=\\"#menu_id=205&amp;action_id=250\\" class=\\"o_app\\" data-menu=\\"205\\">
<i class=\\"fa fa-pen o_app_icon fa-3x fa-fw\\"></i>
<div class=\\"o_caption\\">
Notes
</div>
</a><a href=\\"#menu_id=409&amp;action_id=597\\" class=\\"o_app\\" data-menu=\\"409\\">
<i class=\\"fa fa-handshake o_app_icon fa-3x fa-fw\\"></i>
<div class=\\"o_caption\\">
CRM
</div>
</a>
</div>
</div>
<div class=\\"o_content o_hidden\\"></div>
<div class=\\"o_notification_container\\">
</div>
<div class=\\"o_loading d-none\\">Loading</div>
</div>"
`;
exports[`can be rendered (in home menu) 1`] = `"<div class=\\"o_web_client\\"><div class=\\"o_navbar o_in_home\\"></div><div class=\\"o_home_menu\\"><div class=\\"o_apps\\"><a href=\\"#menu_id=96&amp;action_id=131\\" class=\\"o_app\\" data-menu=\\"96\\"><i class=\\"fa fa-comment o_app_icon fa-3x fa-fw\\"></i><div class=\\"o_caption\\">Discuss</div></a><a href=\\"#menu_id=205&amp;action_id=250\\" class=\\"o_app\\" data-menu=\\"205\\"><i class=\\"fa fa-pen o_app_icon fa-3x fa-fw\\"></i><div class=\\"o_caption\\">Notes</div></a><a href=\\"#menu_id=409&amp;action_id=597\\" class=\\"o_app\\" data-menu=\\"409\\"><i class=\\"fa fa-handshake o_app_icon fa-3x fa-fw\\"></i><div class=\\"o_caption\\">CRM</div></a></div></div><div class=\\"o_content o_hidden\\"></div><div class=\\"o_notification_container\\"></div><div class=\\"o_loading d-none\\">Loading</div></div>"`;
exports[`clicks on client action with invalid key => empty widget is rendered + warning 1`] = `
"<div class=\\"o_web_client\\">
<div class=\\"o_navbar\\">
<a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a>
<a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">
Discuss
</a>
<ul class=\\"o_menu_sections\\">
</ul>
</div>
<div class=\\"o_content\\"><div class=\\"o_action_controller\\"></div></div>
<div class=\\"o_notification_container\\">
<div class=\\"o_notification o_error\\">
<div class=\\"o_notification_title\\">
<span class=\\"o_icon fa fa-3x fa-exclamation\\" role=\\"img\\" aria-label=\\"Notification Invalid Client Action\\" title=\\"Notification Invalid Client Action\\"></span>
Invalid Client Action
</div>
<div class=\\"o_notification_content\\">
Cannot find widget 'mail.discuss' in the action registry
</div>
</div>
</div>
<div class=\\"o_loading d-none\\">Loading</div>
</div>"
`;
exports[`clicks on client action with invalid key => empty widget is rendered + warning 1`] = `"<div class=\\"o_web_client\\"><div class=\\"o_navbar\\"><a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a><a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">Discuss</a><ul class=\\"o_menu_sections\\"></ul></div><div class=\\"o_content\\"><div class=\\"o_action_controller\\"></div></div><div class=\\"o_notification_container\\"><div class=\\"o_notification o_error\\"><div class=\\"o_notification_title\\"><span class=\\"o_icon fa fa-3x fa-exclamation\\" role=\\"img\\" aria-label=\\"Notification Invalid Client Action\\" title=\\"Notification Invalid Client Action\\"></span>Invalid Client Action</div><div class=\\"o_notification_content\\">Cannot find widget 'mail.discuss' in the action registry</div></div></div><div class=\\"o_loading d-none\\">Loading</div></div>"`;
exports[`if url has action_id, will render action and navigate to proper menu_id 1`] = `
"<div class=\\"o_web_client\\">
<div class=\\"o_navbar\\">
<a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a>
<a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">
Discuss
</a>
<ul class=\\"o_menu_sections\\">
</ul>
</div>
<div class=\\"o_content\\"><div class=\\"o_discuss o_action_controller\\">
<div class=\\"o_control_panel\\">
<ol class=\\"breadcrumb\\" role=\\"navigation\\">
<li class=\\"breadcrumb-item active\\">
Discuss
</li>
</ol>
<div class=\\"o_cp_searchview\\" role=\\"search\\">
<div class=\\"o_searchview\\" role=\\"search\\" aria-autocomplete=\\"list\\">
<span class=\\"o_searchview_more fa\\" title=\\"Advanced Search...\\" role=\\"img\\" aria-label=\\"Advanced Search...\\"></span>
</div>
</div>
<div class=\\"o_cp_left\\">
<div class=\\"o_cp_buttons\\" role=\\"toolbar\\" aria-label=\\"Control panel toolbar\\"></div>
<aside class=\\"o_cp_sidebar\\"></aside>
</div>
<div class=\\"o_cp_right\\">
<div class=\\"btn-group o_search_options\\" role=\\"search\\"></div>
<nav class=\\"o_cp_pager\\" role=\\"search\\" aria-label=\\"Pager\\"></nav>
<nav class=\\"btn-group o_cp_switch_buttons\\" role=\\"toolbar\\" aria-label=\\"View switcher\\"></nav>
</div>
</div>
<div class=\\"o_content\\">
<div class=\\"o_discuss_sidebar\\">
<span>DISCUSS!!</span>
</div>
<div class=\\"o_discuss_content\\">
<button>Reset first counter</button>
<button>Reset counter 2 in 3s</button>
<button>Toggle Clock/counters</button>
<button>Toggle Color</button>
<button>Rerender this widget</button>
<input>
<div>
<button>-</button>
<span style=\\"font-weight:bold\\">Value: 4</span>
<button>+</button>
</div>
<div>
<button>-</button>
<span style=\\"font-weight:bold\\">Value: 400</span>
<button>+</button>
</div>
<div>
<span>Current Color: </span>
red
</div>
<button>Add notif</button>
<button>Add sticky notif</button>
<button>Add warning</button>
</div>
</div>
</div></div>
<div class=\\"o_notification_container\\">
</div>
<div class=\\"o_loading d-none\\">Loading</div>
</div>"
`;
exports[`if url has action_id, will render action and navigate to proper menu_id 1`] = `"<div class=\\"o_web_client\\"><div class=\\"o_navbar\\"><a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a><a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">Discuss</a><ul class=\\"o_menu_sections\\"></ul></div><div class=\\"o_content\\"><div class=\\"o_discuss o_action_controller\\"><div class=\\"o_control_panel\\"><ol class=\\"breadcrumb\\" role=\\"navigation\\"><li class=\\"breadcrumb-item active\\">Discuss</li></ol><div class=\\"o_cp_searchview\\" role=\\"search\\"><div class=\\"o_searchview\\" role=\\"search\\" aria-autocomplete=\\"list\\"><span class=\\"o_searchview_more fa\\" title=\\"Advanced Search...\\" role=\\"img\\" aria-label=\\"Advanced Search...\\"></span></div></div><div class=\\"o_cp_left\\"><div class=\\"o_cp_buttons\\" role=\\"toolbar\\" aria-label=\\"Control panel toolbar\\"></div><aside class=\\"o_cp_sidebar\\"></aside></div><div class=\\"o_cp_right\\"><div class=\\"btn-group o_search_options\\" role=\\"search\\"></div><nav class=\\"o_cp_pager\\" role=\\"search\\" aria-label=\\"Pager\\"></nav><nav class=\\"btn-group o_cp_switch_buttons\\" role=\\"toolbar\\" aria-label=\\"View switcher\\"></nav></div></div><div class=\\"o_content\\"><div class=\\"o_discuss_sidebar\\"><span>DISCUSS!!</span></div><div class=\\"o_discuss_content\\"><button>Reset first counter</button><button>Reset counter 2 in 3s</button><button>Toggle Clock/counters</button><button>Toggle Color</button><button>Rerender this widget</button><input><div><button>-</button><span style=\\"font-weight:bold\\">Value: 4</span><button>+</button></div><div><button>-</button><span style=\\"font-weight:bold\\">Value: 400</span><button>+</button></div><div><span>Current Color: </span>red</div><button>Add notif</button><button>Add sticky notif</button><button>Add warning</button></div></div></div></div><div class=\\"o_notification_container\\"></div><div class=\\"o_loading d-none\\">Loading</div></div>"`;
exports[`open act window action with invalid viewtype => empty widget is rendered + warning 1`] = `
"<div class=\\"o_web_client\\">
<div class=\\"o_navbar\\">
<a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a>
<a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">
Notes
</a>
<ul class=\\"o_menu_sections\\">
</ul>
</div>
<div class=\\"o_content\\"><div class=\\"o_action_controller\\"></div></div>
<div class=\\"o_notification_container\\">
<div class=\\"o_notification o_error\\">
<div class=\\"o_notification_title\\">
<span class=\\"o_icon fa fa-3x fa-exclamation\\" role=\\"img\\" aria-label=\\"Notification Invalid View type\\" title=\\"Notification Invalid View type\\"></span>
Invalid View type
</div>
<div class=\\"o_notification_content\\">
Cannot find view of type 'kanban' in the view registry
</div>
</div>
</div>
<div class=\\"o_loading d-none\\">Loading</div>
</div>"
`;
exports[`open act window action with invalid viewtype => empty widget is rendered + warning 1`] = `"<div class=\\"o_web_client\\"><div class=\\"o_navbar\\"><a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a><a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">Notes</a><ul class=\\"o_menu_sections\\"></ul></div><div class=\\"o_content\\"><div class=\\"o_action_controller\\"></div></div><div class=\\"o_notification_container\\"><div class=\\"o_notification o_error\\"><div class=\\"o_notification_title\\"><span class=\\"o_icon fa fa-3x fa-exclamation\\" role=\\"img\\" aria-label=\\"Notification Invalid View type\\" title=\\"Notification Invalid View type\\"></span>Invalid View type</div><div class=\\"o_notification_content\\">Cannot find view of type 'kanban' in the view registry</div></div></div><div class=\\"o_loading d-none\\">Loading</div></div>"`;
exports[`start with no action => clicks on client action => discuss is rendered 1`] = `
"<div class=\\"o_web_client\\">
<div class=\\"o_navbar\\">
<a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a>
<a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">
Discuss
</a>
<ul class=\\"o_menu_sections\\">
</ul>
</div>
<div class=\\"o_content\\"><div class=\\"o_discuss o_action_controller\\">
<div class=\\"o_control_panel\\">
<ol class=\\"breadcrumb\\" role=\\"navigation\\">
<li class=\\"breadcrumb-item active\\">
Discuss
</li>
</ol>
<div class=\\"o_cp_searchview\\" role=\\"search\\">
<div class=\\"o_searchview\\" role=\\"search\\" aria-autocomplete=\\"list\\">
<span class=\\"o_searchview_more fa\\" title=\\"Advanced Search...\\" role=\\"img\\" aria-label=\\"Advanced Search...\\"></span>
</div>
</div>
<div class=\\"o_cp_left\\">
<div class=\\"o_cp_buttons\\" role=\\"toolbar\\" aria-label=\\"Control panel toolbar\\"></div>
<aside class=\\"o_cp_sidebar\\"></aside>
</div>
<div class=\\"o_cp_right\\">
<div class=\\"btn-group o_search_options\\" role=\\"search\\"></div>
<nav class=\\"o_cp_pager\\" role=\\"search\\" aria-label=\\"Pager\\"></nav>
<nav class=\\"btn-group o_cp_switch_buttons\\" role=\\"toolbar\\" aria-label=\\"View switcher\\"></nav>
</div>
</div>
<div class=\\"o_content\\">
<div class=\\"o_discuss_sidebar\\">
<span>DISCUSS!!</span>
</div>
<div class=\\"o_discuss_content\\">
<button>Reset first counter</button>
<button>Reset counter 2 in 3s</button>
<button>Toggle Clock/counters</button>
<button>Toggle Color</button>
<button>Rerender this widget</button>
<input>
<div>
<button>-</button>
<span style=\\"font-weight:bold\\">Value: 4</span>
<button>+</button>
</div>
<div>
<button>-</button>
<span style=\\"font-weight:bold\\">Value: 400</span>
<button>+</button>
</div>
<div>
<span>Current Color: </span>
red
</div>
<button>Add notif</button>
<button>Add sticky notif</button>
<button>Add warning</button>
</div>
</div>
</div></div>
<div class=\\"o_notification_container\\">
</div>
<div class=\\"o_loading d-none\\">Loading</div>
</div>"
`;
exports[`start with no action => clicks on client action => discuss is rendered 1`] = `"<div class=\\"o_web_client\\"><div class=\\"o_navbar\\"><a aria-label=\\"Applications\\" class=\\"o_title fa fa-th\\" href=\\"#\\" title=\\"Applications\\" accesskey=\\"h\\"></a><a class=\\"o_menu_brand\\" href=\\"\\" role=\\"button\\">Discuss</a><ul class=\\"o_menu_sections\\"></ul></div><div class=\\"o_content\\"><div class=\\"o_discuss o_action_controller\\"><div class=\\"o_control_panel\\"><ol class=\\"breadcrumb\\" role=\\"navigation\\"><li class=\\"breadcrumb-item active\\">Discuss</li></ol><div class=\\"o_cp_searchview\\" role=\\"search\\"><div class=\\"o_searchview\\" role=\\"search\\" aria-autocomplete=\\"list\\"><span class=\\"o_searchview_more fa\\" title=\\"Advanced Search...\\" role=\\"img\\" aria-label=\\"Advanced Search...\\"></span></div></div><div class=\\"o_cp_left\\"><div class=\\"o_cp_buttons\\" role=\\"toolbar\\" aria-label=\\"Control panel toolbar\\"></div><aside class=\\"o_cp_sidebar\\"></aside></div><div class=\\"o_cp_right\\"><div class=\\"btn-group o_search_options\\" role=\\"search\\"></div><nav class=\\"o_cp_pager\\" role=\\"search\\" aria-label=\\"Pager\\"></nav><nav class=\\"btn-group o_cp_switch_buttons\\" role=\\"toolbar\\" aria-label=\\"View switcher\\"></nav></div></div><div class=\\"o_content\\"><div class=\\"o_discuss_sidebar\\"><span>DISCUSS!!</span></div><div class=\\"o_discuss_content\\"><button>Reset first counter</button><button>Reset counter 2 in 3s</button><button>Toggle Clock/counters</button><button>Toggle Color</button><button>Rerender this widget</button><input><div><button>-</button><span style=\\"font-weight:bold\\">Value: 4</span><button>+</button></div><div><button>-</button><span style=\\"font-weight:bold\\">Value: 400</span><button>+</button></div><div><span>Current Color: </span>red</div><button>Add notif</button><button>Add sticky notif</button><button>Add warning</button></div></div></div></div><div class=\\"o_notification_container\\"></div><div class=\\"o_loading d-none\\">Loading</div></div>"`;
+1 -1
View File
@@ -1,5 +1,5 @@
module.exports = {
roots: ["<rootDir>/examples/web/static", "<rootDir>/tests"],
roots: ["<rootDir>/src", "<rootDir>/examples/web/static", "<rootDir>/tests"],
transform: {
"^.+\\.ts?$": "ts-jest"
},
+12 -8
View File
@@ -1,12 +1,16 @@
{
"name": "web-core",
"version": "0.3.0",
"description": "Core Utils for Odoo Web Client",
"name": "owl",
"version": "0.5.0",
"description": "Odoo Web Lab",
"main": "src/index.ts",
"scripts": {
"prebuild": "tsc --lib es2017,dom --target esnext --outDir dist/core src/*",
"build": "rollup -c",
"postbuild": "uglifyjs dist/core.js -o dist/core.min.js --compress --mangle",
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
"build:js:es5": "tsc --target es5 --module es6 --outDir dist/owl src/*",
"build:bundle": "rollup -c",
"build": "npm run build:js && npm run build:bundle",
"build:es5": "npm run build:js:es5 && npm run build:bundle",
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
"dev": "npm-run-all --parallel \"build:* -- --watch\"",
"test": "jest",
"test:watch": "jest --watch",
"example:web:build": "cpx \"examples/web/**/*.{html,js,css,ttf,woff,woff2}\" dist/examples/web && npm-run-all --parallel example:web:build:*",
@@ -17,12 +21,12 @@
"example:web:serve": "live-server --entry-file=index.html dist/examples/web/",
"preexample:web:dev": "npm run example:web:build",
"example:web:dev": "npm-run-all --parallel \"example:web:build:* -- --watch\" example:web:serve",
"example:benchmarks:build:core": "npm run build && cpx \"dist/core.js\" dist/examples/benchmarks",
"example:benchmarks:build:core": "npm run build && cpx \"dist/owl.js\" dist/examples/benchmarks",
"example:benchmarks:build:js": "cpx \"examples/benchmarks/{*.html,*.js,*.css}\" dist/examples/benchmarks",
"example:benchmarks:build": "npm-run-all --parallel example:benchmarks:build:*",
"example:benchmarks:serve": "live-server --entry-file=index.html dist/examples/benchmarks/",
"example:benchmarks:dev": "npm-run-all --parallel \"example:benchmarks:build:* -- --watch\" example:benchmarks:serve",
"example:todoapp:build:core": "npm run build && cpx \"dist/core.js\" dist/examples/todoapp",
"example:todoapp:build:core": "npm run build && cpx \"dist/owl.js\" dist/examples/todoapp",
"example:todoapp:build:js": "cpx \"examples/todoapp/{*.html,**/*.js,*.css,*.xml}\" dist/examples/todoapp",
"example:todoapp:build": "npm-run-all --parallel example:todoapp:build:*",
"example:todoapp:serve": "live-server --entry-file=index.html dist/examples/todoapp/",
+4 -4
View File
@@ -3,12 +3,12 @@ import git from "git-rev-sync";
// rollup.config.js
export default {
input: "dist/core/src/index.js",
input: "dist/owl/src/index.js",
output: {
file: "dist/core.js",
file: "dist/owl.js",
format: "iife",
name: "odoo",
name: "owl",
extend: true,
outro: `exports.core._version = '${version}';\nexports.core._date = '${new Date().toISOString()}';\nexports.core._hash = '${git.short()}';`
outro: `exports._version = '${version}';\nexports._date = '${new Date().toISOString()}';\nexports._hash = '${git.short()}';`
}
};
+49 -12
View File
@@ -48,7 +48,7 @@ export interface Type<T> extends Function {
export class Component<
T extends Env,
Props,
Props extends {},
State extends {}
> extends EventBus {
readonly __widget__: Meta<Env, Props>;
@@ -96,7 +96,7 @@ export class Component<
// Pro: if props is empty, we can create easily a widget
// Con: this is not really safe
// Pro: but creating widget (by a template) is always unsafe anyway
this.props = <Props>props;
this.props = <Props>props || <Props>{};
let id: number = getId();
let p: Component<T, any, any> | null = null;
if (parent instanceof Component) {
@@ -149,6 +149,16 @@ export class Component<
*/
mounted() {}
/**
* This hook is called whenever a component did actually update its props,
* state or env.
*
* This method is not called on the initial render. It is useful to interact
* with the DOM (for example, through an external library) whenever the
* component was updated.
*/
componentDidUpdate() {}
/**
* willUnmount is a hook that is called each time a component is detached from
* the DOM. This is a good place to remove some listeners, for example.
@@ -224,11 +234,11 @@ export class Component<
}
}
async render(): Promise<void> {
async render(force: boolean = false): Promise<void> {
if (this.__widget__.isDestroyed) {
return;
}
const renderVDom = this._render();
const renderVDom = this._render(force);
const renderId = this.__widget__.renderId;
const vnode = await renderVDom;
if (renderId === this.__widget__.renderId) {
@@ -280,15 +290,21 @@ export class Component<
this.env = Object.create(this.env);
}
Object.assign(this.env, nextEnv);
return this.render();
if (this.__widget__.isMounted) {
await this.render(true);
}
this.componentDidUpdate();
}
async updateProps(nextProps: Props): Promise<void> {
if (nextProps === this.__widget__.renderProps) {
async updateProps(
nextProps: Props,
forceUpdate: boolean = false
): Promise<void> {
if (nextProps === this.__widget__.renderProps && !forceUpdate) {
await this.__widget__.renderPromise;
return;
}
const shouldUpdate = this.shouldUpdate(nextProps);
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve();
}
@@ -307,8 +323,9 @@ export class Component<
}
Object.assign(this.state, nextState);
if (this.__widget__.isStarted) {
return this.render();
await this.render();
}
this.componentDidUpdate();
}
//--------------------------------------------------------------------------
@@ -317,7 +334,8 @@ export class Component<
async _updateProps(nextProps: Props): Promise<void> {
this.props = nextProps;
return this.render();
await this.render();
this.componentDidUpdate();
}
_patch(vnode) {
@@ -346,13 +364,14 @@ export class Component<
return this.__widget__.renderPromise;
}
async _render(): Promise<VNode> {
async _render(force: boolean = false): Promise<VNode> {
this.__widget__.renderId++;
const promises: Promise<void>[] = [];
const template = this.inlineTemplate || this.template;
let vnode = this.env.qweb.render(template, this, {
promises,
handlers: this.__widget__.boundHandlers
handlers: this.__widget__.boundHandlers,
forceUpdate: force
});
// this part is critical for the patching process to be done correctly. The
@@ -401,3 +420,21 @@ export class Component<
}
}
}
export class PureComponent<E extends Env, P, S> extends Component<E, P, S> {
shouldUpdate(nextProps: P): boolean {
for (let k in nextProps) {
if (nextProps[k] !== this.props[k]) {
return true;
}
}
return false;
}
async updateState(nextState: Partial<S>) {
for (let k in nextState) {
if (nextState[k] !== this.state[k]) {
return super.updateState(nextState);
}
}
}
}
+18 -4
View File
@@ -1,6 +1,20 @@
import { QWeb } from "./qweb";
import { Component, PureComponent } from "./component";
import { EventBus } from "./event_bus";
import { Component } from "./component";
import { Store, StoreMixin } from "./store";
import { QWeb } from "./qweb";
import { Registry } from "./registry";
import { connect, Store } from "./store";
import * as utils from "./utils";
export const core = { QWeb, EventBus, Component, Store, StoreMixin };
export const core = {
QWeb,
EventBus,
Component,
PureComponent,
utils
};
export const extras = {
Store,
connect,
Registry
};
+165 -122
View File
@@ -32,6 +32,9 @@ const DISABLED_TAGS = [
"optgroup"
];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
@@ -49,10 +52,13 @@ export class Context {
shouldDefineOwner: boolean = false;
shouldProtectContext: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
constructor() {
constructor(name?: string) {
this.rootContext = this;
this.addLine("let h = this.utils.h;");
this.templateName = name || "noname";
this.addLine("var h = this.utils.h;");
}
generateID(): number {
@@ -61,38 +67,18 @@ export class Context {
}
withParent(node: number): Context {
const newContext: Context = Object.create(this);
if (this === this.rootContext && this.parentNode) {
throw new Error("A template should not have more than one root node");
}
newContext.parentNode = node;
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
return newContext;
return this.subContext("parentNode", node);
}
withVariables(variables: { [key: string]: any }) {
subContext(key: keyof Context, value: any): Context {
const newContext = Object.create(this);
newContext.variables = Object.create(variables);
return newContext;
}
withInLoop(): Context {
const newContext = Object.create(this);
newContext.inLoop = true;
return newContext;
}
withCaller(node: Element): Context {
const newContext = Object.create(this);
newContext.caller = node;
return newContext;
}
withEscaping(): Context {
const newContext = Object.create(this);
newContext.escaping = true;
newContext[key] = value;
return newContext;
}
@@ -128,6 +114,65 @@ export class Context {
getValue(val: any): any {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
formatExpression(e: string): string {
e = e.trim();
if (e[0] === "{" && e[e.length - 1] === "}") {
const innerExpr = e
.slice(1, -1)
.split(",")
.map(p => {
let [key, val] = p.trim().split(":");
if (key === "") {
return "";
}
if (!val) {
val = key;
}
return `${key}: ${this.formatExpression(val)}`;
})
.join(",");
return "{" + innerExpr + "}";
}
// Thanks CHM for this code...
const chars = e.split("");
let instring = "";
let invar = "";
let invarPos = 0;
let r = "";
chars.push(" ");
for (var i = 0, ilen = chars.length; i < ilen; i++) {
var c = chars[i];
if (instring.length) {
if (c === instring && chars[i - 1] !== "\\") {
instring = "";
}
} else if (c === '"' || c === "'") {
instring = c;
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
invar = c;
invarPos = i;
continue;
} else if (c.match(/\W/) && invar.length) {
// TODO: Should check for possible spaces before dot
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
invar =
WORD_REPLACEMENT[invar] ||
(invar in this.variables && this.variables[invar]) ||
"context['" + invar + "']";
}
r += invar;
invar = "";
} else if (invar.length) {
invar += c;
continue;
}
r += c;
}
const result = r.slice(0, -1);
return result;
}
}
//------------------------------------------------------------------------------
@@ -137,10 +182,9 @@ export class Context {
export class QWeb {
processedTemplates: { [name: string]: ProcessedTemplate } = {};
templates: { [name: string]: CompiledTemplate<VNode> } = {};
exprCache: { [key: string]: string } = {};
directives: Directive[] = [];
constructor() {
constructor(data?: string) {
[
forEachDirective,
escDirective,
@@ -154,6 +198,10 @@ export class QWeb {
refDirective,
widgetDirective
].forEach(d => this.addDirective(d));
if (data) {
this.loadTemplates(data);
}
}
utils = {
@@ -162,6 +210,15 @@ export class QWeb {
const temp = document.createElement("template");
temp.innerHTML = str;
return temp.content;
},
objectToAttrString(obj: Object): string {
let classes: string[] = [];
for (let k in obj) {
if (obj[k]) {
classes.push(k);
}
}
return classes.join(" ");
}
};
@@ -249,6 +306,9 @@ export class QWeb {
loadTemplates(xmlstr: string) {
const parser = new DOMParser();
const doc = parser.parseFromString(xmlstr, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
throw new Error("Invalid XML in template");
}
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
return;
@@ -279,7 +339,7 @@ export class QWeb {
const mainNode = this.processedTemplates[name];
const isDebug = (<Element>mainNode).attributes.hasOwnProperty("t-debug");
const ctx = new Context();
const ctx = new Context(name);
this._compileNode(mainNode, ctx);
if (ctx.shouldProtectContext) {
@@ -306,7 +366,12 @@ export class QWeb {
ctx.code.join("\n")
) as CompiledTemplate<VNode>;
} catch (e) {
throw new Error(`Invalid template (or compiled code): ${e.message}`);
throw new Error(
`Invalid generated code while compiling template '${ctx.templateName.replace(
/`/g,
"'"
)}': ${e.message}`
);
}
if (isDebug) {
console.log(
@@ -327,13 +392,19 @@ export class QWeb {
if (!(node instanceof Element)) {
// this is a text node, there are no directive to apply
let text = node.textContent!;
if (!ctx.inPreTag) {
if (lineBreakRE.test(text) && !text.trim()) {
return;
}
text = text.replace(whitespaceRE, " ");
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
} else {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentNode = nodeID;
}
@@ -401,6 +472,9 @@ export class QWeb {
}
}
}
if (node.nodeName === "pre") {
ctx = ctx.subContext("inPreTag", true);
}
this._compileChildren(node, ctx);
@@ -455,9 +529,12 @@ export class QWeb {
const value = attributes[i].textContent!;
// regular attributes
if (!name.startsWith("t-")) {
if (
!name.startsWith("t-") &&
!(<Element>node).getAttribute("t-attf-" + name)
) {
const attID = ctx.generateID();
ctx.addLine(`let _${attID} = '${value}';`);
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
@@ -469,7 +546,13 @@ export class QWeb {
// dynamic attributes
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
let formattedValue = this._formatExpression(ctx.getValue(value!));
let formattedValue = ctx.formatExpression(ctx.getValue(value!));
if (
formattedValue[0] === "{" &&
formattedValue[formattedValue.length - 1] === "}"
) {
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
}
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
@@ -480,10 +563,14 @@ export class QWeb {
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att =>
att.startsWith(attName + ":")
);
attrs.splice(attrIndex, 1);
}
ctx.addLine(`let _${attID} = ${formattedValue};`);
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
@@ -496,17 +583,24 @@ export class QWeb {
}
const formattedExpr = value!.replace(
/\{\{.*?\}\}/g,
s => "${" + this._formatExpression(s.slice(2, -2)) + "}"
s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}"
);
const attID = ctx.generateID();
ctx.addLine(`let _${attID} = \`${formattedExpr}\`;`);
let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) {
ctx.addLine(
`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`
);
} else {
ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`);
}
attrs.push(`${attName}: _${attID}`);
}
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`let _${id} = ${this._formatExpression(value!)};`);
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
@@ -522,7 +616,7 @@ export class QWeb {
parts.push(`on:{}`);
}
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
ctx.addLine(`var c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
for (let id of tattrs) {
ctx.addIf(`_${id} instanceof Array`);
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
@@ -535,7 +629,7 @@ export class QWeb {
ctx.closeIf();
}
ctx.addLine(
`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
@@ -551,69 +645,6 @@ export class QWeb {
}
}
}
_formatExpression(e: string, ctx?: Context): string {
e = e.trim();
if (e in this.exprCache) {
return this.exprCache[e];
}
if (e[0] === "{" && e[e.length - 1] === "}") {
const innerExpr = e
.slice(1, -1)
.split(",")
.map(p => {
let [key, val] = p.trim().split(":");
if (key === "") {
return "";
}
if (!val) {
val = key;
}
return `${key}: ${this._formatExpression(val, ctx)}`;
})
.join(",");
return "{" + innerExpr + "}";
}
// Thanks CHM for this code...
const chars = e.split("");
let instring = "";
let invar = "";
let invarPos = 0;
let r = "";
chars.push(" ");
for (var i = 0, ilen = chars.length; i < ilen; i++) {
var c = chars[i];
if (instring.length) {
if (c === instring && chars[i - 1] !== "\\") {
instring = "";
}
} else if (c === '"' || c === "'") {
instring = c;
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
invar = c;
invarPos = i;
continue;
} else if (c.match(/\W/) && invar.length) {
// TODO: Should check for possible spaces before dot
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
invar =
WORD_REPLACEMENT[invar] ||
(ctx && invar in ctx.variables && ctx.variables[invar]) ||
"context['" + invar + "']";
}
r += invar;
invar = "";
} else if (invar.length) {
invar += c;
continue;
}
r += c;
}
const result = r.slice(0, -1);
this.exprCache[e] = result;
return result;
}
}
//------------------------------------------------------------------------------
@@ -647,7 +678,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
if (typeof value === "string") {
const exprID = ctx.generateID();
ctx.addLine(`let e${exprID} = ${qweb._formatExpression(value)};`);
ctx.addLine(`var e${exprID} = ${ctx.formatExpression(value)};`);
ctx.addIf(`e${exprID} || e${exprID} === 0`);
let text = `e${exprID}`;
@@ -658,14 +689,14 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
ctx.addLine(`c${ctx.parentNode}.push({text: ${text}});`);
} else {
let fragID = ctx.generateID();
ctx.addLine(`let frag${fragID} = this.utils.getFragment(e${exprID})`);
ctx.addLine(`var frag${fragID} = this.utils.getFragment(e${exprID})`);
let tempNodeID = ctx.generateID();
ctx.addLine(`let p${tempNodeID} = {hook: {`);
ctx.addLine(`var p${tempNodeID} = {hook: {`);
ctx.addLine(
` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`
);
ctx.addLine(`}};`);
ctx.addLine(`let vn${tempNodeID} = h('div', p${tempNodeID})`);
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
}
if (node.childNodes.length) {
@@ -691,7 +722,7 @@ const escDirective: Directive = {
ctx = ctx.withParent(nodeID);
}
let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.withEscaping());
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true;
}
};
@@ -730,7 +761,7 @@ const ifDirective: Directive = {
priority: 20,
atNodeEncounter({ node, qweb, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(`${qweb._formatExpression(cond)}`);
ctx.addIf(`${ctx.formatExpression(cond)}`);
return false;
},
finalize({ ctx }) {
@@ -743,7 +774,7 @@ const elifDirective: Directive = {
priority: 30,
atNodeEncounter({ node, qweb, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine(`else if (${qweb._formatExpression(cond)}) {`);
ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
ctx.indent();
return false;
},
@@ -784,7 +815,9 @@ const callDirective: Directive = {
const tempCtx = new Context();
qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
const subCtx = ctx.withCaller(nodeCopy).withVariables(vars);
const subCtx = ctx
.subContext("caller", nodeCopy)
.subContext("variables", Object.create(vars));
qweb._compileNode(nodeTemplate, subCtx);
return true;
@@ -796,11 +829,11 @@ const forEachDirective: Directive = {
priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true;
ctx = ctx.withInLoop();
ctx = ctx.subContext("inLoop", true);
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`let _${arrayID} = ${qweb._formatExpression(elems)};`);
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
);
@@ -809,11 +842,11 @@ const forEachDirective: Directive = {
);
let keysID = ctx.generateID();
ctx.addLine(
`let _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
);
let valuesID = ctx.generateID();
ctx.addLine(
`let _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
);
ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
ctx.indent();
@@ -843,15 +876,19 @@ const onDirective: Directive = {
extraArgs = args.slice(1, -1);
return "";
});
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)})()`;
if (extraArgs) {
ctx.addLine(
`p${nodeID}.on['${eventName}'] = context['${handler}'].bind(owner, ${qweb._formatExpression(
`p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs
)});`
);
} else {
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || context['${handler}'].bind(owner);`
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
);
ctx.addLine(
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
@@ -896,11 +933,11 @@ const widgetDirective: Directive = {
} else {
key = node.getAttribute("t-att-key");
if (key) {
key = qweb._formatExpression(key);
key = ctx.formatExpression(key);
}
}
if (props) {
props = qweb._formatExpression(props, ctx);
props = ctx.formatExpression(props);
}
let dummyID = ctx.generateID();
let defID = ctx.generateID();
@@ -922,13 +959,15 @@ const widgetDirective: Directive = {
ctx.addLine(
`let w${widgetID} = ${templateID} in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[${templateID}]] : false;`
);
ctx.addLine(`let props${widgetID} = ${props};`);
ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
ctx.addLine(`let isNew${widgetID} = !w${widgetID};`);
// check if we can reuse current rendering promise
ctx.addIf(`w${widgetID} && w${widgetID}.__widget__.renderPromise`);
ctx.addIf(`w${widgetID}.__widget__.isStarted`);
ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID});`);
ctx.addLine(
`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`
);
ctx.addElse();
ctx.addLine(`isNew${widgetID} = true`);
ctx.addIf(`props${widgetID} === w${widgetID}.__widget__.renderProps`);
@@ -942,7 +981,9 @@ const widgetDirective: Directive = {
ctx.addIf(`!def${defID}`);
ctx.addIf(`w${widgetID}`);
ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID});`);
ctx.addLine(
`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`
);
ctx.addElse();
ctx.addLine(
`w${widgetID} = new context.widgets['${value}'](owner, props${widgetID});`
@@ -968,6 +1009,8 @@ const widgetDirective: Directive = {
ctx.parentNode
}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w${widgetID}.${
keepAlive ? "detach" : "destroy"
}()},destroy(){w${widgetID}.${
keepAlive ? "detach" : "destroy"
}()}}; w${widgetID}.__widget__.pvnode = pvnode;});`
);
ctx.addElse();
+59 -7
View File
@@ -1,10 +1,44 @@
import { EventBus } from "./event_bus";
import { Component } from "./component";
import { shallowEqual } from "./utils";
export function StoreMixin(Component) {
return class extends Component {
mounted() {
this.env.store.on("update", this, this.render);
}
export function connect(mapStateToProps) {
return function(Comp) {
return class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const ownProps = Object.assign({}, props || {});
const storeProps = mapStateToProps(env.store.state, ownProps);
const mergedProps = Object.assign(props || {}, storeProps);
super(parent, mergedProps);
this.__widget__.currentStoreProps = storeProps;
this.__widget__.ownProps = ownProps;
}
mounted() {
this.env.store.on("update", this, () => {
const ownProps = this.__widget__.ownProps;
const storeProps = mapStateToProps(this.env.store.state, ownProps);
if (!shallowEqual(storeProps, this.__widget__.currentStoreProps)) {
this.__widget__.currentStoreProps = storeProps;
this.updateProps(ownProps, false);
}
});
super.mounted();
}
willUnmount() {
this.env.store.off("update", this);
super.willUnmount();
}
updateProps(nextProps, forceUpdate) {
this.__widget__.ownProps = nextProps;
const mergedProps = Object.assign(
{},
nextProps,
this.__widget__.currentStoreProps
);
return super.updateProps(mergedProps, forceUpdate);
}
};
};
}
@@ -13,17 +47,28 @@ interface StoreConfig {
actions?: any;
mutations?: any;
}
interface StoreOption {
debug?: boolean;
}
export class Store extends EventBus {
_state: any;
actions: any;
mutations: any;
_isMutating: boolean = false;
history: any[] = [];
debug: boolean;
constructor(config: StoreConfig = {}) {
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
this.debug = options.debug || false;
this._state = Object.assign({}, config.state);
this.actions = config.actions;
this.mutations = config.mutations;
if (this.debug) {
this.history.push({ state: this.state });
}
}
get state() {
@@ -50,10 +95,17 @@ export class Store extends EventBus {
this._isMutating = true;
this.mutations[type].call(null, this._state, payload);
if (this.debug) {
this.history.push({
state: this.state,
mutation: type,
payload: payload
});
}
await Promise.resolve();
if (this._isMutating) {
this._isMutating = false;
this.trigger("update");
this.trigger("update", this.state);
}
}
+64
View File
@@ -112,3 +112,67 @@ export function findInTree<T extends Tree<T>>(
}
return null;
}
export function shallowEqual(objA, objB) {
if (objA === objB) {
return true;
}
const keysA = Object.keys(objA);
for (let key of keysA) {
if (!(key in objB) || objA[key] !== objB[key]) {
return false;
}
}
return true;
}
export function patch(C: any, patchName: string, patch: any) {
const proto = C.prototype;
if (!proto.__patches) {
proto.__patches = {
origMethods: {},
patches: {},
current: []
};
}
if (proto.__patches.patches[patchName]) {
throw new Error(`Patch [${patchName}] already exists`);
}
proto.__patches.patches[patchName] = patch;
applyPatch(proto, patch);
proto.__patches.current.push(patchName);
function applyPatch(proto, patch) {
Object.keys(patch).forEach(function(methodName) {
const method = patch[methodName];
if (typeof method === "function") {
const original = proto[methodName];
if (!(methodName in proto.__patches.origMethods)) {
proto.__patches.origMethods[methodName] = original;
}
proto[methodName] = function(...args) {
this._super = original;
return method.call(this, ...args);
};
}
});
}
}
export function unpatch(C: any, patchName: string) {
const proto = C.prototype;
const patchInfo = proto.__patches;
delete proto.__patches;
// reset to original
for (let k in patchInfo.origMethods) {
proto[k] = patchInfo.origMethods[k];
}
// apply other patches
for (let name of patchInfo.current) {
if (name !== patchName) {
patch(C, name, patchInfo.patches[name]);
}
}
}
+62 -24
View File
@@ -5,16 +5,14 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
) {
let owner = context;
context = Object.create(context);
let h = this.utils.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
c1.push({text: \`
\`});
let _2 = context['state'].numbers;
var h = this.utils.h;
var c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['state'].numbers;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
let _3 = _2 instanceof Array ? _2 : Object.keys(_2);
let _4 = _2 instanceof Array ? _2 : Object.values(_2);
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
context.number_first = i === 0;
context.number_last = i === _3.length - 1;
@@ -22,19 +20,17 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
context.number_index = i;
context.number = _3[i];
context.number_value = _4[i];
c1.push({text: \`
\`});
//WIDGET
let key8 = context['number'];
let _5_index = c1.length;
c1.push(null);
let def6;
let w7 = key8 in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[key8]] : false;
let props7 = null;
let props7 = {};
let isNew7 = !w7;
if (w7 && w7.__widget__.renderPromise) {
if (w7.__widget__.isStarted) {
def6 = w7.updateProps(props7);
def6 = w7.updateProps(props7, extra.forceUpdate);
} else {
isNew7 = true
if (props7 === w7.__widget__.renderProps) {
@@ -47,7 +43,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
}
if (!def6) {
if (w7) {
def6 = w7.updateProps(props7);
def6 = w7.updateProps(props7, extra.forceUpdate);
} else {
w7 = new context.widgets['ChildWidget'](owner, props7);
context.__widget__.cmap[key8] = w7.__widget__.id;
@@ -55,16 +51,12 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
}
}
if (isNew7) {
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8});c1[_5_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w7._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w7.destroy()}}; w7.__widget__.pvnode = pvnode;});
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8});c1[_5_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w7._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w7.destroy()},destroy(){w7.destroy()}}; w7.__widget__.pvnode = pvnode;});
} else {
def6 = def6.then(()=>{if (w7.__widget__.isDestroyed) {return};let vnode;if (!w7.__widget__.vnode){vnode=w7.__widget__.pvnode} else { vnode=h(w7.__widget__.vnode.sel, {key: key8});vnode.elm=w7.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w7.el,a.elm);a.elm=w7.el;w7.__mount();},remove(){w7.destroy()}}}c1[_5_index]=vnode;});
}
extra.promises.push(def6);
c1.push({text: \`
\`});
}
c1.push({text: \`
\`});
return vn1;
}"
`;
@@ -73,9 +65,9 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
"function anonymous(context,extra
) {
let owner = context;
let h = this.utils.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let key5 = \\"somestring\\";
let _2_index = c1.length;
@@ -86,7 +78,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
let isNew4 = !w4;
if (w4 && w4.__widget__.renderPromise) {
if (w4.__widget__.isStarted) {
def3 = w4.updateProps(props4);
def3 = w4.updateProps(props4, extra.forceUpdate);
} else {
isNew4 = true
if (props4 === w4.__widget__.renderProps) {
@@ -99,7 +91,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
}
if (!def3) {
if (w4) {
def3 = w4.updateProps(props4);
def3 = w4.updateProps(props4, extra.forceUpdate);
} else {
w4 = new context.widgets['child'](owner, props4);
context.__widget__.cmap[key5] = w4.__widget__.id;
@@ -107,7 +99,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
}
}
if (isNew4) {
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w4.destroy()}}; w4.__widget__.pvnode = pvnode;});
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__widget__.pvnode = pvnode;});
} else {
def3 = def3.then(()=>{if (w4.__widget__.isDestroyed) {return};let vnode;if (!w4.__widget__.vnode){vnode=w4.__widget__.pvnode} else { vnode=h(w4.__widget__.vnode.sel, {key: key5});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}}}c1[_2_index]=vnode;});
}
@@ -115,3 +107,49 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
return vn1;
}"
`;
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
"function anonymous(context,extra
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[4]] : false;
let props4 = {};
let isNew4 = !w4;
if (w4 && w4.__widget__.renderPromise) {
if (w4.__widget__.isStarted) {
def3 = w4.updateProps(props4, extra.forceUpdate);
} else {
isNew4 = true
if (props4 === w4.__widget__.renderProps) {
def3 = w4.__widget__.renderPromise;
} else {
w4.destroy();
w4 = false
}
}
}
if (!def3) {
if (w4) {
def3 = w4.updateProps(props4, extra.forceUpdate);
} else {
w4 = new context.widgets['child'](owner, props4);
context.__widget__.cmap[4] = w4.__widget__.id;
def3 = w4._start();
}
}
if (isNew4) {
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__widget__.pvnode = pvnode;});
} else {
def3 = def3.then(()=>{if (w4.__widget__.isDestroyed) {return};let vnode;if (!w4.__widget__.vnode){vnode=w4.__widget__.pvnode} else { vnode=h(w4.__widget__.vnode.sel, {key: 4});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}}}c1[_2_index]=vnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`;
exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></div>"`;
+156
View File
@@ -70,6 +70,12 @@ class WidgetB extends Widget {
//------------------------------------------------------------------------------
describe("basic widget properties", () => {
test("props and state are properly defined", async () => {
const widget = new Widget(env);
expect(widget.props).toEqual({});
expect(widget.state).toEqual({});
});
test("has no el after creation", async () => {
const widget = new Widget(env);
expect(widget.el).toBe(null);
@@ -381,6 +387,61 @@ describe("lifecycle hooks", () => {
]);
});
test("componentDidUpdate hook is called after updateState", async () => {
let n = 0;
class TestWidget extends Widget {
state = { a: 1 };
componentDidUpdate() {
n++;
}
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(n).toBe(0);
await widget.updateState({}); // empty update, should do nothing
expect(n).toBe(0);
await widget.updateState({ a: 3 });
expect(n).toBe(1);
});
test("componentDidUpdate hook is called after updateProps", async () => {
let n = 0;
class TestWidget extends Widget {
componentDidUpdate() {
n++;
}
}
const widget = new TestWidget(env, { a: 1 });
await widget.mount(fixture);
expect(n).toBe(0);
await widget.updateProps({ a: 2 });
expect(n).toBe(1);
});
test("componentDidUpdate hook is called after updateEnv", async () => {
let n = 0;
class TestWidget extends Widget {
state = { a: 1 };
componentDidUpdate() {
n++;
}
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(n).toBe(0);
await widget.updateEnv({ isMobile: true });
expect(n).toBe(1);
});
test("shouldUpdate hook prevent rerendering", async () => {
let shouldUpdate = false;
class TestWidget extends Widget {
@@ -398,6 +459,45 @@ describe("lifecycle hooks", () => {
await widget.updateProps({ val: 666 });
expect(fixture.innerHTML).toBe("<div>666</div>");
});
test("sub widget (inside sub node): hooks are correctly called", async () => {
let destroyed = false;
let created = false;
let mounted = false;
class ParentWidget extends Widget {
inlineTemplate = `
<div>
<t t-if="state.flag">
<div><t t-widget="child"/></div>
</t>
</div>`;
widgets = { child: ChildWidget };
state = { flag: false };
}
class ChildWidget extends Widget {
constructor(parent, props) {
super(parent, props);
created = true;
}
mounted() {
mounted = true;
}
destroyed() {
destroyed = true;
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(created).toBe(false);
expect(mounted).toBe(false);
await widget.updateState({ flag: true });
expect(mounted).toBe(true);
expect(created).toBe(true);
expect(destroyed).toBe(false);
await widget.updateState({ flag: false });
expect(destroyed).toBe(true);
});
});
describe("destroy method", () => {
@@ -933,6 +1033,22 @@ describe("random stuff/miscellaneous", () => {
await widget.mount(fixture);
expect(env.qweb.templates.parent.toString()).toMatchSnapshot();
});
test("t-props should not be undefined (snapshotting)", async () => {
env.qweb.addTemplate("parent", `<div><t t-widget="child"/></div>`);
class Parent extends Widget {
inlineTemplate = "parent";
widgets = { child: Child };
}
class Child extends Widget {
inlineTemplate = `<span>abc</span>`;
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(env.qweb.templates.parent.toString()).toMatchSnapshot();
});
});
describe("async rendering", () => {
@@ -1096,6 +1212,29 @@ describe("updating environment", () => {
expect((<any>widget).env.somekey).toBe(4);
});
test("updating widget env does not render widget (if not mounted)", async () => {
let n = 0;
class TestWidget extends Widget {
_render() {
n++;
return super._render();
}
}
const widget = new TestWidget(env);
expect(n).toBe(0);
await widget.updateEnv(<any>{ somekey: 4 });
expect(n).toBe(0);
await widget.mount(fixture);
expect(n).toBe(1);
await widget.updateEnv(<any>{ somekey: 5 });
expect(n).toBe(2);
widget.detach();
expect(n).toBe(2);
await widget.updateEnv(<any>{ somekey: 5 });
expect(n).toBe(2);
});
test("updating child env does not modify parent env", async () => {
class ParentWidget extends Widget {
inlineTemplate = `<div><t t-widget="child"/></div>`;
@@ -1148,4 +1287,21 @@ describe("updating environment", () => {
await widget.updateEnv(<any>{ someKey: "rerendered" });
expect(fixture.innerHTML).toBe("<div>rerendered</div>");
});
test("updating env force rerendering children", async () => {
class Parent extends Widget {
inlineTemplate = `<div><t t-widget="Child"/></div>`;
widgets = { Child };
}
class Child extends Widget {
inlineTemplate = `<div><t t-esc="env.someKey"/></div>`;
}
(<any>env).someKey = "hey";
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
await widget.updateEnv(<any>{ someKey: "rerendered" });
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>rerendered</div></div>");
});
});
+96 -6
View File
@@ -101,6 +101,29 @@ describe("error handling", () => {
qweb.addTemplate("test", `<t></t>`);
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
});
test("loadTemplates throw if parser error", () => {
expect(() => {
qweb.loadTemplates("<templates><abc>></templates>");
}).toThrow("Invalid XML in template");
});
test("nice error when t-on-directive is evaluated with a missing handler", () => {
qweb.addTemplate("templatename", `<div t-on-click="somemethod"></div>`);
expect(() => qweb.render("templatename", {}, { handlers: [] })).toThrow(
"Missing handler 'somemethod' when evaluating template 'templatename'"
);
});
test("error when compiled code is invalid", () => {
qweb.addTemplate(
"templatename",
`<div t-att-hey="}/^function invalid{{>'"></div>`
);
expect(() => qweb.render("templatename")).toThrow(
"Invalid generated code while compiling template 'templatename': Unexpected token }"
);
});
});
describe("t-esc", () => {
@@ -395,6 +418,12 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="bar"></div>`);
});
test("t-attf-class should combine with class", () => {
qweb.addTemplate("test", `<div class="hello" t-attf-class="world"/>`);
const result = renderToString(qweb, "test");
expect(result).toBe(`<div class="hello world"></div>`);
});
test("format value", () => {
qweb.addTemplate("test", `<div t-attf-foo="b{{value}}r"/>`);
const result = renderToString(qweb, "test", { value: "a" });
@@ -410,14 +439,11 @@ describe("attributes", () => {
expect(result).toBe('<div><span class="def"></span></div>');
});
test.skip("from object variables set previously", () => {
// qweb is stupid and does not support this.
// To do that properly, we need to extend the formatExpression method to
// be able to deal with objects. I think that it is not easy:
// it should properly deal with stuff like {a:expr, b: {c: otherexpr}}
test("from object variables set previously", () => {
// Note: standard qweb does not allow this...
qweb.addTemplate(
"test",
`<div t-debug="1"><t t-set="o" t-value="{a:'b'}"/><span t-att-class="o.a"/></div>`
`<div><t t-set="o" t-value="{a:'b'}"/><span t-att-class="o.a"/></div>`
);
const result = renderToString(qweb, "test");
expect(result).toBe('<div><span class="b"></span></div>');
@@ -467,6 +493,15 @@ describe("attributes", () => {
const result = renderToString(qweb, "test", { value: "world" });
expect(result).toBe(`<div class="hello world"></div>`);
});
test("t-att-class with object", () => {
qweb.addTemplate(
"test",
`<div class="static" t-att-class="{a: b, c: d, e: f}"/>`
);
const result = renderToString(qweb, "test", { b: true, d: false, f: true });
expect(result).toBe(`<div class="static a e"></div>`);
});
});
describe("t-call (template calling", () => {
@@ -866,6 +901,16 @@ describe("t-ref", () => {
});
describe("loading templates", () => {
test("can initialize qweb with a string", () => {
const data = `
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div>
</templates>`;
const qweb = new QWeb(data);
expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>");
});
test("can load a few templates from a xml string", () => {
const data = `
<?xml version="1.0" encoding="UTF-8"?>
@@ -915,3 +960,48 @@ describe("special cases for some boolean html attributes/properties", () => {
renderToString(qweb, "test", { flag: true });
});
});
describe("whitespace handling", () => {
test("white space only text nodes are condensed into a single space", () => {
qweb.addTemplate("test", `<div> </div>`);
const result = renderToString(qweb, "test");
expect(result).toBe(`<div> </div>`);
});
test("consecutives whitespaces are condensed into a single space", () => {
qweb.addTemplate("test", `<div> abc </div>`);
const result = renderToString(qweb, "test");
expect(result).toBe(`<div> abc </div>`);
});
test("whitespace only text nodes with newlines are removed", () => {
qweb.addTemplate(
"test",
`<div>
<span>abc</span>
</div>`
);
const result = renderToString(qweb, "test");
expect(result).toBe(`<div><span>abc</span></div>`);
});
test("nothing is done in pre tags", () => {
qweb.addTemplate("test", `<pre> </pre>`);
const result = renderToString(qweb, "test");
expect(result).toBe(`<pre> </pre>`);
const pretagtext = `<pre>
some text
</pre>`;
qweb.addTemplate("test2", pretagtext);
const result2 = renderToString(qweb, "test2");
expect(result2).toBe(pretagtext);
const pretagwithonlywhitespace = `<pre>
</pre>`;
qweb.addTemplate("test3", pretagwithonlywhitespace);
const result3 = renderToString(qweb, "test3");
expect(result3).toBe(pretagwithonlywhitespace);
});
});
+196
View File
@@ -0,0 +1,196 @@
import { Store, connect } from "../src/store";
import { Component, Env } from "../src/component";
import {
nextMicroTick,
makeTestWEnv,
makeTestFixture,
nextTick
} from "./helpers";
describe("basic use", () => {
test("commit a mutation", () => {
const state = { n: 1 };
const mutations = {
inc(state, delta) {
state.n += delta;
}
};
const store = new Store({ state, mutations });
expect(store.state.n).toBe(1);
store.commit("inc", 14);
expect(store.state.n).toBe(15);
});
test("dispatch an action", () => {
const state = { n: 1 };
const mutations = {
inc(state, delta) {
state.n += delta;
}
};
const actions = {
inc({ commit }, delta) {
commit("inc", delta);
}
};
const store = new Store({ state, mutations, actions });
expect(store.state.n).toBe(1);
store.dispatch("inc", 14);
expect(store.state.n).toBe(15);
});
test("multiple commits trigger one update", async () => {
let updateCounter = 0;
const state = { n: 1 };
const mutations = {
inc(state, delta) {
state.n += delta;
}
};
const store = new Store({ state, mutations });
store.on("update", null, () => updateCounter++);
store.commit("inc", 14);
expect(updateCounter).toBe(0);
store.commit("inc", 50);
expect(updateCounter).toBe(0);
await nextMicroTick();
expect(updateCounter).toBe(1);
});
});
describe("connecting a component to store", () => {
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestWEnv();
});
afterEach(() => {
fixture.remove();
});
class App extends Component<any, any, any> {
inlineTemplate = `
<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="Todo" t-props="todo"/>
</t>
</div>`;
widgets = { Todo };
}
class Todo extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.msg"/></span>`;
}
test("connecting a component works", async () => {
const state = { todos: [] };
const mutations = {
addTodo(state, msg) {
state.todos.push({ msg });
}
};
const TodoApp = connect(s => s)(App);
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
store.commit("addTodo", "hello");
await nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
});
test("connected child components with custom hooks", async () => {
let steps: any = [];
class Child extends Component<any, any, any> {
inlineTemplate = `<div/>`;
mounted() {
steps.push("child:mounted");
}
willUnmount() {
steps.push("child:willUnmount");
}
destroyed() {
steps.push("child:destroyed");
}
}
const ConnectedChild = connect(s => s)(Child);
class Parent extends Component<any, any, any> {
inlineTemplate = `
<div>
<t t-if="state.child" t-widget="ConnectedChild"/>
</div>`;
widgets = { ConnectedChild };
constructor(env: Env) {
super(env);
this.state = { child: true };
}
}
const store = new Store({ state: {} });
(<any>env).store = store;
const parent = new Parent(env);
await parent.mount(fixture);
expect(steps).toEqual(["child:mounted"]);
await parent.updateState({ child: false });
expect(steps).toEqual([
"child:mounted",
"child:willUnmount",
"child:destroyed"
]);
});
test("connect receives ownprops as second argument", async () => {
const state = { todos: [{ id: 1, text: "jupiler" }] };
let nextId = 2;
const mutations = {
addTodo(state, text) {
state.todos.push({ text, id: nextId++ });
}
};
const store = new Store({ state, mutations });
class TodoItem extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.text"/></span>`;
}
const ConnectedTodo = connect((state, props) => {
const todo = state.todos.find(t => t.id === props.id);
return todo;
})(TodoItem);
class TodoList extends Component<any, any, any> {
inlineTemplate = `<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
</t>
</div>`;
widgets = { ConnectedTodo };
}
const ConnectedTodoList = connect(state => state)(TodoList);
(<any>env).store = store;
const app = new ConnectedTodoList(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
store.commit("addTodo", "hoegaarden");
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><span>jupiler</span><span>hoegaarden</span></div>"
);
});
});
+68 -1
View File
@@ -4,7 +4,10 @@ import {
idGenerator,
memoize,
debounce,
findInTree
findInTree,
shallowEqual,
patch,
unpatch
} from "../src/utils";
describe("escape", () => {
@@ -91,3 +94,67 @@ describe("findInTree", () => {
expect(match2).toBe(null);
});
});
describe("shallowEqual", () => {
test("simple comparisons", () => {
const obj1 = {};
expect(shallowEqual(obj1, obj1)).toBe(true);
expect(shallowEqual({}, {})).toBe(true);
expect(shallowEqual({ a: 1 }, {})).toBe(false);
expect(shallowEqual({ a: 1 }, { a: 1 })).toBe(true);
expect(shallowEqual({ a: 1 }, ["a"])).toBe(false);
});
});
describe("patch/unpatch", () => {
test("can monkey patch a class", () => {
class Test {
n = 1;
doSomething(): string {
return "hey";
}
}
patch(Test, "some_custo", {
doSomething(): string {
this.n = this.n + 1;
return this._super();
}
});
const t = new Test();
expect(t.n).toBe(1);
expect(t.doSomething()).toBe("hey");
expect(t.n).toBe(2);
});
test("cannot patch a class twice with same patch name", () => {
class Test {}
patch(Test, "some_custo", {});
expect(() => {
patch(Test, "some_custo", {});
}).toThrow();
});
test("can unpatch a class", () => {
class Test {
doSomething(): number {
return 1;
}
}
patch(Test, "some_custo", {
doSomething(): number {
return this._super() + 2;
}
});
const t = new Test();
expect(t.doSomething()).toBe(3);
unpatch(Test, "some_custo");
expect(t.doSomething()).toBe(1);
});
});
+1 -2
View File
@@ -3,11 +3,10 @@
"module": "commonjs",
"preserveConstEnums": true,
"noImplicitThis": true,
"lib": ["es2015", "dom"],
"removeComments": false,
"inlineSourceMap": true,
"declaration": true,
"target": "es6",
"target": "es5",
"outDir": "dist",
"alwaysStrict": true,
"noUnusedLocals": true,