Compare commits

...

13 Commits

Author SHA1 Message Date
Géry Debongnie 211f6ebdd6 [REL] v1.0.6 2020-04-17 15:16:24 +02:00
Géry Debongnie 1707bd240d [IMP] add browser bindings to standard environment
This could be done by each application, but it does cost only a few
lines of code, and it helps standardizing the Owl ecosystem.

For example, some library (such as o_spreadsheet) needs to mock side
effects, and Odoo also needs to do that, so this prevents duplicated effort.

closes #686
2020-04-17 13:39:46 +02:00
Géry Debongnie 142b69823f [IMP] types: do not make Env an indexed type
Before this commit, Env was an indexed type, this means that one could
write env.anything, and it would accept it as a valid type. This is
actually quite dangerous, because we lose the typing advantages for all
keys that are properly defined.

For example, if a component is defined as:

class MyComponent extends Component<Props> {
 ...
}

Then Typescript will let it use anything from the environment, even if
it is wrong.  So, most properly typed Typescript applications should use
instead a sub environment:

interface MyAppEnv extends Env {
  someKey: someValue
}

Then, the component should be defined this way:

class MyComponent extends Component<Props, MyAppEnv> {
 ...
}

Before this commit, any typos in the environment accesses would not be
noticed by typescript.
2020-04-17 13:39:46 +02:00
Joseph Caburnay 94c8bce810 [IMP] allow anonymous Component extensions
It is possible that a Component is extended dynamically and if this is
the case, the class that extends it can be anonymous, with property
name=''. If this is the case, current implementation interprets the empty
string to be false so the while loop is terminated without further
scanning the super classes.

In this proposal, we allow anonymous class to be scanned until its
Component ancestor. Basically, the anonymous class assumes the name of
it super.
2020-04-15 08:54:02 +02:00
Géry Debongnie ae172d42e7 [IMP] slots: add new t-set-slot directive
This new t-set-slot directive is meant to replace t-set when we need to
define the content of a sub slot. All new code should use that
directive.

The old t-set directive is still supported for now, but this should be
removed when we publish Owl 2.0.
2020-04-06 15:42:32 +02:00
Géry Debongnie ddf30a8a97 [CLEANUP] run prettier on the codebase 2020-04-06 15:42:32 +02:00
Géry Debongnie 8d0d8538ad [FIX] slots: better heuristic to determine named slot content
Unfortunately, we chose to use the directive `t-set` to define sub slot
contents in a template.

The goal was to reuse a directive for a similar use case: defining sub
template is almost the same as defining a slot content.

Obviously, this introduces a name conflict: the inner content of a
component cannot use t-set t-value anymore (nor t-set with a body
value), since they are interpreted as slot names.

We mitigate the issue here by only interpreting as slot content the
`t-set` statement located immediately below the parent component tag
name and with a body content.

However, a real fix need to introduce an additional directive to resolve
the ambiguity.
2020-04-06 15:42:32 +02:00
Joseph Caburnay 2b0315c03f [FIX] hooks: order of brackets for multiple async hooks 2020-03-30 08:46:16 +02:00
lucas c469cac315 [FIX] qweb: protect scope in t-call writing recursively
Have something like
```xml
 <div t-name="Parent">
   <t t-call="nodeTemplate">
     <t t-set="recursive_idx" t-value="1"/>
     <t t-set="node" t-value="root"/>
   </t>
 </div>

 <div t-name="nodeTemplate">
   <t t-set="recursive_idx" t-value="recursive_idx + 1"/>
   <p><t t-esc="node.val"/> <t t-esc="recursive_idx"/></p>
   <t t-foreach="node.children or []" t-as="subtree">
     <t t-call="nodeTemplate">
       <t t-set="node" t-value="subtree"/>
     </t>
   </t>
 </div>
```

Where we want to propagate a recursion index through recursive t-calls

Before this commit, it did not work as we protected the scope in order
to not leak, in the wronf manner. Namely the protected scope only took
firt level prototype properties of the original scope.

After this commit, this case works as we mark the scope as read only

solves #672
2020-03-12 13:15:28 +01:00
Lucas Perais (lpe) b4ad14edc0 [FIX] component: self mounting position keeps the reference
Have

```xml
<body t-name="webclient" />
```
and
```js

const comp = new WebClient();
comp.mount(document.body, {position: 'self'});
```

Before this commit, the body that was there before anything had happened
was *replaced* by the new body node created by the WebClient OWL component

After this commit, we ensure that the element body is the same at reference level
2020-03-05 16:35:33 +01:00
Aaron Bohy 718e5264ae [FIX] test: missing await 2020-03-03 10:42:22 +01:00
Aaron Bohy f5d019bb69 [FIX] component: concurrency issue with cancelled fiber
Since ee956a197, the rendering is skipped if the currentFiber is
completed. Unfortunately, cancelled fibers remain set in __owl__,
so when a fiber is cancelled, subsequent calls to render are
skipped.

