mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea5d2be502 | |||
| 4a761a7403 | |||
| 8702db03fb | |||
| b2685b6709 | |||
| c30678f3ea | |||
| 6ca6717965 | |||
| ad4adb930e | |||
| cea82e945d | |||
| a69f8a39e7 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.0.3",
|
"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",
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -31,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,
|
||||||
@@ -48,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;
|
||||||
}
|
}
|
||||||
@@ -109,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>(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user