Compare commits

..

12 Commits

Author SHA1 Message Date
Samuel Degueldre ea5d2be502 [REL] v2.0.5
# v2.0.5

 - [FIX] reactivity: improve performance for long-lived reactives
2023-01-27 15:29:07 +01:00
Samuel Degueldre 4a761a7403 [FIX] reactivity: improve performance for long-lived reactives
Previously, when having a long lived reactive object and writing a lot
of keys to it, clearing the reactive subscriptions on that object would
slow down as time went on. This is because when clearing a target's
subsctiptions for a callback, we look at all the keys that are observed, and for all
the observed keys, we remove the callback from the set of callbacks
observing that key. The problem arises from the fact that after doing
that, even if the set of callbacks observing that key is now empty, we
don't remove the key from the observed keys, meaning that any further
clearing of subscriptions will have to iterate over that key to attempt
to clear the callbacks even if there are none.

This commit fixes this problem by simply removing the key from the
observedKeys if there are no longer any callbacks observing it.

This commit also improves performance for bare reactives (reactives
created with no callback). The initial implementation simply creates a
default empty callback when creating a reactive, and treats it like any
other, meaning it can observe keys and be notified of changes even
though we know in advance that it does nothing. This can compound with
the previous issue when you're doing a lot of manipulations on a bare
reactive internally for the sole purpose of notifying outside observers,
as this creates a lot of useless work in the reactivity system.
2023-01-27 09:34:55 +01:00
Lucas Lefèvre 8702db03fb [FIX] tools: allow to pre-compile templates with - in their name
Compiling a template with a dash ("-") in its name generates an
invalid function name in the compiled code.

A template named `"my-component"` leads to the function
`function my-component(app, bdom, helpers) { ... }` which has an
invalid name.

closes #1333
2023-01-25 07:56:30 +01:00
Samuel Degueldre b2685b6709 [IMP] tools: pull playground before trying to publish
Previously, if you were not the last person to publish to the
playground, your playground branch would be behind the remote and trying
to push to it would fail.

This commit attempts to pull the changes before updating owl so that
even if you were not the last person to push to the playground, as long
as the pull is a fast-forward, publishing to the playground won't fail.

This commit also adapts some of the status number manipulation to use
bitwise or instead of addition, since return status code can be both
positive or negative and may cancel one another. Using bitwise or
ensures than any non-zero code will make the status non-zero and stay
that way.
2023-01-23 12:39:50 +01:00
Samuel Degueldre c30678f3ea [REL] v2.0.4
# v2.0.4

 - [IMP] app: expose live apps for the devtools
 - [FIX] compiler: support t-model radio group in t-foreach
 - [IMP] runtime: improve useExternalListener typing
2023-01-23 12:21:13 +01:00
Samuel Degueldre 6ca6717965 [IMP] tools: release scripts creates a template for release notes
This template contains the release version as a markdown title followed
by a markdown list with all the commit titles since the previous
release.
2023-01-23 12:19:14 +01:00
Julien Carion ad4adb930e [IMP] app: expose live apps for the devtools
This commit exposes owl apps in a global variable so that the owl
devtools can hook themselves on these apps. While the devtools are not
yet ready to be merged, exposing the apps will allow us to test the
devtools in production scenarios while polishing the development of
them.
2023-01-23 11:40:02 +01:00
Lucas Perais cea82e945d [FIX] compiler: t-model supports radio group in t-foreach
Have a radio group defined inside a t-foreach:
```xml
  <t t-foreach="values" t-as="val" t-key="val">
    <input name="radiogroup" t-att-value="val" t-model="state.radioGroup" />
  </t>
```

Before this commit the algorithm that set the "checked" attribute on the current active
radio button according to the state did not support having a dynamic value (`t-att-value`)

After this commit, this use case works as we go look in the dynamic attributes too.
2023-01-20 09:26:54 +01:00
Samuel Degueldre a69f8a39e7 [IMP] runtime: improve useExternalListener typing
Previously the type of the target for useExternalListener was
HTMLElement or Window, this makes document an invalid target. Here there
is no reason to use the EventTarget interface instead, as
useExternalListener only uses methods from that interface and should
work with any event target.

Closes odoo/owl#1323
2023-01-18 13:03:23 +01:00
Géry Debongnie 316eb06279 [REL] v2.0.3
# v2.0.2

Some small bug fixes

- fix: compiler: correctly escape backslashes when emitting block string
- fix: reactivity: don't subscribe to keys when making reactive
- fix: t-call-context: fix capture making component available in ctx
- fix: t-call-context: make `this` unavailable in rendering context
2023-01-12 16:29:11 +01:00
Samuel Degueldre df59ec49ae [FIX] t-call-context: make this unavailable in rendering context
t-call-context is a feature that's supposed to mask the rendering
context completely, but currently the component remains available
through `this`.

This commit stops treating `this` as a reserved word, so that it's
compiled to a lookup in the rendering context, and adds `this` to the
rendering context when binding the component's rendering function. With
these changes, `this` behaves the same as before when outside a
t-call-context, but when the rendering context is overriden, the
template can no longer access `this`. `this` still represents the
instance of the component inside of the rendering function since it's
needed by owl internally.

