mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a75e5ec19 | |||
| 3e8682b741 | |||
| fd26775a0f | |||
| a86d0b5b02 | |||
| 7ebea5a486 | |||
| 56be87f26e | |||
| 990a08dc04 | |||
| e7a4ad99de | |||
| d6e61c2203 | |||
| e6b0d06e3e | |||
| 5cd45b59af | |||
| 1e58c7b12b | |||
| afc89e6f66 | |||
| 04679a6d03 | |||
| cb2e76498b | |||
| 7ac4e63e51 | |||
| d804bcf2dd | |||
| 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,68 +1,120 @@
|
||||
**Warning!**
|
||||
<h1 align="center">🦉 Odoo Web Lab 🦉</h1>
|
||||
|
||||
This is currently a proof of concept, definitely not a production-ready codebase.
|
||||
We hope to use it in the Odoo web client soon.
|
||||
## Project Overview
|
||||
|
||||
---
|
||||
Odoo Web Lab (OWL) is a project to collect some useful, reusable, (hopefully)
|
||||
well designed building blocks for building web applications. However, since this is the basis for the Odoo web client, we will not hesitate
|
||||
to design the code here to better match the Odoo architecture/design principles.
|
||||
|
||||
# Core Utility for Odoo Web Client
|
||||
The most important element of this repository is certainly the component system.
|
||||
It is designed to be:
|
||||
|
||||
## Overview
|
||||
1. **declarative:** the user interface should be described in term of the state
|
||||
of the application, not as a sequence of imperative steps.
|
||||
|
||||
This repository contains some useful building block for building web applications.
|
||||
We made some efforts to make sure the code is reusable and generic. However, since this is the basis for the Odoo web client, we will not hesitate to design
|
||||
the code here to better match the Odoo architecture/design principles.
|
||||
2. **composable:** each widget can seamlessly be created in a parent widget by
|
||||
a simple directive in its template.
|
||||
|
||||
Currently, this repository contains:
|
||||
3. **asynchronous rendering:** the framework will transparently wait for each
|
||||
subwidgets to be ready before applying the rendering. It uses native promises
|
||||
under the hood.
|
||||
|
||||
- some utility functions/classes
|
||||
- an implementation/extension of the QWeb template engine that outputs a virtual
|
||||
dom (using the snabbdom library)
|
||||
- a Component class, which uses the QWeb engine as its underlying rendering
|
||||
mechanism. The component class is designed to be declarative, with
|
||||
asynchronous rendering.
|
||||
4. **uses QWeb as a template system:** the templates are described in XML
|
||||
and follow the QWeb specification. This is a requirement for Odoo.
|
||||
|
||||
In the future, this repository may includes other features. Here are some possible
|
||||
ideas:
|
||||
|
||||
- a (frontend) router could be included.
|
||||
- a store base class (as in the flux/redux architecture)
|
||||
5. **with an imperative escape hatch:** if necessary, sub widgets can easily be
|
||||
manually created/destroyed.
|
||||
|
||||
Note: the code is written in typescript. This does not mean that the main web
|
||||
client will ever be converted to typescript (even though I would really like it).
|
||||
|
||||
## Main scripts
|
||||
## Installing/Building
|
||||
|
||||
To install every dependency needed to play with this code:
|
||||
Some npm scripts are available:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
To build a bundle of this as a library:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
To run tests:
|
||||
|
||||
```
|
||||
npm run test
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
Note that the test scripts also run the example tests suites.
|
||||
| Command | Description |
|
||||
| ------------------ | --------------------------------------------------------- |
|
||||
| npm install | install every dependency required for this project |
|
||||
| npm run build | build a bundle of _owl_ in the _/dist/_ folder |
|
||||
| npm run build:es5 | build a bundle of _owl_ in the _/dist/_ folder (ES5 code) |
|
||||
| npm run minify | minify the prebuilt owl.js file |
|
||||
| npm run test | run all tests |
|
||||
| npm run test:watch | run all tests, and keep a watcher |
|
||||
|
||||
## Documentation
|
||||
|
||||
The complete documentation can be found [here](doc/readme.md). The most important sections are:
|
||||
|
||||
- [Quick Start](doc/quick_start.md)
|
||||
- [Tutorial](doc/tutorial.md)
|
||||
- [Component](doc/component.md)
|
||||
- [QWeb](doc/qweb.md)
|
||||
|
||||
# Examples
|
||||
|
||||
There are two examples of how to work with this web framework:
|
||||
Here is a minimal Hello World example:
|
||||
|
||||
- [Demo Application](examples/readme.md#demo)
|
||||
```javascript
|
||||
class HelloWorld extends owl.core.Component {
|
||||
inlineTemplate = `<div>Hello <t t-esc="props.name"/></div>`;
|
||||
}
|
||||
|
||||
const env = {
|
||||
qweb: new owl.core.QWeb()
|
||||
};
|
||||
|
||||
const hello = new HelloWorld(env, { name: "World" });
|
||||
hello.mount(document.body);
|
||||
```
|
||||
|
||||
The next example show how interactive widgets can be created and how widget
|
||||
composition works:
|
||||
|
||||
```javascript
|
||||
class Counter extends owl.core.Component {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.value"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>`;
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = {
|
||||
value: props.initialState || 0
|
||||
};
|
||||
}
|
||||
|
||||
increment(delta) {
|
||||
this.updateState({ value: this.state.value + delta });
|
||||
}
|
||||
}
|
||||
|
||||
class App extends owl.core.Component {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<t t-widget="Counter" t-props="{initialState: 1}"/>
|
||||
<t t-widget="Counter" t-props="{initialState: 42}"/>
|
||||
</div>`;
|
||||
|
||||
widgets = { Counter };
|
||||
}
|
||||
|
||||
const env = {
|
||||
qweb: new owl.core.QWeb()
|
||||
};
|
||||
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
More interesting examples on how to work with this web framework can be found in the _examples/_ folder:
|
||||
|
||||
- [Todo Application](examples/readme.md#todo-app)
|
||||
- [Web Client](examples/readme.md#web-client-example)
|
||||
- [Benchmarks](examples/readme.md#benchmarks)
|
||||
|
||||
## License
|
||||
|
||||
React is [GPL licensed](./LICENSE).
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<!-- Application JS/CSS -->
|
||||
<script src="/core.js"></script>
|
||||
<script src="/owl.js"></script>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<script type="module" src="/main.js"></script>
|
||||
|
||||
|
||||
@@ -2,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 };
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<!-- Application JS/CSS -->
|
||||
<script src="/core.js"></script>
|
||||
<script src="/owl.js"></script>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<script type="module" src="/main.js"></script>
|
||||
|
||||
|
||||
@@ -1,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"
|
||||
},
|
||||
|
||||
+12
-8
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"name": "web-core",
|
||||
"version": "0.3.0",
|
||||
"description": "Core Utils for Odoo Web Client",
|
||||
"name": "owl",
|
||||
"version": "0.5.0",
|
||||
"description": "Odoo Web Lab",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"prebuild": "tsc --lib es2017,dom --target esnext --outDir dist/core src/*",
|
||||
"build": "rollup -c",
|
||||
"postbuild": "uglifyjs dist/core.js -o dist/core.min.js --compress --mangle",
|
||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
|
||||
"build:js:es5": "tsc --target es5 --module es6 --outDir dist/owl src/*",
|
||||
"build:bundle": "rollup -c",
|
||||
"build": "npm run build:js && npm run build:bundle",
|
||||
"build:es5": "npm run build:js:es5 && npm run build:bundle",
|
||||
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
|
||||
"dev": "npm-run-all --parallel \"build:* -- --watch\"",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"example:web:build": "cpx \"examples/web/**/*.{html,js,css,ttf,woff,woff2}\" dist/examples/web && npm-run-all --parallel example:web:build:*",
|
||||
@@ -17,12 +21,12 @@
|
||||
"example:web:serve": "live-server --entry-file=index.html dist/examples/web/",
|
||||
"preexample:web:dev": "npm run example:web:build",
|
||||
"example:web:dev": "npm-run-all --parallel \"example:web:build:* -- --watch\" example:web:serve",
|
||||
"example:benchmarks:build:core": "npm run build && cpx \"dist/core.js\" dist/examples/benchmarks",
|
||||
"example:benchmarks:build:core": "npm run build && cpx \"dist/owl.js\" dist/examples/benchmarks",
|
||||
"example:benchmarks:build:js": "cpx \"examples/benchmarks/{*.html,*.js,*.css}\" dist/examples/benchmarks",
|
||||
"example:benchmarks:build": "npm-run-all --parallel example:benchmarks:build:*",
|
||||
"example:benchmarks:serve": "live-server --entry-file=index.html dist/examples/benchmarks/",
|
||||
"example:benchmarks:dev": "npm-run-all --parallel \"example:benchmarks:build:* -- --watch\" example:benchmarks:serve",
|
||||
"example:todoapp:build:core": "npm run build && cpx \"dist/core.js\" dist/examples/todoapp",
|
||||
"example:todoapp:build:core": "npm run build && cpx \"dist/owl.js\" dist/examples/todoapp",
|
||||
"example:todoapp:build:js": "cpx \"examples/todoapp/{*.html,**/*.js,*.css,*.xml}\" dist/examples/todoapp",
|
||||
"example:todoapp:build": "npm-run-all --parallel example:todoapp:build:*",
|
||||
"example:todoapp:serve": "live-server --entry-file=index.html dist/examples/todoapp/",
|
||||
|
||||
+4
-4
@@ -3,12 +3,12 @@ import git from "git-rev-sync";
|
||||
|
||||
// rollup.config.js
|
||||
export default {
|
||||
input: "dist/core/src/index.js",
|
||||
input: "dist/owl/src/index.js",
|
||||
output: {
|
||||
file: "dist/core.js",
|
||||
file: "dist/owl.js",
|
||||
format: "iife",
|
||||
name: "odoo",
|
||||
name: "owl",
|
||||
extend: true,
|
||||
outro: `exports.core._version = '${version}';\nexports.core._date = '${new Date().toISOString()}';\nexports.core._hash = '${git.short()}';`
|
||||
outro: `exports._version = '${version}';\nexports._date = '${new Date().toISOString()}';\nexports._hash = '${git.short()}';`
|
||||
}
|
||||
};
|
||||
|
||||
+49
-12
@@ -48,7 +48,7 @@ export interface Type<T> extends Function {
|
||||
|
||||
export class Component<
|
||||
T extends Env,
|
||||
Props,
|
||||
Props extends {},
|
||||
State extends {}
|
||||
> extends EventBus {
|
||||
readonly __widget__: Meta<Env, Props>;
|
||||
@@ -96,7 +96,7 @@ export class Component<
|
||||
// Pro: if props is empty, we can create easily a widget
|
||||
// Con: this is not really safe
|
||||
// Pro: but creating widget (by a template) is always unsafe anyway
|
||||
this.props = <Props>props;
|
||||
this.props = <Props>props || <Props>{};
|
||||
let id: number = getId();
|
||||
let p: Component<T, any, any> | null = null;
|
||||
if (parent instanceof Component) {
|
||||
@@ -149,6 +149,16 @@ export class Component<
|
||||
*/
|
||||
mounted() {}
|
||||
|
||||
/**
|
||||
* This hook is called whenever a component did actually update its props,
|
||||
* state or env.
|
||||
*
|
||||
* This method is not called on the initial render. It is useful to interact
|
||||
* with the DOM (for example, through an external library) whenever the
|
||||
* component was updated.
|
||||
*/
|
||||
componentDidUpdate() {}
|
||||
|
||||
/**
|
||||
* willUnmount is a hook that is called each time a component is detached from
|
||||
* the DOM. This is a good place to remove some listeners, for example.
|
||||
@@ -224,11 +234,11 @@ export class Component<
|
||||
}
|
||||
}
|
||||
|
||||
async render(): Promise<void> {
|
||||
async render(force: boolean = false): Promise<void> {
|
||||
if (this.__widget__.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
const renderVDom = this._render();
|
||||
const renderVDom = this._render(force);
|
||||
const renderId = this.__widget__.renderId;
|
||||
const vnode = await renderVDom;
|
||||
if (renderId === this.__widget__.renderId) {
|
||||
@@ -280,15 +290,21 @@ export class Component<
|
||||
this.env = Object.create(this.env);
|
||||
}
|
||||
Object.assign(this.env, nextEnv);
|
||||
return this.render();
|
||||
if (this.__widget__.isMounted) {
|
||||
await this.render(true);
|
||||
}
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
async updateProps(nextProps: Props): Promise<void> {
|
||||
if (nextProps === this.__widget__.renderProps) {
|
||||
async updateProps(
|
||||
nextProps: Props,
|
||||
forceUpdate: boolean = false
|
||||
): Promise<void> {
|
||||
if (nextProps === this.__widget__.renderProps && !forceUpdate) {
|
||||
await this.__widget__.renderPromise;
|
||||
return;
|
||||
}
|
||||
const shouldUpdate = this.shouldUpdate(nextProps);
|
||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||
return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -307,8 +323,9 @@ export class Component<
|
||||
}
|
||||
Object.assign(this.state, nextState);
|
||||
if (this.__widget__.isStarted) {
|
||||
return this.render();
|
||||
await this.render();
|
||||
}
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -317,7 +334,8 @@ export class Component<
|
||||
|
||||
async _updateProps(nextProps: Props): Promise<void> {
|
||||
this.props = nextProps;
|
||||
return this.render();
|
||||
await this.render();
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
_patch(vnode) {
|
||||
@@ -346,13 +364,14 @@ export class Component<
|
||||
return this.__widget__.renderPromise;
|
||||
}
|
||||
|
||||
async _render(): Promise<VNode> {
|
||||
async _render(force: boolean = false): Promise<VNode> {
|
||||
this.__widget__.renderId++;
|
||||
const promises: Promise<void>[] = [];
|
||||
const template = this.inlineTemplate || this.template;
|
||||
let vnode = this.env.qweb.render(template, this, {
|
||||
promises,
|
||||
handlers: this.__widget__.boundHandlers
|
||||
handlers: this.__widget__.boundHandlers,
|
||||
forceUpdate: force
|
||||
});
|
||||
|
||||
// this part is critical for the patching process to be done correctly. The
|
||||
@@ -401,3 +420,21 @@ export class Component<
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PureComponent<E extends Env, P, S> extends Component<E, P, S> {
|
||||
shouldUpdate(nextProps: P): boolean {
|
||||
for (let k in nextProps) {
|
||||
if (nextProps[k] !== this.props[k]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async updateState(nextState: Partial<S>) {
|
||||
for (let k in nextState) {
|
||||
if (nextState[k] !== this.state[k]) {
|
||||
return super.updateState(nextState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-4
@@ -1,6 +1,20 @@
|
||||
import { QWeb } from "./qweb";
|
||||
import { Component, PureComponent } from "./component";
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Component } from "./component";
|
||||
import { Store, StoreMixin } from "./store";
|
||||
import { QWeb } from "./qweb";
|
||||
import { Registry } from "./registry";
|
||||
import { connect, Store } from "./store";
|
||||
import * as utils from "./utils";
|
||||
|
||||
export const core = { QWeb, EventBus, Component, Store, StoreMixin };
|
||||
export const core = {
|
||||
QWeb,
|
||||
EventBus,
|
||||
Component,
|
||||
PureComponent,
|
||||
utils
|
||||
};
|
||||
|
||||
export const extras = {
|
||||
Store,
|
||||
connect,
|
||||
Registry
|
||||
};
|
||||
|
||||
+165
-122
@@ -32,6 +32,9 @@ const DISABLED_TAGS = [
|
||||
"optgroup"
|
||||
];
|
||||
|
||||
const lineBreakRE = /[\r\n]/;
|
||||
const whitespaceRE = /\s+/g;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -49,10 +52,13 @@ export class Context {
|
||||
shouldDefineOwner: boolean = false;
|
||||
shouldProtectContext: boolean = false;
|
||||
inLoop: boolean = false;
|
||||
inPreTag: boolean = false;
|
||||
templateName: string;
|
||||
|
||||
constructor() {
|
||||
constructor(name?: string) {
|
||||
this.rootContext = this;
|
||||
this.addLine("let h = this.utils.h;");
|
||||
this.templateName = name || "noname";
|
||||
this.addLine("var h = this.utils.h;");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
@@ -61,38 +67,18 @@ export class Context {
|
||||
}
|
||||
|
||||
withParent(node: number): Context {
|
||||
const newContext: Context = Object.create(this);
|
||||
if (this === this.rootContext && this.parentNode) {
|
||||
throw new Error("A template should not have more than one root node");
|
||||
}
|
||||
newContext.parentNode = node;
|
||||
if (!this.rootContext.rootNode) {
|
||||
this.rootContext.rootNode = node;
|
||||
}
|
||||
return newContext;
|
||||
return this.subContext("parentNode", node);
|
||||
}
|
||||
|
||||
withVariables(variables: { [key: string]: any }) {
|
||||
subContext(key: keyof Context, value: any): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext.variables = Object.create(variables);
|
||||
return newContext;
|
||||
}
|
||||
|
||||
withInLoop(): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext.inLoop = true;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
withCaller(node: Element): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext.caller = node;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
withEscaping(): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext.escaping = true;
|
||||
newContext[key] = value;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
@@ -128,6 +114,65 @@ export class Context {
|
||||
getValue(val: any): any {
|
||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||
}
|
||||
|
||||
formatExpression(e: string): string {
|
||||
e = e.trim();
|
||||
if (e[0] === "{" && e[e.length - 1] === "}") {
|
||||
const innerExpr = e
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map(p => {
|
||||
let [key, val] = p.trim().split(":");
|
||||
if (key === "") {
|
||||
return "";
|
||||
}
|
||||
if (!val) {
|
||||
val = key;
|
||||
}
|
||||
return `${key}: ${this.formatExpression(val)}`;
|
||||
})
|
||||
.join(",");
|
||||
return "{" + innerExpr + "}";
|
||||
}
|
||||
|
||||
// Thanks CHM for this code...
|
||||
const chars = e.split("");
|
||||
let instring = "";
|
||||
let invar = "";
|
||||
let invarPos = 0;
|
||||
let r = "";
|
||||
chars.push(" ");
|
||||
for (var i = 0, ilen = chars.length; i < ilen; i++) {
|
||||
var c = chars[i];
|
||||
if (instring.length) {
|
||||
if (c === instring && chars[i - 1] !== "\\") {
|
||||
instring = "";
|
||||
}
|
||||
} else if (c === '"' || c === "'") {
|
||||
instring = c;
|
||||
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
|
||||
invar = c;
|
||||
invarPos = i;
|
||||
continue;
|
||||
} else if (c.match(/\W/) && invar.length) {
|
||||
// TODO: Should check for possible spaces before dot
|
||||
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
|
||||
invar =
|
||||
WORD_REPLACEMENT[invar] ||
|
||||
(invar in this.variables && this.variables[invar]) ||
|
||||
"context['" + invar + "']";
|
||||
}
|
||||
r += invar;
|
||||
invar = "";
|
||||
} else if (invar.length) {
|
||||
invar += c;
|
||||
continue;
|
||||
}
|
||||
r += c;
|
||||
}
|
||||
const result = r.slice(0, -1);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -137,10 +182,9 @@ export class Context {
|
||||
export class QWeb {
|
||||
processedTemplates: { [name: string]: ProcessedTemplate } = {};
|
||||
templates: { [name: string]: CompiledTemplate<VNode> } = {};
|
||||
exprCache: { [key: string]: string } = {};
|
||||
directives: Directive[] = [];
|
||||
|
||||
constructor() {
|
||||
constructor(data?: string) {
|
||||
[
|
||||
forEachDirective,
|
||||
escDirective,
|
||||
@@ -154,6 +198,10 @@ export class QWeb {
|
||||
refDirective,
|
||||
widgetDirective
|
||||
].forEach(d => this.addDirective(d));
|
||||
|
||||
if (data) {
|
||||
this.loadTemplates(data);
|
||||
}
|
||||
}
|
||||
|
||||
utils = {
|
||||
@@ -162,6 +210,15 @@ export class QWeb {
|
||||
const temp = document.createElement("template");
|
||||
temp.innerHTML = str;
|
||||
return temp.content;
|
||||
},
|
||||
objectToAttrString(obj: Object): string {
|
||||
let classes: string[] = [];
|
||||
for (let k in obj) {
|
||||
if (obj[k]) {
|
||||
classes.push(k);
|
||||
}
|
||||
}
|
||||
return classes.join(" ");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -249,6 +306,9 @@ export class QWeb {
|
||||
loadTemplates(xmlstr: string) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xmlstr, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
throw new Error("Invalid XML in template");
|
||||
}
|
||||
const templates = doc.getElementsByTagName("templates")[0];
|
||||
if (!templates) {
|
||||
return;
|
||||
@@ -279,7 +339,7 @@ export class QWeb {
|
||||
|
||||
const mainNode = this.processedTemplates[name];
|
||||
const isDebug = (<Element>mainNode).attributes.hasOwnProperty("t-debug");
|
||||
const ctx = new Context();
|
||||
const ctx = new Context(name);
|
||||
this._compileNode(mainNode, ctx);
|
||||
|
||||
if (ctx.shouldProtectContext) {
|
||||
@@ -306,7 +366,12 @@ export class QWeb {
|
||||
ctx.code.join("\n")
|
||||
) as CompiledTemplate<VNode>;
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid template (or compiled code): ${e.message}`);
|
||||
throw new Error(
|
||||
`Invalid generated code while compiling template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}': ${e.message}`
|
||||
);
|
||||
}
|
||||
if (isDebug) {
|
||||
console.log(
|
||||
@@ -327,13 +392,19 @@ export class QWeb {
|
||||
if (!(node instanceof Element)) {
|
||||
// this is a text node, there are no directive to apply
|
||||
let text = node.textContent!;
|
||||
if (!ctx.inPreTag) {
|
||||
if (lineBreakRE.test(text) && !text.trim()) {
|
||||
return;
|
||||
}
|
||||
text = text.replace(whitespaceRE, " ");
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
||||
} else {
|
||||
// this is an unusual situation: this text node is the result of the
|
||||
// template rendering.
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentNode = nodeID;
|
||||
}
|
||||
@@ -401,6 +472,9 @@ export class QWeb {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.nodeName === "pre") {
|
||||
ctx = ctx.subContext("inPreTag", true);
|
||||
}
|
||||
|
||||
this._compileChildren(node, ctx);
|
||||
|
||||
@@ -455,9 +529,12 @@ export class QWeb {
|
||||
const value = attributes[i].textContent!;
|
||||
|
||||
// regular attributes
|
||||
if (!name.startsWith("t-")) {
|
||||
if (
|
||||
!name.startsWith("t-") &&
|
||||
!(<Element>node).getAttribute("t-attf-" + name)
|
||||
) {
|
||||
const attID = ctx.generateID();
|
||||
ctx.addLine(`let _${attID} = '${value}';`);
|
||||
ctx.addLine(`var _${attID} = '${value}';`);
|
||||
if (!name.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
name = '"' + name + '"';
|
||||
@@ -469,7 +546,13 @@ export class QWeb {
|
||||
// dynamic attributes
|
||||
if (name.startsWith("t-att-")) {
|
||||
let attName = name.slice(6);
|
||||
let formattedValue = this._formatExpression(ctx.getValue(value!));
|
||||
let formattedValue = ctx.formatExpression(ctx.getValue(value!));
|
||||
if (
|
||||
formattedValue[0] === "{" &&
|
||||
formattedValue[formattedValue.length - 1] === "}"
|
||||
) {
|
||||
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
|
||||
}
|
||||
const attID = ctx.generateID();
|
||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
@@ -480,10 +563,14 @@ export class QWeb {
|
||||
const attValue = (<Element>node).getAttribute(attName);
|
||||
if (attValue) {
|
||||
const attValueID = ctx.generateID();
|
||||
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
|
||||
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
|
||||
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
||||
const attrIndex = attrs.findIndex(att =>
|
||||
att.startsWith(attName + ":")
|
||||
);
|
||||
attrs.splice(attrIndex, 1);
|
||||
}
|
||||
ctx.addLine(`let _${attID} = ${formattedValue};`);
|
||||
ctx.addLine(`var _${attID} = ${formattedValue};`);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
handleBooleanProps(attName, attID);
|
||||
}
|
||||
@@ -496,17 +583,24 @@ export class QWeb {
|
||||
}
|
||||
const formattedExpr = value!.replace(
|
||||
/\{\{.*?\}\}/g,
|
||||
s => "${" + this._formatExpression(s.slice(2, -2)) + "}"
|
||||
s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}"
|
||||
);
|
||||
const attID = ctx.generateID();
|
||||
ctx.addLine(`let _${attID} = \`${formattedExpr}\`;`);
|
||||
let staticVal = (<Element>node).getAttribute(attName);
|
||||
if (staticVal) {
|
||||
ctx.addLine(
|
||||
`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`);
|
||||
}
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
}
|
||||
|
||||
// t-att= attributes
|
||||
if (name === "t-att") {
|
||||
let id = ctx.generateID();
|
||||
ctx.addLine(`let _${id} = ${this._formatExpression(value!)};`);
|
||||
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
|
||||
tattrs.push(id);
|
||||
}
|
||||
}
|
||||
@@ -522,7 +616,7 @@ export class QWeb {
|
||||
parts.push(`on:{}`);
|
||||
}
|
||||
|
||||
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
|
||||
ctx.addLine(`var c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
|
||||
for (let id of tattrs) {
|
||||
ctx.addIf(`_${id} instanceof Array`);
|
||||
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
|
||||
@@ -535,7 +629,7 @@ export class QWeb {
|
||||
ctx.closeIf();
|
||||
}
|
||||
ctx.addLine(
|
||||
`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
|
||||
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
|
||||
);
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
|
||||
@@ -551,69 +645,6 @@ export class QWeb {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_formatExpression(e: string, ctx?: Context): string {
|
||||
e = e.trim();
|
||||
if (e in this.exprCache) {
|
||||
return this.exprCache[e];
|
||||
}
|
||||
if (e[0] === "{" && e[e.length - 1] === "}") {
|
||||
const innerExpr = e
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map(p => {
|
||||
let [key, val] = p.trim().split(":");
|
||||
if (key === "") {
|
||||
return "";
|
||||
}
|
||||
if (!val) {
|
||||
val = key;
|
||||
}
|
||||
return `${key}: ${this._formatExpression(val, ctx)}`;
|
||||
})
|
||||
.join(",");
|
||||
return "{" + innerExpr + "}";
|
||||
}
|
||||
|
||||
// Thanks CHM for this code...
|
||||
const chars = e.split("");
|
||||
let instring = "";
|
||||
let invar = "";
|
||||
let invarPos = 0;
|
||||
let r = "";
|
||||
chars.push(" ");
|
||||
for (var i = 0, ilen = chars.length; i < ilen; i++) {
|
||||
var c = chars[i];
|
||||
if (instring.length) {
|
||||
if (c === instring && chars[i - 1] !== "\\") {
|
||||
instring = "";
|
||||
}
|
||||
} else if (c === '"' || c === "'") {
|
||||
instring = c;
|
||||
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
|
||||
invar = c;
|
||||
invarPos = i;
|
||||
continue;
|
||||
} else if (c.match(/\W/) && invar.length) {
|
||||
// TODO: Should check for possible spaces before dot
|
||||
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
|
||||
invar =
|
||||
WORD_REPLACEMENT[invar] ||
|
||||
(ctx && invar in ctx.variables && ctx.variables[invar]) ||
|
||||
"context['" + invar + "']";
|
||||
}
|
||||
r += invar;
|
||||
invar = "";
|
||||
} else if (invar.length) {
|
||||
invar += c;
|
||||
continue;
|
||||
}
|
||||
r += c;
|
||||
}
|
||||
const result = r.slice(0, -1);
|
||||
this.exprCache[e] = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -647,7 +678,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
|
||||
if (typeof value === "string") {
|
||||
const exprID = ctx.generateID();
|
||||
ctx.addLine(`let e${exprID} = ${qweb._formatExpression(value)};`);
|
||||
ctx.addLine(`var e${exprID} = ${ctx.formatExpression(value)};`);
|
||||
ctx.addIf(`e${exprID} || e${exprID} === 0`);
|
||||
let text = `e${exprID}`;
|
||||
|
||||
@@ -658,14 +689,14 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${text}});`);
|
||||
} else {
|
||||
let fragID = ctx.generateID();
|
||||
ctx.addLine(`let frag${fragID} = this.utils.getFragment(e${exprID})`);
|
||||
ctx.addLine(`var frag${fragID} = this.utils.getFragment(e${exprID})`);
|
||||
let tempNodeID = ctx.generateID();
|
||||
ctx.addLine(`let p${tempNodeID} = {hook: {`);
|
||||
ctx.addLine(`var p${tempNodeID} = {hook: {`);
|
||||
ctx.addLine(
|
||||
` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`
|
||||
);
|
||||
ctx.addLine(`}};`);
|
||||
ctx.addLine(`let vn${tempNodeID} = h('div', p${tempNodeID})`);
|
||||
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
|
||||
}
|
||||
if (node.childNodes.length) {
|
||||
@@ -691,7 +722,7 @@ const escDirective: Directive = {
|
||||
ctx = ctx.withParent(nodeID);
|
||||
}
|
||||
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
||||
compileValueNode(value, node, qweb, ctx.withEscaping());
|
||||
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -730,7 +761,7 @@ const ifDirective: Directive = {
|
||||
priority: 20,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-if")!);
|
||||
ctx.addIf(`${qweb._formatExpression(cond)}`);
|
||||
ctx.addIf(`${ctx.formatExpression(cond)}`);
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
@@ -743,7 +774,7 @@ const elifDirective: Directive = {
|
||||
priority: 30,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-elif")!);
|
||||
ctx.addLine(`else if (${qweb._formatExpression(cond)}) {`);
|
||||
ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
@@ -784,7 +815,9 @@ const callDirective: Directive = {
|
||||
const tempCtx = new Context();
|
||||
qweb._compileNode(nodeCopy, tempCtx);
|
||||
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
||||
const subCtx = ctx.withCaller(nodeCopy).withVariables(vars);
|
||||
const subCtx = ctx
|
||||
.subContext("caller", nodeCopy)
|
||||
.subContext("variables", Object.create(vars));
|
||||
|
||||
qweb._compileNode(nodeTemplate, subCtx);
|
||||
return true;
|
||||
@@ -796,11 +829,11 @@ const forEachDirective: Directive = {
|
||||
priority: 10,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
ctx.rootContext.shouldProtectContext = true;
|
||||
ctx = ctx.withInLoop();
|
||||
ctx = ctx.subContext("inLoop", true);
|
||||
const elems = node.getAttribute("t-foreach")!;
|
||||
const name = node.getAttribute("t-as")!;
|
||||
let arrayID = ctx.generateID();
|
||||
ctx.addLine(`let _${arrayID} = ${qweb._formatExpression(elems)};`);
|
||||
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
|
||||
ctx.addLine(
|
||||
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
|
||||
);
|
||||
@@ -809,11 +842,11 @@ const forEachDirective: Directive = {
|
||||
);
|
||||
let keysID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`let _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
|
||||
`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
|
||||
);
|
||||
let valuesID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`let _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
|
||||
`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
|
||||
);
|
||||
ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
|
||||
ctx.indent();
|
||||
@@ -843,15 +876,19 @@ const onDirective: Directive = {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)})()`;
|
||||
if (extraArgs) {
|
||||
ctx.addLine(
|
||||
`p${nodeID}.on['${eventName}'] = context['${handler}'].bind(owner, ${qweb._formatExpression(
|
||||
`p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
||||
extraArgs
|
||||
)});`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || context['${handler}'].bind(owner);`
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
||||
);
|
||||
ctx.addLine(
|
||||
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
|
||||
@@ -896,11 +933,11 @@ const widgetDirective: Directive = {
|
||||
} else {
|
||||
key = node.getAttribute("t-att-key");
|
||||
if (key) {
|
||||
key = qweb._formatExpression(key);
|
||||
key = ctx.formatExpression(key);
|
||||
}
|
||||
}
|
||||
if (props) {
|
||||
props = qweb._formatExpression(props, ctx);
|
||||
props = ctx.formatExpression(props);
|
||||
}
|
||||
let dummyID = ctx.generateID();
|
||||
let defID = ctx.generateID();
|
||||
@@ -922,13 +959,15 @@ const widgetDirective: Directive = {
|
||||
ctx.addLine(
|
||||
`let w${widgetID} = ${templateID} in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[${templateID}]] : false;`
|
||||
);
|
||||
ctx.addLine(`let props${widgetID} = ${props};`);
|
||||
ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
|
||||
ctx.addLine(`let isNew${widgetID} = !w${widgetID};`);
|
||||
|
||||
// check if we can reuse current rendering promise
|
||||
ctx.addIf(`w${widgetID} && w${widgetID}.__widget__.renderPromise`);
|
||||
ctx.addIf(`w${widgetID}.__widget__.isStarted`);
|
||||
ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID});`);
|
||||
ctx.addLine(
|
||||
`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`
|
||||
);
|
||||
ctx.addElse();
|
||||
ctx.addLine(`isNew${widgetID} = true`);
|
||||
ctx.addIf(`props${widgetID} === w${widgetID}.__widget__.renderProps`);
|
||||
@@ -942,7 +981,9 @@ const widgetDirective: Directive = {
|
||||
|
||||
ctx.addIf(`!def${defID}`);
|
||||
ctx.addIf(`w${widgetID}`);
|
||||
ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID});`);
|
||||
ctx.addLine(
|
||||
`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`
|
||||
);
|
||||
ctx.addElse();
|
||||
ctx.addLine(
|
||||
`w${widgetID} = new context.widgets['${value}'](owner, props${widgetID});`
|
||||
@@ -968,6 +1009,8 @@ const widgetDirective: Directive = {
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w${widgetID}.${
|
||||
keepAlive ? "detach" : "destroy"
|
||||
}()},destroy(){w${widgetID}.${
|
||||
keepAlive ? "detach" : "destroy"
|
||||
}()}}; w${widgetID}.__widget__.pvnode = pvnode;});`
|
||||
);
|
||||
ctx.addElse();
|
||||
|
||||
+59
-7
@@ -1,10 +1,44 @@
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Component } from "./component";
|
||||
import { shallowEqual } from "./utils";
|
||||
|
||||
export function StoreMixin(Component) {
|
||||
return class extends Component {
|
||||
mounted() {
|
||||
this.env.store.on("update", this, this.render);
|
||||
}
|
||||
export function connect(mapStateToProps) {
|
||||
return function(Comp) {
|
||||
return class extends Comp {
|
||||
constructor(parent, props?: any) {
|
||||
const env = parent instanceof Component ? parent.env : parent;
|
||||
const ownProps = Object.assign({}, props || {});
|
||||
const storeProps = mapStateToProps(env.store.state, ownProps);
|
||||
const mergedProps = Object.assign(props || {}, storeProps);
|
||||
super(parent, mergedProps);
|
||||
this.__widget__.currentStoreProps = storeProps;
|
||||
this.__widget__.ownProps = ownProps;
|
||||
}
|
||||
mounted() {
|
||||
this.env.store.on("update", this, () => {
|
||||
const ownProps = this.__widget__.ownProps;
|
||||
const storeProps = mapStateToProps(this.env.store.state, ownProps);
|
||||
if (!shallowEqual(storeProps, this.__widget__.currentStoreProps)) {
|
||||
this.__widget__.currentStoreProps = storeProps;
|
||||
this.updateProps(ownProps, false);
|
||||
}
|
||||
});
|
||||
super.mounted();
|
||||
}
|
||||
willUnmount() {
|
||||
this.env.store.off("update", this);
|
||||
super.willUnmount();
|
||||
}
|
||||
updateProps(nextProps, forceUpdate) {
|
||||
this.__widget__.ownProps = nextProps;
|
||||
const mergedProps = Object.assign(
|
||||
{},
|
||||
nextProps,
|
||||
this.__widget__.currentStoreProps
|
||||
);
|
||||
return super.updateProps(mergedProps, forceUpdate);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,17 +47,28 @@ interface StoreConfig {
|
||||
actions?: any;
|
||||
mutations?: any;
|
||||
}
|
||||
|
||||
interface StoreOption {
|
||||
debug?: boolean;
|
||||
}
|
||||
export class Store extends EventBus {
|
||||
_state: any;
|
||||
actions: any;
|
||||
mutations: any;
|
||||
_isMutating: boolean = false;
|
||||
history: any[] = [];
|
||||
debug: boolean;
|
||||
|
||||
constructor(config: StoreConfig = {}) {
|
||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
||||
super();
|
||||
this.debug = options.debug || false;
|
||||
this._state = Object.assign({}, config.state);
|
||||
this.actions = config.actions;
|
||||
this.mutations = config.mutations;
|
||||
|
||||
if (this.debug) {
|
||||
this.history.push({ state: this.state });
|
||||
}
|
||||
}
|
||||
|
||||
get state() {
|
||||
@@ -50,10 +95,17 @@ export class Store extends EventBus {
|
||||
this._isMutating = true;
|
||||
|
||||
this.mutations[type].call(null, this._state, payload);
|
||||
if (this.debug) {
|
||||
this.history.push({
|
||||
state: this.state,
|
||||
mutation: type,
|
||||
payload: payload
|
||||
});
|
||||
}
|
||||
await Promise.resolve();
|
||||
if (this._isMutating) {
|
||||
this._isMutating = false;
|
||||
this.trigger("update");
|
||||
this.trigger("update", this.state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,14 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
) {
|
||||
let owner = context;
|
||||
context = Object.create(context);
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
let _2 = context['state'].numbers;
|
||||
var h = this.utils.h;
|
||||
var c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = context['state'].numbers;
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
let _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
let _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
context.number_first = i === 0;
|
||||
context.number_last = i === _3.length - 1;
|
||||
@@ -22,19 +20,17 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
context.number_index = i;
|
||||
context.number = _3[i];
|
||||
context.number_value = _4[i];
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
//WIDGET
|
||||
let key8 = context['number'];
|
||||
let _5_index = c1.length;
|
||||
c1.push(null);
|
||||
let def6;
|
||||
let w7 = key8 in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[key8]] : false;
|
||||
let props7 = null;
|
||||
let props7 = {};
|
||||
let isNew7 = !w7;
|
||||
if (w7 && w7.__widget__.renderPromise) {
|
||||
if (w7.__widget__.isStarted) {
|
||||
def6 = w7.updateProps(props7);
|
||||
def6 = w7.updateProps(props7, extra.forceUpdate);
|
||||
} else {
|
||||
isNew7 = true
|
||||
if (props7 === w7.__widget__.renderProps) {
|
||||
@@ -47,7 +43,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
}
|
||||
if (!def6) {
|
||||
if (w7) {
|
||||
def6 = w7.updateProps(props7);
|
||||
def6 = w7.updateProps(props7, extra.forceUpdate);
|
||||
} else {
|
||||
w7 = new context.widgets['ChildWidget'](owner, props7);
|
||||
context.__widget__.cmap[key8] = w7.__widget__.id;
|
||||
@@ -55,16 +51,12 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
}
|
||||
}
|
||||
if (isNew7) {
|
||||
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8});c1[_5_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w7._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w7.destroy()}}; w7.__widget__.pvnode = pvnode;});
|
||||
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8});c1[_5_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w7._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w7.destroy()},destroy(){w7.destroy()}}; w7.__widget__.pvnode = pvnode;});
|
||||
} else {
|
||||
def6 = def6.then(()=>{if (w7.__widget__.isDestroyed) {return};let vnode;if (!w7.__widget__.vnode){vnode=w7.__widget__.pvnode} else { vnode=h(w7.__widget__.vnode.sel, {key: key8});vnode.elm=w7.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w7.el,a.elm);a.elm=w7.el;w7.__mount();},remove(){w7.destroy()}}}c1[_5_index]=vnode;});
|
||||
}
|
||||
extra.promises.push(def6);
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
}
|
||||
c1.push({text: \`
|
||||
\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -73,9 +65,9 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
var h = this.utils.h;
|
||||
var c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let key5 = \\"somestring\\";
|
||||
let _2_index = c1.length;
|
||||
@@ -86,7 +78,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
let isNew4 = !w4;
|
||||
if (w4 && w4.__widget__.renderPromise) {
|
||||
if (w4.__widget__.isStarted) {
|
||||
def3 = w4.updateProps(props4);
|
||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
||||
} else {
|
||||
isNew4 = true
|
||||
if (props4 === w4.__widget__.renderProps) {
|
||||
@@ -99,7 +91,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
}
|
||||
if (!def3) {
|
||||
if (w4) {
|
||||
def3 = w4.updateProps(props4);
|
||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
||||
} else {
|
||||
w4 = new context.widgets['child'](owner, props4);
|
||||
context.__widget__.cmap[key5] = w4.__widget__.id;
|
||||
@@ -107,7 +99,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
}
|
||||
}
|
||||
if (isNew4) {
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w4.destroy()}}; w4.__widget__.pvnode = pvnode;});
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__widget__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = def3.then(()=>{if (w4.__widget__.isDestroyed) {return};let vnode;if (!w4.__widget__.vnode){vnode=w4.__widget__.pvnode} else { vnode=h(w4.__widget__.vnode.sel, {key: key5});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}}}c1[_2_index]=vnode;});
|
||||
}
|
||||
@@ -115,3 +107,49 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
var c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
let isNew4 = !w4;
|
||||
if (w4 && w4.__widget__.renderPromise) {
|
||||
if (w4.__widget__.isStarted) {
|
||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
||||
} else {
|
||||
isNew4 = true
|
||||
if (props4 === w4.__widget__.renderProps) {
|
||||
def3 = w4.__widget__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!def3) {
|
||||
if (w4) {
|
||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
||||
} else {
|
||||
w4 = new context.widgets['child'](owner, props4);
|
||||
context.__widget__.cmap[4] = w4.__widget__.id;
|
||||
def3 = w4._start();
|
||||
}
|
||||
}
|
||||
if (isNew4) {
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__widget__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = def3.then(()=>{if (w4.__widget__.isDestroyed) {return};let vnode;if (!w4.__widget__.vnode){vnode=w4.__widget__.pvnode} else { vnode=h(w4.__widget__.vnode.sel, {key: 4});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}}}c1[_2_index]=vnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
@@ -381,6 +387,61 @@ describe("lifecycle hooks", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("componentDidUpdate hook is called after updateState", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
state = { a: 1 };
|
||||
|
||||
componentDidUpdate() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(n).toBe(0);
|
||||
|
||||
await widget.updateState({}); // empty update, should do nothing
|
||||
expect(n).toBe(0);
|
||||
|
||||
await widget.updateState({ a: 3 });
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("componentDidUpdate hook is called after updateProps", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
componentDidUpdate() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
const widget = new TestWidget(env, { a: 1 });
|
||||
await widget.mount(fixture);
|
||||
expect(n).toBe(0);
|
||||
|
||||
await widget.updateProps({ a: 2 });
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("componentDidUpdate hook is called after updateEnv", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
state = { a: 1 };
|
||||
|
||||
componentDidUpdate() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(n).toBe(0);
|
||||
|
||||
await widget.updateEnv({ isMobile: true });
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("shouldUpdate hook prevent rerendering", async () => {
|
||||
let shouldUpdate = false;
|
||||
class TestWidget extends Widget {
|
||||
@@ -398,6 +459,45 @@ describe("lifecycle hooks", () => {
|
||||
await widget.updateProps({ val: 666 });
|
||||
expect(fixture.innerHTML).toBe("<div>666</div>");
|
||||
});
|
||||
|
||||
test("sub widget (inside sub node): hooks are correctly called", async () => {
|
||||
let destroyed = false;
|
||||
let created = false;
|
||||
let mounted = false;
|
||||
class ParentWidget extends Widget {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<t t-if="state.flag">
|
||||
<div><t t-widget="child"/></div>
|
||||
</t>
|
||||
</div>`;
|
||||
widgets = { child: ChildWidget };
|
||||
state = { flag: false };
|
||||
}
|
||||
|
||||
class ChildWidget extends Widget {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
created = true;
|
||||
}
|
||||
mounted() {
|
||||
mounted = true;
|
||||
}
|
||||
destroyed() {
|
||||
destroyed = true;
|
||||
}
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(created).toBe(false);
|
||||
expect(mounted).toBe(false);
|
||||
await widget.updateState({ flag: true });
|
||||
expect(mounted).toBe(true);
|
||||
expect(created).toBe(true);
|
||||
expect(destroyed).toBe(false);
|
||||
await widget.updateState({ flag: false });
|
||||
expect(destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("destroy method", () => {
|
||||
@@ -933,6 +1033,22 @@ describe("random stuff/miscellaneous", () => {
|
||||
await widget.mount(fixture);
|
||||
expect(env.qweb.templates.parent.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-props should not be undefined (snapshotting)", async () => {
|
||||
env.qweb.addTemplate("parent", `<div><t t-widget="child"/></div>`);
|
||||
class Parent extends Widget {
|
||||
inlineTemplate = "parent";
|
||||
widgets = { child: Child };
|
||||
}
|
||||
|
||||
class Child extends Widget {
|
||||
inlineTemplate = `<span>abc</span>`;
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
expect(env.qweb.templates.parent.toString()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("async rendering", () => {
|
||||
@@ -1096,6 +1212,29 @@ describe("updating environment", () => {
|
||||
expect((<any>widget).env.somekey).toBe(4);
|
||||
});
|
||||
|
||||
test("updating widget env does not render widget (if not mounted)", async () => {
|
||||
let n = 0;
|
||||
class TestWidget extends Widget {
|
||||
_render() {
|
||||
n++;
|
||||
return super._render();
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new TestWidget(env);
|
||||
expect(n).toBe(0);
|
||||
await widget.updateEnv(<any>{ somekey: 4 });
|
||||
expect(n).toBe(0);
|
||||
await widget.mount(fixture);
|
||||
expect(n).toBe(1);
|
||||
await widget.updateEnv(<any>{ somekey: 5 });
|
||||
expect(n).toBe(2);
|
||||
widget.detach();
|
||||
expect(n).toBe(2);
|
||||
await widget.updateEnv(<any>{ somekey: 5 });
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("updating child env does not modify parent env", async () => {
|
||||
class ParentWidget extends Widget {
|
||||
inlineTemplate = `<div><t t-widget="child"/></div>`;
|
||||
@@ -1148,4 +1287,21 @@ describe("updating environment", () => {
|
||||
await widget.updateEnv(<any>{ someKey: "rerendered" });
|
||||
expect(fixture.innerHTML).toBe("<div>rerendered</div>");
|
||||
});
|
||||
|
||||
test("updating env force rerendering children", async () => {
|
||||
class Parent extends Widget {
|
||||
inlineTemplate = `<div><t t-widget="Child"/></div>`;
|
||||
widgets = { Child };
|
||||
}
|
||||
class Child extends Widget {
|
||||
inlineTemplate = `<div><t t-esc="env.someKey"/></div>`;
|
||||
}
|
||||
(<any>env).someKey = "hey";
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
|
||||
await widget.updateEnv(<any>{ someKey: "rerendered" });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>rerendered</div></div>");
|
||||
});
|
||||
});
|
||||
|
||||
+96
-6
@@ -101,6 +101,29 @@ describe("error handling", () => {
|
||||
qweb.addTemplate("test", `<t></t>`);
|
||||
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
|
||||
});
|
||||
|
||||
test("loadTemplates throw if parser error", () => {
|
||||
expect(() => {
|
||||
qweb.loadTemplates("<templates><abc>></templates>");
|
||||
}).toThrow("Invalid XML in template");
|
||||
});
|
||||
|
||||
test("nice error when t-on-directive is evaluated with a missing handler", () => {
|
||||
qweb.addTemplate("templatename", `<div t-on-click="somemethod"></div>`);
|
||||
expect(() => qweb.render("templatename", {}, { handlers: [] })).toThrow(
|
||||
"Missing handler 'somemethod' when evaluating template 'templatename'"
|
||||
);
|
||||
});
|
||||
|
||||
test("error when compiled code is invalid", () => {
|
||||
qweb.addTemplate(
|
||||
"templatename",
|
||||
`<div t-att-hey="}/^function invalid{{>'"></div>`
|
||||
);
|
||||
expect(() => qweb.render("templatename")).toThrow(
|
||||
"Invalid generated code while compiling template 'templatename': Unexpected token }"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-esc", () => {
|
||||
@@ -395,6 +418,12 @@ describe("attributes", () => {
|
||||
expect(result).toBe(`<div foo="bar"></div>`);
|
||||
});
|
||||
|
||||
test("t-attf-class should combine with class", () => {
|
||||
qweb.addTemplate("test", `<div class="hello" t-attf-class="world"/>`);
|
||||
const result = renderToString(qweb, "test");
|
||||
expect(result).toBe(`<div class="hello world"></div>`);
|
||||
});
|
||||
|
||||
test("format value", () => {
|
||||
qweb.addTemplate("test", `<div t-attf-foo="b{{value}}r"/>`);
|
||||
const result = renderToString(qweb, "test", { value: "a" });
|
||||
@@ -410,14 +439,11 @@ describe("attributes", () => {
|
||||
expect(result).toBe('<div><span class="def"></span></div>');
|
||||
});
|
||||
|
||||
test.skip("from object variables set previously", () => {
|
||||
// qweb is stupid and does not support this.
|
||||
// To do that properly, we need to extend the formatExpression method to
|
||||
// be able to deal with objects. I think that it is not easy:
|
||||
// it should properly deal with stuff like {a:expr, b: {c: otherexpr}}
|
||||
test("from object variables set previously", () => {
|
||||
// Note: standard qweb does not allow this...
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div t-debug="1"><t t-set="o" t-value="{a:'b'}"/><span t-att-class="o.a"/></div>`
|
||||
`<div><t t-set="o" t-value="{a:'b'}"/><span t-att-class="o.a"/></div>`
|
||||
);
|
||||
const result = renderToString(qweb, "test");
|
||||
expect(result).toBe('<div><span class="b"></span></div>');
|
||||
@@ -467,6 +493,15 @@ describe("attributes", () => {
|
||||
const result = renderToString(qweb, "test", { value: "world" });
|
||||
expect(result).toBe(`<div class="hello world"></div>`);
|
||||
});
|
||||
|
||||
test("t-att-class with object", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div class="static" t-att-class="{a: b, c: d, e: f}"/>`
|
||||
);
|
||||
const result = renderToString(qweb, "test", { b: true, d: false, f: true });
|
||||
expect(result).toBe(`<div class="static a e"></div>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-call (template calling", () => {
|
||||
@@ -866,6 +901,16 @@ describe("t-ref", () => {
|
||||
});
|
||||
|
||||
describe("loading templates", () => {
|
||||
test("can initialize qweb with a string", () => {
|
||||
const data = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<div t-name="hey">jupiler</div>
|
||||
</templates>`;
|
||||
const qweb = new QWeb(data);
|
||||
expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>");
|
||||
});
|
||||
|
||||
test("can load a few templates from a xml string", () => {
|
||||
const data = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -915,3 +960,48 @@ describe("special cases for some boolean html attributes/properties", () => {
|
||||
renderToString(qweb, "test", { flag: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("whitespace handling", () => {
|
||||
test("white space only text nodes are condensed into a single space", () => {
|
||||
qweb.addTemplate("test", `<div> </div>`);
|
||||
const result = renderToString(qweb, "test");
|
||||
expect(result).toBe(`<div> </div>`);
|
||||
});
|
||||
|
||||
test("consecutives whitespaces are condensed into a single space", () => {
|
||||
qweb.addTemplate("test", `<div> abc </div>`);
|
||||
const result = renderToString(qweb, "test");
|
||||
expect(result).toBe(`<div> abc </div>`);
|
||||
});
|
||||
|
||||
test("whitespace only text nodes with newlines are removed", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<span>abc</span>
|
||||
</div>`
|
||||
);
|
||||
const result = renderToString(qweb, "test");
|
||||
expect(result).toBe(`<div><span>abc</span></div>`);
|
||||
});
|
||||
|
||||
test("nothing is done in pre tags", () => {
|
||||
qweb.addTemplate("test", `<pre> </pre>`);
|
||||
const result = renderToString(qweb, "test");
|
||||
expect(result).toBe(`<pre> </pre>`);
|
||||
|
||||
const pretagtext = `<pre>
|
||||
some text
|
||||
</pre>`;
|
||||
qweb.addTemplate("test2", pretagtext);
|
||||
const result2 = renderToString(qweb, "test2");
|
||||
expect(result2).toBe(pretagtext);
|
||||
|
||||
const pretagwithonlywhitespace = `<pre>
|
||||
|
||||
</pre>`;
|
||||
qweb.addTemplate("test3", pretagwithonlywhitespace);
|
||||
const result3 = renderToString(qweb, "test3");
|
||||
expect(result3).toBe(pretagwithonlywhitespace);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { Store, connect } from "../src/store";
|
||||
import { Component, Env } from "../src/component";
|
||||
import {
|
||||
nextMicroTick,
|
||||
makeTestWEnv,
|
||||
makeTestFixture,
|
||||
nextTick
|
||||
} from "./helpers";
|
||||
|
||||
describe("basic use", () => {
|
||||
test("commit a mutation", () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
inc(state, delta) {
|
||||
state.n += delta;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.commit("inc", 14);
|
||||
expect(store.state.n).toBe(15);
|
||||
});
|
||||
|
||||
test("dispatch an action", () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
inc(state, delta) {
|
||||
state.n += delta;
|
||||
}
|
||||
};
|
||||
const actions = {
|
||||
inc({ commit }, delta) {
|
||||
commit("inc", delta);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations, actions });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.dispatch("inc", 14);
|
||||
expect(store.state.n).toBe(15);
|
||||
});
|
||||
|
||||
test("multiple commits trigger one update", async () => {
|
||||
let updateCounter = 0;
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
inc(state, delta) {
|
||||
state.n += delta;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
store.on("update", null, () => updateCounter++);
|
||||
|
||||
store.commit("inc", 14);
|
||||
expect(updateCounter).toBe(0);
|
||||
store.commit("inc", 50);
|
||||
expect(updateCounter).toBe(0);
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("connecting a component to store", () => {
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestWEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
class App extends Component<any, any, any> {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<t t-foreach="props.todos" t-as="todo">
|
||||
<t t-widget="Todo" t-props="todo"/>
|
||||
</t>
|
||||
</div>`;
|
||||
widgets = { Todo };
|
||||
}
|
||||
class Todo extends Component<any, any, any> {
|
||||
inlineTemplate = `<span><t t-esc="props.msg"/></span>`;
|
||||
}
|
||||
|
||||
test("connecting a component works", async () => {
|
||||
const state = { todos: [] };
|
||||
const mutations = {
|
||||
addTodo(state, msg) {
|
||||
state.todos.push({ msg });
|
||||
}
|
||||
};
|
||||
const TodoApp = connect(s => s)(App);
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
const app = new TodoApp(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
|
||||
store.commit("addTodo", "hello");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("connected child components with custom hooks", async () => {
|
||||
let steps: any = [];
|
||||
class Child extends Component<any, any, any> {
|
||||
inlineTemplate = `<div/>`;
|
||||
mounted() {
|
||||
steps.push("child:mounted");
|
||||
}
|
||||
willUnmount() {
|
||||
steps.push("child:willUnmount");
|
||||
}
|
||||
destroyed() {
|
||||
steps.push("child:destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectedChild = connect(s => s)(Child);
|
||||
|
||||
class Parent extends Component<any, any, any> {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<t t-if="state.child" t-widget="ConnectedChild"/>
|
||||
</div>`;
|
||||
widgets = { ConnectedChild };
|
||||
|
||||
constructor(env: Env) {
|
||||
super(env);
|
||||
this.state = { child: true };
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Store({ state: {} });
|
||||
(<any>env).store = store;
|
||||
const parent = new Parent(env);
|
||||
|
||||
await parent.mount(fixture);
|
||||
expect(steps).toEqual(["child:mounted"]);
|
||||
|
||||
await parent.updateState({ child: false });
|
||||
expect(steps).toEqual([
|
||||
"child:mounted",
|
||||
"child:willUnmount",
|
||||
"child:destroyed"
|
||||
]);
|
||||
});
|
||||
|
||||
test("connect receives ownprops as second argument", async () => {
|
||||
const state = { todos: [{ id: 1, text: "jupiler" }] };
|
||||
let nextId = 2;
|
||||
const mutations = {
|
||||
addTodo(state, text) {
|
||||
state.todos.push({ text, id: nextId++ });
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
|
||||
class TodoItem extends Component<any, any, any> {
|
||||
inlineTemplate = `<span><t t-esc="props.text"/></span>`;
|
||||
}
|
||||
const ConnectedTodo = connect((state, props) => {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return todo;
|
||||
})(TodoItem);
|
||||
|
||||
class TodoList extends Component<any, any, any> {
|
||||
inlineTemplate = `<div>
|
||||
<t t-foreach="props.todos" t-as="todo">
|
||||
<t t-widget="ConnectedTodo" t-props="todo"/>
|
||||
</t>
|
||||
</div>`;
|
||||
widgets = { ConnectedTodo };
|
||||
}
|
||||
|
||||
const ConnectedTodoList = connect(state => state)(TodoList);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new ConnectedTodoList(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
store.commit("addTodo", "hoegaarden");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><span>jupiler</span><span>hoegaarden</span></div>"
|
||||
);
|
||||
});
|
||||
});
|
||||
+68
-1
@@ -4,7 +4,10 @@ import {
|
||||
idGenerator,
|
||||
memoize,
|
||||
debounce,
|
||||
findInTree
|
||||
findInTree,
|
||||
shallowEqual,
|
||||
patch,
|
||||
unpatch
|
||||
} from "../src/utils";
|
||||
|
||||
describe("escape", () => {
|
||||
@@ -91,3 +94,67 @@ describe("findInTree", () => {
|
||||
expect(match2).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shallowEqual", () => {
|
||||
test("simple comparisons", () => {
|
||||
const obj1 = {};
|
||||
expect(shallowEqual(obj1, obj1)).toBe(true);
|
||||
expect(shallowEqual({}, {})).toBe(true);
|
||||
expect(shallowEqual({ a: 1 }, {})).toBe(false);
|
||||
expect(shallowEqual({ a: 1 }, { a: 1 })).toBe(true);
|
||||
expect(shallowEqual({ a: 1 }, ["a"])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("patch/unpatch", () => {
|
||||
test("can monkey patch a class", () => {
|
||||
class Test {
|
||||
n = 1;
|
||||
|
||||
doSomething(): string {
|
||||
return "hey";
|
||||
}
|
||||
}
|
||||
|
||||
patch(Test, "some_custo", {
|
||||
doSomething(): string {
|
||||
this.n = this.n + 1;
|
||||
return this._super();
|
||||
}
|
||||
});
|
||||
|
||||
const t = new Test();
|
||||
expect(t.n).toBe(1);
|
||||
expect(t.doSomething()).toBe("hey");
|
||||
expect(t.n).toBe(2);
|
||||
});
|
||||
|
||||
test("cannot patch a class twice with same patch name", () => {
|
||||
class Test {}
|
||||
|
||||
patch(Test, "some_custo", {});
|
||||
expect(() => {
|
||||
patch(Test, "some_custo", {});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("can unpatch a class", () => {
|
||||
class Test {
|
||||
doSomething(): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
patch(Test, "some_custo", {
|
||||
doSomething(): number {
|
||||
return this._super() + 2;
|
||||
}
|
||||
});
|
||||
|
||||
const t = new Test();
|
||||
expect(t.doSomething()).toBe(3);
|
||||
|
||||
unpatch(Test, "some_custo");
|
||||
expect(t.doSomething()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-2
@@ -3,11 +3,10 @@
|
||||
"module": "commonjs",
|
||||
"preserveConstEnums": true,
|
||||
"noImplicitThis": true,
|
||||
"lib": ["es2015", "dom"],
|
||||
"removeComments": false,
|
||||
"inlineSourceMap": true,
|
||||
"declaration": true,
|
||||
"target": "es6",
|
||||
"target": "es5",
|
||||
"outDir": "dist",
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
|
||||
Reference in New Issue
Block a user