Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/parametric object #547

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,4 @@ test-lib
test-lib-es5
src/metaInfo.ts
package-lock.json
.vscode
61 changes: 61 additions & 0 deletions src/Core/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -61,6 +64,7 @@ export default class Component extends IDObject {
private _handlers: ((component: Component) => void)[] = [];
private _additionalAttributesNames: Identity[] = [];
private _initializedInfo: Nullable<ITreeInitializedInfo> = null;
private _parametricContextMap: { [key: string]: ParametricObjectContext } = {};

/**
* whether component enabled.
Expand Down Expand Up @@ -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";
}
}
40 changes: 40 additions & 0 deletions src/Core/ParametricObjectContext.ts
Original file line number Diff line number Diff line change
@@ -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<T = any>(name: string): Nullable<Attribute<T>> {
return this.component.getAttributeRaw<T>(this._ensureFQN(name));
}

public getAttribute<T = any>(name: string): T {
return this.component.getAttribute(this._ensureFQN(name));
}

public setAttribute<T = any>(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`);
}
}
}
10 changes: 10 additions & 0 deletions src/Interface/IParametricObject.ts
Original file line number Diff line number Diff line change
@@ -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;
}
55 changes: 53 additions & 2 deletions test/Core/GomlNodeTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<html></html>");
Expand Down Expand Up @@ -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");
});
Expand Down Expand Up @@ -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<any>; }, private spy: () => void) {

}
owner?: Component;
getAttributeDeclarations(): { [key: string]: IParametricObject | IAttributeDeclaration<any>; } {
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('<a id="parent"></a>'));
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();
Expand Down