A side-effect of this change is that now the rendering context is no
longer the instance of the component by default, but is always an object
with the component in its prototype chain. This was already the case
before in some contexts (eg inside t-foreach, or inside components with
a t-set/t-call anywhere in its template). This can cause issues in rare
cases when a component method was called directly on the rendering
context, as before this change, the method's bound this would be the
component instance (except in a t-foreach, component with a
t-set/t-call, etc), while after this change it is now never the
component instance. When the method only reads on `this` there is no
issue as all the components properties are available on the rendering
contexts, but setting a value on `this` will write on the rendering
context and not the component which is likely a mistake.

While this is a breaking change, simply adding a t-set/t-call to any
template would break components that would be broken by this change,
with this in mind we decided to make this change anyway so that
developers get the error as early as possible in the development cycle
rather than having a seemingly inocuous change break code under them.
2023-01-12 09:38:22 +01:00
Samuel Degueldre 2a008a8679 [IMP] tooling: add eslint ci step to avoid stray .only and debugger 2023-01-11 15:47:09 +01:00
28 changed files with 6015 additions and 85 deletions
+47
View File
@@ -0,0 +1,47 @@
{
"env": {
"browser": true,
"node": true,
"es2022": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"parserOptions": {
"sourceType": "module"
},
"root": true,
"rules": {
"no-restricted-globals": ["error", "event", "self"],
"no-const-assign": ["error"],
"no-debugger": ["error"],
"no-dupe-class-members": ["error"],
"no-dupe-keys": ["error"],
"no-dupe-args": ["error"],
"no-dupe-else-if": ["error"],
"no-unsafe-negation": ["error"],
"no-duplicate-imports": ["error"],
"valid-typeof": ["error"],
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false, "caughtErrors": "all" }],
"no-restricted-syntax": [
"error",
{
"selector": "MemberExpression[object.name='test'][property.name='only']",
"message": "test.only(...) is forbidden",
},
{
"selector": "MemberExpression[object.name='describe'][property.name='only']",
"message": "describe.only(...) is forbidden",
}
],
},
"globals": {
"describe": true,
"expect": true,
"test": true,
"beforeEach": true,
"beforeAll": true,
"afterEach": true,
"afterAll": true,
"jest": true,
},
}
+2 -1
View File
@@ -22,7 +22,8 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm install - run: npm ci
- run: npm run test - run: npm run test
- run: npm run check-formatting - run: npm run check-formatting
- run: npm run lint
- run: npm run build - run: npm run build
-3
View File
@@ -14,9 +14,6 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
package-lock.json
yarn.lock
#ide's #ide's
.vscode .vscode
.idea .idea
+3 -3
View File
@@ -174,9 +174,9 @@ class Root extends Component {
``` ```
The template contains a [`t-foreach`](../reference/templates.md#loops) loop to iterate The template contains a [`t-foreach`](../reference/templates.md#loops) loop to iterate
through the tasks. It can find the `tasks` list from the component, since the through the tasks. It can find the `tasks` list from the component, since the rendering
component is the rendering context. Note that we use the `id` of each task as a context contains the properties of the component. Note that we use the `id` of each task
`t-key`, which is very common. There are two css classes: `task-list` and `task`, as a `t-key`, which is very common. There are two css classes: `task-list` and `task`,
that we will use in the next section. that we will use in the next section.
Finally, notice the use of the `t-att-checked` attribute: Finally, notice the use of the `t-att-checked` attribute:
+3 -4
View File
@@ -115,7 +115,7 @@ It is useful to explain the various rules that apply on these expressions:
<div><p t-if="console.log(1)">NOT valid</p></div> <div><p t-if="console.log(1)">NOT valid</p></div>
``` ```
2. it can use anything in the rendering context (typically, the component): 2. it can use anything in the rendering context (which typically contains the properties of the component):
```xml ```xml
<p t-if="user.birthday === today()">Happy bithday!</p> <p t-if="user.birthday === today()">Happy bithday!</p>
@@ -541,9 +541,8 @@ This can be used to define variables scoped to a sub template:
``` ```
Note: by default, the rendering context for a sub template is simply the current Note: by default, the rendering context for a sub template is simply the current
rendering context (so, the current component). However, it may be useful to be rendering context. However, it may be useful to be able to specify a specific
able to specify a specific object as context. This can be done by using the object as context. This can be done by using the `t-call-context` directive:
`t-call-context` directive:
```xml ```xml
<t t-call="other-template" t-call-context="obj"/> <t t-call="other-template" t-call-context="obj"/>
+5711
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.0.2", "version": "2.0.5",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"module": "dist/owl.es.js", "module": "dist/owl.es.js",
@@ -25,6 +25,7 @@
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"", "playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write", "prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check", "check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
"lint": "eslint src/**/*.ts tests/**/*.ts",
"publish": "npm run build && npm publish", "publish": "npm run build && npm publish",
"release": "node tools/release.js", "release": "node tools/release.js",
"compile_templates": "node tools/compile_xml.js" "compile_templates": "node tools/compile_xml.js"
@@ -42,26 +43,25 @@
"devDependencies": { "devDependencies": {
"@types/jest": "^27.0.1", "@types/jest": "^27.0.1",
"@types/node": "^14.11.8", "@types/node": "^14.11.8",
"@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.1",
"chalk": "^3.0.0", "chalk": "^3.0.0",
"cpx": "^1.5.0",
"current-git-branch": "^1.1.0", "current-git-branch": "^1.1.0",
"eslint": "8.31.0",
"git-rev-sync": "^1.12.0", "git-rev-sync": "^1.12.0",
"github-api": "^3.3.0", "github-api": "^3.3.0",
"jest": "^27.1.0", "jest": "^27.1.0",
"jest-diff": "^27.3.1", "jest-diff": "^27.3.1",
"jest-environment-jsdom": "^27.1.0", "jest-environment-jsdom": "^27.1.0",
"live-server": "^1.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "2.4.1", "prettier": "2.4.1",
"rollup": "^2.56.3", "rollup": "^2.56.3",
"rollup-plugin-dts": "^4.2.2", "rollup-plugin-dts": "^4.2.2",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.31.1", "rollup-plugin-typescript2": "^0.31.1",
"sass": "^1.16.1",
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^27.0.5", "ts-jest": "^27.0.5",
"typescript": "4.5.2", "typescript": "4.5.2"
"uglify-es": "^3.3.9"
}, },
"jest": { "jest": {
"testEnvironment": "jsdom", "testEnvironment": "jsdom",
+9 -1
View File
@@ -632,7 +632,15 @@ export class CodeGenerator {
let idx: number; let idx: number;
if (specialInitTargetAttr) { if (specialInitTargetAttr) {
idx = block!.insertData(`${fullExpression} === '${attrs[targetAttr]}'`, "attr"); let targetExpr = targetAttr in attrs && `'${attrs[targetAttr]}'`;
if (!targetExpr && ast.attrs) {
// look at the dynamic attribute counterpart
const dynamicTgExpr = ast.attrs[`t-att-${targetAttr}`];
if (dynamicTgExpr) {
targetExpr = compileExpr(dynamicTgExpr);
}
}
idx = block!.insertData(`${fullExpression} === ${targetExpr}`, "attr");
attrs[`block-attribute-${idx}`] = specialInitTargetAttr; attrs[`block-attribute-${idx}`] = specialInitTargetAttr;
} else if (hasDynamicChildren) { } else if (hasDynamicChildren) {
const bValueId = generateId("bValue"); const bValueId = generateId("bValue");
+1 -1
View File
@@ -28,7 +28,7 @@ import { OwlError } from "../runtime/error_handling";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const RESERVED_WORDS = const RESERVED_WORDS =
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split( "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date".split(
"," ","
); );
+15 -2
View File
@@ -1,12 +1,11 @@
import { Component, ComponentConstructor, Props } from "./component"; import { Component, ComponentConstructor, Props } from "./component";
import { ComponentNode } from "./component_node"; import { ComponentNode } from "./component_node";
import { nodeErrorHandlers, OwlError } from "./error_handling"; import { nodeErrorHandlers, OwlError, handleError } from "./error_handling";
import { Fiber, MountOptions } from "./fibers"; import { Fiber, MountOptions } from "./fibers";
import { Scheduler } from "./scheduler"; import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers"; import { validateProps } from "./template_helpers";
import { TemplateSet, TemplateSetConfig } from "./template_set"; import { TemplateSet, TemplateSetConfig } from "./template_set";
import { validateTarget } from "./utils"; import { validateTarget } from "./utils";
import { handleError } from "./error_handling";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f // reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
@@ -32,6 +31,18 @@ This is not suitable for production use.
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`; See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
}; };
declare global {
interface Window {
__OWL_DEVTOOLS__: {
apps: Set<App>;
};
}
}
window.__OWL_DEVTOOLS__ ||= {
apps: new Set<App>(),
};
export class App< export class App<
T extends abstract new (...args: any) => any = any, T extends abstract new (...args: any) => any = any,
P extends object = any, P extends object = any,
@@ -49,6 +60,7 @@ export class App<
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) { constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
super(config); super(config);
this.Root = Root; this.Root = Root;
window.__OWL_DEVTOOLS__.apps.add(this);
if (config.test) { if (config.test) {
this.dev = true; this.dev = true;
} }
@@ -110,6 +122,7 @@ export class App<
this.scheduler.flush(); this.scheduler.flush();
this.root.destroy(); this.root.destroy();
} }
window.__OWL_DEVTOOLS__.apps.delete(this);
} }
createComponent<P extends Props>( createComponent<P extends Props>(
+3 -1
View File
@@ -114,7 +114,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
} }
this.component = new C(props, env, this); this.component = new C(props, env, this);
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this); const ctx = Object.assign(Object.create(this.component), { this: this.component });
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
this.component.setup(); this.component.setup();
currentNode = null; currentNode = null;
} }
@@ -317,6 +318,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
_patch() { _patch() {
let hasChildren = false; let hasChildren = false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (let _k in this.children) { for (let _k in this.children) {
hasChildren = true; hasChildren = true;
break; break;
+1 -1
View File
@@ -117,7 +117,7 @@ export function useEffect(effect: Effect, computeDependencies: () => any[] = ()
* `useExternalListener(window, 'click', this._doSomething);` * `useExternalListener(window, 'click', this._doSomething);`
* */ * */
export function useExternalListener( export function useExternalListener(
target: HTMLElement | typeof window, target: EventTarget,
eventName: string, eventName: string,
handler: EventListener, handler: EventListener,
eventParams?: AddEventListenerOptions eventParams?: AddEventListenerOptions
+14 -3
View File
@@ -1,8 +1,13 @@
import { Callback } from "./utils"; import type { Callback } from "./utils";
import { OwlError } from "./error_handling"; import { OwlError } from "./error_handling";
// Special key to subscribe to, to be notified of key creation/deletion // Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes"); const KEYCHANGES = Symbol("Key changes");
// Used to specify the absence of a callback, can be used as WeakMap key but
// should only be used as a sentinel value and never called.
const NO_CALLBACK = () => {
throw new Error("Called NO_CALLBACK. Owl is broken, please report this to the maintainers.");
};
// The following types only exist to signify places where objects are expected // The following types only exist to signify places where objects are expected
// to be reactive or not, they provide no type checking benefit over "object" // to be reactive or not, they provide no type checking benefit over "object"
@@ -86,6 +91,9 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callbac
* @param callback the function to call when the key changes * @param callback the function to call when the key changes
*/ */
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void { function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
if (callback === NO_CALLBACK) {
return;
}
if (!targetToKeysToCallbacks.get(target)) { if (!targetToKeysToCallbacks.get(target)) {
targetToKeysToCallbacks.set(target, new Map()); targetToKeysToCallbacks.set(target, new Map());
} }
@@ -140,8 +148,11 @@ export function clearReactivesForCallback(callback: Callback): void {
if (!observedKeys) { if (!observedKeys) {
continue; continue;
} }
for (const callbacks of observedKeys.values()) { for (const [key, callbacks] of observedKeys.entries()) {
callbacks.delete(callback); callbacks.delete(callback);
if (!callbacks.size) {
observedKeys.delete(key);
}
} }
} }
targetsToClear.clear(); targetsToClear.clear();
@@ -187,7 +198,7 @@ const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>()
* reactive has changed * reactive has changed
* @returns a proxy that tracks changes to it * @returns a proxy that tracks changes to it
*/ */
export function reactive<T extends Target>(target: T, callback: Callback = () => {}): T { export function reactive<T extends Target>(target: T, callback: Callback = NO_CALLBACK): T {
if (!canBeMadeReactive(target)) { if (!canBeMadeReactive(target)) {
throw new OwlError(`Cannot make the given value reactive`); throw new OwlError(`Cannot make the given value reactive`);
} }
+14 -13
View File
@@ -10,19 +10,20 @@ export type Callback = () => void;
*/ */
export function batched(callback: Callback): Callback { export function batched(callback: Callback): Callback {
let called = false; let called = false;
return () => { return async () => {
queueMicrotask(() => { // This await blocks all calls to the callback here, then releases them sequentially
if (!called) { // in the next microtick. This line decides the granularity of the batch.
called = true; await Promise.resolve();
// wait for all calls in this microtick to fall through before resetting "called" if (!called) {
// so that only the first call to the batched function calls the original callback. called = true;
// Schedule this before calling the callback so that calls to the batched function // wait for all calls in this microtick to fall through before resetting "called"
// within the callback will proceed only after resetting called to false, and have // so that only the first call to the batched function calls the original callback.
// a chance to execute the callback again // Schedule this before calling the callback so that calls to the batched function
queueMicrotask(() => (called = false)); // within the callback will proceed only after resetting called to false, and have
callback(); // a chance to execute the callback again
} Promise.resolve().then(() => (called = false));
}); callback();
}
}; };
} }
@@ -519,8 +519,9 @@ exports[`t-on t-on, with arguments and t-call 2`] = `
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`); let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['value']; const v1 = ctx['this'];
let hdlr1 = [()=>this.update(v1), ctx]; const v2 = ctx['value'];
let hdlr1 = [()=>v1.update(v2), ctx];
return block1([hdlr1]); return block1([hdlr1]);
} }
}" }"
+3
View File
@@ -284,6 +284,9 @@ describe("t-on", () => {
expect(this).toBe(owner); expect(this).toBe(owner);
expect(val).toBe(444); expect(val).toBe(444);
}, },
get this() {
return owner;
},
value: 444, value: 444,
}; };
@@ -940,9 +940,10 @@ exports[`can catch errors catching error, rethrow, render parent -- a main comp
for (let i1 = 0; i1 < l_block1; i1++) { for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`cp\`] = v_block1[i1]; ctx[\`cp\`] = v_block1[i1];
const key1 = ctx['cp'].id; const key1 = ctx['cp'].id;
const v1 = ctx['cp']; const v1 = ctx['this'];
const v2 = ctx['cp'];
const ctx1 = capture(ctx); const ctx1 = capture(ctx);
c_block1[i1] = withKey(comp2({onError: ()=>this.cleanUp(v1.id),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2__\${key1}\`, node, this, null), key1); c_block1[i1] = withKey(comp2({onError: ()=>v1.cleanUp(v2.id),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2__\${key1}\`, node, this, null), key1);
} }
return list(c_block1); return list(c_block1);
} }
@@ -1014,13 +1015,14 @@ exports[`can catch errors catching in child makes parent render 1`] = `
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx); ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(Object.entries(this.elements));; const [k_block1, v_block1, l_block1, c_block1] = prepareList(Object.entries(ctx['this'].elements));;
for (let i1 = 0; i1 < l_block1; i1++) { for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`elem\`] = v_block1[i1]; ctx[\`elem\`] = v_block1[i1];
const key1 = ctx['elem'][0]; const key1 = ctx['elem'][0];
const v1 = ctx['elem']; const v1 = ctx['this'];
const v2 = ctx['elem'];
const ctx1 = capture(ctx); const ctx1 = capture(ctx);
c_block1[i1] = withKey(comp2({onError: (_error)=>this.onError(v1[0],_error),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2__\${key1}\`, node, this, null), key1); c_block1[i1] = withKey(comp2({onError: (_error)=>v1.onError(v2[0],_error),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2__\${key1}\`, node, this, null), key1);
} }
return list(c_block1); return list(c_block1);
} }
@@ -1155,7 +1157,7 @@ exports[`can catch errors onError in class inheritance is called if rethrown 2`]
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2,b3; let b2,b3;
if (!ctx['state'].error) { if (!ctx['state'].error) {
b2 = text(this.will.crash); b2 = text(ctx['this'].will.crash);
} else { } else {
b3 = text(ctx['state'].error); b3 = text(ctx['state'].error);
} }
@@ -1186,7 +1188,7 @@ exports[`can catch errors onError in class inheritance is not called if no rethr
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2,b3; let b2,b3;
if (!ctx['state'].error) { if (!ctx['state'].error) {
b2 = text(this.will.crash); b2 = text(ctx['this'].will.crash);
} else { } else {
b3 = text(ctx['state'].error); b3 = text(ctx['state'].error);
} }
@@ -1218,7 +1220,7 @@ exports[`errors and promises a rendering error in a sub component will reject th
let block1 = createBlock(\`<div><block-text-0/></div>\`); let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let txt1 = this.will.crash; let txt1 = ctx['this'].will.crash;
return block1([txt1]); return block1([txt1]);
} }
}" }"
@@ -1232,7 +1234,7 @@ exports[`errors and promises a rendering error will reject the mount promise 1`]
let block1 = createBlock(\`<div><block-text-0/></div>\`); let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let txt1 = this.will.crash; let txt1 = ctx['this'].will.crash;
return block1([txt1]); return block1([txt1]);
} }
}" }"
@@ -1277,7 +1279,7 @@ exports[`errors and promises a rendering error will reject the render promise 1`
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2; let b2;
if (ctx['flag']) { if (ctx['flag']) {
b2 = text(this.will.crash); b2 = text(ctx['this'].will.crash);
} }
return block1([], [b2]); return block1([], [b2]);
} }
@@ -1992,7 +1992,8 @@ exports[`slots slot content is bound to caller (variation) 1`] = `
ctx = Object.create(ctx); ctx = Object.create(ctx);
ctx[isBoundary] = 1 ctx[isBoundary] = 1
setContextValue(ctx, \\"var\\", 1); setContextValue(ctx, \\"var\\", 1);
let hdlr1 = [()=>this.inc(), ctx]; const v1 = ctx['this'];
let hdlr1 = [()=>v1.inc(), ctx];
return block1([hdlr1]); return block1([hdlr1]);
} }
@@ -2359,7 +2360,8 @@ exports[`slots slots are properly bound to correct component 2`] = `
ctx = Object.create(ctx); ctx = Object.create(ctx);
ctx[isBoundary] = 1 ctx[isBoundary] = 1
setContextValue(ctx, \\"var\\", 1); setContextValue(ctx, \\"var\\", 1);
let hdlr1 = [()=>this.increment(), ctx]; const v1 = ctx['this'];
let hdlr1 = [()=>v1.increment(), ctx];
let txt1 = ctx['state'].value; let txt1 = ctx['state'].value;
return block1([hdlr1, txt1]); return block1([hdlr1, txt1]);
} }
@@ -363,7 +363,7 @@ exports[`style and class handling error in subcomponent with class 2`] = `
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let attr1 = ctx['props'].class; let attr1 = ctx['props'].class;
let txt1 = this.will.crash; let txt1 = ctx['this'].will.crash;
return block1([attr1, txt1]); return block1([attr1, txt1]);
} }
}" }"
@@ -109,7 +109,8 @@ exports[`t-call handlers are properly bound through a dynamic t-call 2`] = `
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`); let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let hdlr1 = [()=>this.update(), ctx]; const v1 = ctx['this'];
let hdlr1 = [()=>v1.update(), ctx];
return block1([hdlr1]); return block1([hdlr1]);
} }
}" }"
@@ -168,8 +169,9 @@ exports[`t-call handlers with arguments are properly bound through a t-call 2`]
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`); let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['a']; const v1 = ctx['this'];
let hdlr1 = [()=>this.update(v1), ctx]; const v2 = ctx['a'];
let hdlr1 = [()=>v1.update(v2), ctx];
return block1([hdlr1]); return block1([hdlr1]);
} }
}" }"
@@ -287,7 +289,7 @@ exports[`t-call recursive t-call binding this -- static t-call 2`] = `
ctx[isBoundary] = 1 ctx[isBoundary] = 1
let b2; let b2;
if (ctx['level']<2) { if (ctx['level']<2) {
let hdlr1 = [\\"stop\\", ctx['onClicked'].bind(this), ctx]; let hdlr1 = [\\"stop\\", ctx['onClicked'].bind(ctx['this']), ctx];
let txt1 = ctx['level']; let txt1 = ctx['level'];
const b3 = block3([hdlr1, txt1]); const b3 = block3([hdlr1, txt1]);
ctx = Object.create(ctx); ctx = Object.create(ctx);
@@ -538,7 +540,6 @@ exports[`t-call t-call-context: ComponentNode is not looked up in the context 2`
let block2 = createBlock(\`<div block-ref=\\"0\\">outside slot</div>\`); let block2 = createBlock(\`<div block-ref=\\"0\\">outside slot</div>\`);
let block4 = createBlock(\`<div block-ref=\\"0\\">I'm the default slot</div>\`); let block4 = createBlock(\`<div block-ref=\\"0\\">I'm the default slot</div>\`);
let block5 = createBlock(\`<div><block-text-0/></div>\`); let block5 = createBlock(\`<div><block-text-0/></div>\`);
let block6 = createBlock(\`<div><block-text-0/></div>\`);
function slot1(ctx, node, key = \\"\\") { function slot1(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs; const refs = this.__owl__.refs;
@@ -547,11 +548,9 @@ exports[`t-call t-call-context: ComponentNode is not looked up in the context 2`
ctx[isBoundary] = 1 ctx[isBoundary] = 1
const b4 = block4([ref2]); const b4 = block4([ref2]);
setContextValue(ctx, \\"test\\", 3); setContextValue(ctx, \\"test\\", 3);
let txt1 = this.__owl__.name; let txt1 = ctx['test'];
const b5 = block5([txt1]); const b5 = block5([txt1]);
let txt2 = ctx['test']; return multi([b4, b5]);
const b6 = block6([txt2]);
return multi([b4, b5, b6]);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
@@ -559,8 +558,8 @@ exports[`t-call t-call-context: ComponentNode is not looked up in the context 2`
const ref1 = (el) => refs[\`myRef\`] = el; const ref1 = (el) => refs[\`myRef\`] = el;
const b2 = block2([ref1]); const b2 = block2([ref1]);
const ctx1 = capture(ctx); const ctx1 = capture(ctx);
const b7 = comp1({prop: bind(this, ctx['method']),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__1\`, node, this, null); const b6 = comp1({prop: bind(this, ctx['method']),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__1\`, node, this, null);
return multi([b2, b7]); return multi([b2, b6]);
} }
}" }"
`; `;
@@ -624,3 +623,27 @@ exports[`t-call t-call-context: slots don't make component available again when
} }
}" }"
`; `;
exports[`t-call t-call-context: this is not available inside t-call-context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const callTemplate_1 = app.getTemplate(\`someTemplate\`);
return function template(ctx, node, key = \\"\\") {
let ctx1 = {};
return callTemplate_1.call(this, ctx1, node, key + \`__1\`);
}
}"
`;
exports[`t-call t-call-context: this is not available inside t-call-context 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['this']);
}
}"
`;
@@ -569,6 +569,36 @@ exports[`t-model directive t-model with dynamic values on select options in fore
}" }"
`; `;
exports[`t-model directive t-model with radio button group in t-foreach 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, toNumber, withKey } = helpers;
let block1 = createBlock(\`<div id=\\"get_data\\" block-handler-0=\\"click\\"><block-child-0/></div>\`);
let block3 = createBlock(\`<input type=\\"radio\\" name=\\"radio_group\\" block-attribute-0=\\"value\\" block-attribute-1=\\"id\\" block-attribute-2=\\"checked\\" block-handler-3=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['getData'], ctx];
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['options']);;
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`opt\`] = v_block2[i1];
const key1 = ctx['opt'];
let attr1 = new String((ctx['opt']) || \\"\\");
let attr2 = ctx['opt'];
const bExpr1 = ctx['state'];
const expr1 = 'group';
let attr3 = bExpr1[expr1] === ctx['opt'];
let hdlr2 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
c_block2[i1] = withKey(block3([attr1, attr2, attr3, hdlr2]), key1);
}
const b2 = list(c_block2);
return block1([hdlr1], [b2]);
}
}"
`;
exports[`t-model directive two inputs in a div alternating with a t-if 1`] = ` exports[`t-model directive two inputs in a div alternating with a t-if 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -111,8 +111,9 @@ exports[`t-on t-on method call in t-foreach 1`] = `
const key1 = ctx['val']; const key1 = ctx['val'];
let txt1 = ctx['val_index']; let txt1 = ctx['val_index'];
let txt2 = ctx['val']+''; let txt2 = ctx['val']+'';
const v1 = ctx['val']; const v1 = ctx['this'];
let hdlr1 = [()=>this.addVal(v1), ctx]; const v2 = ctx['val'];
let hdlr1 = [()=>v1.addVal(v2), ctx];
c_block2[i1] = withKey(block3([txt1, txt2, hdlr1]), key1); c_block2[i1] = withKey(block3([txt1, txt2, hdlr1]), key1);
} }
const b2 = list(c_block2); const b2 = list(c_block2);
@@ -197,8 +198,9 @@ exports[`t-on t-on on components and t-foreach 1`] = `
for (let i1 = 0; i1 < l_block1; i1++) { for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`name\`] = v_block1[i1]; ctx[\`name\`] = v_block1[i1];
const key1 = ctx['name']; const key1 = ctx['name'];
const v1 = ctx['name']; const v1 = ctx['this'];
const hdlr1 = [()=>this.log(v1), ctx]; const v2 = ctx['name'];
const hdlr1 = [()=>v1.log(v2), ctx];
c_block1[i1] = withKey(catcher1(comp1({value: ctx['name']}, key + \`__1__\${key1}\`, node, this, null), [hdlr1]), key1); c_block1[i1] = withKey(catcher1(comp1({value: ctx['name']}, key + \`__1__\${key1}\`, node, this, null), [hdlr1]), key1);
} }
return list(c_block1); return list(c_block1);
@@ -293,8 +295,9 @@ exports[`t-on t-on on components, with a handler update 1`] = `
ctx = Object.create(ctx); ctx = Object.create(ctx);
ctx[isBoundary] = 1 ctx[isBoundary] = 1
setContextValue(ctx, \\"name\\", ctx['state'].name); setContextValue(ctx, \\"name\\", ctx['state'].name);
const v1 = ctx['name']; const v1 = ctx['this'];
const hdlr1 = [()=>this.log(v1), ctx]; const v2 = ctx['name'];
const hdlr1 = [()=>v1.log(v2), ctx];
return catcher1(comp1({value: ctx['name']}, key + \`__1\`, node, this, null), [hdlr1]); return catcher1(comp1({value: ctx['name']}, key + \`__1\`, node, this, null), [hdlr1]);
} }
}" }"
@@ -393,7 +396,8 @@ exports[`t-on t-on on t-set-slots 1`] = `
function slot1(ctx, node, key = \\"\\") { function slot1(ctx, node, key = \\"\\") {
const b6 = block6(); const b6 = block6();
const b7 = block7(); const b7 = block7();
const hdlr1 = [()=>this.state.count++, ctx]; const v1 = ctx['this'];
const hdlr1 = [()=>v1.state.count++, ctx];
return catcher1(multi([b6, b7]), [hdlr1]); return catcher1(multi([b6, b7]), [hdlr1]);
} }
@@ -450,7 +454,8 @@ exports[`t-on t-on on t-slots 2`] = `
const b2 = text(\` [\`); const b2 = text(\` [\`);
const b3 = text(ctx['state'].count); const b3 = text(ctx['state'].count);
const b4 = text(\`] \`); const b4 = text(\`] \`);
const hdlr1 = [()=>this.state.count++, ctx]; const v1 = ctx['this'];
const hdlr1 = [()=>v1.state.count++, ctx];
const b5 = catcher1(callSlot(ctx, node, key, 'default', false, {}), [hdlr1]); const b5 = catcher1(callSlot(ctx, node, key, 'default', false, {}), [hdlr1]);
return multi([b2, b3, b4, b5]); return multi([b2, b3, b4, b5]);
} }
+1 -2
View File
@@ -1,6 +1,5 @@
import { App, Component, mount, onMounted, useRef, useState } from "../../src/index"; import { App, Component, mount, onMounted, useRef, useState, xml } from "../../src/index";
import { logStep, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers"; import { logStep, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
import { xml } from "../../src/index";
snapshotEverything(); snapshotEverything();
let fixture: HTMLElement; let fixture: HTMLElement;
+17 -2
View File
@@ -345,14 +345,13 @@ describe("t-call", () => {
<Child prop.bind="method"> <Child prop.bind="method">
<div t-ref="myRef2">I'm the default slot</div> <div t-ref="myRef2">I'm the default slot</div>
<t t-set="test" t-value="3"/> <t t-set="test" t-value="3"/>
<div t-esc="this.__owl__.name"/>
<div t-esc="test"/> <div t-esc="test"/>
</Child> </Child>
</t> </t>
</templates>`, </templates>`,
}); });
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
"<div>outside slot</div><div>I'm the default slot</div><div>Root</div><div>3</div>" "<div>outside slot</div><div>I'm the default slot</div><div>3</div>"
); );
expect(Object.keys(child.__owl__.refs)).toEqual([]); expect(Object.keys(child.__owl__.refs)).toEqual([]);
expect(Object.keys(root.__owl__.refs)).toEqual(["myRef", "myRef2"]); expect(Object.keys(root.__owl__.refs)).toEqual(["myRef", "myRef2"]);
@@ -383,4 +382,20 @@ describe("t-call", () => {
}); });
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
}); });
test("t-call-context: this is not available inside t-call-context", async () => {
class Root extends Component {
static template = xml`<t t-call="someTemplate" t-call-context="{}"/>`;
}
await mount(Root, fixture, {
templates: `
<templates>
<t t-name="someTemplate">
<t t-esc="this"/>
</t>
</templates>`,
});
expect(fixture.innerHTML).toBe("");
});
}); });
+31
View File
@@ -644,4 +644,35 @@ describe("t-model directive", () => {
const input = fixture.querySelector("input")!; const input = fixture.querySelector("input")!;
await editInput(input, "Beam me up, Scotty"); await editInput(input, "Beam me up, Scotty");
}); });
test("t-model with radio button group in t-foreach", async () => {
expect.assertions(6);
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`
<div t-on-click="getData" id="get_data">
<t t-foreach="options" t-as="opt" t-key="opt">
<input type="radio" name="radio_group" t-model="state.group" t-att-value="opt" t-att-id="opt"/>
</t>
</div>
`;
state = useState({ group: "scotty" });
options = ["beam", "scotty"];
getData() {
steps.push(`group: ${this.state.group}`);
}
}
await mount(SomeComponent, fixture);
const divEl = fixture.querySelector("#get_data") as HTMLElement;
expect(fixture.querySelector("input:checked")!.getAttribute("id")).toBe("scotty");
divEl.click();
expect(steps).toEqual(["group: scotty"]);
fixture.querySelector("input")!.click();
expect(steps).toEqual(["group: scotty", "group: beam"]);
await nextTick();
expect(fixture.querySelector("input:checked")!.getAttribute("id")).toBe("beam");
divEl.click();
expect(steps).toEqual(["group: scotty", "group: beam", "group: beam"]);
});
}); });
+1 -1
View File
@@ -646,7 +646,7 @@ exports[`Portal portal destroys on crash 2`] = `
let block1 = createBlock(\`<span><block-text-0/></span>\`); let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['props'].error&&this.will.crash; let txt1 = ctx['props'].error&&ctx['this'].will.crash;
return block1([txt1]); return block1([txt1]);
} }
}" }"
+1 -1
View File
@@ -48,7 +48,7 @@ function writeToFile(filepath, data) {
} }
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1 // adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
const a = "·_,:;"; const a = -_,:;";
const p = new RegExp(a.split("").join("|"), "g"); const p = new RegExp(a.split("").join("|"), "g");
function slugify(str) { function slugify(str) {
+35 -6
View File
@@ -47,6 +47,18 @@ async function startRelease() {
let file = await ask(`Release notes (${REL_NOTES_FILE}): `); let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
file = file || REL_NOTES_FILE; file = file || REL_NOTES_FILE;
let content; let content;
if (!fs.existsSync(`./${file}`)) {
let lastRelease = await getOutput("git log --grep='\\[REL\\]' -n 1 --pretty=%H");
const commitsSinceLastRelease = await getOutput(`git log ${lastRelease.trim()}..HEAD --pretty=%s`);
const commitsAsMdList = commitsSinceLastRelease.trim().split("\n").map(l => " - " + l).join("\n");
log(`${file} did not exist, created a template containing all commits since last release.`)
fs.writeFileSync(file, `# v${next}\n\n${commitsAsMdList}`);
const shouldContinue = await ask(`Check that the contents of ${file} is correct, then press y to continue: `);
if (shouldContinue.toLowerCase() !== "y") {
log("aborted");
return;
}
}
try { try {
content = await readFile("./" + file); content = await readFile("./" + file);
} catch (e) { } catch (e) {
@@ -104,7 +116,7 @@ async function startRelease() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 7/${STEPS}: Creating the release...`); log(`Step 7/${STEPS}: Creating the release...`);
const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F ${REL_NOTES_FILE}`); const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F ${file}`);
if (relaseResult !== 0) { if (relaseResult !== 0) {
logError("github release failed. Aborting."); logError("github release failed. Aborting.");
return; return;
@@ -120,7 +132,7 @@ async function startRelease() {
if (shouldUploadPlayground) { if (shouldUploadPlayground) {
log(`Bonus step: publishing new release on playground...`); log(`Bonus step: publishing new release on playground...`);
let owl_code = null; let owl_code = null;
status = 0 let status = 0
try { try {
owl_code = await readFile("dist/owl.iife.js"); owl_code = await readFile("dist/owl.iife.js");
@@ -130,7 +142,8 @@ async function startRelease() {
return; return;
} }
status += await execCommand("git checkout gh-pages"); status |= await execCommand("git checkout gh-pages");
status |= await execCommand("git pull --rebase");
if (status !== 0) { if (status !== 0) {
logError("Couldn't switch to gh-pages branch") logError("Couldn't switch to gh-pages branch")
@@ -144,9 +157,9 @@ async function startRelease() {
return; return;
} }
status += await execCommand(`git commit -am "[IMP] update owl to v${next}"`); status |= await execCommand(`git commit -am "[IMP] update owl to v${next}"`);
status += await execCommand(`git push origin gh-pages`); status |= await execCommand(`git push origin gh-pages`);
status += await execCommand("git checkout -"); status |= await execCommand("git checkout -");
if (status !== 0) { if (status !== 0) {
logError("Something went wrong for the playground update.") logError("Something went wrong for the playground update.")
} }
@@ -231,3 +244,19 @@ async function replaceInFile(file, from, to) {
}); });
}); });
} }
async function getOutput(command) {
return new Promise((resolve, reject) => {
const childProcess = exec(command, (err, stdout, stderr) => {
if (err) {
reject(err);
}
resolve(stdout);
});
childProcess.on("exit", code => {
if (code !== 0) {
reject(code);
}
});
});
}