mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5951cc98e | |||
| 68731a435b | |||
| 8e8d4a28c5 | |||
| 8c5919cbc9 | |||
| 1efa65ff04 | |||
| 7473d45700 | |||
| 80e2124cbf | |||
| e53287b52e | |||
| 2758269139 | |||
| 766d2f2301 | |||
| 597adb3981 | |||
| 6ea9ccf56f | |||
| a444e3dcac | |||
| a7d2edd0c9 | |||
| d81e5d8f4f | |||
| d48cb5aae4 | |||
| a236ae396a | |||
| add380f59b | |||
| ad9fb77458 | |||
| 4326d18961 | |||
| 88a3b6d25e | |||
| a34fc39100 | |||
| a8b3ec7726 | |||
| d799e3dd47 | |||
| b446d5d338 | |||
| 71c1469124 |
@@ -1,37 +1,34 @@
|
||||
**Warning!**
|
||||
# 🦉 Odoo Web Lab 🦉
|
||||
|
||||
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:
|
||||
|
||||
@@ -56,13 +53,78 @@ Note that the test scripts also run the example tests suites.
|
||||
|
||||
## 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="state.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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tutorial
|
||||
|
||||
todo...
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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,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
@@ -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.
|
||||
|
||||
@@ -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,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,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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&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&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&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&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&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&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&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&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&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&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&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&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
@@ -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"
|
||||
},
|
||||
|
||||
+8
-6
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"name": "web-core",
|
||||
"version": "0.3.0",
|
||||
"description": "Core Utils for Odoo Web Client",
|
||||
"name": "owl",
|
||||
"version": "0.4.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 --lib es2017,dom --target esnext --outDir dist/core src/*",
|
||||
"build:bundle": "rollup -c",
|
||||
"build": "npm run build:js && npm run build:bundle",
|
||||
"minify": "uglifyjs dist/core.js -o dist/core.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:*",
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ export default {
|
||||
output: {
|
||||
file: "dist/core.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()}';`
|
||||
}
|
||||
};
|
||||
|
||||
+34
-10
@@ -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) {
|
||||
@@ -224,11 +224,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 +280,20 @@ export class Component<
|
||||
this.env = Object.create(this.env);
|
||||
}
|
||||
Object.assign(this.env, nextEnv);
|
||||
return this.render();
|
||||
if (this.__widget__.isMounted) {
|
||||
return this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -346,13 +351,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 +407,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
@@ -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
|
||||
};
|
||||
|
||||
+115
-102
@@ -32,6 +32,9 @@ const DISABLED_TAGS = [
|
||||
"optgroup"
|
||||
];
|
||||
|
||||
const lineBreakRE = /[\r\n]/;
|
||||
const whitespaceRE = /\s+/g;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -49,6 +52,7 @@ export class Context {
|
||||
shouldDefineOwner: boolean = false;
|
||||
shouldProtectContext: boolean = false;
|
||||
inLoop: boolean = false;
|
||||
inPreTag: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.rootContext = this;
|
||||
@@ -61,38 +65,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 +112,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,7 +180,6 @@ export class Context {
|
||||
export class QWeb {
|
||||
processedTemplates: { [name: string]: ProcessedTemplate } = {};
|
||||
templates: { [name: string]: CompiledTemplate<VNode> } = {};
|
||||
exprCache: { [key: string]: string } = {};
|
||||
directives: Directive[] = [];
|
||||
|
||||
constructor() {
|
||||
@@ -162,6 +204,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(" ");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -327,6 +378,12 @@ 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 {
|
||||
@@ -401,6 +458,9 @@ export class QWeb {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.nodeName === "pre") {
|
||||
ctx = ctx.subContext("inPreTag", true);
|
||||
}
|
||||
|
||||
this._compileChildren(node, ctx);
|
||||
|
||||
@@ -469,7 +529,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
|
||||
@@ -482,6 +548,10 @@ export class QWeb {
|
||||
const attValueID = ctx.generateID();
|
||||
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
|
||||
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
||||
const attrIndex = attrs.findIndex(att =>
|
||||
att.startsWith(attName + ":")
|
||||
);
|
||||
attrs.splice(attrIndex, 1);
|
||||
}
|
||||
ctx.addLine(`let _${attID} = ${formattedValue};`);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
@@ -496,7 +566,7 @@ 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}\`;`);
|
||||
@@ -506,7 +576,7 @@ export class QWeb {
|
||||
// t-att= attributes
|
||||
if (name === "t-att") {
|
||||
let id = ctx.generateID();
|
||||
ctx.addLine(`let _${id} = ${this._formatExpression(value!)};`);
|
||||
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`);
|
||||
tattrs.push(id);
|
||||
}
|
||||
}
|
||||
@@ -551,69 +621,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 +654,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(`let e${exprID} = ${ctx.formatExpression(value)};`);
|
||||
ctx.addIf(`e${exprID} || e${exprID} === 0`);
|
||||
let text = `e${exprID}`;
|
||||
|
||||
@@ -691,7 +698,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 +737,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 +750,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 +791,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 +805,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(`let _${arrayID} = ${ctx.formatExpression(elems)};`);
|
||||
ctx.addLine(
|
||||
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
|
||||
);
|
||||
@@ -845,7 +854,7 @@ const onDirective: Directive = {
|
||||
});
|
||||
if (extraArgs) {
|
||||
ctx.addLine(
|
||||
`p${nodeID}.on['${eventName}'] = context['${handler}'].bind(owner, ${qweb._formatExpression(
|
||||
`p${nodeID}.on['${eventName}'] = context['${handler}'].bind(owner, ${ctx.formatExpression(
|
||||
extraArgs
|
||||
)});`
|
||||
);
|
||||
@@ -896,11 +905,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();
|
||||
@@ -928,7 +937,9 @@ const widgetDirective: Directive = {
|
||||
// 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 +953,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});`
|
||||
|
||||
+56
-7
@@ -1,10 +1,41 @@
|
||||
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 storeProps = mapStateToProps(env.store.state);
|
||||
props = Object.assign(props || {}, storeProps);
|
||||
super(parent, props);
|
||||
this.__widget__.currentStoreProps = storeProps;
|
||||
}
|
||||
mounted() {
|
||||
this.env.store.on("update", this, () => {
|
||||
const storeProps = mapStateToProps(this.env.store.state);
|
||||
if (!shallowEqual(storeProps, this.__widget__.currentStoreProps)) {
|
||||
this.__widget__.currentStoreProps = storeProps;
|
||||
// probably not optimal, will do 2 object.assign, one here and
|
||||
// one in updateProps.
|
||||
const nextProps = Object.assign(
|
||||
{},
|
||||
this.props,
|
||||
this.__widget__.currentStoreProps
|
||||
);
|
||||
this.updateProps(nextProps, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
willUnmount() {
|
||||
this.env.store.off("update", this);
|
||||
}
|
||||
updateProps(nextProps, forceUpdate) {
|
||||
nextProps = Object.assign(nextProps, this.__widget__.currentStoreProps);
|
||||
return super.updateProps(nextProps, forceUpdate);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,17 +44,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 +92,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = context['state'].numbers;
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
@@ -22,8 +20,6 @@ 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;
|
||||
@@ -34,7 +30,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
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;
|
||||
@@ -60,11 +56,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
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;
|
||||
}"
|
||||
`;
|
||||
@@ -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;
|
||||
|
||||
@@ -99,6 +99,20 @@ exports[`attributes format value 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes from object variables set previously 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
let _2 = {a:'b'}.a;
|
||||
let c3 = [], p3 = {key:3,attrs:{class: _2}};
|
||||
let vn3 = h('span', p3, c3);
|
||||
c1.push(vn3);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes from variables set previously 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
@@ -174,7 +188,20 @@ exports[`attributes t-att-class and class should combine together 1`] = `
|
||||
let _1 = 'hello';
|
||||
let _3 = context['value'];
|
||||
let _2 = 'hello' + (_3 ? ' ' + _3 : '');
|
||||
let c4 = [], p4 = {key:4,attrs:{class: _1,class: _2}};
|
||||
let c4 = [], p4 = {key:4,attrs:{class: _2}};
|
||||
let vn4 = h('div', p4, c4);
|
||||
return vn4;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes t-att-class with object 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let h = this.utils.h;
|
||||
let _1 = 'static';
|
||||
let _3 = this.utils.objectToAttrString({a: context['b'],c: context['d'],e: context['f']});
|
||||
let _2 = 'static' + (_3 ? ' ' + _3 : '');
|
||||
let c4 = [], p4 = {key:4,attrs:{class: _2}};
|
||||
let vn4 = h('div', p4, c4);
|
||||
return vn4;
|
||||
}"
|
||||
@@ -223,8 +250,6 @@ exports[`foreach does not pollute the rendering context 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = [1];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
@@ -242,8 +267,6 @@ exports[`foreach does not pollute the rendering context 1`] = `
|
||||
c1.push({text: e5});
|
||||
}
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -255,8 +278,6 @@ exports[`foreach iterate on items (on a element node) 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = [1, 2];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
@@ -277,8 +298,6 @@ exports[`foreach iterate on items (on a element node) 1`] = `
|
||||
c5.push({text: e6});
|
||||
}
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -290,8 +309,6 @@ exports[`foreach iterate on items 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = [3, 2, 1];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
@@ -304,8 +321,7 @@ exports[`foreach iterate on items 1`] = `
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
c1.push({text: \`
|
||||
[\`});
|
||||
c1.push({text: \` [\`});
|
||||
let e5 = context['item_index'];
|
||||
if (e5 || e5 === 0) {
|
||||
c1.push({text: e5});
|
||||
@@ -320,11 +336,8 @@ exports[`foreach iterate on items 1`] = `
|
||||
if (e7 || e7 === 0) {
|
||||
c1.push({text: e7});
|
||||
}
|
||||
c1.push({text: \`]
|
||||
\`});
|
||||
c1.push({text: \`] \`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -336,8 +349,6 @@ exports[`foreach iterate, dict param 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = context['value'];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
@@ -350,8 +361,7 @@ exports[`foreach iterate, dict param 1`] = `
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
c1.push({text: \`
|
||||
[\`});
|
||||
c1.push({text: \` [\`});
|
||||
let e5 = context['item_index'];
|
||||
if (e5 || e5 === 0) {
|
||||
c1.push({text: e5});
|
||||
@@ -371,11 +381,8 @@ exports[`foreach iterate, dict param 1`] = `
|
||||
if (e8 || e8 === 0) {
|
||||
c1.push({text: e8});
|
||||
}
|
||||
c1.push({text: \`]
|
||||
\`});
|
||||
c1.push({text: \`] \`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -399,8 +406,7 @@ exports[`foreach iterate, integer param 1`] = `
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
c1.push({text: \`
|
||||
[\`});
|
||||
c1.push({text: \` [\`});
|
||||
let e5 = context['item_index'];
|
||||
if (e5 || e5 === 0) {
|
||||
c1.push({text: e5});
|
||||
@@ -415,8 +421,7 @@ exports[`foreach iterate, integer param 1`] = `
|
||||
if (e7 || e7 === 0) {
|
||||
c1.push({text: e7});
|
||||
}
|
||||
c1.push({text: \`]
|
||||
\`});
|
||||
c1.push({text: \`] \`});
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
@@ -429,8 +434,6 @@ exports[`foreach iterate, position 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = 5;
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
@@ -443,8 +446,7 @@ exports[`foreach iterate, position 1`] = `
|
||||
context.elem_index = i;
|
||||
context.elem = _3[i];
|
||||
context.elem_value = _4[i];
|
||||
c1.push({text: \`
|
||||
-\`});
|
||||
c1.push({text: \` -\`});
|
||||
if (context['elem_first']) {
|
||||
c1.push({text: \` first\`});
|
||||
}
|
||||
@@ -456,11 +458,8 @@ exports[`foreach iterate, position 1`] = `
|
||||
if (e5 || e5 === 0) {
|
||||
c1.push({text: e5});
|
||||
}
|
||||
c1.push({text: \`)
|
||||
\`});
|
||||
c1.push({text: \`) \`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -490,8 +489,6 @@ exports[`misc global 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = [4,5,6];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
@@ -504,8 +501,6 @@ exports[`misc global 1`] = `
|
||||
context.value_index = i;
|
||||
context.value = _3[i];
|
||||
context.value_value = _4[i];
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let c5 = [], p5 = {key:5,on:{}};
|
||||
let vn5 = h('span', p5, c5);
|
||||
c1.push(vn5);
|
||||
@@ -513,14 +508,10 @@ exports[`misc global 1`] = `
|
||||
if (e6 || e6 === 0) {
|
||||
c5.push({text: e6});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _7 = 'agüero';
|
||||
let c8 = [], p8 = {key:8,attrs:{\\"falló\\": _7},on:{}};
|
||||
let vn8 = h('Año', p8, c8);
|
||||
c1.push(vn8);
|
||||
c8.push({text: \`
|
||||
\`});
|
||||
let c9 = [], p9 = {key:9,on:{}};
|
||||
let vn9 = h('span', p9, c9);
|
||||
c8.push(vn9);
|
||||
@@ -530,8 +521,6 @@ exports[`misc global 1`] = `
|
||||
} else {
|
||||
c9.push({text: \`foo default\`});
|
||||
}
|
||||
c8.push({text: \`
|
||||
\`});
|
||||
let c11 = [], p11 = {key:11,on:{}};
|
||||
let vn11 = h('span', p11, c11);
|
||||
c8.push(vn11);
|
||||
@@ -541,10 +530,6 @@ exports[`misc global 1`] = `
|
||||
} else {
|
||||
c11.push({text: \`foo default\`});
|
||||
}
|
||||
c8.push({text: \`
|
||||
\`});
|
||||
c8.push({text: \`
|
||||
\`});
|
||||
let c13 = [], p13 = {key:13,on:{}};
|
||||
let vn13 = h('span', p13, c13);
|
||||
c8.push(vn13);
|
||||
@@ -554,13 +539,7 @@ exports[`misc global 1`] = `
|
||||
} else {
|
||||
c13.push({text: \`foo default\`});
|
||||
}
|
||||
c8.push({text: \`
|
||||
\`});
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let c15 = [], p15 = {key:15,on:{}};
|
||||
let vn15 = h('div', p15, c15);
|
||||
c1.push(vn15);
|
||||
@@ -575,8 +554,6 @@ exports[`misc global 1`] = `
|
||||
} else {
|
||||
c15.push({text: \`toto default\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -599,52 +576,36 @@ exports[`special cases for some boolean html attributes/properties various boole
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = 'checkbox';
|
||||
let _3 = 'checked';
|
||||
let c4 = [], p4 = {key:4,attrs:{type: _2,checked: _3},props:{checked: _3}};
|
||||
let vn4 = h('input', p4, c4);
|
||||
c1.push(vn4);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _5 = 'checked';
|
||||
let c6 = [], p6 = {key:6,attrs:{checked: _5}};
|
||||
let vn6 = h('input', p6, c6);
|
||||
c1.push(vn6);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _7 = 'checked';
|
||||
let c8 = [], p8 = {key:8,attrs:{checked: _7}};
|
||||
let vn8 = h('div', p8, c8);
|
||||
c1.push(vn8);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _9 = 'selected';
|
||||
let c10 = [], p10 = {key:10,attrs:{selected: _9}};
|
||||
let vn10 = h('div', p10, c10);
|
||||
c1.push(vn10);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _11 = 'selected';
|
||||
let _12 = '1';
|
||||
let c13 = [], p13 = {key:13,attrs:{selected: _11,other: _12},props:{selected: _11}};
|
||||
let vn13 = h('option', p13, c13);
|
||||
c1.push(vn13);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _14 = 'readonly';
|
||||
let c15 = [], p15 = {key:15,attrs:{readonly: _14},props:{readonly: _14}};
|
||||
let vn15 = h('input', p15, c15);
|
||||
c1.push(vn15);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _16 = 'disabled';
|
||||
let c17 = [], p17 = {key:17,attrs:{disabled: _16},props:{disabled: _16}};
|
||||
let vn17 = h('button', p17, c17);
|
||||
c1.push(vn17);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -724,17 +685,11 @@ exports[`t-call (template calling scoped parameters 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
c1.push({text: \`ok\`});
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let e2 = context['foo'];
|
||||
if (e2 || e2 === 0) {
|
||||
c1.push({text: e2});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -888,28 +843,20 @@ exports[`t-if boolean value condition else 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let c2 = [], p2 = {key:2};
|
||||
let vn2 = h('span', p2, c2);
|
||||
c1.push(vn2);
|
||||
c2.push({text: \`begin\`});
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
if (context['condition']) {
|
||||
c1.push({text: \`ok\`});
|
||||
}
|
||||
else {
|
||||
c1.push({text: \`ok-else\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let c3 = [], p3 = {key:3};
|
||||
let vn3 = h('span', p3, c3);
|
||||
c1.push(vn3);
|
||||
c3.push({text: \`end\`});
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -983,48 +930,30 @@ exports[`t-if can use some boolean operators in expressions 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
if (context['cond1'] && context['cond2']) {
|
||||
c1.push({text: \`and\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
if (context['cond1'] && context['cond3']) {
|
||||
c1.push({text: \`nope\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
if (context['cond1'] || context['cond3']) {
|
||||
c1.push({text: \`or\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
if (context['cond3'] || context['cond4']) {
|
||||
c1.push({text: \`nope\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
if (context['m'] > 3) {
|
||||
c1.push({text: \`mgt\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
if (context['n'] > 3) {
|
||||
c1.push({text: \`ngt\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
if (context['m'] < 3) {
|
||||
c1.push({text: \`mlt\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
if (context['n'] < 3) {
|
||||
c1.push({text: \`nlt\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -1090,8 +1019,6 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('ul', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = ['someval'];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
@@ -1113,8 +1040,6 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
|
||||
p6.on['click'] = context['activate'].bind(owner, context['action']);
|
||||
c6.push({text: \`link\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -1360,3 +1285,76 @@ exports[`t-set value priority 1`] = `
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \` abc \`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`whitespace handling nothing is done in pre tags 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('pre', p1, c1);
|
||||
c1.push({text: \` \`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`whitespace handling nothing is done in pre tags 2`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('pre', p1, c1);
|
||||
c1.push({text: \`
|
||||
some text
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`whitespace handling nothing is done in pre tags 3`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('pre', p1, c1);
|
||||
c1.push({text: \`
|
||||
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`whitespace handling white space only text nodes are condensed into a single space 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \` \`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`whitespace handling whitespace only text nodes with newlines are removed 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2};
|
||||
let vn2 = h('span', p2, c2);
|
||||
c1.push(vn2);
|
||||
c2.push({text: \`abc\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -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>"`;
|
||||
@@ -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);
|
||||
@@ -1096,6 +1102,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 +1177,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>");
|
||||
});
|
||||
});
|
||||
|
||||
+57
-6
@@ -410,14 +410,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 +464,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", () => {
|
||||
@@ -915,3 +921,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
+68
-1
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user