mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c085eef441 | |||
| b4f84513a6 |
@@ -1,4 +1,4 @@
|
|||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "./owl_error";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Expression Parser
|
* Owl QWeb Expression Parser
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
interpolate,
|
interpolate,
|
||||||
INTERP_REGEXP,
|
INTERP_REGEXP,
|
||||||
replaceDynamicParts,
|
replaceDynamicParts,
|
||||||
} from "./inline_expressions";
|
} from "../common/inline_expressions";
|
||||||
import {
|
import {
|
||||||
AST,
|
AST,
|
||||||
ASTComment,
|
ASTComment,
|
||||||
|
|||||||
+57
-37
@@ -1,14 +1,14 @@
|
|||||||
|
import { OwlError } from "../common/owl_error";
|
||||||
import { version } from "../version";
|
import { version } from "../version";
|
||||||
import { Component, ComponentConstructor, Props } from "./component";
|
import { Component, ComponentConstructor, Props } from "./component";
|
||||||
import { ComponentNode, saveCurrent } from "./component_node";
|
import { ComponentNode, saveCurrent } from "./component_node";
|
||||||
import { nodeErrorHandlers, handleError } from "./error_handling";
|
import { handleError, nodeErrorHandlers } from "./error_handling";
|
||||||
import { OwlError } from "../common/owl_error";
|
import { Fiber, MountOptions, RootFiber } from "./fibers";
|
||||||
import { Fiber, RootFiber, MountOptions } from "./fibers";
|
import { reactive, toRaw } from "./reactivity";
|
||||||
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 { toRaw, reactive } from "./reactivity";
|
|
||||||
|
|
||||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Root<P extends Props, E> {
|
interface Root<P extends Props, E> {
|
||||||
node: ComponentNode<P, E>;
|
node: ComponentNode<P, E> | null;
|
||||||
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
|
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
}
|
}
|
||||||
@@ -74,9 +74,10 @@ export class App<
|
|||||||
props: P;
|
props: P;
|
||||||
env: E;
|
env: E;
|
||||||
scheduler = new Scheduler();
|
scheduler = new Scheduler();
|
||||||
subRoots: Set<ComponentNode> = new Set();
|
subRoots: Set<Root<any, any>> = new Set();
|
||||||
root: ComponentNode<P, E> | null = null;
|
root: Root<P, E> | null = null;
|
||||||
warnIfNoStaticProps: boolean;
|
warnIfNoStaticProps: boolean;
|
||||||
|
_lastRootEl: HTMLElement | ShadowRoot | null = null; // temporary ref to propagate to roots
|
||||||
|
|
||||||
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
|
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
|
||||||
super(config);
|
super(config);
|
||||||
@@ -101,10 +102,8 @@ export class App<
|
|||||||
target: HTMLElement | ShadowRoot,
|
target: HTMLElement | ShadowRoot,
|
||||||
options?: MountOptions
|
options?: MountOptions
|
||||||
): Promise<Component<P, E> & InstanceType<T>> {
|
): Promise<Component<P, E> & InstanceType<T>> {
|
||||||
const root = this.createRoot(this.Root, { props: this.props });
|
this.root = this.createRoot(this.Root, { props: this.props });
|
||||||
this.root = root.node;
|
return this.root.mount(target, options) as any;
|
||||||
this.subRoots.delete(root.node);
|
|
||||||
return root.mount(target, options) as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoot<Props extends object, SubEnv = any>(
|
createRoot<Props extends object, SubEnv = any>(
|
||||||
@@ -112,26 +111,38 @@ export class App<
|
|||||||
config: RootConfig<Props, SubEnv> = {}
|
config: RootConfig<Props, SubEnv> = {}
|
||||||
): Root<Props, SubEnv> {
|
): Root<Props, SubEnv> {
|
||||||
const props = config.props || ({} as Props);
|
const props = config.props || ({} as Props);
|
||||||
// hack to make sure the sub root get the sub env if necessary. for owl 3,
|
|
||||||
// would be nice to rethink the initialization process to make sure that
|
|
||||||
// we can create a ComponentNode and give it explicitely the env, instead
|
|
||||||
// of looking it up in the app
|
|
||||||
const env = this.env;
|
const env = this.env;
|
||||||
if (config.env) {
|
const root: Root<Props, SubEnv> = {
|
||||||
this.env = config.env as any;
|
node: null,
|
||||||
}
|
|
||||||
|
|
||||||
const restore = saveCurrent();
|
|
||||||
const node = this.makeNode(Root, props);
|
|
||||||
restore();
|
|
||||||
if (config.env) {
|
|
||||||
this.env = env;
|
|
||||||
}
|
|
||||||
this.subRoots.add(node);
|
|
||||||
return {
|
|
||||||
node,
|
|
||||||
mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => {
|
mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => {
|
||||||
App.validateTarget(target);
|
App.validateTarget(target);
|
||||||
|
|
||||||
|
// hack to make sure the sub root get the sub env if necessary. for owl 3,
|
||||||
|
// would be nice to rethink the initialization process to make sure that
|
||||||
|
// we can create a ComponentNode and give it explicitely the env, instead
|
||||||
|
// of looking it up in the app
|
||||||
|
if (config.env) {
|
||||||
|
this.env = config.env as any;
|
||||||
|
}
|
||||||
|
const restore = saveCurrent();
|
||||||
|
if (options?.position === "attach") {
|
||||||
|
if (Root.template) {
|
||||||
|
throw new Error("Cannot attach a component with a template");
|
||||||
|
}
|
||||||
|
this._lastRootEl = target;
|
||||||
|
} else {
|
||||||
|
if (!Root.template) {
|
||||||
|
// no template => trigger an error
|
||||||
|
this.getTemplate("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const node = this.makeNode(Root, props);
|
||||||
|
root.node = node;
|
||||||
|
this._lastRootEl = null;
|
||||||
|
restore();
|
||||||
|
if (config.env) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
if (this.dev) {
|
if (this.dev) {
|
||||||
validateProps(Root, props, { __owl__: { app: this } });
|
validateProps(Root, props, { __owl__: { app: this } });
|
||||||
}
|
}
|
||||||
@@ -139,11 +150,16 @@ export class App<
|
|||||||
return prom;
|
return prom;
|
||||||
},
|
},
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
this.subRoots.delete(node);
|
this.subRoots.delete(root);
|
||||||
node.destroy();
|
if (root.node) {
|
||||||
this.scheduler.processTasks();
|
root.node?.destroy();
|
||||||
|
this.scheduler.processTasks();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.subRoots.add(root);
|
||||||
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
||||||
@@ -178,13 +194,17 @@ export class App<
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.root) {
|
const roots = [...this.subRoots].reverse();
|
||||||
for (let subroot of this.subRoots) {
|
for (let root of roots) {
|
||||||
subroot.destroy();
|
root.destroy();
|
||||||
}
|
|
||||||
this.root.destroy();
|
|
||||||
this.scheduler.processTasks();
|
|
||||||
}
|
}
|
||||||
|
// if (this.root) {
|
||||||
|
// for (let subroot of this.subRoots) {
|
||||||
|
// subroot.destroy();
|
||||||
|
// }
|
||||||
|
// this.root.destroy();
|
||||||
|
this.scheduler.processTasks();
|
||||||
|
// }
|
||||||
apps.delete(this);
|
apps.delete(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type Props = { [key: string]: any };
|
|||||||
|
|
||||||
interface StaticComponentProperties {
|
interface StaticComponentProperties {
|
||||||
template: string;
|
template: string;
|
||||||
|
dynamicContent?: { [spec: string]: string };
|
||||||
defaultProps?: any;
|
defaultProps?: any;
|
||||||
props?: Schema;
|
props?: Schema;
|
||||||
components?: { [componentName: string]: ComponentConstructor };
|
components?: { [componentName: string]: ComponentConstructor };
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { OwlError } from "../common/owl_error";
|
|||||||
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
|
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
|
||||||
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
|
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
import { batched, Callback } from "./utils";
|
import { batched, Callback, Markup } from "./utils";
|
||||||
|
import { xml } from "./template_set";
|
||||||
|
import { compileExpr } from "../common/inline_expressions";
|
||||||
|
|
||||||
let currentNode: ComponentNode | null = null;
|
let currentNode: ComponentNode | null = null;
|
||||||
|
|
||||||
@@ -124,11 +126,102 @@ 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);
|
||||||
const ctx = Object.assign(Object.create(this.component), { this: this.component });
|
const ctx = Object.assign(Object.create(this.component), { this: this.component });
|
||||||
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
|
if (C.template) {
|
||||||
|
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
|
||||||
|
} else {
|
||||||
|
// component will be attached
|
||||||
|
this.renderFn = app.getTemplate(xml``).bind(this.component, ctx, this);
|
||||||
|
if (C.dynamicContent) {
|
||||||
|
this.prepareAttach(app._lastRootEl!, C.dynamicContent, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.component.setup();
|
this.component.setup();
|
||||||
currentNode = null;
|
currentNode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareAttach(
|
||||||
|
el: HTMLElement | ShadowRoot,
|
||||||
|
dynamicContent: { [spec: string]: string },
|
||||||
|
ctx: any
|
||||||
|
) {
|
||||||
|
const attrs: { selector: string; attr: string; fn: Function }[] = [];
|
||||||
|
const handlers: { selector: string; event: string; fn: any }[] = [];
|
||||||
|
const tOuts: { selector: string; fn: Function }[] = [];
|
||||||
|
for (let key in dynamicContent) {
|
||||||
|
const value = dynamicContent[key];
|
||||||
|
const parts = key.split(":");
|
||||||
|
if (parts[1].startsWith("t-att-")) {
|
||||||
|
const attr = parts[1].slice(6);
|
||||||
|
const fn = new Function("ctx", `return ${compileExpr(value)};`);
|
||||||
|
attrs.push({
|
||||||
|
selector: parts[0],
|
||||||
|
attr,
|
||||||
|
fn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (parts[1].startsWith("t-on-")) {
|
||||||
|
const event = parts[1].slice(5);
|
||||||
|
// const fn = new Function("ctx", "ev", `${compileExpr(value)}(ev);`);
|
||||||
|
const fn = (ev: any) => (this as any).component[value](ev);
|
||||||
|
handlers.push({
|
||||||
|
selector: parts[0],
|
||||||
|
event,
|
||||||
|
fn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (parts[1] === "t-out") {
|
||||||
|
const fn = new Function("ctx", `return ${compileExpr(value)};`);
|
||||||
|
tOuts.push({ selector: parts[0], fn });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleAttrs = () => {
|
||||||
|
for (let attr of attrs) {
|
||||||
|
const val = attr.fn.call(this.component, ctx);
|
||||||
|
// todo: cache the queryselector result?
|
||||||
|
const target = attr.selector === "root" ? el : (el.querySelector(attr.selector) as any);
|
||||||
|
if (target) {
|
||||||
|
target.setAttribute(attr.attr, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleEvents = () => {
|
||||||
|
for (let handler of handlers) {
|
||||||
|
// const val = attr.fn.call(this.component, ctx);
|
||||||
|
// todo: cache the queryselector result?
|
||||||
|
const target =
|
||||||
|
handler.selector === "root" ? el : (el.querySelector(handler.selector) as any);
|
||||||
|
if (target) {
|
||||||
|
target.addEventListener(handler.event, handler.fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleTOuts = () => {
|
||||||
|
for (let tOut of tOuts) {
|
||||||
|
const val = tOut.fn.call(this.component, ctx);
|
||||||
|
// todo: cache the queryselector result?
|
||||||
|
const target = tOut.selector === "root" ? el : (el.querySelector(tOut.selector) as any);
|
||||||
|
if (target) {
|
||||||
|
if (val instanceof Markup) {
|
||||||
|
target.innerHTML = val as any;
|
||||||
|
} else {
|
||||||
|
target.textContent = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (attrs.length) {
|
||||||
|
this.mounted.push(handleAttrs);
|
||||||
|
this.patched.push(handleAttrs);
|
||||||
|
}
|
||||||
|
if (handlers.length) {
|
||||||
|
this.mounted.push(handleEvents);
|
||||||
|
}
|
||||||
|
if (tOuts.length) {
|
||||||
|
this.mounted.push(handleTOuts);
|
||||||
|
this.patched.push(handleTOuts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mountComponent(target: any, options?: MountOptions) {
|
mountComponent(target: any, options?: MountOptions) {
|
||||||
const fiber = new MountFiber(this, target, options);
|
const fiber = new MountFiber(this, target, options);
|
||||||
this.app.scheduler.addFiber(fiber);
|
this.app.scheduler.addFiber(fiber);
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export class RootFiber extends Fiber {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Position = "first-child" | "last-child";
|
type Position = "first-child" | "last-child" | "attach";
|
||||||
|
|
||||||
export interface MountOptions {
|
export interface MountOptions {
|
||||||
position?: Position;
|
position?: Position;
|
||||||
|
|||||||
@@ -136,3 +136,20 @@ export function useExternalListener(
|
|||||||
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
|
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
|
||||||
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
|
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// useAttachedEl
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purpose of this hook is to allow attached components to get a reference to
|
||||||
|
* the element they have been attached on.
|
||||||
|
*/
|
||||||
|
export function useAttachedEl(): HTMLElement {
|
||||||
|
const node = getCurrent();
|
||||||
|
const el = node.app._lastRootEl as HTMLElement;
|
||||||
|
if (!el) {
|
||||||
|
throw new Error("useAttachedEl can only be called with component that are attached");
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,17 +94,6 @@ exports[`subroot can create a root in a setup function, then use a hook 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`subroot can create a root in a setup function, then use a hook 2`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
return text(\`c\`);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`subroot can mount subroot 1`] = `
|
exports[`subroot can mount subroot 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { compileExpr, tokenize } from "../../src/compiler/inline_expressions";
|
import { compileExpr, tokenize } from "../../src/common/inline_expressions";
|
||||||
|
|
||||||
describe("tokenizer", () => {
|
describe("tokenizer", () => {
|
||||||
test("simple tokens", () => {
|
test("simple tokens", () => {
|
||||||
|
|||||||
@@ -332,6 +332,31 @@ exports[`lifecycle hooks lifecycle semantics, part 3 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`lifecycle hooks lifecycle semantics, part 3 3`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`GrandChild\`, true, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comp1({}, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`lifecycle hooks lifecycle semantics, part 3 4`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`lifecycle hooks lifecycle semantics, part 4 1`] = `
|
exports[`lifecycle hooks lifecycle semantics, part 4 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import { Component, markup, mount, useState, xml } from "../../src";
|
||||||
|
import { useAttachedEl } from "../../src/runtime/hooks";
|
||||||
|
import { makeTestFixture, nextTick, steps, useLogLifecycle } from "../helpers";
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("basics", () => {
|
||||||
|
test("can attach an empty component", async () => {
|
||||||
|
fixture.innerHTML = "<div>hello</div>";
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"Test:setup",
|
||||||
|
"Test:willStart",
|
||||||
|
"Test:willRender",
|
||||||
|
"Test:rendered",
|
||||||
|
"Test:mounted",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>hello</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("attaching a component with a template throws", async () => {
|
||||||
|
fixture.innerHTML = "<div>hello</div>";
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
static template = xml`hello`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let error: Error | null = null;
|
||||||
|
try {
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
} catch (e: any) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error!.message).toBe("Cannot attach a component with a template");
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div>hello</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can attach a component with simple dynamic content", async () => {
|
||||||
|
fixture.innerHTML = "<div><p>hello</p></div>";
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
static dynamicContent = {
|
||||||
|
"p:t-att-a": "value",
|
||||||
|
};
|
||||||
|
value: string = "";
|
||||||
|
setup() {
|
||||||
|
this.value = "b";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(fixture.innerHTML).toBe('<div><p a="b">hello</p></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useAttachedEl returns the attached element", async () => {
|
||||||
|
fixture.innerHTML = "<div><p>hello</p></div>";
|
||||||
|
let el: any = null;
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
setup() {
|
||||||
|
el = useAttachedEl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(el).toBe(fixture);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useAttachedEl throws if component is not attached", async () => {
|
||||||
|
class Test extends Component {
|
||||||
|
static template = xml`hello`;
|
||||||
|
setup() {
|
||||||
|
useAttachedEl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let error: any = null;
|
||||||
|
try {
|
||||||
|
await mount(Test, fixture);
|
||||||
|
} catch (_e: any) {
|
||||||
|
error = _e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.message).toBe("useAttachedEl can only be called with component that are attached");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multiple dynamic attribute", async () => {
|
||||||
|
fixture.innerHTML = "<div><p>hello</p></div>";
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
static dynamicContent = {
|
||||||
|
"p:t-att-a": "value",
|
||||||
|
"p:t-att-b": "value + 'coucou'",
|
||||||
|
};
|
||||||
|
value: string = "";
|
||||||
|
setup() {
|
||||||
|
this.value = "b";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(fixture.innerHTML).toBe('<div><p a="b" b="bcoucou">hello</p></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("attrs can target root", async () => {
|
||||||
|
fixture.innerHTML = "<p>hello</p>";
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
static dynamicContent = {
|
||||||
|
"root:t-att-a": "value",
|
||||||
|
};
|
||||||
|
value: string = "";
|
||||||
|
setup() {
|
||||||
|
this.value = "b";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(fixture.outerHTML).toBe('<div a="b"><p>hello</p></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dynamic attribute is updated on rerender", async () => {
|
||||||
|
fixture.innerHTML = "<div><p>hello</p></div>";
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
static dynamicContent = {
|
||||||
|
"p:t-att-a": "state.value",
|
||||||
|
};
|
||||||
|
state: any;
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ value: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const test = await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(fixture.innerHTML).toBe('<div><p a="1">hello</p></div>');
|
||||||
|
test.state.value = 2;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe('<div><p a="2">hello</p></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-on-click, basic", async () => {
|
||||||
|
fixture.innerHTML = "<div><p>hello</p></div>";
|
||||||
|
|
||||||
|
let ev: Event | null = null;
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
static dynamicContent = {
|
||||||
|
"p:t-att-a": "state.value",
|
||||||
|
"p:t-on-click": "onClick",
|
||||||
|
};
|
||||||
|
state: any;
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ value: 1 });
|
||||||
|
}
|
||||||
|
onClick(_ev: any) {
|
||||||
|
ev = _ev;
|
||||||
|
this.state.value = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(fixture.innerHTML).toBe('<div><p a="1">hello</p></div>');
|
||||||
|
fixture.querySelector("p")!.click();
|
||||||
|
expect(ev).toBeInstanceOf(Event);
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe('<div><p a="2">hello</p></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-on-click, target root element", async () => {
|
||||||
|
fixture.innerHTML = "hello";
|
||||||
|
let click = false;
|
||||||
|
class Test extends Component {
|
||||||
|
static dynamicContent = {
|
||||||
|
"root:t-on-click": "onClick",
|
||||||
|
};
|
||||||
|
onClick() {
|
||||||
|
click = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
fixture.click();
|
||||||
|
expect(click).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-out, basic", async () => {
|
||||||
|
fixture.innerHTML = "<div><p>hello</p></div>";
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
static dynamicContent = {
|
||||||
|
"p:t-out": "state.value",
|
||||||
|
};
|
||||||
|
state: any;
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ value: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-out, on root", async () => {
|
||||||
|
fixture.innerHTML = "hello";
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
static dynamicContent = {
|
||||||
|
"root:t-out": "state.value",
|
||||||
|
};
|
||||||
|
state: any;
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ value: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(fixture.outerHTML).toBe("<div>hello</div>");
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(fixture.outerHTML).toBe("<div>1</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-out, with markup", async () => {
|
||||||
|
fixture.innerHTML = `<p class="p1">hello</p><p class="p2">hello</p>`;
|
||||||
|
|
||||||
|
class Test extends Component {
|
||||||
|
static dynamicContent = {
|
||||||
|
"p.p1:t-out": "value1",
|
||||||
|
"p.p2:t-out": "value2",
|
||||||
|
};
|
||||||
|
|
||||||
|
value1: any;
|
||||||
|
value2: any;
|
||||||
|
setup() {
|
||||||
|
this.value1 = "<div>value1</div>";
|
||||||
|
this.value2 = markup("<div>value2</div>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Test, fixture, { position: "attach" });
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
`<p class="p1"><div>value1</div></p><p class="p2"><div>value2</div></p>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -702,7 +702,7 @@ describe("props validation", () => {
|
|||||||
const app = new App(Parent, { test: true });
|
const app = new App(Parent, { test: true });
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("12");
|
expect(fixture.innerHTML).toBe("12");
|
||||||
expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
|
expect(app.root!.node!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("props are validated whenever component is updated", async () => {
|
test("props are validated whenever component is updated", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user