mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b04013f82f | |||
| dd292472b9 | |||
| 9b18b57fdf | |||
| 68f491cd32 | |||
| 7b3e39ba27 | |||
| 61fc3f4fdc | |||
| 7b454dae66 |
@@ -61,6 +61,8 @@ The `config` object is an object with some of the following keys:
|
||||
templates (see [translations](translations.md))
|
||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||
the components created by the application.
|
||||
- **`getTemplate ((s: string) => Element | Function | string | void)`**: a function that will be called by owl when it
|
||||
needs a template. If undefined is returned, owl looks into the app templates.
|
||||
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
|
||||
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ This may seem counter-intuitive, but it makes perfect sense in the context of co
|
||||
```js
|
||||
class DoubleCounter extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="state.selected + ': ' + state[state.selected].value"/>
|
||||
<t t-esc="'selected: ' + state.selected + ', value: ' + state[state.selected]"/>
|
||||
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
||||
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
||||
<button t-on-click="changeCounter">Switch counter</button>
|
||||
|
||||
+25
-49
@@ -1850,8 +1850,9 @@ const NO_CALLBACK = () => {
|
||||
};
|
||||
const objectToString = Object.prototype.toString;
|
||||
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
||||
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
||||
// Use arrays because Array.includes is faster than Set.has for small arrays
|
||||
const SUPPORTED_RAW_TYPES = ["Object", "Array", "Set", "Map", "WeakMap"];
|
||||
const COLLECTION_RAW_TYPES = ["Set", "Map", "WeakMap"];
|
||||
/**
|
||||
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
||||
* many native objects such as Promise (whose toString is [object Promise])
|
||||
@@ -1874,7 +1875,7 @@ function canBeMadeReactive(value) {
|
||||
if (typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
||||
return SUPPORTED_RAW_TYPES.includes(rawType(value));
|
||||
}
|
||||
/**
|
||||
* Creates a reactive from the given object/callback if possible and returns it,
|
||||
@@ -2044,7 +2045,7 @@ function reactive(target, callback = NO_CALLBACK) {
|
||||
const reactivesForTarget = reactiveCache.get(target);
|
||||
if (!reactivesForTarget.has(callback)) {
|
||||
const targetRawType = rawType(target);
|
||||
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
||||
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||||
? collectionsProxyHandler(target, callback, targetRawType)
|
||||
: basicProxyHandler(callback);
|
||||
const proxy = new Proxy(target, handler);
|
||||
@@ -3159,8 +3160,14 @@ const helpers = {
|
||||
makeRefWrapper,
|
||||
};
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
||||
function parseXML$1(xml) {
|
||||
/**
|
||||
* Parses an XML string into an XML document, throwing errors on parser errors
|
||||
* instead of returning an XML document containing the parseerror.
|
||||
*
|
||||
* @param xml the string to parse
|
||||
* @returns an XML document corresponding to the content of the string
|
||||
*/
|
||||
function parseXML(xml) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
@@ -3187,7 +3194,9 @@ function parseXML$1(xml) {
|
||||
throw new OwlError(msg);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
||||
class TemplateSet {
|
||||
constructor(config = {}) {
|
||||
this.rawTemplates = Object.create(globalTemplates);
|
||||
@@ -3206,6 +3215,7 @@ class TemplateSet {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.getRawTemplate = config.getTemplate;
|
||||
}
|
||||
static registerTemplate(name, fn) {
|
||||
globalTemplates[name] = fn;
|
||||
@@ -3235,15 +3245,16 @@ class TemplateSet {
|
||||
// empty string
|
||||
return;
|
||||
}
|
||||
xml = xml instanceof Document ? xml : parseXML$1(xml);
|
||||
xml = xml instanceof Document ? xml : parseXML(xml);
|
||||
for (const template of xml.querySelectorAll("[t-name]")) {
|
||||
const name = template.getAttribute("t-name");
|
||||
this.addTemplate(name, template);
|
||||
}
|
||||
}
|
||||
getTemplate(name) {
|
||||
var _a;
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
const rawTemplate = ((_a = this.getRawTemplate) === null || _a === void 0 ? void 0 : _a.call(this, name)) || this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
let extraInfo = "";
|
||||
try {
|
||||
@@ -4953,9 +4964,9 @@ function parseDOMNode(node, ctx) {
|
||||
const isSelect = tagName === "select";
|
||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||
const isRadioInput = isInput && typeAttr === "radio";
|
||||
const hasLazyMod = attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const hasTrimMod = attr.includes(".trim");
|
||||
const hasLazyMod = hasTrimMod || attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||||
model = {
|
||||
baseExpr,
|
||||
@@ -5501,41 +5512,6 @@ function normalizeTEscTOut(el) {
|
||||
function normalizeXML(el) {
|
||||
normalizeTIf(el);
|
||||
normalizeTEscTOut(el);
|
||||
}
|
||||
/**
|
||||
* Parses an XML string into an XML document, throwing errors on parser errors
|
||||
* instead of returning an XML document containing the parseerror.
|
||||
*
|
||||
* @param xml the string to parse
|
||||
* @returns an XML document corresponding to the content of the string
|
||||
*/
|
||||
function parseXML(xml) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new OwlError(msg);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
function compile(template, options = {}) {
|
||||
@@ -5562,7 +5538,7 @@ function compile(template, options = {}) {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.2.7";
|
||||
const version = "2.2.9";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -5991,6 +5967,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
||||
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||
|
||||
|
||||
__info__.date = '2023-12-06T13:56:01.636Z';
|
||||
__info__.hash = 'e94428a';
|
||||
__info__.date = '2024-01-12T14:43:56.804Z';
|
||||
__info__.hash = '7b3e39b';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.7",
|
||||
"version": "2.2.9",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.7",
|
||||
"version": "2.2.9",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
|
||||
@@ -20,8 +20,9 @@ type CollectionRawType = "Set" | "Map" | "WeakMap";
|
||||
const objectToString = Object.prototype.toString;
|
||||
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
||||
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
||||
// Use arrays because Array.includes is faster than Set.has for small arrays
|
||||
const SUPPORTED_RAW_TYPES = ["Object", "Array", "Set", "Map", "WeakMap"];
|
||||
const COLLECTION_RAW_TYPES = ["Set", "Map", "WeakMap"];
|
||||
|
||||
/**
|
||||
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
||||
@@ -45,7 +46,7 @@ function canBeMadeReactive(value: any): boolean {
|
||||
if (typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
||||
return SUPPORTED_RAW_TYPES.includes(rawType(value));
|
||||
}
|
||||
/**
|
||||
* Creates a reactive from the given object/callback if possible and returns it,
|
||||
@@ -220,12 +221,36 @@ export function reactive<T extends Target>(target: T, callback: Callback = NO_CA
|
||||
const reactivesForTarget = reactiveCache.get(target)!;
|
||||
if (!reactivesForTarget.has(callback)) {
|
||||
const targetRawType = rawType(target);
|
||||
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
||||
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||||
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
||||
: basicProxyHandler<T>(callback);
|
||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||
reactivesForTarget.set(callback, proxy);
|
||||
targets.set(proxy, target);
|
||||
// FIXME: this probably slows down reactive creation significantly, we probably don't want to do
|
||||
// it all the time. Maybe should be a separate function.
|
||||
const derivedDescriptors = Object.entries(Object.getOwnPropertyDescriptors(target)).filter(
|
||||
([k, descriptor]) => {
|
||||
if (toRaw(descriptor.value)?.[IS_DERIVED_DESCRIPTOR]) {
|
||||
delete target[k as keyof typeof target]; // prevent circular call in effect below
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
for (const [
|
||||
key,
|
||||
{
|
||||
value: [deps, compute],
|
||||
},
|
||||
] of derivedDescriptors) {
|
||||
effect(
|
||||
(proxy, deps) => {
|
||||
proxy[key as keyof typeof proxy] = Reflect.apply(compute, proxy, deps);
|
||||
},
|
||||
[proxy, deps]
|
||||
);
|
||||
}
|
||||
}
|
||||
return reactivesForTarget.get(callback) as Reactive<T>;
|
||||
}
|
||||
@@ -462,3 +487,18 @@ function collectionsProxyHandler<T extends Collection>(
|
||||
},
|
||||
}) as ProxyHandler<T>;
|
||||
}
|
||||
|
||||
const IS_DERIVED_DESCRIPTOR = Symbol("is derived descriptor");
|
||||
export function derived<T extends Reactive<any>[], U>(deps: T, compute: (...args: T) => U) {
|
||||
return Object.assign([deps, compute], { [IS_DERIVED_DESCRIPTOR]: true }) as unknown as U;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a side-effect that runs based on the content of reactive objects.
|
||||
*/
|
||||
export function effect<T extends object[]>(cb: (...args: [...T]) => void, deps: [...T]) {
|
||||
const reactiveDeps = reactive(deps, () => {
|
||||
cb(...reactiveDeps);
|
||||
});
|
||||
cb(...reactiveDeps);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface TemplateSetConfig {
|
||||
translatableAttributes?: string[];
|
||||
translateFn?: (s: string) => string;
|
||||
templates?: string | Document | Record<string, string>;
|
||||
getTemplate?: (s: string) => Element | Function | string | void;
|
||||
}
|
||||
|
||||
export class TemplateSet {
|
||||
@@ -22,6 +23,7 @@ export class TemplateSet {
|
||||
dev: boolean;
|
||||
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
||||
templates: { [name: string]: Template } = {};
|
||||
getRawTemplate?: (s: string) => Element | Function | string | void;
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
Portal = Portal;
|
||||
@@ -39,6 +41,7 @@ export class TemplateSet {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.getRawTemplate = config.getTemplate;
|
||||
}
|
||||
|
||||
addTemplate(name: string, template: string | Element) {
|
||||
@@ -77,7 +80,7 @@ export class TemplateSet {
|
||||
|
||||
getTemplate(name: string): Template {
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
const rawTemplate = this.getRawTemplate?.(name) || this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
let extraInfo = "";
|
||||
try {
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.2.7";
|
||||
export const version = "2.2.9";
|
||||
|
||||
@@ -101,3 +101,55 @@ exports[`loading templates can load a few templates from an XMLDocument 2`] = `
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`loading templates getTemplate: element returned (2) 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>Hello World!</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`loading templates getTemplate: element returned 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>Hello World!</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`loading templates getTemplate: template string returned 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>Hello World!</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`loading templates getTemplate: undefined returned 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>Hello World!</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -78,4 +78,62 @@ describe("loading templates", () => {
|
||||
context.addTemplates(xml);
|
||||
expect(Object.keys(context.rawTemplates)).toEqual([]);
|
||||
});
|
||||
|
||||
test("getTemplate: element returned", () => {
|
||||
const context = new TestContext({
|
||||
getTemplate: (name) => {
|
||||
if (name === "main") {
|
||||
const data = `<div>Hello World!</div>`;
|
||||
const xml = new DOMParser().parseFromString(data, "text/xml");
|
||||
return xml.firstChild as Element;
|
||||
}
|
||||
return;
|
||||
},
|
||||
});
|
||||
const result = context.renderToString("main");
|
||||
expect(result).toBe("<div>Hello World!</div>");
|
||||
});
|
||||
|
||||
test("getTemplate: element returned (2)", () => {
|
||||
const context = new TestContext({
|
||||
getTemplate: (name) => {
|
||||
if (name === "main") {
|
||||
const doc = new Document();
|
||||
const div = doc.createElement("div");
|
||||
div.append(doc.createTextNode("Hello World!"));
|
||||
return div;
|
||||
}
|
||||
return;
|
||||
},
|
||||
});
|
||||
const result = context.renderToString("main");
|
||||
expect(result).toBe("<div>Hello World!</div>");
|
||||
});
|
||||
|
||||
test("getTemplate: template string returned", () => {
|
||||
const context = new TestContext({
|
||||
getTemplate: (name) => {
|
||||
if (name === "main") {
|
||||
return `<div>Hello World!</div>`;
|
||||
}
|
||||
return;
|
||||
},
|
||||
});
|
||||
const result = context.renderToString("main");
|
||||
expect(result).toBe("<div>Hello World!</div>");
|
||||
});
|
||||
|
||||
test("getTemplate: undefined returned", () => {
|
||||
const context = new TestContext({
|
||||
getTemplate: () => {},
|
||||
});
|
||||
const data = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<div t-name="main">Hello World!</div>
|
||||
</templates>`;
|
||||
const xml = new DOMParser().parseFromString(data, "text/xml");
|
||||
context.addTemplates(xml);
|
||||
const result = context.renderToString("main");
|
||||
expect(result).toBe("<div>Hello World!</div>");
|
||||
});
|
||||
});
|
||||
|
||||
+154
-1
@@ -9,7 +9,7 @@ import {
|
||||
markRaw,
|
||||
toRaw,
|
||||
} from "../src";
|
||||
import { reactive, getSubscriptions } from "../src/runtime/reactivity";
|
||||
import { reactive, getSubscriptions, derived } from "../src/runtime/reactivity";
|
||||
import { batched } from "../src/runtime/utils";
|
||||
import {
|
||||
makeDeferred,
|
||||
@@ -2424,3 +2424,156 @@ describe("Reactivity: useState", () => {
|
||||
expect(fixture.innerHTML).toBe("<div><p><span>2b</span></p></div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("derived", () => {
|
||||
test("can read", async () => {
|
||||
const state = reactive({ a: derived([], () => 1) });
|
||||
expect(state.a).toBe(1);
|
||||
});
|
||||
|
||||
test("can create new keys", () => {
|
||||
const state: any = reactive({ b: derived([], () => 2) });
|
||||
state.a = 1;
|
||||
expect(state.a).toBe(1);
|
||||
});
|
||||
|
||||
test("can update", () => {
|
||||
const o = reactive({ a: 1 });
|
||||
let computeCall = 0;
|
||||
const state = reactive({
|
||||
a: derived([o], (o) => {
|
||||
computeCall++;
|
||||
return o.a;
|
||||
}),
|
||||
});
|
||||
expect(computeCall).toBe(1);
|
||||
expect(state.a).toBe(1);
|
||||
o.a = 2;
|
||||
expect(computeCall).toBe(2);
|
||||
expect(state.a).toBe(2);
|
||||
});
|
||||
|
||||
test("callback is called when changing an observed property", async () => {
|
||||
let notifyCount = 0;
|
||||
const o = reactive({ a: 1 });
|
||||
let computeCall = 0;
|
||||
const state = reactive(
|
||||
{
|
||||
a: derived([o], (o) => {
|
||||
computeCall++;
|
||||
return o.a;
|
||||
}),
|
||||
},
|
||||
() => notifyCount++
|
||||
);
|
||||
expect(computeCall).toBe(1);
|
||||
expect(notifyCount).toBe(0);
|
||||
expect(state.a).toBe(1);
|
||||
o.a = 2;
|
||||
expect(computeCall).toBe(2);
|
||||
expect(notifyCount).toBe(1);
|
||||
expect(state.a).toBe(2);
|
||||
o.a = 5;
|
||||
expect(computeCall).toBe(3);
|
||||
expect(notifyCount).toBe(2);
|
||||
expect(state.a).toBe(5);
|
||||
});
|
||||
|
||||
test("multiple dependencies", async () => {
|
||||
let notifyCount = 0;
|
||||
const a = reactive({ val: 1 });
|
||||
const b = reactive({ val: 2 });
|
||||
let computeCall = 0;
|
||||
const state = reactive(
|
||||
{
|
||||
c: derived([a, b], (a, b) => {
|
||||
computeCall++;
|
||||
return a.val + b.val;
|
||||
}),
|
||||
},
|
||||
() => notifyCount++
|
||||
);
|
||||
expect(computeCall).toBe(1);
|
||||
a.val = 2;
|
||||
expect(computeCall).toBe(2);
|
||||
expect(notifyCount).toBe(0);
|
||||
expect(state.c).toBe(4);
|
||||
a.val = 4;
|
||||
expect(computeCall).toBe(3);
|
||||
expect(notifyCount).toBe(1);
|
||||
expect(state.c).toBe(6);
|
||||
b.val = 3;
|
||||
expect(computeCall).toBe(4);
|
||||
expect(notifyCount).toBe(2);
|
||||
expect(state.c).toBe(7);
|
||||
});
|
||||
|
||||
test("dependency on own fields", async () => {
|
||||
let notifyCount = 0;
|
||||
const a = reactive({ val: 1 });
|
||||
let computeCall = 0;
|
||||
const state = reactive(
|
||||
{
|
||||
b: 2,
|
||||
c: derived([a], function (this: any, a) {
|
||||
computeCall++;
|
||||
return a.val + this.b;
|
||||
}),
|
||||
},
|
||||
() => notifyCount++
|
||||
);
|
||||
expect(computeCall).toBe(1);
|
||||
a.val = 2;
|
||||
expect(computeCall).toBe(2);
|
||||
expect(notifyCount).toBe(0);
|
||||
expect(state.c).toBe(4);
|
||||
a.val = 4;
|
||||
expect(computeCall).toBe(3);
|
||||
expect(notifyCount).toBe(1);
|
||||
expect(state.c).toBe(6);
|
||||
state.b = 3;
|
||||
expect(computeCall).toBe(4);
|
||||
expect(notifyCount).toBe(2);
|
||||
expect(state.c).toBe(7);
|
||||
});
|
||||
|
||||
test("dependency on derived property", () => {
|
||||
let computeB = 0;
|
||||
let computeC = 0;
|
||||
const state = reactive({
|
||||
a: 1,
|
||||
b: derived([], function (this: any) {
|
||||
computeB++;
|
||||
return this.a + 1;
|
||||
}),
|
||||
c: derived([], function (this: any) {
|
||||
computeC++;
|
||||
return this.b + 1;
|
||||
}),
|
||||
});
|
||||
expect(computeB).toBe(1);
|
||||
expect(computeC).toBe(1);
|
||||
expect(state.c).toBe(3);
|
||||
});
|
||||
|
||||
test("dependency on derived property appearing later in object", () => {
|
||||
let computeB = 0;
|
||||
let computeC = 0;
|
||||
const state = reactive({
|
||||
a: 1,
|
||||
c: derived([], function (this: any) {
|
||||
computeC++;
|
||||
return this.b + 1;
|
||||
}),
|
||||
b: derived([], function (this: any) {
|
||||
computeB++;
|
||||
return this.a + 1;
|
||||
}),
|
||||
});
|
||||
expect(computeB).toBe(1);
|
||||
// because computation is eager and naive, C is first computed to be undefined, then B is computed
|
||||
// to be 2, and the computation of B causes C to recompute and become 3. This causes C to compute twice.
|
||||
expect(computeC).toBe(2);
|
||||
expect(state.c).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,43 @@
|
||||
{
|
||||
"Basic owl component": {
|
||||
"Basic OWL Component": {
|
||||
"prefix": "owlcomponent",
|
||||
"scope": "javascript,typescript",
|
||||
"body": [
|
||||
"export class ${1:component-name} extends Component {",
|
||||
" static template = \"${2:template-name}\";",
|
||||
"import { Component } from \"@odoo/owl\";",
|
||||
"",
|
||||
"class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}} extends ${2:Component} {",
|
||||
"",
|
||||
" static template = \"${3:${RELATIVE_FILEPATH/(.*[\\|\\/])??([a-zA-Z_]+)([\\|\\/]static[\\|\\/].*)/${2}/g}}.${4:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}}\";",
|
||||
" static components = {};",
|
||||
" static props = {};",
|
||||
"",
|
||||
" setup() {",
|
||||
"",
|
||||
" ${5:super.setup();}",
|
||||
" }",
|
||||
"",
|
||||
" ${6:// Do Something}",
|
||||
"}",
|
||||
""
|
||||
|
||||
],
|
||||
"description": "The starting base for an owl component"
|
||||
},
|
||||
|
||||
"Basic OWL Template": {
|
||||
"prefix": "owltemplate",
|
||||
"scope": "xml",
|
||||
"body": [
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>",
|
||||
"",
|
||||
"<templates xml:space=\"preserve\">",
|
||||
"",
|
||||
" <t t-name=\"${2:${RELATIVE_FILEPATH/(.*[\\|\\/])??([a-zA-Z_]+)([\\|\\/]static[\\|\\/].*)/${2}/g}}.${3:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}}\">",
|
||||
" ${3:<h1>Hello World</h1>}",
|
||||
" </t>",
|
||||
"",
|
||||
"</templates>",
|
||||
""
|
||||
],
|
||||
"description": "Generate a basic OWL template XML file"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user