mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afc94c51a8 | |||
| 90f6808f57 | |||
| f960149ba2 | |||
| 74ce6a671e | |||
| bc07c49a5f | |||
| 816ffe1b44 | |||
| 94c33af8e9 | |||
| 6e0ef0fd5c | |||
| 370a92b67e | |||
| c10b84cd99 | |||
| 7ea782bd60 | |||
| bf4cbaea33 | |||
| cde85269fd | |||
| 3340401f54 | |||
| 7027ff1eb6 | |||
| d10426c720 | |||
| a951c484be | |||
| 14d3eb83eb | |||
| ca73ed07a6 | |||
| f2fc20f8e3 | |||
| 8badcbd21e |
@@ -28,6 +28,10 @@ It is designed to be:
|
||||
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).
|
||||
|
||||
## Try it online
|
||||
|
||||
You can experiment with the OWL project online: [https://odoo.github.io/owl/](https://odoo.github.io/owl/)
|
||||
|
||||
## Installing/Building
|
||||
|
||||
Some npm scripts are available:
|
||||
@@ -117,4 +121,4 @@ More interesting examples on how to work with this web framework can be found in
|
||||
|
||||
## License
|
||||
|
||||
React is [GPL licensed](./LICENSE).
|
||||
OWL is [GPL licensed](./LICENSE).
|
||||
|
||||
+94
-10
@@ -7,16 +7,8 @@ For example:
|
||||
|
||||
```javascript
|
||||
export class Counter extends Component {
|
||||
inlineTemplate = `
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.counter"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>`;
|
||||
|
||||
state = {
|
||||
counter: 0
|
||||
};
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
@@ -28,3 +20,95 @@ export class Counter extends Component {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.counter"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>`;
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
A solid and robust component system needs useful hooks/methods to help
|
||||
developers write components. Here is a description of the lifecycle of a owl
|
||||
component:
|
||||
|
||||
- **[constructor](#constructor)**
|
||||
- **[willStart](#willStart)**
|
||||
- **[mounted](#mounted)**
|
||||
- **[willPatch](#willPatch)**
|
||||
- **[updated](#updated)**
|
||||
- **[willUnmount](#willUnmount)**
|
||||
|
||||
Note: no hook method should ever be called manually. They are supposed to be
|
||||
called by the owl framework whenever it is required.
|
||||
|
||||
### constructor
|
||||
|
||||
The constructor is not exactly a hook, it is the constructor of the component.
|
||||
|
||||
### willStart
|
||||
|
||||
willStart is an asynchronous hook that can be implemented to perform some
|
||||
action before the initial rendering of a component.
|
||||
|
||||
It will be called exactly once before the initial rendering. It is useful
|
||||
in some cases, for example, to load external assets (such as a JS library)
|
||||
before the widget is rendered. Another use case is to load data from a server.
|
||||
|
||||
Note that a slow willStart method will slow down the rendering of the user
|
||||
interface. Therefore, some effort should be made to make this method as
|
||||
fast as possible.
|
||||
|
||||
### mounted
|
||||
|
||||
mounted is a hook that is called each time a component is attached to the
|
||||
DOM. This is a good place to add some listeners, or to interact with the
|
||||
DOM, if the component needs to perform some measure for example.
|
||||
|
||||
It is the opposite of _willUnmount_. If a component has been mounted, it will
|
||||
be unmounted at some point.
|
||||
|
||||
### willUpdateProps
|
||||
|
||||
The willUpdateProps is an asynchronous hook, called just before new props
|
||||
are set. This is useful if the component needs some asynchronous task
|
||||
performed, depending on the props (for example, assuming that the props are
|
||||
some record Id, fetching the record data).
|
||||
|
||||
This hook is not called during the first render (but willStart is called
|
||||
and performs a similar job).
|
||||
|
||||
### willPatch
|
||||
|
||||
The willPatch hook is called just before the DOM patching process starts.
|
||||
It is not called on the initial render. This is useful to get some
|
||||
information which are in the DOM. For example, the current position of the
|
||||
scrollbar
|
||||
|
||||
Note that at this point, it is not safe to rerender the widget. In
|
||||
particular, updateState calls should be avoided.
|
||||
|
||||
### patched
|
||||
|
||||
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 patched.
|
||||
|
||||
Updating the widget state in this hook is possible, but not encouraged.
|
||||
One need to be careful, because updates here will cause rerender, which in
|
||||
turn will cause other calls to patched. So, we need to be particularly
|
||||
careful at avoiding endless cycles.
|
||||
|
||||
### willUnmount
|
||||
|
||||
willUnmount is a hook that is called each time just before a component is unmounted from
|
||||
the DOM. This is a good place to remove some listeners, for example.
|
||||
|
||||
This is the opposite method of _mounted_. The _willUnmount_ method will be
|
||||
called in reverse order: first the children, then the parents.
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<div t-name="web.web_client" class="o_web_client">
|
||||
<t t-widget="Navbar" t-props="{inHome:state.inHome,app:state.currentApp}" />
|
||||
<t t-widget="HomeMenu" t-if="state.inHome" t-keep-alive="1" t-props="{menuInfo:props.menuInfo}" />
|
||||
<div class="o_content" t-att-class="state.inHome ? 'o_hidden' : ''" t-ref="content"/>
|
||||
<div class="o_content" t-att-class="state.inHome ? 'o_hidden' : ''" t-ref="'content'"/>
|
||||
<div class="o_notification_container">
|
||||
<t t-foreach="state.notifications" t-as="notif">
|
||||
<t t-widget="Notification" t-props="notif"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_loading d-none" t-ref="loading_indicator">Loading</div>
|
||||
<div class="o_loading d-none" t-ref="'loading_indicator'">Loading</div>
|
||||
</div>
|
||||
|
||||
<div t-name="web.navbar" class="o_navbar" t-att-class="props.inHome ? 'o_in_home' : ''">
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "owl",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"description": "Odoo Web Lab",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
+44
-22
@@ -149,6 +149,28 @@ export class Component<
|
||||
*/
|
||||
mounted() {}
|
||||
|
||||
/**
|
||||
* The willUpdateProps is an asynchronous hook, called just before new props
|
||||
* are set. This is useful if the component needs some asynchronous task
|
||||
* performed, depending on the props (for example, assuming that the props are
|
||||
* some record Id, fetching the record data).
|
||||
*
|
||||
* This hook is not called during the first render (but willStart is called
|
||||
* and performs a similar job).
|
||||
*/
|
||||
async willUpdateProps(nextProps: Props) {}
|
||||
|
||||
/**
|
||||
* The willPatch hook is called just before the DOM patching process starts.
|
||||
* It is not called on the initial render. This is useful to get some
|
||||
* information which are in the DOM. For example, the current position of the
|
||||
* scrollbar
|
||||
*
|
||||
* Note that at this point, it is not safe to rerender the widget. In
|
||||
* particular, updateState calls should be avoided.
|
||||
*/
|
||||
willPatch() {}
|
||||
|
||||
/**
|
||||
* This hook is called whenever a component did actually update its props,
|
||||
* state or env.
|
||||
@@ -156,12 +178,18 @@ export class Component<
|
||||
* 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.
|
||||
*
|
||||
* Updating the widget state in this hook is possible, but not encouraged.
|
||||
* One need to be careful, because updates here will cause rerender, which in
|
||||
* turn will cause other calls to updated. So, we need to be particularly
|
||||
* careful at avoiding endless cycles.
|
||||
*/
|
||||
componentDidUpdate() {}
|
||||
patched() {}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* willUnmount is a hook that is called each time just before a component is
|
||||
* unmounted from the DOM. This is a good place to remove some listeners, for
|
||||
* example.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*
|
||||
@@ -169,14 +197,6 @@ export class Component<
|
||||
*/
|
||||
willUnmount() {}
|
||||
|
||||
/**
|
||||
* destroyed is a hook called exactly once, when a component is destroyed.
|
||||
* When a component is destroyed, its children will be destroyed first.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*/
|
||||
destroyed() {}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -200,7 +220,7 @@ export class Component<
|
||||
}
|
||||
|
||||
async mount(target: HTMLElement): Promise<void> {
|
||||
const vnode = await this._start();
|
||||
const vnode = await this._prepare();
|
||||
if (this.__widget__.isDestroyed) {
|
||||
// widget was destroyed before we get here...
|
||||
return;
|
||||
@@ -220,7 +240,7 @@ export class Component<
|
||||
}
|
||||
}
|
||||
|
||||
detach() {
|
||||
unmount() {
|
||||
if (this.el) {
|
||||
this._visitSubTree(w => {
|
||||
if (w.__widget__.isMounted) {
|
||||
@@ -268,7 +288,6 @@ export class Component<
|
||||
}
|
||||
this.clear();
|
||||
this.__widget__.isDestroyed = true;
|
||||
this.destroyed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +312,7 @@ export class Component<
|
||||
if (this.__widget__.isMounted) {
|
||||
await this.render(true);
|
||||
}
|
||||
this.componentDidUpdate();
|
||||
this.patched();
|
||||
}
|
||||
|
||||
async updateProps(
|
||||
@@ -325,7 +344,7 @@ export class Component<
|
||||
if (this.__widget__.isStarted) {
|
||||
await this.render();
|
||||
}
|
||||
this.componentDidUpdate();
|
||||
this.patched();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -333,19 +352,22 @@ export class Component<
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async _updateProps(nextProps: Props): Promise<void> {
|
||||
await this.willUpdateProps(nextProps);
|
||||
this.props = nextProps;
|
||||
await this.render();
|
||||
this.componentDidUpdate();
|
||||
this.patched();
|
||||
}
|
||||
|
||||
_patch(vnode) {
|
||||
this.__widget__.renderPromise = null;
|
||||
this.__widget__.vnode = patch(
|
||||
this.__widget__.vnode || document.createElement(vnode.sel!),
|
||||
vnode
|
||||
);
|
||||
if (this.__widget__.vnode) {
|
||||
this.willPatch();
|
||||
this.__widget__.vnode = patch(this.__widget__.vnode, vnode);
|
||||
} else {
|
||||
this.__widget__.vnode = patch(document.createElement(vnode.sel!), vnode);
|
||||
}
|
||||
}
|
||||
async _start(): Promise<VNode> {
|
||||
async _prepare(): Promise<VNode> {
|
||||
this.__widget__.renderProps = this.props;
|
||||
this.__widget__.renderPromise = this.willStart().then(() => {
|
||||
if (this.__widget__.isDestroyed) {
|
||||
|
||||
+13
-9
@@ -900,10 +900,12 @@ const onDirective: Directive = {
|
||||
const refDirective: Directive = {
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, node, nodeID }) {
|
||||
let ref = node.getAttribute("t-ref");
|
||||
atNodeCreation({ ctx, node }) {
|
||||
let ref = node.getAttribute("t-ref")!;
|
||||
ctx.addLine(`p${ctx.parentNode}.hook = {
|
||||
create: (_, n) => context.refs['${ref}'] = n.elm,
|
||||
create: (_, n) => context.refs[${ctx.formatExpression(
|
||||
ref
|
||||
)}] = n.elm,
|
||||
};`);
|
||||
}
|
||||
};
|
||||
@@ -996,10 +998,10 @@ const widgetDirective: Directive = {
|
||||
}
|
||||
let ref = node.getAttribute("t-ref");
|
||||
if (ref) {
|
||||
ctx.addLine(`context.refs['${ref}'] = w${widgetID};`);
|
||||
ctx.addLine(`context.refs[${ctx.formatExpression(ref)}] = w${widgetID};`);
|
||||
}
|
||||
|
||||
ctx.addLine(`def${defID} = w${widgetID}._start();`);
|
||||
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
|
||||
ctx.closeIf();
|
||||
ctx.closeIf();
|
||||
|
||||
@@ -1008,16 +1010,18 @@ const widgetDirective: Directive = {
|
||||
`def${defID} = def${defID}.then(vnode=>{let pvnode=h(vnode.sel, {key: ${templateID}});c${
|
||||
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"
|
||||
keepAlive ? "unmount" : "destroy"
|
||||
}()},destroy(){w${widgetID}.${
|
||||
keepAlive ? "detach" : "destroy"
|
||||
keepAlive ? "unmount" : "destroy"
|
||||
}()}}; w${widgetID}.__widget__.pvnode = pvnode;});`
|
||||
);
|
||||
ctx.addElse();
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__widget__.isDestroyed) {return};let vnode;if (!w${widgetID}.__widget__.vnode){vnode=w${widgetID}.__widget__.pvnode} else { vnode=h(w${widgetID}.__widget__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;w${widgetID}.__mount();},remove(){w${widgetID}.${
|
||||
keepAlive ? "detach" : "destroy"
|
||||
}()}}}c${ctx.parentNode}[_${dummyID}_index]=vnode;});`
|
||||
keepAlive ? "unmount" : "destroy"
|
||||
}()}, destroy() {w${widgetID}.${keepAlive ? "unmount" : "destroy"}()}}}c${
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=vnode;});`
|
||||
);
|
||||
ctx.closeIf();
|
||||
|
||||
|
||||
+180
-67
@@ -2,6 +2,95 @@ import { EventBus } from "./event_bus";
|
||||
import { Component } from "./component";
|
||||
import { shallowEqual } from "./utils";
|
||||
|
||||
interface StoreConfig {
|
||||
env?: any;
|
||||
state?: any;
|
||||
actions?: any;
|
||||
mutations?: any;
|
||||
}
|
||||
|
||||
interface StoreOption {
|
||||
debug?: boolean;
|
||||
}
|
||||
export class Store extends EventBus {
|
||||
state: any;
|
||||
mstate: any;
|
||||
actions: any;
|
||||
mutations: any;
|
||||
_isMutating: boolean = false;
|
||||
_isDirty: boolean = false;
|
||||
history: any[] = [];
|
||||
debug: boolean;
|
||||
env: any;
|
||||
|
||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
||||
super();
|
||||
this.debug = options.debug || false;
|
||||
this.state = Object.assign({}, config.state);
|
||||
const self = this;
|
||||
this.mstate = magify({
|
||||
raw: this.state,
|
||||
key: "state",
|
||||
parent: this,
|
||||
onDirty: function() {
|
||||
self._isDirty = true;
|
||||
}
|
||||
});
|
||||
this.actions = config.actions;
|
||||
this.mutations = config.mutations;
|
||||
this.env = config.env;
|
||||
|
||||
if (this.debug) {
|
||||
this.history.push({ state: this.state });
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(action, payload?: any): Promise<void> | void {
|
||||
if (!this.actions[action]) {
|
||||
throw new Error(`[Error] action ${action} is undefined`);
|
||||
}
|
||||
const result = this.actions[action](
|
||||
{
|
||||
commit: this.commit.bind(this),
|
||||
dispatch: this.dispatch.bind(this),
|
||||
env: this.env,
|
||||
state: this.state
|
||||
},
|
||||
payload
|
||||
);
|
||||
if (result instanceof Promise) {
|
||||
return new Promise((resolve, reject) => {
|
||||
result.then(() => resolve());
|
||||
result.catch(reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async commit(type, payload?: any) {
|
||||
if (!this.mutations[type]) {
|
||||
throw new Error(`[Error] mutation ${type} is undefined`);
|
||||
}
|
||||
this._isMutating = true;
|
||||
|
||||
this.mutations[type].call(null, this.mstate, payload);
|
||||
if (this.debug) {
|
||||
this.history.push({
|
||||
state: this.state,
|
||||
mutation: type,
|
||||
payload: payload
|
||||
});
|
||||
}
|
||||
await Promise.resolve();
|
||||
if (this._isMutating) {
|
||||
this._isMutating = false;
|
||||
if (this._isDirty) {
|
||||
this._isDirty = false;
|
||||
this.trigger("update", this.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function connect(mapStateToProps) {
|
||||
return function(Comp) {
|
||||
return class extends Comp {
|
||||
@@ -30,6 +119,12 @@ export function connect(mapStateToProps) {
|
||||
super.willUnmount();
|
||||
}
|
||||
updateProps(nextProps, forceUpdate) {
|
||||
if (this.__widget__.ownProps !== nextProps) {
|
||||
this.__widget__.currentStoreProps = mapStateToProps(
|
||||
this.env.store.state,
|
||||
nextProps
|
||||
);
|
||||
}
|
||||
this.__widget__.ownProps = nextProps;
|
||||
const mergedProps = Object.assign(
|
||||
{},
|
||||
@@ -42,74 +137,92 @@ export function connect(mapStateToProps) {
|
||||
};
|
||||
}
|
||||
|
||||
interface StoreConfig {
|
||||
state?: any;
|
||||
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, 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 });
|
||||
function _magifyArray({ raw, key, parent, magic, onDirty }) {
|
||||
Object.defineProperty(magic, "length", {
|
||||
get() {
|
||||
return magic.raw.length;
|
||||
}
|
||||
}
|
||||
|
||||
get state() {
|
||||
return this._clone(this._state);
|
||||
}
|
||||
|
||||
dispatch(action, payload) {
|
||||
if (!this.actions[action]) {
|
||||
throw new Error(`[Error] action ${action} is undefined`);
|
||||
}
|
||||
this.actions[action](
|
||||
{
|
||||
commit: this.commit.bind(this),
|
||||
state: this.state
|
||||
},
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
async commit(type, payload) {
|
||||
if (!this.mutations[type]) {
|
||||
throw new Error(`[Error] mutation ${type} is undefined`);
|
||||
}
|
||||
this._isMutating = true;
|
||||
|
||||
this.mutations[type].call(null, this._state, payload);
|
||||
if (this.debug) {
|
||||
this.history.push({
|
||||
state: this.state,
|
||||
mutation: type,
|
||||
payload: payload
|
||||
});
|
||||
Object.assign(magic, {
|
||||
push: function(item) {
|
||||
onDirty();
|
||||
parent.raw[key] = [...magic.raw, item];
|
||||
magic.raw = parent.raw[key];
|
||||
const index = magic.raw.length - 1;
|
||||
let prop = magify({ raw: item, key: index, parent: magic, onDirty });
|
||||
Object.defineProperty(magic, index, {
|
||||
set(newVal) {
|
||||
onDirty();
|
||||
parent.raw[key] = [...magic.raw];
|
||||
parent.raw[key][index] = newVal;
|
||||
magic.raw = parent.raw[key];
|
||||
prop = magify({ raw: newVal, key: index, parent: magic, onDirty });
|
||||
},
|
||||
get() {
|
||||
return prop;
|
||||
}
|
||||
});
|
||||
}
|
||||
await Promise.resolve();
|
||||
if (this._isMutating) {
|
||||
this._isMutating = false;
|
||||
this.trigger("update", this.state);
|
||||
}
|
||||
}
|
||||
|
||||
_clone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
});
|
||||
raw.forEach((value, index) => {
|
||||
let prop = magify({
|
||||
raw: value,
|
||||
key: index,
|
||||
parent: magic,
|
||||
onDirty
|
||||
});
|
||||
Object.defineProperty(magic, index, {
|
||||
set(newVal) {
|
||||
onDirty();
|
||||
parent.raw[key] = [...magic.raw];
|
||||
parent.raw[key][index] = newVal;
|
||||
magic.raw = parent.raw[key];
|
||||
prop = magify({ raw: newVal, key: index, parent: magic, onDirty });
|
||||
},
|
||||
get() {
|
||||
return prop;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function magify({ raw, key, parent, onDirty }) {
|
||||
if (!parent.magic) {
|
||||
parent = {
|
||||
raw: parent,
|
||||
magic: true,
|
||||
parent: null
|
||||
};
|
||||
}
|
||||
if (raw.magic) {
|
||||
return raw;
|
||||
}
|
||||
if (typeof raw !== "object") {
|
||||
return raw;
|
||||
}
|
||||
let magic = { raw, key, parent, magic: true };
|
||||
if (Array.isArray(raw)) {
|
||||
_magifyArray({ raw, key, parent, magic, onDirty });
|
||||
} else {
|
||||
Object.keys(raw).forEach(propKey => {
|
||||
let prop = magify({
|
||||
raw: raw[propKey],
|
||||
key: propKey,
|
||||
parent: magic,
|
||||
onDirty
|
||||
});
|
||||
Object.defineProperty(magic, propKey, {
|
||||
set(newVal) {
|
||||
onDirty();
|
||||
parent.raw[key] = { ...magic.raw, [propKey]: newVal };
|
||||
magic.raw = parent.raw[key];
|
||||
prop = magify({ raw: newVal, key: propKey, parent: magic, onDirty });
|
||||
},
|
||||
get() {
|
||||
return prop;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return magic;
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
} else {
|
||||
w7 = new context.widgets['ChildWidget'](owner, props7);
|
||||
context.__widget__.cmap[key8] = w7.__widget__.id;
|
||||
def6 = w7._start();
|
||||
def6 = w7._prepare();
|
||||
}
|
||||
}
|
||||
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()},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;});
|
||||
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()}, destroy() {w7.destroy()}}}c1[_5_index]=vnode;});
|
||||
}
|
||||
extra.promises.push(def6);
|
||||
}
|
||||
@@ -95,13 +95,13 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
} else {
|
||||
w4 = new context.widgets['child'](owner, props4);
|
||||
context.__widget__.cmap[key5] = w4.__widget__.id;
|
||||
def3 = w4._start();
|
||||
def3 = w4._prepare();
|
||||
}
|
||||
}
|
||||
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()},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;});
|
||||
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()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
@@ -141,13 +141,13 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
|
||||
} else {
|
||||
w4 = new context.widgets['child'](owner, props4);
|
||||
context.__widget__.cmap[4] = w4.__widget__.id;
|
||||
def3 = w4._start();
|
||||
def3 = w4._prepare();
|
||||
}
|
||||
}
|
||||
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;});
|
||||
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()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
|
||||
@@ -1189,6 +1189,22 @@ exports[`t-raw variable 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-ref can get a dynamic ref on a node 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
var c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var c2 = [], p2 = {key:2};
|
||||
var vn2 = h('span', p2, c2);
|
||||
c1.push(vn2);
|
||||
p2.hook = {
|
||||
create: (_, n) => context.refs['myspan' + 3] = n.elm,
|
||||
};
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-ref can get a ref on a node 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
|
||||
+111
-48
@@ -312,21 +312,53 @@ describe("lifecycle hooks", () => {
|
||||
willUnmount() {
|
||||
steps.push("willunmount");
|
||||
}
|
||||
destroyed() {
|
||||
steps.push("destroyed");
|
||||
}
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(steps).toEqual(["init", "willstart", "mounted"]);
|
||||
await widget.updateState({ ok: false });
|
||||
expect(steps).toEqual([
|
||||
"init",
|
||||
"willstart",
|
||||
"mounted",
|
||||
"willunmount",
|
||||
"destroyed"
|
||||
]);
|
||||
expect(steps).toEqual(["init", "willstart", "mounted", "willunmount"]);
|
||||
});
|
||||
|
||||
test("widgets are unmounted and destroyed if no longer in DOM, even after updateprops", async () => {
|
||||
let childUnmounted = false;
|
||||
class ChildWidget extends Widget {
|
||||
inlineTemplate = `<span><t t-esc="props.n"/></span>`;
|
||||
willUnmount() {
|
||||
childUnmounted = true;
|
||||
}
|
||||
increment() {
|
||||
this.updateState({ n: this.state.n + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { ChildWidget };
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<div t-if="state.flag">
|
||||
<t t-widget="ChildWidget" t-props="{n: state.n}"/>
|
||||
</div>
|
||||
</div>`;
|
||||
state = { n: 0, flag: true };
|
||||
increment() {
|
||||
this.updateState({ n: this.state.n + 1 });
|
||||
}
|
||||
toggleSubWidget() {
|
||||
this.updateState({ flag: !this.state.flag });
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div><span>0</span></div></div>");
|
||||
widget.increment();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div><span>1</span></div></div>");
|
||||
widget.toggleSubWidget();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
expect(childUnmounted).toBe(true);
|
||||
});
|
||||
|
||||
test("hooks are called in proper order in widget creation/destruction", async () => {
|
||||
@@ -347,9 +379,6 @@ describe("lifecycle hooks", () => {
|
||||
willUnmount() {
|
||||
steps.push("p willunmount");
|
||||
}
|
||||
destroyed() {
|
||||
steps.push("p destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
class ChildWidget extends Widget {
|
||||
@@ -366,9 +395,6 @@ describe("lifecycle hooks", () => {
|
||||
willUnmount() {
|
||||
steps.push("c willunmount");
|
||||
}
|
||||
destroyed() {
|
||||
steps.push("c destroyed");
|
||||
}
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -381,19 +407,38 @@ describe("lifecycle hooks", () => {
|
||||
"p mounted",
|
||||
"c mounted",
|
||||
"c willunmount",
|
||||
"c destroyed",
|
||||
"p willunmount",
|
||||
"p destroyed"
|
||||
"p willunmount"
|
||||
]);
|
||||
});
|
||||
|
||||
test("componentDidUpdate hook is called after updateState", async () => {
|
||||
test("willUpdateProps hook is called", async () => {
|
||||
let def = makeDeferred();
|
||||
class HookWidget extends Widget {
|
||||
inlineTemplate = '<span><t t-esc="props.n"/></span>';
|
||||
|
||||
willUpdateProps(nextProps) {
|
||||
expect(nextProps.n).toBe(2);
|
||||
return def;
|
||||
}
|
||||
}
|
||||
const widget = new HookWidget(env, { n: 1 });
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<span>1</span>");
|
||||
widget.updateProps({ n: 2 });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<span>1</span>");
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<span>2</span>");
|
||||
});
|
||||
|
||||
test("patched hook is called after updateState", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
state = { a: 1 };
|
||||
|
||||
componentDidUpdate() {
|
||||
patched() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
@@ -408,11 +453,11 @@ describe("lifecycle hooks", () => {
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("componentDidUpdate hook is called after updateProps", async () => {
|
||||
test("patched hook is called after updateProps", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
componentDidUpdate() {
|
||||
patched() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
@@ -424,13 +469,13 @@ describe("lifecycle hooks", () => {
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("componentDidUpdate hook is called after updateEnv", async () => {
|
||||
test("patched hook is called after updateEnv", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
state = { a: 1 };
|
||||
|
||||
componentDidUpdate() {
|
||||
patched() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
@@ -461,7 +506,6 @@ describe("lifecycle hooks", () => {
|
||||
});
|
||||
|
||||
test("sub widget (inside sub node): hooks are correctly called", async () => {
|
||||
let destroyed = false;
|
||||
let created = false;
|
||||
let mounted = false;
|
||||
class ParentWidget extends Widget {
|
||||
@@ -483,9 +527,6 @@ describe("lifecycle hooks", () => {
|
||||
mounted() {
|
||||
mounted = true;
|
||||
}
|
||||
destroyed() {
|
||||
destroyed = true;
|
||||
}
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -494,9 +535,46 @@ describe("lifecycle hooks", () => {
|
||||
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);
|
||||
});
|
||||
|
||||
test("willPatch/patched hook", async () => {
|
||||
const steps: string[] = [];
|
||||
class ParentWidget extends Widget {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<t t-widget="child" t-props="{v: state.n}"/>
|
||||
</div>`;
|
||||
widgets = { child: ChildWidget };
|
||||
state = { n: 1 };
|
||||
willPatch() {
|
||||
steps.push("parent:willPatch");
|
||||
}
|
||||
patched() {
|
||||
steps.push("parent:patched");
|
||||
}
|
||||
}
|
||||
|
||||
class ChildWidget extends Widget {
|
||||
willPatch() {
|
||||
steps.push("child:willPatch");
|
||||
}
|
||||
patched() {
|
||||
steps.push("child:patched");
|
||||
}
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(steps).toEqual([]);
|
||||
await widget.updateState({ n: 2 });
|
||||
|
||||
// Not sure about this order. If you disagree, feel free to open an issue...
|
||||
expect(steps).toEqual([
|
||||
"child:willPatch",
|
||||
"child:patched",
|
||||
"parent:willPatch",
|
||||
"parent:patched"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -511,21 +589,6 @@ describe("destroy method", () => {
|
||||
expect(widget.__widget__.isDestroyed).toBe(true);
|
||||
});
|
||||
|
||||
test("destroying a widget twice only call destroyed once", async () => {
|
||||
let count = 0;
|
||||
class TestWidget extends Widget {
|
||||
destroyed() {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
await widget.mount(fixture);
|
||||
widget.destroy();
|
||||
expect(count).toBe(1);
|
||||
widget.destroy();
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
test("destroying a parent also destroys its children", async () => {
|
||||
const parent = new WidgetA(env);
|
||||
await parent.mount(fixture);
|
||||
@@ -589,7 +652,7 @@ describe("composition", () => {
|
||||
|
||||
test("t-refs on widget are widgets", async () => {
|
||||
class WidgetC extends Widget {
|
||||
inlineTemplate = `<div>Hello<t t-ref="mywidgetb" t-widget="b"/></div>`;
|
||||
inlineTemplate = `<div>Hello<t t-ref="'mywidgetb'" t-widget="b"/></div>`;
|
||||
widgets = { b: WidgetB };
|
||||
}
|
||||
const widget = new WidgetC(env);
|
||||
@@ -1229,7 +1292,7 @@ describe("updating environment", () => {
|
||||
expect(n).toBe(1);
|
||||
await widget.updateEnv(<any>{ somekey: 5 });
|
||||
expect(n).toBe(2);
|
||||
widget.detach();
|
||||
widget.unmount();
|
||||
expect(n).toBe(2);
|
||||
await widget.updateEnv(<any>{ somekey: 5 });
|
||||
expect(n).toBe(2);
|
||||
|
||||
+8
-1
@@ -893,11 +893,18 @@ describe("t-on", () => {
|
||||
|
||||
describe("t-ref", () => {
|
||||
test("can get a ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
||||
qweb.addTemplate("test", `<div><span t-ref="'myspan'"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs });
|
||||
expect(refs.myspan.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
test("can get a dynamic ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="'myspan' + 3"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs });
|
||||
expect(refs.myspan3.tagName).toBe("SPAN");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loading templates", () => {
|
||||
|
||||
+465
-10
@@ -41,6 +41,414 @@ describe("basic use", () => {
|
||||
expect(store.state.n).toBe(15);
|
||||
});
|
||||
|
||||
test("can dispatch an action in an action", () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
inc(state, delta) {
|
||||
state.n += delta;
|
||||
}
|
||||
};
|
||||
const actions = {
|
||||
inc({ commit }, delta) {
|
||||
commit("inc", delta);
|
||||
},
|
||||
inc100({ dispatch }) {
|
||||
dispatch("inc", 100);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations, actions });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.dispatch("inc100");
|
||||
expect(store.state.n).toBe(101);
|
||||
});
|
||||
|
||||
test("dispatch allow synchronizing between actions", async () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
inc(state, delta) {
|
||||
state.n += delta;
|
||||
},
|
||||
setN(state, n) {
|
||||
state.n = n;
|
||||
}
|
||||
};
|
||||
const actions = {
|
||||
async dosomething({ commit, dispatch }) {
|
||||
await dispatch("setTo10");
|
||||
commit("inc", 3);
|
||||
},
|
||||
async setTo10({ commit }) {
|
||||
await Promise.resolve();
|
||||
commit("setN", 10);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations, actions });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.dispatch("dosomething");
|
||||
expect(store.state.n).toBe(1);
|
||||
await nextTick();
|
||||
expect(store.state.n).toBe(13);
|
||||
});
|
||||
|
||||
test("env is given to actions", () => {
|
||||
expect.assertions(1);
|
||||
const someEnv = {};
|
||||
const actions = {
|
||||
someaction({ env }) {
|
||||
expect(env).toBe(someEnv);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state: {}, actions, env: someEnv });
|
||||
|
||||
store.dispatch("someaction");
|
||||
});
|
||||
});
|
||||
|
||||
describe("advanced state properties", () => {
|
||||
test("state in the store is reference equal after empty mutation", async () => {
|
||||
const mutations = {
|
||||
donothing() {}
|
||||
};
|
||||
const store = new Store({ state: {}, mutations });
|
||||
const state = store.state;
|
||||
store.commit("donothing");
|
||||
expect(store.state).toBe(state);
|
||||
});
|
||||
|
||||
test("state in the store is not reference equal after changing one number value", async () => {
|
||||
const mutations = {
|
||||
dosomething(state) {
|
||||
expect(state.rochefort).toBe(8);
|
||||
state.rochefort += 2;
|
||||
expect(state.rochefort).toBe(10);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state: { rochefort: 8 }, mutations });
|
||||
const state = store.state;
|
||||
expect(state.rochefort).toBe(8);
|
||||
store.commit("dosomething");
|
||||
expect(store.state.rochefort).toBe(10);
|
||||
expect(store.state).not.toBe(state);
|
||||
});
|
||||
|
||||
test("can push an item in a list with an existing item", async () => {
|
||||
const mutations = {
|
||||
addRochefort(state) {
|
||||
state.rocheforts.push({ taste: 87 });
|
||||
}
|
||||
};
|
||||
const store = new Store({
|
||||
state: { rocheforts: [{ taste: 84 }] },
|
||||
mutations
|
||||
});
|
||||
const state = store.state;
|
||||
store.commit("addRochefort");
|
||||
expect(store.state.rocheforts).toEqual([{ taste: 84 }, { taste: 87 }]);
|
||||
expect(store.state).toBe(state);
|
||||
});
|
||||
|
||||
test("state is reference equal after pushing in a list", async () => {
|
||||
const mutations = {
|
||||
addRochefort(state) {
|
||||
state.rocheforts.push(10);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state: { rocheforts: [] }, mutations });
|
||||
const state = store.state;
|
||||
store.commit("addRochefort");
|
||||
expect(store.state.rocheforts).toEqual([10]);
|
||||
expect(store.state).toBe(state);
|
||||
});
|
||||
|
||||
test("nested state in the store behaves properly", async () => {
|
||||
const state = { jupiler: { maes: 1 } };
|
||||
const mutations = {
|
||||
dostuff(state, inc) {
|
||||
state.jupiler.maes += inc;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
expect(store.state.jupiler.maes).toBe(1);
|
||||
const stateA = store.state;
|
||||
const jupiler = store.state.jupiler;
|
||||
store.commit("dostuff", 10);
|
||||
expect(store.state.jupiler.maes).toBe(11);
|
||||
expect(store.state).toBe(stateA);
|
||||
expect(store.state.jupiler).not.toBe(jupiler);
|
||||
});
|
||||
|
||||
test("sibling properties are not affected", async () => {
|
||||
const state = { jupiler: { maes: 1 }, stella: 3 };
|
||||
const mutations = {
|
||||
dostuff(state, inc) {
|
||||
state.stella += inc;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
const jupiler = store.state.jupiler;
|
||||
const stateA = store.state;
|
||||
store.commit("dostuff", 10);
|
||||
expect(store.state.stella).toBe(13);
|
||||
expect(store.state).not.toBe(stateA);
|
||||
expect(store.state.jupiler).toBe(jupiler);
|
||||
});
|
||||
|
||||
test("interaction between mutations on sibling properties", async () => {
|
||||
const state = { jupiler: { maes: 1 }, stella: 3 };
|
||||
const mutations = {
|
||||
dostuffA(state, inc) {
|
||||
state.stella += inc;
|
||||
},
|
||||
dostuffB(state, inc) {
|
||||
state.jupiler.maes += inc;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
store.commit("dostuffA", 10);
|
||||
const jupiler = store.state.jupiler;
|
||||
store.commit("dostuffB", 10);
|
||||
expect(store.state.jupiler).not.toBe(jupiler);
|
||||
});
|
||||
|
||||
test("can use object assign in store", async () => {
|
||||
const mutations = {
|
||||
dosomething(state) {
|
||||
Object.assign(state.westmalle, { a: 3, b: 4 });
|
||||
}
|
||||
};
|
||||
const store = new Store({
|
||||
state: { westmalle: { a: 1, b: 2 } },
|
||||
mutations
|
||||
});
|
||||
store.commit("dosomething");
|
||||
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
|
||||
});
|
||||
|
||||
test("aku reactive store state 1", async () => {
|
||||
const mutations = {
|
||||
inc(state) {
|
||||
state.counter++;
|
||||
}
|
||||
};
|
||||
const state = { counter: 0 };
|
||||
const store = new Store({ state, mutations });
|
||||
const curState = store.state;
|
||||
expect(store.state.counter).toBe(0);
|
||||
store.commit("inc", {});
|
||||
expect(store.state.counter).toBe(1);
|
||||
expect(store.state).not.toBe(curState);
|
||||
});
|
||||
|
||||
test("aku reactive store state 2", async () => {
|
||||
const mutations = {
|
||||
inc(state) {
|
||||
state.convo.counter++;
|
||||
}
|
||||
};
|
||||
const state = { convo: { counter: 0 } };
|
||||
const store = new Store({ state, mutations });
|
||||
const curState = store.state;
|
||||
const convo = state.convo;
|
||||
expect(store.state.convo.counter).toBe(0);
|
||||
store.commit("inc", {});
|
||||
expect(store.state.convo.counter).toBe(1);
|
||||
expect(store.state.convo).not.toBe(convo);
|
||||
expect(store.state).toBe(curState);
|
||||
});
|
||||
|
||||
test("aku reactive store state 3", async () => {
|
||||
const mutations = {
|
||||
inc1(state) {
|
||||
state.threads[1].counter++;
|
||||
},
|
||||
inc2(state) {
|
||||
state.threads[2].counter++;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
threads: {
|
||||
1: { counter: 0 },
|
||||
2: { counter: 0 }
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
const curState = store.state;
|
||||
const threads = state.threads;
|
||||
const thread1 = state.threads[1];
|
||||
const thread2 = state.threads[2];
|
||||
expect(store.state.threads[1].counter).toBe(0);
|
||||
expect(store.state.threads[2].counter).toBe(0);
|
||||
store.commit("inc1", {});
|
||||
expect(store.state.threads[1].counter).toBe(1);
|
||||
expect(store.state.threads[2].counter).toBe(0);
|
||||
expect(store.state.threads[1]).not.toBe(thread1);
|
||||
expect(store.state.threads[2]).toBe(thread2);
|
||||
expect(store.state.threads).toBe(threads);
|
||||
expect(store.state).toBe(curState);
|
||||
const newThread1 = curState.threads[1];
|
||||
store.commit("inc2", {});
|
||||
expect(store.state.threads[1].counter).toBe(1);
|
||||
expect(store.state.threads[2].counter).toBe(1);
|
||||
expect(store.state.threads[1]).toBe(newThread1);
|
||||
expect(store.state.threads[2]).not.toBe(thread2);
|
||||
expect(store.state.threads).toBe(threads);
|
||||
expect(store.state).toBe(curState);
|
||||
});
|
||||
|
||||
test("aku reactive store state 4", async () => {
|
||||
const mutations = {
|
||||
incT(state) {
|
||||
state.threads[1].counter++;
|
||||
},
|
||||
incM(state) {
|
||||
state.messages[1].counter++;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
threads: {
|
||||
1: { counter: 0 }
|
||||
},
|
||||
messages: {
|
||||
1: { counter: 0 }
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
const curState = store.state;
|
||||
const threads = state.threads;
|
||||
const messages = state.messages;
|
||||
const thread = state.threads[1];
|
||||
const message = state.messages[1];
|
||||
expect(store.state.threads[1].counter).toBe(0);
|
||||
expect(store.state.messages[1].counter).toBe(0);
|
||||
store.commit("incT", {});
|
||||
expect(store.state.threads[1].counter).toBe(1);
|
||||
expect(store.state.messages[1].counter).toBe(0);
|
||||
expect(store.state.threads[1]).not.toBe(thread);
|
||||
expect(store.state.messages[1]).toBe(message);
|
||||
expect(store.state.threads).toBe(threads);
|
||||
expect(store.state.messages).toBe(messages);
|
||||
expect(store.state).toBe(curState);
|
||||
const newThread = curState.threads[1];
|
||||
store.commit("incM", {});
|
||||
expect(store.state.threads[1].counter).toBe(1);
|
||||
expect(store.state.messages[1].counter).toBe(1);
|
||||
expect(store.state.threads[1]).toBe(newThread);
|
||||
expect(store.state.messages[1]).not.toBe(message);
|
||||
expect(store.state.threads).toBe(threads);
|
||||
expect(store.state.messages).toBe(messages);
|
||||
expect(store.state).toBe(curState);
|
||||
});
|
||||
|
||||
test("aku reactive store state 5", async () => {
|
||||
const mutations = {
|
||||
inc(state) {
|
||||
state.counter++;
|
||||
},
|
||||
incT(state) {
|
||||
state.threads[1].counter++;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
threads: {
|
||||
1: { counter: 0 }
|
||||
},
|
||||
counter: 0
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
const curState = store.state;
|
||||
const threads = state.threads;
|
||||
const thread = state.threads[1];
|
||||
expect(store.state.counter).toBe(0);
|
||||
expect(store.state.threads[1].counter).toBe(0);
|
||||
store.commit("inc", {});
|
||||
expect(store.state.counter).toBe(1);
|
||||
expect(store.state.threads[1].counter).toBe(0);
|
||||
expect(store.state.threads[1]).toBe(thread);
|
||||
expect(store.state.threads).toBe(threads);
|
||||
expect(store.state).not.toBe(curState);
|
||||
const newCurState = store.state;
|
||||
store.commit("incT", {});
|
||||
expect(store.state.counter).toBe(1);
|
||||
expect(store.state.threads[1].counter).toBe(1);
|
||||
expect(store.state.threads[1]).not.toBe(thread);
|
||||
expect(store.state.threads).toBe(threads);
|
||||
expect(store.state).toBe(newCurState);
|
||||
});
|
||||
|
||||
test("aku reactive store state 6", async () => {
|
||||
const mutations = {
|
||||
inc1(state) {
|
||||
state.threads[1].c1++;
|
||||
},
|
||||
inc2(state) {
|
||||
state.threads[1].c2++;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
threads: {
|
||||
1: { c1: 0, c2: 0 }
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
const curState = store.state;
|
||||
const threads = state.threads;
|
||||
const thread = state.threads[1];
|
||||
expect(store.state.threads[1].c1).toBe(0);
|
||||
expect(store.state.threads[1].c2).toBe(0);
|
||||
store.commit("inc1", {});
|
||||
expect(store.state.threads[1].c1).toBe(1);
|
||||
expect(store.state.threads[1].c2).toBe(0);
|
||||
expect(store.state.threads[1]).not.toBe(thread);
|
||||
expect(store.state.threads).toBe(threads);
|
||||
expect(store.state).toBe(curState);
|
||||
const newThread = store.state.threads[1];
|
||||
store.commit("inc2", {});
|
||||
expect(store.state.threads[1].c1).toBe(1);
|
||||
expect(store.state.threads[1].c2).toBe(1);
|
||||
expect(store.state.threads[1]).not.toBe(newThread);
|
||||
expect(store.state.threads).toBe(threads);
|
||||
expect(store.state).toBe(curState);
|
||||
});
|
||||
|
||||
test("aku reactive store state 7", async () => {
|
||||
const mutations = {
|
||||
register(state) {
|
||||
state.threads[1].messages.push(1);
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
threads: {
|
||||
1: {
|
||||
messages: []
|
||||
}
|
||||
},
|
||||
messages: {
|
||||
1: {}
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
const curState = store.state;
|
||||
const threads = state.threads;
|
||||
const messages = state.messages;
|
||||
const thread = state.threads[1];
|
||||
const threadMessages = state.threads[1].messages;
|
||||
const message = state.messages[1];
|
||||
expect(store.state.threads[1].messages.length).toBe(0);
|
||||
store.commit("register", {});
|
||||
expect(store.state.threads[1].messages.length).toBe(1);
|
||||
expect(store.state.threads[1].messages[0]).toBe(1);
|
||||
expect(store.state.threads[1].messages).not.toBe(threadMessages);
|
||||
expect(store.state.threads[1]).toBe(thread);
|
||||
expect(store.state.threads).toBe(threads);
|
||||
expect(store.state.messages[1]).toBe(message);
|
||||
expect(store.state.messages).toBe(messages);
|
||||
expect(store.state).toBe(curState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updates triggered by the store", () => {
|
||||
test("multiple commits trigger one update", async () => {
|
||||
let updateCounter = 0;
|
||||
const state = { n: 1 };
|
||||
@@ -59,6 +467,26 @@ describe("basic use", () => {
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(1);
|
||||
});
|
||||
|
||||
test("empty commits do not trigger updates", async () => {
|
||||
let updateCounter = 0;
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
inc(state, delta) {
|
||||
state.n += delta;
|
||||
},
|
||||
noop() {}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
store.on("update", null, () => updateCounter++);
|
||||
|
||||
store.commit("noop");
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(0);
|
||||
store.commit("inc", 50);
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("connecting a component to store", () => {
|
||||
@@ -94,7 +522,10 @@ describe("connecting a component to store", () => {
|
||||
state.todos.push({ msg });
|
||||
}
|
||||
};
|
||||
const TodoApp = connect(s => s)(App);
|
||||
function mapStateToProps(s) {
|
||||
return { todos: s.todos };
|
||||
}
|
||||
const TodoApp = connect(mapStateToProps)(App);
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
const app = new TodoApp(env);
|
||||
@@ -117,9 +548,6 @@ describe("connecting a component to store", () => {
|
||||
willUnmount() {
|
||||
steps.push("child:willUnmount");
|
||||
}
|
||||
destroyed() {
|
||||
steps.push("child:destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectedChild = connect(s => s)(Child);
|
||||
@@ -145,11 +573,7 @@ describe("connecting a component to store", () => {
|
||||
expect(steps).toEqual(["child:mounted"]);
|
||||
|
||||
await parent.updateState({ child: false });
|
||||
expect(steps).toEqual([
|
||||
"child:mounted",
|
||||
"child:willUnmount",
|
||||
"child:destroyed"
|
||||
]);
|
||||
expect(steps).toEqual(["child:mounted", "child:willUnmount"]);
|
||||
});
|
||||
|
||||
test("connect receives ownprops as second argument", async () => {
|
||||
@@ -179,7 +603,10 @@ describe("connecting a component to store", () => {
|
||||
widgets = { ConnectedTodo };
|
||||
}
|
||||
|
||||
const ConnectedTodoList = connect(state => state)(TodoList);
|
||||
function mapStateToProps(state) {
|
||||
return { todos: state.todos };
|
||||
}
|
||||
const ConnectedTodoList = connect(mapStateToProps)(TodoList);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new ConnectedTodoList(env);
|
||||
@@ -193,4 +620,32 @@ describe("connecting a component to store", () => {
|
||||
"<div><span>jupiler</span><span>hoegaarden</span></div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("connected component is updated when props are updated", async () => {
|
||||
class Beer extends Component<any, any, any> {
|
||||
inlineTemplate = `<span><t t-esc="props.name"/></span>`;
|
||||
}
|
||||
const ConnectedBeer = connect((state, props) => {
|
||||
return state.beers[props.id];
|
||||
})(Beer);
|
||||
|
||||
class App extends Component<any, any, any> {
|
||||
inlineTemplate = `<div>
|
||||
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
|
||||
</div>`;
|
||||
widgets = { ConnectedBeer };
|
||||
state = { beerId: 1 };
|
||||
}
|
||||
|
||||
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
|
||||
const store = new Store({ state });
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
await app.updateState({ beerId: 2 });
|
||||
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user