diff --git a/.gitignore b/.gitignore index 0fd83de01..a6cb50766 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ test-lib test-lib-es5 src/metaInfo.ts package-lock.json +.vscode \ No newline at end of file diff --git a/src/Core/Component.ts b/src/Core/Component.ts index 2cfd6b299..55b2e1d86 100644 --- a/src/Core/Component.ts +++ b/src/Core/Component.ts @@ -9,6 +9,9 @@ import ComponentDeclaration from "./ComponentDeclaration"; import GomlNode from "./GomlNode"; import Identity from "./Identity"; import IdentityMap from "./IdentityMap"; +import IParametricObject from "../Interface/IParametricObject"; +import Namespace from "./Namespace"; +import ParametricObjectContext from "./ParametricObjectContext"; /** * Base class for any components @@ -61,6 +64,7 @@ export default class Component extends IDObject { private _handlers: ((component: Component) => void)[] = []; private _additionalAttributesNames: Identity[] = []; private _initializedInfo: Nullable = null; + private _parametricContextMap: { [key: string]: ParametricObjectContext } = {}; /** * whether component enabled. @@ -256,4 +260,61 @@ export default class Component extends IDObject { protected __setCompanionWithSelfNS(name: string, value: any) { this.companion.set(this.name.ns.for(name), value); } + + /** + * + * @param obj Parametric object that is managed in this component + * @param baseName namespace base of this parametric object. Used for determining fqn. + */ + protected __attachParametricObject(obj: IParametricObject, baseName: string): ParametricObjectContext { + const decls = obj.getAttributeDeclarations(); + if (obj.owner) { + throw new Error(`Parametric object is not attachable for multiple component.`); + } + if (this._parametricContextMap[baseName]) { + throw new Error(`Parametric object for ${baseName} is already registered`); + } + obj.owner = this; + const ns = Namespace.define(baseName); + const nsMap: { [key: string]: string | ParametricObjectContext } = {}; + for (let key in decls) { + const decl = decls[key]; + if (this._isParametricObject(decl)) { + const poc = this.__attachParametricObject(decl, `${baseName}.${key}`); + nsMap[key] = poc; + } else { + const identity = ns.for(key); + this.__addAttribute(identity.fqn, decl); + nsMap[key] = identity.fqn; + } + } + const poc = new ParametricObjectContext(obj, this, baseName, nsMap); + this._parametricContextMap[baseName] = poc; + obj.onAttachComponent(this, poc); + return poc; + } + + /** + * Remove specified parametric object if exists + * @param baseName parametric object base name + */ + protected __detachParametricObject(baseName: string): void { + const poc = this._parametricContextMap[baseName]; + if (!poc) { + return; + } + for (let name in poc.nameToKey) { + const key = poc.nameToKey[name]; + if (typeof key === "string") { + this.__removeAttributes(key); + } else { + this.__detachParametricObject(poc.baseName); + } + } + poc.target.onDetachComponent(this, poc); + } + + private _isParametricObject(obj: any): obj is IParametricObject { + return obj && typeof obj.getAttributeDeclarations === "function"; + } } diff --git a/src/Core/ParametricObjectContext.ts b/src/Core/ParametricObjectContext.ts new file mode 100644 index 000000000..980fee82c --- /dev/null +++ b/src/Core/ParametricObjectContext.ts @@ -0,0 +1,40 @@ +import IParametricObject from "../Interface/IParametricObject"; +import Component from "./Component"; +import Attribute from "./Attribute"; +import { Nullable } from "../Tool/Types"; + +export default class ParametricObjectContext { + constructor(public target: IParametricObject, public component: Component, public baseName: string, public nameToKey: { [key: string]: string | ParametricObjectContext }) { + + } + + public getAttributeRaw(name: string): Nullable> { + return this.component.getAttributeRaw(this._ensureFQN(name)); + } + + public getAttribute(name: string): T { + return this.component.getAttribute(this._ensureFQN(name)); + } + + public setAttribute(name: string, val: T): void { + this.component.setAttribute(this._ensureFQN(name), val); + } + + public bindAttributes(target: any = this.target): void { + for (let name in this.nameToKey) { + const key = this.nameToKey[name]; + if (typeof key === "string") { + this.component.getAttributeRaw(key)!.bindTo(name, target); + } + } + } + + private _ensureFQN(name: string): string { + const fqnOrCop = this.nameToKey[name]; + if (typeof fqnOrCop === "string") { + return fqnOrCop; + } else { + throw new Error(`${name} is not valid for this parametric object`); + } + } +} \ No newline at end of file diff --git a/src/Interface/IParametricObject.ts b/src/Interface/IParametricObject.ts new file mode 100644 index 000000000..ddfc9c346 --- /dev/null +++ b/src/Interface/IParametricObject.ts @@ -0,0 +1,10 @@ +import Component from "../Core/Component"; +import IAttributeDeclaration from "./IAttributeDeclaration"; +import ParametricObjectContext from "../Core/ParametricObjectContext"; + +export default interface IParametricObject { + owner?: Component; + getAttributeDeclarations(): { [key: string]: IAttributeDeclaration | IParametricObject }; + onAttachComponent(component: Component, ctx: ParametricObjectContext): void; + onDetachComponent(lastComponent: Component, ctx: ParametricObjectContext): void; +} \ No newline at end of file diff --git a/test/Core/GomlNodeTest.ts b/test/Core/GomlNodeTest.ts index 6594923e9..c8dca1272 100644 --- a/test/Core/GomlNodeTest.ts +++ b/test/Core/GomlNodeTest.ts @@ -11,10 +11,13 @@ import GrimoireInterface from "../../src/Core/GrimoireInterface"; import Identity from "../../src/Core/Identity"; import TestEnvManager from "../TestEnvManager"; import TestUtil from "../TestUtil"; +import IParametricObject from "../../src/Interface/IParametricObject"; +import IAttributeDeclaration from "../../src/Interface/IAttributeDeclaration"; +import ParametricObjectContext from "../../src/Core/ParametricObjectContext"; TestEnvManager.init(); -test.beforeEach(async() => { +test.beforeEach(async () => { GrimoireInterface.debug = false; GrimoireInterface.clear(); TestEnvManager.loadPage(""); @@ -88,7 +91,7 @@ test("append works correctly with string argument", t => { test("append works correctly with gom argument", t => { const node = new GomlNode(GrimoireInterface.nodeDeclarations.get("goml")); t.truthy(node.children.length === 0); - node.append({name: "goml"}); + node.append({ name: "goml" }); t.truthy(node.children.length === 1); t.truthy(node.children[0].declaration.name.fqn === "grimoirejs.goml"); }); @@ -279,6 +282,54 @@ test("getComponents method overload works correctly", t => { t.truthy(components.length === 3); }); +test("attach ParametricObject should work", async (t) => { + const gr = Environment.GrimoireInterface; + class TestParametric implements IParametricObject { + constructor(private params: { [key: string]: IParametricObject | IAttributeDeclaration; }, private spy: () => void) { + + } + owner?: Component; + getAttributeDeclarations(): { [key: string]: IParametricObject | IAttributeDeclaration; } { + return this.params; + } + onAttachComponent(component: Component, ctx: ParametricObjectContext): void { + this.spy() + } + onDetachComponent(lastComponent: Component, ctx: ParametricObjectContext): void { + this.spy(); + } + } + const spy1 = spy(); + const spy2 = spy(); + gr.registerComponent({ + componentName: "Aaa", + attributes: {}, + $mount() { + this.__attachParametricObject(new TestParametric({ + attr1: { + converter: "String", + default: "HELLO" + }, + attr2: new TestParametric({ + attr3: { + converter: "String", + default: "WORLD" + } + }, spy2) + }, spy1), "test"); + } + }); + gr.registerNode("a", ["Aaa"]); + await TestEnvManager.loadPage(TestUtil.GenerateGomlEmbeddedHtml('')); + const parent = gr("*")("#parent").first(); + t.true(parent.getAttribute("attr1") === "HELLO"); + t.true(parent.getAttribute("test.attr1") === "HELLO"); + t.true(parent.getAttribute("attr3") === "WORLD"); + t.true(parent.getAttribute("test.attr2.attr3") === "WORLD"); + spy1.calledAfter(spy2); + t.true(spy1.calledOnce) +}); + test("async message reciever called with await", async t => { const gr = Environment.GrimoireInterface; const spy1 = spy();