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
|
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).
|
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
|
## Installing/Building
|
||||||
|
|
||||||
Some npm scripts are available:
|
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
|
## License
|
||||||
|
|
||||||
React is [GPL licensed](./LICENSE).
|
OWL is [GPL licensed](./LICENSE).
|
||||||
|
|||||||
+94
-10
@@ -7,16 +7,8 @@ For example:
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
export class Counter extends Component {
|
export class Counter extends Component {
|
||||||
inlineTemplate = `
|
template = "counter";
|
||||||
<div t-name="counter">
|
state = { counter: 0 };
|
||||||
<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
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(parent, props) {
|
constructor(parent, props) {
|
||||||
super(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">
|
<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="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}" />
|
<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">
|
<div class="o_notification_container">
|
||||||
<t t-foreach="state.notifications" t-as="notif">
|
<t t-foreach="state.notifications" t-as="notif">
|
||||||
<t t-widget="Notification" t-props="notif"/>
|
<t t-widget="Notification" t-props="notif"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div t-name="web.navbar" class="o_navbar" t-att-class="props.inHome ? 'o_in_home' : ''">
|
<div t-name="web.navbar" class="o_navbar" t-att-class="props.inHome ? 'o_in_home' : ''">
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "owl",
|
"name": "owl",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"description": "Odoo Web Lab",
|
"description": "Odoo Web Lab",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+44
-22
@@ -149,6 +149,28 @@ export class Component<
|
|||||||
*/
|
*/
|
||||||
mounted() {}
|
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,
|
* This hook is called whenever a component did actually update its props,
|
||||||
* state or env.
|
* state or env.
|
||||||
@@ -156,12 +178,18 @@ export class Component<
|
|||||||
* This method is not called on the initial render. It is useful to interact
|
* 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
|
* with the DOM (for example, through an external library) whenever the
|
||||||
* component was updated.
|
* 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
|
* willUnmount is a hook that is called each time just before a component is
|
||||||
* the DOM. This is a good place to remove some listeners, for example.
|
* unmounted from the DOM. This is a good place to remove some listeners, for
|
||||||
|
* example.
|
||||||
*
|
*
|
||||||
* Note: this method should not be called manually.
|
* Note: this method should not be called manually.
|
||||||
*
|
*
|
||||||
@@ -169,14 +197,6 @@ export class Component<
|
|||||||
*/
|
*/
|
||||||
willUnmount() {}
|
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
|
// Public
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -200,7 +220,7 @@ export class Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async mount(target: HTMLElement): Promise<void> {
|
async mount(target: HTMLElement): Promise<void> {
|
||||||
const vnode = await this._start();
|
const vnode = await this._prepare();
|
||||||
if (this.__widget__.isDestroyed) {
|
if (this.__widget__.isDestroyed) {
|
||||||
// widget was destroyed before we get here...
|
// widget was destroyed before we get here...
|
||||||
return;
|
return;
|
||||||
@@ -220,7 +240,7 @@ export class Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detach() {
|
unmount() {
|
||||||
if (this.el) {
|
if (this.el) {
|
||||||
this._visitSubTree(w => {
|
this._visitSubTree(w => {
|
||||||
if (w.__widget__.isMounted) {
|
if (w.__widget__.isMounted) {
|
||||||
@@ -268,7 +288,6 @@ export class Component<
|
|||||||
}
|
}
|
||||||
this.clear();
|
this.clear();
|
||||||
this.__widget__.isDestroyed = true;
|
this.__widget__.isDestroyed = true;
|
||||||
this.destroyed();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +312,7 @@ export class Component<
|
|||||||
if (this.__widget__.isMounted) {
|
if (this.__widget__.isMounted) {
|
||||||
await this.render(true);
|
await this.render(true);
|
||||||
}
|
}
|
||||||
this.componentDidUpdate();
|
this.patched();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProps(
|
async updateProps(
|
||||||
@@ -325,7 +344,7 @@ export class Component<
|
|||||||
if (this.__widget__.isStarted) {
|
if (this.__widget__.isStarted) {
|
||||||
await this.render();
|
await this.render();
|
||||||
}
|
}
|
||||||
this.componentDidUpdate();
|
this.patched();
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -333,19 +352,22 @@ export class Component<
|
|||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
async _updateProps(nextProps: Props): Promise<void> {
|
async _updateProps(nextProps: Props): Promise<void> {
|
||||||
|
await this.willUpdateProps(nextProps);
|
||||||
this.props = nextProps;
|
this.props = nextProps;
|
||||||
await this.render();
|
await this.render();
|
||||||
this.componentDidUpdate();
|
this.patched();
|
||||||
}
|
}
|
||||||
|
|
||||||
_patch(vnode) {
|
_patch(vnode) {
|
||||||
this.__widget__.renderPromise = null;
|
this.__widget__.renderPromise = null;
|
||||||
this.__widget__.vnode = patch(
|
if (this.__widget__.vnode) {
|
||||||
this.__widget__.vnode || document.createElement(vnode.sel!),
|
this.willPatch();
|
||||||
vnode
|
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__.renderProps = this.props;
|
||||||
this.__widget__.renderPromise = this.willStart().then(() => {
|
this.__widget__.renderPromise = this.willStart().then(() => {
|
||||||
if (this.__widget__.isDestroyed) {
|
if (this.__widget__.isDestroyed) {
|
||||||
|
|||||||
+13
-9
@@ -900,10 +900,12 @@ const onDirective: Directive = {
|
|||||||
const refDirective: Directive = {
|
const refDirective: Directive = {
|
||||||
name: "ref",
|
name: "ref",
|
||||||
priority: 95,
|
priority: 95,
|
||||||
atNodeCreation({ ctx, node, nodeID }) {
|
atNodeCreation({ ctx, node }) {
|
||||||
let ref = node.getAttribute("t-ref");
|
let ref = node.getAttribute("t-ref")!;
|
||||||
ctx.addLine(`p${ctx.parentNode}.hook = {
|
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");
|
let ref = node.getAttribute("t-ref");
|
||||||
if (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();
|
||||||
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${
|
`def${defID} = def${defID}.then(vnode=>{let pvnode=h(vnode.sel, {key: ${templateID}});c${
|
||||||
ctx.parentNode
|
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}.${
|
}[_${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}.${
|
}()},destroy(){w${widgetID}.${
|
||||||
keepAlive ? "detach" : "destroy"
|
keepAlive ? "unmount" : "destroy"
|
||||||
}()}}; w${widgetID}.__widget__.pvnode = pvnode;});`
|
}()}}; w${widgetID}.__widget__.pvnode = pvnode;});`
|
||||||
);
|
);
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
ctx.addLine(
|
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}.${
|
`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"
|
keepAlive ? "unmount" : "destroy"
|
||||||
}()}}}c${ctx.parentNode}[_${dummyID}_index]=vnode;});`
|
}()}, destroy() {w${widgetID}.${keepAlive ? "unmount" : "destroy"}()}}}c${
|
||||||
|
ctx.parentNode
|
||||||
|
}[_${dummyID}_index]=vnode;});`
|
||||||
);
|
);
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
|
|
||||||
|
|||||||
+180
-67
@@ -2,6 +2,95 @@ import { EventBus } from "./event_bus";
|
|||||||
import { Component } from "./component";
|
import { Component } from "./component";
|
||||||
import { shallowEqual } from "./utils";
|
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) {
|
export function connect(mapStateToProps) {
|
||||||
return function(Comp) {
|
return function(Comp) {
|
||||||
return class extends Comp {
|
return class extends Comp {
|
||||||
@@ -30,6 +119,12 @@ export function connect(mapStateToProps) {
|
|||||||
super.willUnmount();
|
super.willUnmount();
|
||||||
}
|
}
|
||||||
updateProps(nextProps, forceUpdate) {
|
updateProps(nextProps, forceUpdate) {
|
||||||
|
if (this.__widget__.ownProps !== nextProps) {
|
||||||
|
this.__widget__.currentStoreProps = mapStateToProps(
|
||||||
|
this.env.store.state,
|
||||||
|
nextProps
|
||||||
|
);
|
||||||
|
}
|
||||||
this.__widget__.ownProps = nextProps;
|
this.__widget__.ownProps = nextProps;
|
||||||
const mergedProps = Object.assign(
|
const mergedProps = Object.assign(
|
||||||
{},
|
{},
|
||||||
@@ -42,74 +137,92 @@ export function connect(mapStateToProps) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoreConfig {
|
function _magifyArray({ raw, key, parent, magic, onDirty }) {
|
||||||
state?: any;
|
Object.defineProperty(magic, "length", {
|
||||||
actions?: any;
|
get() {
|
||||||
mutations?: any;
|
return magic.raw.length;
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
Object.assign(magic, {
|
||||||
get state() {
|
push: function(item) {
|
||||||
return this._clone(this._state);
|
onDirty();
|
||||||
}
|
parent.raw[key] = [...magic.raw, item];
|
||||||
|
magic.raw = parent.raw[key];
|
||||||
dispatch(action, payload) {
|
const index = magic.raw.length - 1;
|
||||||
if (!this.actions[action]) {
|
let prop = magify({ raw: item, key: index, parent: magic, onDirty });
|
||||||
throw new Error(`[Error] action ${action} is undefined`);
|
Object.defineProperty(magic, index, {
|
||||||
}
|
set(newVal) {
|
||||||
this.actions[action](
|
onDirty();
|
||||||
{
|
parent.raw[key] = [...magic.raw];
|
||||||
commit: this.commit.bind(this),
|
parent.raw[key][index] = newVal;
|
||||||
state: this.state
|
magic.raw = parent.raw[key];
|
||||||
},
|
prop = magify({ raw: newVal, key: index, parent: magic, onDirty });
|
||||||
payload
|
},
|
||||||
);
|
get() {
|
||||||
}
|
return prop;
|
||||||
|
}
|
||||||
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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await Promise.resolve();
|
});
|
||||||
if (this._isMutating) {
|
raw.forEach((value, index) => {
|
||||||
this._isMutating = false;
|
let prop = magify({
|
||||||
this.trigger("update", this.state);
|
raw: value,
|
||||||
}
|
key: index,
|
||||||
}
|
parent: magic,
|
||||||
|
onDirty
|
||||||
_clone(obj) {
|
});
|
||||||
return JSON.parse(JSON.stringify(obj));
|
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 {
|
} else {
|
||||||
w7 = new context.widgets['ChildWidget'](owner, props7);
|
w7 = new context.widgets['ChildWidget'](owner, props7);
|
||||||
context.__widget__.cmap[key8] = w7.__widget__.id;
|
context.__widget__.cmap[key8] = w7.__widget__.id;
|
||||||
def6 = w7._start();
|
def6 = w7._prepare();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isNew7) {
|
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;});
|
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 {
|
} 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);
|
extra.promises.push(def6);
|
||||||
}
|
}
|
||||||
@@ -95,13 +95,13 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
|||||||
} else {
|
} else {
|
||||||
w4 = new context.widgets['child'](owner, props4);
|
w4 = new context.widgets['child'](owner, props4);
|
||||||
context.__widget__.cmap[key5] = w4.__widget__.id;
|
context.__widget__.cmap[key5] = w4.__widget__.id;
|
||||||
def3 = w4._start();
|
def3 = w4._prepare();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isNew4) {
|
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;});
|
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 {
|
} 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);
|
extra.promises.push(def3);
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -141,13 +141,13 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
|
|||||||
} else {
|
} else {
|
||||||
w4 = new context.widgets['child'](owner, props4);
|
w4 = new context.widgets['child'](owner, props4);
|
||||||
context.__widget__.cmap[4] = w4.__widget__.id;
|
context.__widget__.cmap[4] = w4.__widget__.id;
|
||||||
def3 = w4._start();
|
def3 = w4._prepare();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isNew4) {
|
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;});
|
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 {
|
} 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);
|
extra.promises.push(def3);
|
||||||
return vn1;
|
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`] = `
|
exports[`t-ref can get a ref on a node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
+111
-48
@@ -312,21 +312,53 @@ describe("lifecycle hooks", () => {
|
|||||||
willUnmount() {
|
willUnmount() {
|
||||||
steps.push("willunmount");
|
steps.push("willunmount");
|
||||||
}
|
}
|
||||||
destroyed() {
|
|
||||||
steps.push("destroyed");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const widget = new ParentWidget(env);
|
const widget = new ParentWidget(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
expect(steps).toEqual(["init", "willstart", "mounted"]);
|
expect(steps).toEqual(["init", "willstart", "mounted"]);
|
||||||
await widget.updateState({ ok: false });
|
await widget.updateState({ ok: false });
|
||||||
expect(steps).toEqual([
|
expect(steps).toEqual(["init", "willstart", "mounted", "willunmount"]);
|
||||||
"init",
|
});
|
||||||
"willstart",
|
|
||||||
"mounted",
|
test("widgets are unmounted and destroyed if no longer in DOM, even after updateprops", async () => {
|
||||||
"willunmount",
|
let childUnmounted = false;
|
||||||
"destroyed"
|
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 () => {
|
test("hooks are called in proper order in widget creation/destruction", async () => {
|
||||||
@@ -347,9 +379,6 @@ describe("lifecycle hooks", () => {
|
|||||||
willUnmount() {
|
willUnmount() {
|
||||||
steps.push("p willunmount");
|
steps.push("p willunmount");
|
||||||
}
|
}
|
||||||
destroyed() {
|
|
||||||
steps.push("p destroyed");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChildWidget extends Widget {
|
class ChildWidget extends Widget {
|
||||||
@@ -366,9 +395,6 @@ describe("lifecycle hooks", () => {
|
|||||||
willUnmount() {
|
willUnmount() {
|
||||||
steps.push("c willunmount");
|
steps.push("c willunmount");
|
||||||
}
|
}
|
||||||
destroyed() {
|
|
||||||
steps.push("c destroyed");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const widget = new ParentWidget(env);
|
const widget = new ParentWidget(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
@@ -381,19 +407,38 @@ describe("lifecycle hooks", () => {
|
|||||||
"p mounted",
|
"p mounted",
|
||||||
"c mounted",
|
"c mounted",
|
||||||
"c willunmount",
|
"c willunmount",
|
||||||
"c destroyed",
|
"p willunmount"
|
||||||
"p willunmount",
|
|
||||||
"p destroyed"
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
let n = 0;
|
||||||
|
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
state = { a: 1 };
|
state = { a: 1 };
|
||||||
|
|
||||||
componentDidUpdate() {
|
patched() {
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,11 +453,11 @@ describe("lifecycle hooks", () => {
|
|||||||
expect(n).toBe(1);
|
expect(n).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("componentDidUpdate hook is called after updateProps", async () => {
|
test("patched hook is called after updateProps", async () => {
|
||||||
let n = 0;
|
let n = 0;
|
||||||
|
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
componentDidUpdate() {
|
patched() {
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,13 +469,13 @@ describe("lifecycle hooks", () => {
|
|||||||
expect(n).toBe(1);
|
expect(n).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("componentDidUpdate hook is called after updateEnv", async () => {
|
test("patched hook is called after updateEnv", async () => {
|
||||||
let n = 0;
|
let n = 0;
|
||||||
|
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
state = { a: 1 };
|
state = { a: 1 };
|
||||||
|
|
||||||
componentDidUpdate() {
|
patched() {
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,7 +506,6 @@ describe("lifecycle hooks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("sub widget (inside sub node): hooks are correctly called", async () => {
|
test("sub widget (inside sub node): hooks are correctly called", async () => {
|
||||||
let destroyed = false;
|
|
||||||
let created = false;
|
let created = false;
|
||||||
let mounted = false;
|
let mounted = false;
|
||||||
class ParentWidget extends Widget {
|
class ParentWidget extends Widget {
|
||||||
@@ -483,9 +527,6 @@ describe("lifecycle hooks", () => {
|
|||||||
mounted() {
|
mounted() {
|
||||||
mounted = true;
|
mounted = true;
|
||||||
}
|
}
|
||||||
destroyed() {
|
|
||||||
destroyed = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const widget = new ParentWidget(env);
|
const widget = new ParentWidget(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
@@ -494,9 +535,46 @@ describe("lifecycle hooks", () => {
|
|||||||
await widget.updateState({ flag: true });
|
await widget.updateState({ flag: true });
|
||||||
expect(mounted).toBe(true);
|
expect(mounted).toBe(true);
|
||||||
expect(created).toBe(true);
|
expect(created).toBe(true);
|
||||||
expect(destroyed).toBe(false);
|
|
||||||
await widget.updateState({ flag: 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);
|
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 () => {
|
test("destroying a parent also destroys its children", async () => {
|
||||||
const parent = new WidgetA(env);
|
const parent = new WidgetA(env);
|
||||||
await parent.mount(fixture);
|
await parent.mount(fixture);
|
||||||
@@ -589,7 +652,7 @@ describe("composition", () => {
|
|||||||
|
|
||||||
test("t-refs on widget are widgets", async () => {
|
test("t-refs on widget are widgets", async () => {
|
||||||
class WidgetC extends Widget {
|
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 };
|
widgets = { b: WidgetB };
|
||||||
}
|
}
|
||||||
const widget = new WidgetC(env);
|
const widget = new WidgetC(env);
|
||||||
@@ -1229,7 +1292,7 @@ describe("updating environment", () => {
|
|||||||
expect(n).toBe(1);
|
expect(n).toBe(1);
|
||||||
await widget.updateEnv(<any>{ somekey: 5 });
|
await widget.updateEnv(<any>{ somekey: 5 });
|
||||||
expect(n).toBe(2);
|
expect(n).toBe(2);
|
||||||
widget.detach();
|
widget.unmount();
|
||||||
expect(n).toBe(2);
|
expect(n).toBe(2);
|
||||||
await widget.updateEnv(<any>{ somekey: 5 });
|
await widget.updateEnv(<any>{ somekey: 5 });
|
||||||
expect(n).toBe(2);
|
expect(n).toBe(2);
|
||||||
|
|||||||
+8
-1
@@ -893,11 +893,18 @@ describe("t-on", () => {
|
|||||||
|
|
||||||
describe("t-ref", () => {
|
describe("t-ref", () => {
|
||||||
test("can get a ref on a node", () => {
|
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 = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs });
|
renderToDOM(qweb, "test", { refs });
|
||||||
expect(refs.myspan.tagName).toBe("SPAN");
|
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", () => {
|
describe("loading templates", () => {
|
||||||
|
|||||||
+465
-10
@@ -41,6 +41,414 @@ describe("basic use", () => {
|
|||||||
expect(store.state.n).toBe(15);
|
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 () => {
|
test("multiple commits trigger one update", async () => {
|
||||||
let updateCounter = 0;
|
let updateCounter = 0;
|
||||||
const state = { n: 1 };
|
const state = { n: 1 };
|
||||||
@@ -59,6 +467,26 @@ describe("basic use", () => {
|
|||||||
await nextMicroTick();
|
await nextMicroTick();
|
||||||
expect(updateCounter).toBe(1);
|
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", () => {
|
describe("connecting a component to store", () => {
|
||||||
@@ -94,7 +522,10 @@ describe("connecting a component to store", () => {
|
|||||||
state.todos.push({ msg });
|
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 });
|
const store = new Store({ state, mutations });
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new TodoApp(env);
|
const app = new TodoApp(env);
|
||||||
@@ -117,9 +548,6 @@ describe("connecting a component to store", () => {
|
|||||||
willUnmount() {
|
willUnmount() {
|
||||||
steps.push("child:willUnmount");
|
steps.push("child:willUnmount");
|
||||||
}
|
}
|
||||||
destroyed() {
|
|
||||||
steps.push("child:destroyed");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectedChild = connect(s => s)(Child);
|
const ConnectedChild = connect(s => s)(Child);
|
||||||
@@ -145,11 +573,7 @@ describe("connecting a component to store", () => {
|
|||||||
expect(steps).toEqual(["child:mounted"]);
|
expect(steps).toEqual(["child:mounted"]);
|
||||||
|
|
||||||
await parent.updateState({ child: false });
|
await parent.updateState({ child: false });
|
||||||
expect(steps).toEqual([
|
expect(steps).toEqual(["child:mounted", "child:willUnmount"]);
|
||||||
"child:mounted",
|
|
||||||
"child:willUnmount",
|
|
||||||
"child:destroyed"
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("connect receives ownprops as second argument", async () => {
|
test("connect receives ownprops as second argument", async () => {
|
||||||
@@ -179,7 +603,10 @@ describe("connecting a component to store", () => {
|
|||||||
widgets = { ConnectedTodo };
|
widgets = { ConnectedTodo };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectedTodoList = connect(state => state)(TodoList);
|
function mapStateToProps(state) {
|
||||||
|
return { todos: state.todos };
|
||||||
|
}
|
||||||
|
const ConnectedTodoList = connect(mapStateToProps)(TodoList);
|
||||||
|
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new ConnectedTodoList(env);
|
const app = new ConnectedTodoList(env);
|
||||||
@@ -193,4 +620,32 @@ describe("connecting a component to store", () => {
|
|||||||
"<div><span>jupiler</span><span>hoegaarden</span></div>"
|
"<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