It would be nice to reset __owl__.currentFiber to null when the
fiber is cancelled, but when trying to do so, a lot of tests fail.

Part of issue #622
Closes #665
2020-03-02 11:35:47 +01:00
Géry Debongnie afa36f52a0 [DOC] doc: fix t-debug example
also, remove a useless console.log

closes #661
2020-02-24 11:05:12 +01:00
32 changed files with 591 additions and 89 deletions
+2 -2
View File
@@ -121,8 +121,8 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.5.js](https://github.com/odoo/owl/releases/download/v1.0.5/owl.js)
- [owl-1.0.5.min.js](https://github.com/odoo/owl/releases/download/v1.0.5/owl.min.js)
- [owl-1.0.6.js](https://github.com/odoo/owl/releases/download/v1.0.6/owl.js)
- [owl-1.0.6.min.js](https://github.com/odoo/owl/releases/download/v1.0.6/owl.min.js)
## License
+25 -1
View File
@@ -6,6 +6,7 @@
- [Setting an Environment](#setting-an-environment)
- [Using a sub environment](#using-a-sub-environment)
- [Content of an Environment](#content-of-an-environment)
- [Special keys](#special-keys)
## Overview
@@ -93,7 +94,7 @@ Some good use cases for additional keys in the environment are:
- some configuration keys,
- session information,
- generic services (such as doing rpcs, or accessing local storage).
- generic services (such as doing rpcs).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
@@ -125,3 +126,26 @@ async function start() {
await app.mount(document.body);
}
```
## Special Keys
There are two special key/value added by Owl if not provided in the environment:
the `QWeb` instance and a `browser` object:
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
for Owl to be able to render anything
- `browser`: this is an object that contains some common access points to the
browser methods with a side effect. This is particularly useful when one want
to test more advanced components, and be able to mock those methods.
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+1 -1
View File
@@ -532,7 +532,7 @@ The javascript QWeb implementation provides two useful debugging directives:
```xml
<t t-if="a_test">
<t t-debug="">
<t t-debug=""/>
</t>
```
+6 -3
View File
@@ -37,16 +37,16 @@ rendering context.
</div>
```
Slots are defined by the caller, with the `t-set` directive:
Slots are defined by the caller, with the `t-set-slot` directive:
```xml
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="Some Dialog">
<t t-set="content">
<t t-set-slot="content">
<div>hey</div>
</t>
<t t-set="footer">
<t t-set-slot="footer">
<button t-on-click="doSomething">ok</button>
</t>
</Dialog>
@@ -57,6 +57,9 @@ In this example, the component `Dialog` will render the slots `content` and `foo
with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog.
Note: Owl previously used the `t-set` directive to define the content of a slot.
This is deprecated and should no longer be used in new code.
## Reference
Default slot: the first element inside the component which is not a named slot will
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "1.0.5",
"version": "1.0.6",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.js",
"types": "dist/types/index.d.ts",
+3 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.5
- Current version: 1.0.6
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
@@ -16,6 +16,8 @@ change!
### 2.x (2020? 2021? 2022?)
- stop support for `t-set` directive to define the content of a slot
Maybe:
- reimplement vdom to use *block* system, like Vue 3, which should make Owl
+23
View File
@@ -0,0 +1,23 @@
export interface Browser {
setTimeout: Window["setTimeout"];
clearTimeout: Window["clearTimeout"];
setInterval: Window["setInterval"];
clearInterval: Window["clearInterval"];
requestAnimationFrame: Window["requestAnimationFrame"];
random: Math["random"];
Date: typeof Date;
fetch: Window["fetch"];
localStorage: Window["localStorage"];
}
export const browser: Browser = {
setTimeout: window.setTimeout.bind(window),
clearTimeout: window.clearTimeout.bind(window),
setInterval: window.setInterval.bind(window),
clearInterval: window.clearInterval.bind(window),
requestAnimationFrame: window.requestAnimationFrame.bind(window),
random: Math.random,
Date: window.Date,
fetch: (window.fetch || (() => {})).bind(window),
localStorage: window.localStorage
};
+12 -6
View File
@@ -7,6 +7,7 @@ import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles";
import { Browser, browser } from "../browser";
/**
* Owl Component System
@@ -34,7 +35,7 @@ import { activateSheet } from "./styles";
*/
export interface Env {
qweb: QWeb;
[key: string]: any;
browser: Browser;
}
export type MountPosition = "first-child" | "last-child" | "self";
@@ -158,6 +159,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (!this.env.qweb) {
this.env.qweb = new QWeb();
}
if (!this.env.browser) {
this.env.browser = browser;
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
@@ -357,14 +361,15 @@ export class Component<Props extends {} = any, T extends Env = Env> {
*/
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (!__owl__.isMounted && !__owl__.currentFiber) {
const currentFiber = __owl__.currentFiber;
if (!__owl__.isMounted && !currentFiber) {
// if we get here, this means that the component was either never mounted,
// or was unmounted and some state change triggered a render. Either way,
// we do not want to actually render anything in this case.
return;
}
if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) {
return scheduler.addFiber(__owl__.currentFiber.root);
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
return scheduler.addFiber(currentFiber.root);
}
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
@@ -603,9 +608,10 @@ export class Component<Props extends {} = any, T extends Env = Env> {
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
let template: string;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
let template: string = p.name;
while (!(template in qweb.templates) && p !== Component) {
p = p.__proto__;
template = p.name;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
+24 -3
View File
@@ -400,15 +400,36 @@ QWeb.addDirective({
if (hasSlots) {
const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
// The next code is a fallback for compatibility reason. It accepts t-set
// elements that are direct children with a non empty body as nodes defining
// the content of a slot.
//
// This is wrong, but is necessary to prevent breaking all existing Owl
// code using slots. This will be removed in v2.0 someday. Meanwhile,
// please use t-set-slot everywhere you need to set the content of a
// slot.
for (let el of clone.children) {
if (el.getAttribute("t-set") && el.hasChildNodes()) {
slotNodes.push(el);
}
}
const slotId = QWeb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
let key = slotNode.getAttribute("t-set-slot")!;
slotNode.removeAttribute("t-set-slot");
// here again, this code should be removed when we stop supporting
// using t-set to define the content of named slots.
if (!key) {
key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
}
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
QWeb.slots[`${slotId}_${key}`] = slotFn;
}
+6
View File
@@ -225,6 +225,12 @@ export class Fiber {
`Cannot attach '${component.constructor.name}' to target node (not same tag name)`
);
}
// In self mode, we *know* we are to take possession of the target
// Hence we manually create the corresponding VNode and copy the "key" in data
const selfVnodeData = fiber.vnode!.data ? { key: fiber.vnode!.data.key } : {};
const selfVnode = h(fiber.vnode!.sel, selfVnodeData);
selfVnode.elm = target;
target = selfVnode;
} else {
target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!);
}
+4 -4
View File
@@ -1,4 +1,5 @@
import { Fiber } from "./fiber";
import { browser } from "../browser";
/**
* Owl Scheduler Class
@@ -19,9 +20,9 @@ interface Task {
export class Scheduler {
tasks: Task[] = [];
isRunning: boolean = false;
requestAnimationFrame: typeof window.requestAnimationFrame;
requestAnimationFrame: Window["requestAnimationFrame"];
constructor(requestAnimationFrame) {
constructor(requestAnimationFrame: Window["requestAnimationFrame"]) {
this.requestAnimationFrame = requestAnimationFrame;
}
@@ -109,5 +110,4 @@ export class Scheduler {
}
}
const raf = window.requestAnimationFrame.bind(window);
export const scheduler = new Scheduler(raf);
export const scheduler = new Scheduler(browser.requestAnimationFrame);
+1 -1
View File
@@ -70,7 +70,7 @@ function makeAsyncHook(method: string) {
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function(...args) {
return Promise.all[(current.call(component, ...args), cb.call(component, ...args))];
return Promise.all([current.call(component, ...args), cb.call(component, ...args)]);
};
} else {
component.__owl__[method] = cb;
+4 -3
View File
@@ -196,11 +196,12 @@ export class CompilationContext {
const protectID = this.generateID();
this.rootContext.protectedScopeNumber++;
this.rootContext.shouldDefineScope = true;
const scopeExpr = codeBlock
? `Object.create(scope);`
: `Object.assign(Object.create(context), scope);`;
const scopeExpr = `Object.create(scope);`;
this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = ${scopeExpr}`);
if (!codeBlock) {
this.addLine(`scope.__access_mode__ = 'ro';`);
}
return protectID;
}
stopProtectScope(protectID: number) {
+5 -1
View File
@@ -133,7 +133,11 @@ const UTILS: Utils = {
},
getScope(obj, property: string) {
const obj0 = obj;
while (obj && !obj.hasOwnProperty(property)) {
while (
obj &&
!obj.hasOwnProperty(property) &&
!(obj.hasOwnProperty("__access_mode__") && obj.__access_mode__ === "ro")
) {
const newObj = obj.__proto__;
if (!newObj || isComponent(newObj)) {
return obj0;
+2 -1
View File
@@ -1,7 +1,8 @@
import { Component } from "../component/component";
import { xml } from "../tags";
import { EnvWithRouter } from "./router";
export class RouteComponent extends Component {
export class RouteComponent extends Component<{}, EnvWithRouter> {
static template = xml`
<t>
<t
+9 -1
View File
@@ -49,6 +49,10 @@ interface Options {
mode: Router["mode"];
}
export interface EnvWithRouter extends Env {
router: Router;
}
const paramRegexp = /\{\{(.*?)\}\}/;
export class Router {
@@ -60,7 +64,11 @@ export class Router {
routeIds: string[];
env: RouterEnv;
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) {
constructor(
env: Partial<EnvWithRouter>,
routes: Partial<Route>[],
options: Options = { mode: "history" }
) {
env.router = this;
this.mode = options.mode;
this.env = env as RouterEnv;
+7 -3
View File
@@ -24,6 +24,10 @@ import { onWillUpdateProps } from "./hooks";
// Store Definition
//------------------------------------------------------------------------------
export interface EnvWithStore extends Env {
store: Store;
}
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
export type Getter = ({ state: any, getters }, payload?) => any;
@@ -83,7 +87,7 @@ interface SelectorOptions {
const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any {
const component: Component = Component.current!;
const component = Component.current as Component<any, EnvWithStore>;
const componentId = component.__owl__.id;
const store = options.store || (component.env.store as Store);
if (!(store instanceof Store)) {
@@ -149,11 +153,11 @@ export function useStore(selector, options: SelectorOptions = {}): any {
}
export function useDispatch(store?: Store): Store["dispatch"] {
store = store || (Component.current!.env.store as Store);
store = store || (Component.current!.env as EnvWithStore).store;
return store.dispatch.bind(store);
}
export function useGetters(store?: Store): Store["getters"] {
store = store || (Component.current!.env.store as Store);
store = store || (Component.current!.env as EnvWithStore).store;
return store.getters;
}
+5 -3
View File
@@ -10,6 +10,8 @@
* - debounce
*/
import { browser } from "./browser";
export function whenReady(fn?: any) {
return new Promise(function(resolve) {
if (document.readyState !== "loading") {
@@ -44,7 +46,7 @@ export function loadJS(url: string): Promise<void> {
}
export async function loadFile(url: string): Promise<string> {
const result = await fetch(url);
const result = await browser.fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
@@ -83,8 +85,8 @@ export function debounce(func: Function, wait: number, immediate?: boolean): Fun
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
browser.clearTimeout(timeout);
timeout = browser.setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
@@ -1282,7 +1282,8 @@ exports[`other directives with t-component t-set can't alter in t-call body 1`]
}
{
let _origScope4 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'iter').iter = 'inCall';
@@ -90,6 +90,96 @@ exports[`t-slot directive can define and call slots 4`] = `
}"
`;
exports[`t-slot directive can define and call slots using old t-set keyword 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Dialog'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive can define and call slots using old t-set keyword 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c6 = [], p6 = {key:6};
let vn6 = h('div', p6, c6);
let c7 = [], p7 = {key:7};
let vn7 = h('div', p7, c7);
c6.push(vn7);
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
c6.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot10) {
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
return vn6;
}"
`;
exports[`t-slot directive can define and call slots using old t-set keyword 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`header\`});
}"
`;
exports[`t-slot directive can define and call slots using old t-set keyword 4`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`footer\`});
}"
`;
exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra
) {
@@ -469,6 +559,21 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
}"
`;
exports[`t-slot directive t-set t-value in a slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
}
return vn4;
}"
`;
exports[`t-slot directive template can just return a slot 1`] = `
"function anonymous(context, extra
) {
+22
View File
@@ -1366,6 +1366,28 @@ describe("async rendering", () => {
await prom;
});
test("concurrent renderings scenario 17", async () => {
class Parent extends Component {
static template = xml`<span><t t-esc="state.value"/></span>`;
state = useState({ value: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>1</span>");
parent.state.value = 2;
parent.__owl__.currentFiber!.cancel();
parent.state.value = 3; // update value directly
await nextTick();
expect(fixture.innerHTML).toBe("<span>3</span>");
parent.state.value = 4; // update value after a tick
await nextTick();
expect(fixture.innerHTML).toBe("<span>4</span>");
});
test("change state and call manually render: no unnecessary rendering", async () => {
class Widget extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
+14 -2
View File
@@ -3080,6 +3080,15 @@ describe("can deduce template from name", () => {
expect(fixture.innerHTML).toBe("<span>Orval</span>");
});
test("can find template of anonymous component", async () => {
class ABC extends Component {}
const Anonymous = class extends ABC {};
env.qweb.addTemplate("ABC", "<span>Orval</span>");
const def = new Anonymous();
await def.mount(fixture);
expect(fixture.innerHTML).toBe("<span>Orval</span>");
});
test("can find template of parent component, defined by template key", async () => {
class ABC extends Component {
static template = "Achel";
@@ -3475,11 +3484,14 @@ describe("environment and plugins", () => {
});
test("can define specific env for root components", async () => {
class App1 extends Component {
interface AppEnv extends Env {
test: number;
}
class App1 extends Component<{}, AppEnv> {
static template = xml`<span></span>`;
}
App1.env = { test: 1 };
class App2 extends Component {
class App2 extends Component<{}, AppEnv> {
static template = xml`<span></span>`;
}
App2.env = { test: 2 };
+66 -10
View File
@@ -41,8 +41,8 @@ describe("t-slot directive", () => {
<templates>
<div t-name="Parent">
<Dialog>
<t t-set="header"><span>header</span></t>
<t t-set="footer"><span>footer</span></t>
<t t-set-slot="header"><span>header</span></t>
<t t-set-slot="footer"><span>footer</span></t>
</Dialog>
</div>
<div t-name="Dialog">
@@ -67,6 +67,38 @@ describe("t-slot directive", () => {
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("can define and call slots using old t-set keyword", async () => {
// NOTE: this test should be removed once we stop supporting the t-set directive
// for defining slot content.
class Dialog extends Component {
static template = xml`
<div>
<div><t t-slot="header"/></div>
<div><t t-slot="footer"/></div>
</div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Dialog>
<t t-set="header"><span>header</span></t>
<t t-set="footer"><span>footer</span></t>
</Dialog>
</div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><div><span>header</span></div><div><span>footer</span></div></div></div>"
);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
expect(QWeb.slots["1_header"].toString()).toMatchSnapshot();
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("named slots can define a default content", async () => {
class Dialog extends Component {
static template = xml`
@@ -128,7 +160,7 @@ describe("t-slot directive", () => {
</span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog><t t-set="header">hey</t></Dialog></div>`;
static template = xml`<div><Dialog><t t-set-slot="header">hey</t></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
@@ -143,7 +175,7 @@ describe("t-slot directive", () => {
<div t-name="Parent">
<span class="counter"><t t-esc="state.val"/></span>
<Dialog>
<t t-set="footer"><button t-on-click="doSomething">do something</button></t>
<t t-set-slot="footer"><button t-on-click="doSomething">do something</button></t>
</Dialog>
</div>
<span t-name="Dialog"><t t-slot="footer"/></span>
@@ -302,7 +334,7 @@ describe("t-slot directive", () => {
<div>
<span class="counter"><t t-esc="state.val"/></span>
<Dialog>
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
<t t-set-slot="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
</Dialog>
</div>
`;
@@ -376,7 +408,7 @@ describe("t-slot directive", () => {
<templates>
<div t-name="Parent">
<Dialog>
<t t-set="content">
<t t-set-slot="content">
<span>sts</span>
<span>rocks</span>
</t>
@@ -442,14 +474,14 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe("<div><span><span>some content</span></span></div>");
});
test("t-debug on a t-set (defining a slot)", async () => {
test("t-debug on a t-set-slot (defining a slot)", async () => {
const consoleLog = console.log;
console.log = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog><t t-set="content" t-debug="">abc</t></Dialog>
<Dialog><t t-set-slot="content" t-debug="">abc</t></Dialog>
</div>
<span t-name="Dialog">
<t t-slot="content"/>
@@ -623,8 +655,8 @@ describe("t-slot directive", () => {
class A extends Component {
static template = xml`
<B>
<t t-set="s1"><C val="1"/></t>
<t t-set="s2"><C val="2"/></t>
<t t-set-slot="s1"><C val="1"/></t>
<t t-set-slot="s2"><C val="2"/></t>
</B>`;
static components = { B, C };
}
@@ -873,4 +905,28 @@ describe("t-slot directive", () => {
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span><p>heyaaron</p></span></div></div>");
});
test("t-set t-value in a slot", async () => {
class Dialog extends Component {
static template = xml`
<span>
<t t-slot="default"/>
</span>`;
}
class Parent extends Component {
static template = xml`
<div>
<Dialog>
<t t-set="rainbow" t-value="'dash'"/>
<t t-esc="rainbow"/>
</Dialog>
</div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>dash</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
});
});
+22 -2
View File
@@ -28,14 +28,34 @@ afterEach(() => {
describe("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => {
class App extends Component {
static template = xml`<div>app</div>`;
static template = xml`<div t-att-class="state.customClass">app<p>another tag</p></div>`;
state = useState({ customClass: "custom" });
}
const div = document.createElement("div");
div.classList.add("arbitrary");
div.innerHTML = `<p>pre-existing</p>`;
fixture.appendChild(div);
const app = new App();
await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe("<div>app</div>");
expect(fixture.innerHTML).toBe(
`<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
);
expect(div).toBe(app.el);
app.state.customClass = "custom2";
await nextTick();
expect(fixture.innerHTML).toBe(
`<div class="arbitrary custom2"><p>pre-existing</p>app<p>another tag</p></div>`
);
expect(div).toBe(app.el);
app.unmount();
// This assert is a best guess
// The use case it covers was not really thought through
// and may change in the future
expect(fixture.innerHTML).toBe("");
});
test("cannot attach a component to an existing node (if not same tagname)", async () => {
+3 -1
View File
@@ -6,6 +6,7 @@ import { patch } from "../src/vdom";
import "../src/qweb/base_directives";
import "../src/qweb/extensions";
import "../src/component/directive";
import { browser } from "../src/browser";
// modifies scheduler to make it faster to test components
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) {
@@ -75,7 +76,8 @@ export function makeDeferred(): Deferred {
export function makeTestEnv(): Env {
return {
qweb: new QWeb()
qweb: new QWeb(),
browser: browser
};
}
+37 -4
View File
@@ -559,13 +559,31 @@ describe("hooks", () => {
test("can use onWillStart, onWillUpdateProps", async () => {
const steps: string[] = [];
async function slow(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
resolve("slow");
}, 0);
});
}
function useMyHook() {
onWillStart(() => {
onWillStart(async () => {
steps.push(await slow());
steps.push("onWillStart");
});
onWillUpdateProps(async nextProps => {
expect(nextProps).toEqual({ value: 2 });
steps.push(await slow());
steps.push("onWillUpdateProps");
});
}
function use2ndHook() {
onWillStart(() => {
steps.push("on2ndStart");
});
onWillUpdateProps(nextProps => {
expect(nextProps).toEqual({ value: 2 });
steps.push("onWillUpdateProps");
steps.push("on2ndUpdate");
});
}
class MyComponent extends Component {
@@ -573,6 +591,7 @@ describe("hooks", () => {
constructor(parent, props) {
super(parent, props);
useMyHook();
use2ndHook();
}
}
class App extends Component {
@@ -586,12 +605,26 @@ describe("hooks", () => {
expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps");
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(steps).toEqual(["onWillStart"]);
// NOTE: 'on2ndStart' appears first in the list even though
// the 'use2ndHook' is declared after 'useMyHook'. This is
// because Promise.all is used to call the callbacks specified
// in the hooks, which runs them simultaneously.
// Additionally, 'slow' should be listed before 'onWillStart'
// because call to `slow` is awaited.
expect(steps).toEqual(["on2ndStart", "slow", "onWillStart"]);
app.state.value = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]);
expect(steps).toEqual([
"on2ndStart",
"slow",
"onWillStart",
"on2ndUpdate",
"slow",
"onWillUpdateProps"
]);
});
test("useExternalListener", async () => {
+135 -25
View File
@@ -774,12 +774,14 @@ exports[`misc global 1`] = `
}
{
let _origScope10 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
{
let _origScope13 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'foo').foo = 'aaa';
@@ -1150,7 +1152,8 @@ exports[`t-call (template calling call with several sub nodes on same line 1`] =
let vn1 = h('div', p1, c1);
{
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
let c4 = [], p4 = {key:4};
@@ -1198,7 +1201,8 @@ exports[`t-call (template calling cascading t-call t-raw='0' 1`] = `
let vn1 = h('div', p1, c1);
{
let _origScope12 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
let c13 = [], p13 = {key:13};
@@ -1288,7 +1292,8 @@ exports[`t-call (template calling recursive template, part 2 1`] = `
let vn1 = h('div', p1, c1);
{
let _origScope11 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'node').node = scope['root'];
@@ -1339,7 +1344,8 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
let key1 = i1;
{
let _origScope9 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'node').node = scope['subtree'];
@@ -1365,7 +1371,8 @@ exports[`t-call (template calling recursive template, part 3 1`] = `
let vn1 = h('div', p1, c1);
{
let _origScope11 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'node').node = scope['root'];
@@ -1416,7 +1423,93 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
let key1 = i1;
{
let _origScope9 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'node').node = scope['subtree'];
scope[utils.zero] = c__0;
}
let k10 = \`__10__\${key0}__\${key1}__\`;
this.subTemplates['nodeTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c2, parent: utils.getComponent(context), key: k10}));
scope = _origScope9;
}
}
scope = _origScope8;
}"
`;
exports[`t-call (template calling recursive template, part 4: with t-set recursive index 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
{
let _origScope11 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'recursive_idx').recursive_idx = 1;
utils.getScope(scope, 'node').node = scope['root'];
scope[utils.zero] = c__0;
}
this.subTemplates['nodeTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__12__'}));
scope = _origScope11;
}
return vn1;
}"
`;
exports[`t-call (template calling recursive template, part 4: with t-set recursive index 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = extra.parentNode;
let key0 = extra.key || \\"\\";
let c2 = [], p2 = {key:\`\${key0}_2\`};
let vn2 = h('div', p2, c2);
c1.push(vn2);
scope.recursive_idx = scope.recursive_idx+1;
let c3 = [], p3 = {key:\`\${key0}_3\`};
let vn3 = h('p', p3, c3);
c2.push(vn3);
let _4 = scope['node'].val;
if (_4 != null) {
c3.push({text: _4});
}
c3.push({text: \` \`});
if (scope.recursive_idx != null) {
c3.push({text: scope.recursive_idx});
}
let _5 = scope['node'].children||[];
if (!_5) { throw new Error('QWeb error: Invalid loop expression')}
let _6 = _7 = _5;
if (!(_5 instanceof Array)) {
_6 = Object.keys(_5);
_7 = Object.values(_5);
}
let _length6 = _6.length;
let _origScope8 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length6; i1++) {
scope.subtree_first = i1 === 0
scope.subtree_last = i1 === _length6 - 1
scope.subtree_index = i1
scope.subtree = _6[i1]
scope.subtree_value = _7[i1]
let key1 = i1;
{
let _origScope9 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'node').node = scope['subtree'];
@@ -1442,7 +1535,8 @@ exports[`t-call (template calling scoped parameters 1`] = `
let vn1 = h('div', p1, c1);
{
let _origScope2 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'foo').foo = 42;
@@ -1517,7 +1611,8 @@ exports[`t-call (template calling t-call with t-set inside and outside 1`] = `
utils.getScope(scope, 'val').val = scope['v'].val;
{
let _origScope8 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'val3').val3 = scope.val*3;
@@ -1564,7 +1659,8 @@ exports[`t-call (template calling t-call, conditional and t-set in t-call body 1
else if (scope.v1==='elif') {
{
let _origScope6 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'v').v = 'success';
@@ -1602,7 +1698,8 @@ exports[`t-call (template calling with unused body 1`] = `
let h = this.h;
{
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
c__0.push({text: \`WHEEE\`});
@@ -1627,7 +1724,8 @@ exports[`t-call (template calling with unused setbody 1`] = `
let h = this.h;
{
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'qux').qux = 3;
@@ -1652,7 +1750,8 @@ exports[`t-call (template calling with used body 1`] = `
let h = this.h;
{
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
c__0.push({text: \`ok\`});
@@ -1678,7 +1777,8 @@ exports[`t-call (template calling with used set body 1`] = `
let vn1 = h('span', p1, c1);
{
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'foo').foo = 'ok';
@@ -1839,7 +1939,8 @@ exports[`t-esc t-esc is escaped 1`] = `
scope.var = c2
if (scope.var != null) {
let _origScope4 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.var = scope.var instanceof utils.VDomArray ? utils.vDomToString(scope.var) : scope.var;
c1.push({text: scope.var});
scope = _origScope4;
@@ -1875,7 +1976,8 @@ exports[`t-esc t-esc=0 is escaped 1`] = `
let vn1 = h('div', p1, c1);
{
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
let c4 = [], p4 = {key:4};
@@ -2948,7 +3050,8 @@ exports[`t-set set from body literal 1`] = `
scope.value = c1
if (scope.value != null) {
let _origScope2 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.value = scope.value instanceof utils.VDomArray ? utils.vDomToString(scope.value) : scope.value;
let vn3 = {text: scope.value};
result = vn3
@@ -2975,7 +3078,8 @@ exports[`t-set set from body lookup 1`] = `
scope.stuff = c2
if (scope.stuff != null) {
let _origScope4 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.stuff = scope.stuff instanceof utils.VDomArray ? utils.vDomToString(scope.stuff) : scope.stuff;
c1.push({text: scope.stuff});
scope = _origScope4;
@@ -3123,7 +3227,8 @@ exports[`t-set t-set with content and sub t-esc 1`] = `
scope.setvar = c2
if (scope.setvar != null) {
let _origScope4 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.setvar = scope.setvar instanceof utils.VDomArray ? utils.vDomToString(scope.setvar) : scope.setvar;
c1.push({text: scope.setvar});
scope = _origScope4;
@@ -3215,7 +3320,8 @@ exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 1`] = `
}
if (scope.ourvar != null) {
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar;
c1.push({text: scope.ourvar});
scope = _origScope3;
@@ -3243,7 +3349,8 @@ exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 2`] = `
}
if (scope.ourvar != null) {
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar;
c1.push({text: scope.ourvar});
scope = _origScope3;
@@ -3271,7 +3378,8 @@ exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 1`] = `
}
if (scope.ourvar != null) {
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar;
c1.push({text: scope.ourvar});
scope = _origScope3;
@@ -3299,7 +3407,8 @@ exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 2`] = `
}
if (scope.ourvar != null) {
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar;
c1.push({text: scope.ourvar});
scope = _origScope3;
@@ -3325,7 +3434,8 @@ exports[`t-set value priority 1`] = `
}
if (scope.value != null) {
let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope);
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.value = scope.value instanceof utils.VDomArray ? utils.vDomToString(scope.value) : scope.value;
c1.push({text: scope.value});
scope = _origScope3;
+32
View File
@@ -944,6 +944,38 @@ describe("t-call (template calling", () => {
expect(recursiveFn.toString()).toMatchSnapshot();
});
test("recursive template, part 4: with t-set recursive index", () => {
qweb.addTemplates(`
<templates>
<div t-name="Parent">
<t t-call="nodeTemplate">
<t t-set="recursive_idx" t-value="1"/>
<t t-set="node" t-value="root"/>
</t>
</div>
<div t-name="nodeTemplate">
<t t-set="recursive_idx" t-value="recursive_idx + 1"/>
<p><t t-esc="node.val"/> <t t-esc="recursive_idx"/></p>
<t t-foreach="node.children or []" t-as="subtree">
<t t-call="nodeTemplate">
<t t-set="node" t-value="subtree"/>
</t>
</t>
</div>
</templates>
`);
const root = {
val: "a",
children: [{ val: "b", children: [{ val: "c", children: [{ val: "d" }] }] }]
};
const expected =
"<div><div><p>a 2</p><div><p>b 3</p><div><p>c 4</p><div><p>d 5</p></div></div></div></div></div>";
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
test("t-call, global templates", () => {
QWeb.registerTemplate("abcd", '<div><t t-call="john"/></div>');
qweb.addTemplate("john", `<span>desk</span>`);
+3 -3
View File
@@ -1,5 +1,5 @@
import { Component, Env } from "../src/component/component";
import { Store, useStore, useDispatch, useGetters } from "../src/store";
import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
import { useState } from "../src/hooks";
import { xml } from "../src/tags";
import { shallowEqual } from "../src/utils";
@@ -885,7 +885,7 @@ describe("connecting a component to store", () => {
actions
});
class TodoItem extends Component {
class TodoItem extends Component<{}, EnvWithStore> {
static template = xml`
<div class="todo">
<t t-esc="state.todo.title"/>
@@ -958,7 +958,7 @@ describe("connecting a component to store", () => {
actions
});
class TodoItem extends Component {
class TodoItem extends Component<{}, EnvWithStore> {
static template = xml`
<div class="todo">
<t t-esc="state.todo.title"/>
+5
View File
@@ -1,4 +1,5 @@
import { escape, debounce } from "../src/utils";
import { browser } from "../src/browser";
describe("escape", () => {
test("normal strings", () => {
@@ -15,6 +16,10 @@ describe("escape", () => {
describe("debounce", () => {
test("works as expected", () => {
jest.useFakeTimers();
// need to reset them on browser because they were mocked in window, but not
// on browser object
browser.setTimeout = window.setTimeout.bind(window);
browser.clearTimeout = window.clearTimeout.bind(window);
let n = 0;
let f = debounce(() => n++, 100);
expect(n).toBe(0);
+5 -5
View File
@@ -1169,7 +1169,7 @@ const RESPONSIVE_CSS = `body {
const SLOTS = `// We show here how slots can be used to create generic components.
// In this example, the Card component is basically only a container. It is not
// aware of its content. It just knows where it should be (with t-slot).
// The parent component define the content with t-set.
// The parent component define the content with t-set-slot.
//
// Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component
@@ -1235,15 +1235,15 @@ const SLOTS_XML = `<templates>
<div t-name="App" class="main">
<Card title="'Title card A'">
<t t-set="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set="footer"><button t-on-click="inc('a', 1)">Increment A</button></t>
<t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set-slot="footer"><button t-on-click="inc('a', 1)">Increment A</button></t>
</Card>
<Card title="'Title card B'">
<t t-set="content">
<t t-set-slot="content">
<div>Card 2... [<t t-esc="state.b"/>]</div>
<Counter />
</t>
<t t-set="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t>
<t t-set-slot="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t>
</Card>
</div>
</templates>`;
-1
View File
@@ -106,7 +106,6 @@ async function startRelease() {
draft: true // todo: remove this someday
};
const result = await createRelease(token, options);
console.log(result);
await ask("Ready for next step...");
// ---------------------------------------------------------------------------