Skip to content

Commit

Permalink
Property injection support (#191)
Browse files Browse the repository at this point in the history
* working on #189

* Working on #189

* Working on #189

* Added property injection support
  • Loading branch information
remojansen committed May 6, 2016
1 parent d89d0a8 commit f8fbafd
Show file tree
Hide file tree
Showing 28 changed files with 920 additions and 134 deletions.
134 changes: 128 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ InversifyJS has been developed with 3 main goals:

You can get the latest release and the type definitions using npm:
```sh
npm install [email protected].2 --save
npm install [email protected].3 --save
```
> **Note**: We have decided to [drop support for bower](https://twitter.com/nachocoloma/status/663622545162280960) and tsd.
Expand Down Expand Up @@ -319,6 +319,7 @@ kernel.bind<INinja>(Symbols.INinja).to(Ninja);
kernel.bind<IKatana>(Symbols.IKatana).to(Katana);
kernel.bind<IShuriken>(Symbols.IShuriken).to(Shuriken);
```

#### Declaring kernel modules

Kernel modules can help you to manage the complexity of your bindings in very large applications.
Expand Down Expand Up @@ -821,10 +822,10 @@ let katana = kernel.getTagged<IWeapon>("IWeapon", "faction", "samurai");
let shuriken = kernel.getTagged<IWeapon>("IWeapon", "faction", "ninja");
```

#### Contextual bindings & @paramNames
The `@paramName` decorator is used to access the names of the constructor arguments from a
#### Contextual bindings & @targetName
The `@targetName` decorator is used to access the names of the constructor arguments from a
contextual constraint even when the code is compressed. The `constructor(katana, shuriken) { ...`
becomes `constructor(a, b) { ...` after compression but thanks to `@paramName` we can still
becomes `constructor(a, b) { ...` after compression but thanks to `@targetName` we can still
refer to the design-time names `katana` and `shuriken` at runtime.

```ts
Expand All @@ -846,8 +847,8 @@ class Ninja implements INinja {
public katana: IWeapon;
public shuriken: IWeapon;
public constructor(
@inject("IWeapon") @paramName("katana") katana: IWeapon,
@inject("IWeapon") @paramName("shuriken") shuriken: IWeapon
@inject("IWeapon") @targetName("katana") katana: IWeapon,
@inject("IWeapon") @targetName("shuriken") shuriken: IWeapon
) {
this.katana = katana;
this.shuriken = shuriken;
Expand Down Expand Up @@ -919,6 +920,127 @@ interface IBindingWhenSyntax<T> {
}
```

#### Property injection
InversifyJS supports property injection because sometimes constructor injection is not the best kind of injection pattern.
```ts
let kernel = new Kernel();
let inject = makePropertyInjectDecorator(kernel);

interface ISomeService {
count: number;
increment(): void;
}

@injectable()
class SomeService implements ISomeService {
public count: number;
public constructor() {
this.count = 0;
}
public increment() {
this.count = this.count + 1;
}
}

class SomeWebComponent {
@inject("ISomeService")
private _service: ISomeService;
public doSomething() {
let count = this._service.count;
this._service.increment();
return count;
}
}

kernel.bind<ISomeService>("ISomeService").to(SomeService);

let someComponent = new SomeWebComponent();
expect(someComponent.doSomething()).eql(0);
expect(someComponent.doSomething()).eql(1);
```

Property injection is quite different of constructor injection and has some limitations.

- The `@inject` decorator requires an instance of kernel.
- Injection takes place the first time the property is accessed via its getter.
- The `@targetName` decorator is not supported.
- The only supported contextual constraints are `whenTargetNamed` and `whenTargetTagged`.
- Property injection supports the `@named` and `@tagged` decorators.

```ts
class Warrior {

@injectNamed(TYPES.IWeapon, "not-throwwable")
@named("not-throwwable")
public primaryWeapon: IWeapon;

@injectNamed(TYPES.IWeapon, "throwwable")
@named("throwwable")
public secondaryWeapon: IWeapon;

}

class Warrior {

@injectTagged(TYPES.IWeapon, "throwwable", false)
@tagged("throwwable", false)
public primaryWeapon: IWeapon;

@injectTagged(TYPES.IWeapon, "throwwable", true)
@tagged("throwwable", true)
public secondaryWeapon: IWeapon;

}
```
- Property injection supports multi-injection.

```ts
let kernel = new Kernel();
let multiInject = makePropertyMultiInjectDecorator(kernel);

let TYPES = { IWeapon: "IWeapon" };

interface IWeapon {
durability: number;
use(): void;
}

@injectable()
class Sword implements IWeapon {
public durability: number;
public constructor() {
this.durability = 100;
}
public use() {
this.durability = this.durability - 10;
}
}

@injectable()
class WarHammer implements IWeapon {
public durability: number;
public constructor() {
this.durability = 100;
}
public use() {
this.durability = this.durability - 10;
}
}

class Warrior {
@multiInject(TYPES.IWeapon)
public weapons: IWeapon[];
}

kernel.bind<IWeapon>(TYPES.IWeapon).to(Sword);
kernel.bind<IWeapon>(TYPES.IWeapon).to(WarHammer);

let warrior1 = new Warrior();

expect(warrior1.weapons[0]).to.be.instanceof(Sword);
expect(warrior1.weapons[1]).to.be.instanceof(WarHammer);
```

#### Circular dependencies
InversifyJS is able to identify circular dependencies and will throw an exception to help you to
identify the location of the problem if a circular dependency is detected:
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "inversify",
"version": "2.0.0-beta.2",
"version": "2.0.0-beta.3",
"description": "A lightweight IoC container written in TypeScript.",
"main": "lib/inversify.js",
"jsnext:main": "es/inversify.js",
Expand Down Expand Up @@ -55,8 +55,8 @@
"reflect-metadata": "^0.1.3",
"run-sequence": "^1.1.5",
"sinon": "^1.17.3",
"tsify": "^0.15.2",
"tslint": "^3.2.2",
"tsify": "^0.15.3",
"tslint": "^3.9.0",
"typescript": "^1.8.10",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
Expand Down
51 changes: 33 additions & 18 deletions src/annotation/decorator_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,55 @@ interface IReflectResult {
[key: string]: IMetadata[];
}

function tagParameter(target: any, targetKey: string, index: number, metadata: IMetadata) {
function tagParameter(annotationTarget: any, propertyName: string, parameterIndex: number, metadata: IMetadata) {
let metadataKey = METADATA_KEY.TAGGED;
return _tagParameterOrProperty(metadataKey, annotationTarget, propertyName, metadata, parameterIndex);
}

function tagProperty(annotationTarget: any, propertyName: string, metadata: IMetadata) {
let metadataKey = METADATA_KEY.TAGGED_PROP;
return _tagParameterOrProperty(metadataKey, annotationTarget.constructor, propertyName, metadata);
}

let paramsMetadata: IReflectResult = null;
function _tagParameterOrProperty(
metadataKey: string, annotationTarget: any, propertyName: string, metadata: IMetadata, parameterIndex?: number
) {

// this decorator can be used in a constructor not a method
if (targetKey !== undefined) {
let paramsOrPropertiesMetadata: IReflectResult = null;
let isParameterDecorator = (typeof parameterIndex === "number");
let key: string = (isParameterDecorator) ? parameterIndex.toString() : propertyName;

// If the decorator is used as a parameter decorator property name must be provided
if (isParameterDecorator === true && propertyName !== undefined) {
throw new Error(ERROR_MSGS.INVALID_DECORATOR_OPERATION);
}

// read metadata if avalible
if (Reflect.hasOwnMetadata(METADATA_KEY.TAGGED, target) !== true) {
paramsMetadata = {};
if (Reflect.hasOwnMetadata(metadataKey, annotationTarget) !== true) {
paramsOrPropertiesMetadata = {};
} else {
paramsMetadata = Reflect.getMetadata(METADATA_KEY.TAGGED, target);
paramsOrPropertiesMetadata = Reflect.getMetadata(metadataKey, annotationTarget);
}

// get metadata for the decorated parameter by its index
let paramMetadata: IMetadata[] = paramsMetadata[index.toString()];
if (Array.isArray(paramMetadata) !== true) {
paramMetadata = [];
let paramOrPropertyMetadata: IMetadata[] = paramsOrPropertiesMetadata[key];
if (Array.isArray(paramOrPropertyMetadata) !== true) {
paramOrPropertyMetadata = [];
} else {
for (let i = 0; i < paramMetadata.length; i++) {
let m: IMetadata = paramMetadata[i];
for (let i = 0; i < paramOrPropertyMetadata.length; i++) {
let m: IMetadata = paramOrPropertyMetadata[i];
if (m.key === metadata.key) {
throw new Error(`${ERROR_MSGS.DUPLICATED_METADATA} ${m.key}`);
}
}
}

// set metadata
paramMetadata.push(metadata);
paramsMetadata[index.toString()] = paramMetadata;
Reflect.defineMetadata(METADATA_KEY.TAGGED, paramsMetadata, target);
return target;
paramOrPropertyMetadata.push(metadata);
paramsOrPropertiesMetadata[key] = paramOrPropertyMetadata;
Reflect.defineMetadata(metadataKey, paramsOrPropertiesMetadata, annotationTarget);
return annotationTarget;

}

function _decorate(decorators: ClassDecorator[], target: any): void {
Expand All @@ -53,7 +68,7 @@ function _param(paramIndex: number, decorator: ParameterDecorator): ClassDecorat

// Allows VanillaJS developers to use decorators:
// decorate(injectable("IFoo", "IBar"), FooBar);
// decorate(paramNames("foo", "bar"), FooBar);
// decorate(targetName("foo", "bar"), FooBar);
// decorate(named("foo"), FooBar, 0);
// decorate(tagged("bar"), FooBar, 1);
function decorate(
Expand All @@ -68,4 +83,4 @@ function decorate(
}
}

export { decorate, tagParameter };
export { decorate, tagParameter, tagProperty };
14 changes: 9 additions & 5 deletions src/annotation/named.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
///<reference path="../interfaces/interfaces.d.ts" />

import Metadata from "../planning/metadata";
import { tagParameter } from "./decorator_utils";
import { tagParameter, tagProperty } from "./decorator_utils";
import * as METADATA_KEY from "../constants/metadata_keys";

// Used to add named metadata which is used to resolve name-based contextual bindings.
function named(name: string) {
return function(target: any, targetKey: string, index: number) {
let metadata = new Metadata(METADATA_KEY.NAMED_TAG, name);
return tagParameter(target, targetKey, index, metadata);
};
return function(target: any, targetKey: string, index?: number) {
let metadata = new Metadata(METADATA_KEY.NAMED_TAG, name);
if (typeof index === "number") {
return tagParameter(target, targetKey, index, metadata);
} else {
return tagProperty(target, targetKey, metadata);
}
};
}

export default named;
Loading

0 comments on commit f8fbafd

Please sign in to comment.