mirror of
https://github.com/FullScreenShenanigans/BabyIoC.git
synced 2026-08-12 11:18:25 -07:00
Added a separate factory function for non-class components
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
@@ -73,7 +79,7 @@ class DependencyA {
|
||||
const createDependencyA = () => new DependencyA("value");
|
||||
|
||||
class Container {
|
||||
@component(createDependencyA)
|
||||
@factory(createDependencyA)
|
||||
public readonly dependencyA: DependencyA;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -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";
|
||||
@@ -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 {
|
||||
|
||||
+94
-92
@@ -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", () => {
|
||||
@@ -78,96 +78,6 @@ describe("container", () => {
|
||||
expect(dependencyB).to.be.instanceOf(DependencyB);
|
||||
});
|
||||
|
||||
it("creates a component using a factory", () => {
|
||||
// Arrange
|
||||
class Dependency {
|
||||
public constructor(
|
||||
public readonly member: string,
|
||||
) { }
|
||||
}
|
||||
const memberValue = "memberValue";
|
||||
const createDependency = () => new Dependency(memberValue);
|
||||
|
||||
class Container {
|
||||
@component(createDependency)
|
||||
public readonly dependency: Dependency;
|
||||
}
|
||||
|
||||
// Act
|
||||
const { dependency } = new Container();
|
||||
|
||||
// Assert
|
||||
expect(dependency.member).to.be.equal(memberValue);
|
||||
});
|
||||
|
||||
it("creates different components using factories and their naming classes", () => {
|
||||
// Arrange
|
||||
class DependencyA {
|
||||
public constructor(
|
||||
public readonly memberA: string,
|
||||
) { }
|
||||
}
|
||||
class DependencyB {
|
||||
public constructor(
|
||||
public readonly memberB: string,
|
||||
) { }
|
||||
}
|
||||
const memberValueA = "memberValueA";
|
||||
const memberValueB = "memberValueB";
|
||||
const createDependencyA = () => new DependencyA(memberValueA);
|
||||
const createDependencyB = () => new DependencyB(memberValueB);
|
||||
|
||||
class Container {
|
||||
@component(createDependencyA)
|
||||
public readonly dependencyA: DependencyA;
|
||||
|
||||
@component(createDependencyB)
|
||||
public readonly dependencyB: DependencyB;
|
||||
}
|
||||
|
||||
// Act
|
||||
const { dependencyA, dependencyB } = new Container();
|
||||
|
||||
// Assert
|
||||
expect(dependencyA.memberA).to.be.equal(memberValueA);
|
||||
expect(dependencyB.memberB).to.be.equal(memberValueB);
|
||||
});
|
||||
|
||||
it("passes the container after creating getters to factories", () => {
|
||||
// Arrange
|
||||
class DependencyA {
|
||||
public constructor(
|
||||
public readonly memberA: string,
|
||||
) { }
|
||||
}
|
||||
class DependencyB {
|
||||
public constructor(
|
||||
public readonly referenceA: DependencyA,
|
||||
public readonly valueC: string,
|
||||
) { }
|
||||
}
|
||||
const memberValueA = "memberValueA";
|
||||
const createDependencyA = () => new DependencyA(memberValueA);
|
||||
const createDependencyB = (instance: Container) => new DependencyB(dependencyA, instance.valueC);
|
||||
|
||||
class Container {
|
||||
@component(createDependencyA)
|
||||
public readonly dependencyA: DependencyA;
|
||||
|
||||
@component(createDependencyB)
|
||||
public readonly dependencyB: DependencyB;
|
||||
|
||||
public readonly valueC: string;
|
||||
}
|
||||
|
||||
// Act
|
||||
const { dependencyA, dependencyB } = new Container();
|
||||
|
||||
// Assert
|
||||
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 { }
|
||||
@@ -250,3 +160,95 @@ describe("container", () => {
|
||||
expect(grandChild).to.be.instanceOf(GrandChild);
|
||||
});
|
||||
});
|
||||
|
||||
describe("factory", () => {
|
||||
it("creates a component using a factory", () => {
|
||||
// Arrange
|
||||
class Dependency {
|
||||
public constructor(
|
||||
public readonly member: string,
|
||||
) { }
|
||||
}
|
||||
const memberValue = "memberValue";
|
||||
const createDependency = () => new Dependency(memberValue);
|
||||
|
||||
class Container {
|
||||
@factory(createDependency)
|
||||
public readonly dependency: Dependency;
|
||||
}
|
||||
|
||||
// Act
|
||||
const { dependency } = new Container();
|
||||
|
||||
// Assert
|
||||
expect(dependency.member).to.be.equal(memberValue);
|
||||
});
|
||||
|
||||
it("creates different components using factories and their naming classes", () => {
|
||||
// Arrange
|
||||
class DependencyA {
|
||||
public constructor(
|
||||
public readonly memberA: string,
|
||||
) { }
|
||||
}
|
||||
class DependencyB {
|
||||
public constructor(
|
||||
public readonly memberB: string,
|
||||
) { }
|
||||
}
|
||||
const memberValueA = "memberValueA";
|
||||
const memberValueB = "memberValueB";
|
||||
const createDependencyA = () => new DependencyA(memberValueA);
|
||||
const createDependencyB = () => new DependencyB(memberValueB);
|
||||
|
||||
class Container {
|
||||
@factory(createDependencyA)
|
||||
public readonly dependencyA: DependencyA;
|
||||
|
||||
@factory(createDependencyB)
|
||||
public readonly dependencyB: DependencyB;
|
||||
}
|
||||
|
||||
// Act
|
||||
const { dependencyA, dependencyB } = new Container();
|
||||
|
||||
// Assert
|
||||
expect(dependencyA.memberA).to.be.equal(memberValueA);
|
||||
expect(dependencyB.memberB).to.be.equal(memberValueB);
|
||||
});
|
||||
|
||||
it("passes the container after creating getters to factories", () => {
|
||||
// Arrange
|
||||
class DependencyA {
|
||||
public constructor(
|
||||
public readonly memberA: string,
|
||||
) { }
|
||||
}
|
||||
class DependencyB {
|
||||
public constructor(
|
||||
public readonly referenceA: DependencyA,
|
||||
public readonly valueC: string,
|
||||
) { }
|
||||
}
|
||||
const memberValueA = "memberValueA";
|
||||
const createDependencyA = () => new DependencyA(memberValueA);
|
||||
const createDependencyB = (instance: Container) => new DependencyB(dependencyA, instance.valueC);
|
||||
|
||||
class Container {
|
||||
@factory(createDependencyA)
|
||||
public readonly dependencyA: DependencyA;
|
||||
|
||||
@factory(createDependencyB)
|
||||
public readonly dependencyB: DependencyB;
|
||||
|
||||
public readonly valueC: string;
|
||||
}
|
||||
|
||||
// Act
|
||||
const { dependencyA, dependencyB } = new Container();
|
||||
|
||||
// Assert
|
||||
expect(dependencyA.memberA).to.be.equal(memberValueA);
|
||||
expect(dependencyB.referenceA).to.be.equal(dependencyA);
|
||||
});
|
||||
});
|
||||
|
||||
+19
-10
@@ -1,29 +1,30 @@
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -31,3 +32,11 @@ export const component = <TContainer extends {}, TInstance>(componentFunction: I
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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));
|
||||
|
||||
@@ -4,14 +4,5 @@
|
||||
"exclude": [
|
||||
"./node_modules/**/*"
|
||||
]
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user