Skip to content
This repository has been archived by the owner on Jul 13, 2020. It is now read-only.

Commit

Permalink
Merge pull request #60 from FullScreenShenanigans/firefox-factory
Browse files Browse the repository at this point in the history
Added a separate factory function for non-class components
  • Loading branch information
Josh Goldberg authored Sep 15, 2019
2 parents 859dfc4 + e31d2be commit 9a35dae
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 111 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ BabyIoC is the smallest IoC container you'll ever see _(under 50 lines of code!)
It's also got the fewest toys - it's only targeted for use by [GameStartr](https://github.com/FullScreenShenanigans/GameStartr).

Key tenants:
* All `@components` are members of the container class instance.
* All `@component`s are members of the container class instance.
* Components are stored as lazily evaluated getters: circular dependencies are fine!
* Use TypeScript.

Expand All @@ -37,6 +37,8 @@ Components receive the instance of the container as a single constructor paramet
They can use it to reference other components.

```typescript
import { component } from "babyioc";

class DependencyA { }

class DependencyB {
Expand All @@ -63,7 +65,11 @@ Your components don't have to be direct classes with dependencies.
Pass functions that take in your container as an argument.
The values returned by those functions are used as the component value.

Use `factory` instead of `component` for these.

```typescript
import { factory } from "babyioc";

class DependencyA {
public constructor(
public readonly member: string,
Expand All @@ -73,7 +79,7 @@ class DependencyA {
const createDependencyA = () => new DependencyA("value");

class Container {
@component(createDependencyA)
@factory(createDependencyA)
public readonly dependencyA: DependencyA;
}

Expand All @@ -83,6 +89,8 @@ const { dependencyA } = new Container();
These factory functions have access to all the values on the container, including computed getters.

```typescript
import { factory } from "babyioc";

class DependencyA {
public constructor(
public readonly memberA: string,
Expand All @@ -100,10 +108,10 @@ const createDependencyA = () => new DependencyA("valueA");
const createDependencyB = (instance: Container) => new DependencyB(dependencyA, container.valueC);

class Container {
@component(createDependencyA)
@factory(createDependencyA)
public readonly dependencyA: DependencyA;

@component(createDependencyB)
@factory(createDependencyB)
public readonly dependencyB: DependencyB;

public readonly valueC = "valueC";
Expand All @@ -116,13 +124,15 @@ const { dependencyA, dependencyB } = new Container();

## Technical Details

Marking a member as a `@component` creates a double-layer getter on the class prototype.
Marking a member with `@component` or `@factory` creates a double-layer getter on the class prototype.
The prototype will have a getter defined that writes a getter on the calling object.
Both getters return a new instance of the component.

For example, with this component:

```typescript
import { component } from "babyioc";

class Dependency { }

class Container {
Expand Down
180 changes: 91 additions & 89 deletions src/BabyIoC.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { expect } from "chai";

import { component } from "./index";
import { component, factory } from "./index";

// tslint:disable completed-docs no-use-before-declare
// tslint:disable completed-docs max-classes-per-file no-parameter-properties

describe("container", () => {
it("resolves a component dependency", () => {
Expand Down Expand Up @@ -78,6 +78,90 @@ describe("container", () => {
expect(dependencyB).to.be.instanceOf(DependencyB);
});

it("allows access to created components in class constructors", () => {
// Arrange
class Dependency { }
let internal: Dependency | undefined;

class Container {
@component(Dependency)
public readonly dependency: Dependency;

public constructor() {
internal = this.dependency;
}
}

// Act
const { dependency } = new Container();

// Assert
expect(internal).to.be.equal(dependency);
});

it("allows child classes to access parent values", () => {
// Arrange
class Dependency { }

class ParentContainer {
@component(Dependency)
public readonly dependencyA: Dependency;
}

class ChildContainer extends ParentContainer { }

// Act
const { dependencyA } = new ChildContainer();

// Assert
expect(dependencyA).to.be.instanceOf(Dependency);
});

it("overrides parent class components with child components under the same name", () => {
// Arrange
class ChildDependency { }
class ParentDependency { }

class ParentContainer {
@component(ParentDependency)
public readonly dependency: ParentDependency;
}

class ChildContainer extends ParentContainer {
@component(ChildDependency)
public readonly dependency: ChildDependency;
}

// Act
const { dependency } = new ChildContainer();

// Assert
expect(dependency).to.be.instanceOf(ChildDependency);
});

it("allows child components to declare their own sub-components", () => {
// Arrange
class GrandChild { }

class Child {
@component(GrandChild)
public readonly grandChild: GrandChild;
}

class Parent {
@component(Child)
public readonly child: Child;
}

// Act
const { grandChild } = new Parent().child;

// Assert
expect(grandChild).to.be.instanceOf(GrandChild);
});
});

describe("factory", () => {
it("creates a component using a factory", () => {
// Arrange
class Dependency {
Expand All @@ -89,7 +173,7 @@ describe("container", () => {
const createDependency = () => new Dependency(memberValue);

class Container {
@component(createDependency)
@factory(createDependency)
public readonly dependency: Dependency;
}

Expand Down Expand Up @@ -118,10 +202,10 @@ describe("container", () => {
const createDependencyB = () => new DependencyB(memberValueB);

class Container {
@component(createDependencyA)
@factory(createDependencyA)
public readonly dependencyA: DependencyA;

@component(createDependencyB)
@factory(createDependencyB)
public readonly dependencyB: DependencyB;
}

Expand Down Expand Up @@ -151,10 +235,10 @@ describe("container", () => {
const createDependencyB = (instance: Container) => new DependencyB(dependencyA, instance.valueC);

class Container {
@component(createDependencyA)
@factory(createDependencyA)
public readonly dependencyA: DependencyA;

@component(createDependencyB)
@factory(createDependencyB)
public readonly dependencyB: DependencyB;

public readonly valueC: string;
Expand All @@ -167,86 +251,4 @@ describe("container", () => {
expect(dependencyA.memberA).to.be.equal(memberValueA);
expect(dependencyB.referenceA).to.be.equal(dependencyA);
});

it("allows access to created components in class constructors", () => {
// Arrange
class Dependency { }
let internal: Dependency | undefined;

class Container {
@component(Dependency)
public readonly dependency: Dependency;

public constructor() {
internal = this.dependency;
}
}

// Act
const { dependency } = new Container();

// Assert
expect(internal).to.be.equal(dependency);
});

it("allows child classes to access parent values", () => {
// Arrange
class Dependency { }

class ParentContainer {
@component(Dependency)
public readonly dependencyA: Dependency;
}

class ChildContainer extends ParentContainer { }

// Act
const { dependencyA } = new ChildContainer();

// Assert
expect(dependencyA).to.be.instanceOf(Dependency);
});

it("overrides parent class components with child components under the same name", () => {
// Arrange
class ChildDependency { }
class ParentDependency { }

class ParentContainer {
@component(ParentDependency)
public readonly dependency: ParentDependency;
}

class ChildContainer extends ParentContainer {
@component(ChildDependency)
public readonly dependency: ChildDependency;
}

// Act
const { dependency } = new ChildContainer();

// Assert
expect(dependency).to.be.instanceOf(ChildDependency);
});

it("allows child components to declare their own sub-components", () => {
// Arrange
class GrandChild { }

class Child {
@component(GrandChild)
public readonly grandChild: GrandChild;
}

class Parent {
@component(Child)
public readonly child: Child;
}

// Act
const { grandChild } = new Parent().child;

// Assert
expect(grandChild).to.be.instanceOf(GrandChild);
});
});
29 changes: 19 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
export type IClassWithArg<TContainer, TInstance> = new(arg: TContainer) => TInstance;
export type IClassWithArg<TContainer, TInstance> = new (arg: TContainer) => TInstance;

export type IClassWithoutArgs<TInstance> = new() => TInstance;
export type IClassWithoutArgs<TInstance> = new () => TInstance;

export type IComponentFunction<TContainer, TInstance> = (container: TContainer) => TInstance;

export type IComponentClassOrFunction<TContainer, TInstance> =
export type IComponentClass<TContainer, TInstance> =
| IClassWithArg<TContainer, TInstance>
| IClassWithoutArgs<TInstance>
| IComponentFunction<TContainer, TInstance>
;
;

/**
* Adds a member component to a parent container.
* Decorates a caching getter on a class prototype.
*
* @param componentFunction Class or function that creates the component.
* @param factory Method used once within the getter to create an instance member.
*/
export const component = <TContainer extends {}, TInstance>(componentFunction: IComponentClassOrFunction<TContainer, TInstance>) =>
export const factory = <TContainer extends {}, TInstance>(
factory: IComponentFunction<TContainer, TInstance>,
) =>
(parentPrototype: TContainer, memberName: string) => {
Object.defineProperty(parentPrototype, memberName, {
configurable: true,
get(this: TContainer): TInstance {
const value: TInstance = new (componentFunction as IClassWithArg<TContainer, TInstance>)(this);
const value: TInstance = factory(this);

Object.defineProperty(this, memberName, {
configurable: true,
configurable: false,
get: () => value,
});

return value;
},
});
};

/**
* Decorates a member component class on a class prototype.
*
* @param componentClass Class to be initialized for the member instance.
*/
export const component = <TContainer extends {}, TInstance>(componentClass: IComponentClass<TContainer, TInstance>) =>
factory((container: TContainer) => new componentClass(container));
8 changes: 1 addition & 7 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@
]
},
"rules": {
"ban-types": false,
"completed-docs": false,
"max-classes-per-file": [true, "exclude-class-expressions"],
"no-non-null-assertion": false,
"no-parameter-properties": false,
"only-arrow-functions": false,
"no-shadowed-variable": false
"no-use-before-declare": false
}
}

0 comments on commit 9a35dae

Please sign in to comment.