├── LICENSE ├── README.md ├── jsInject.js ├── jsInject.min.js ├── jsInject.ts ├── jsInjectTests.js └── jsTestDriver.conf /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jeremy Likness 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jsInject 2 | ======== 3 | 4 | Simple, easy dependency injection framework for JavaScript. 5 | 6 | Inspired by the `$injector` service in the Angular framework library, I built this from scratch as a standalone experiment in dependency injection for JavaScript. 7 | 8 | It handles nested dependencies, avoids infinite recursion, takes multiple patterns for object creation, and uses annotations for dependencies that are 9 | minification-friendly. You may either set an array on the object to indicate the list of dependencies to inject into the constructor or specify the dependencies 10 | when you add the object to the container. 11 | 12 | Learn more about jsInject in [this blog post](http://csharperimage.jeremylikness.com/2014/06/dependency-injection-explained-via.html). 13 | 14 | Create an instance of the container (you may have as many containers as you like, but they are not aware of each other): 15 | 16 | var $jsInject = new $$jsInject(); 17 | 18 | `$$jsInject` can handle multiple patterns for object creation: 19 | 20 | **Factory** 21 | 22 | function serviceA(dependencyB) { 23 | return { 24 | id: dependencyB.getId() 25 | }; 26 | } 27 | 28 | **Constructor Function** 29 | 30 | function ServiceA(dependencyB) { 31 | this.id = dependencyB.getId(); 32 | } 33 | 34 | **Self-Invoking Function** 35 | 36 | var ServiceA = (function() { 37 | function ServiceA(dependencyB) { 38 | this.id = dependencyB.getId(); 39 | } 40 | return ServiceA; 41 | })(); 42 | 43 | **Function Annotation:** 44 | 45 | ServiceA.$$deps = ["dependencyB"]; 46 | $jsInject.register("serviceA", [ServiceA]); 47 | 48 | **Registration-time Annotation** 49 | 50 | $jsInject.register("serviceA", ["dependencyB", ServiceA]); 51 | 52 | **Retrieving instances** 53 | 54 | var svcA = $jsInject.get("serviceA"); 55 | var depB = $jsInject.get("dependencyB"); 56 | 57 | Pass a unique name for the instance to the registration function, then an array. The array should either contain the function if the function itself is annotated 58 | with the special property `$$deps`, or a list of named dependencies followed by the function in an array if you want to annotate the dependencies at run-time. 59 | 60 | See the tests to learn more. 61 | 62 | http://twitter.com/JeremyLikness 63 | 64 | http://csharperimage.jeremylikness.com 65 | -------------------------------------------------------------------------------- /jsInject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function (w) { 4 | 5 | var stack = {}, 6 | isArray = function (arr) { 7 | return Object.prototype.toString.call(arr) === '[object Array]'; 8 | }; 9 | 10 | function JsInject () { 11 | this.container = {}; 12 | } 13 | 14 | JsInject.ERROR_RECURSION = 'Recursive failure : Circular reference for dependency '; 15 | JsInject.ERROR_REGISTRATION = 'Already registered.'; 16 | JsInject.ERROR_ARRAY = 'Must pass array.'; 17 | JsInject.ERROR_FUNCTION = 'Must pass function to invoke.'; 18 | JsInject.ERROR_SERVICE = 'Service does not exist.'; 19 | 20 | JsInject.prototype.get = function(name) { 21 | var wrapper = this.container[name]; 22 | if (wrapper) { 23 | return wrapper(); 24 | } 25 | throw JsInject.ERROR_SERVICE; 26 | }; 27 | 28 | JsInject.prototype.invoke = function (fn, deps, instance, name) { 29 | var i = 0, 30 | args = []; 31 | if (stack[name]) { 32 | throw JsInject.ERROR_RECURSION + name + " : " + JSON.stringify(Object.keys(stack)); 33 | } 34 | 35 | stack[name] = instance; 36 | for (; i < deps.length; i += 1) { 37 | args.push(this.get(deps[i])); 38 | } 39 | delete stack[name]; 40 | 41 | return fn.apply(instance, args); 42 | }; 43 | 44 | JsInject.prototype.register = function (name, annotatedArray) { 45 | if (!isArray(annotatedArray)) { 46 | throw JsInject.ERROR_ARRAY; 47 | } 48 | 49 | if (this.container[name]) { 50 | throw JsInject.ERROR_REGISTRATION; 51 | } 52 | 53 | if (typeof annotatedArray[annotatedArray.length - 1] !== 'function') { 54 | throw JsInject.ERROR_FUNCTION; 55 | } 56 | 57 | var _this = this; 58 | this.container[name] = function () { 59 | var Template = function () {}, 60 | result = {}, 61 | instance, 62 | fn = annotatedArray[annotatedArray.length - 1], 63 | deps = annotatedArray.length === 1 ? (annotatedArray[0].$$deps || []) : 64 | annotatedArray.slice(0, annotatedArray.length - 1), 65 | injected; 66 | Template.prototype = fn.prototype; 67 | instance = new Template(); 68 | injected = _this.invoke(fn, deps, instance, name); 69 | result = injected || instance; 70 | _this.container[name] = function () { 71 | return result; 72 | }; 73 | return result; 74 | }; 75 | }; 76 | 77 | function Wrapper() { 78 | var ioc = new JsInject(), _that = this; 79 | this.get = ioc.get.bind(ioc); 80 | this.register = ioc.register.bind(ioc); 81 | ioc.container['$$jsInject'] = function () { 82 | return _that; 83 | }; 84 | } 85 | 86 | w.$$jsInject = Wrapper; 87 | })(window); -------------------------------------------------------------------------------- /jsInject.min.js: -------------------------------------------------------------------------------- 1 | "use strict";(function(e){function r(){this.container={}}function i(){var e=new r,t=this;this.get=e.get.bind(e);this.register=e.register.bind(e);e.container["$$jsInject"]=function(){return t}}var t=20,n=function(e){return Object.prototype.toString.call(e)==="[object Array]"};r.ERROR_RECURSION="Maximum recursion at ";r.ERROR_REGISTRATION="Already registered.";r.ERROR_ARRAY="Must pass array.";r.ERROR_FUNCTION="Must pass function to invoke.";r.ERROR_SERVICE="Service does not exist.";r.prototype.get=function(e,t){var n=this.container[e],i=t||0;if(n){return n(i)}throw r.ERROR_SERVICE};r.prototype.invoke=function(e,n,i,s){var o=0,u=[],a=s||0;if(a>t){throw r.ERROR_RECURSION+a}for(;o any}; 16 | 17 | constructor() { 18 | this.container = {}; 19 | this.container["$$jsInject"] = () => this; 20 | } 21 | 22 | get(name: string, level?: number): any { 23 | var wrapper: (lvl: number) => any = this.container[name], 24 | lvl: number = level || 0; 25 | if (wrapper) { 26 | return wrapper(lvl); 27 | } 28 | throw ERROR_SERVICE; 29 | } 30 | 31 | invoke(fn: Function, deps: string[], instance: any, level: number): any { 32 | var i: number = 0, 33 | args: any[] = [], 34 | lvl: number = level || 0; 35 | if (lvl > maxRecursion) { 36 | throw ERROR_RECURSION + lvl; 37 | } 38 | for (; i < deps.length; i += 1) { 39 | args.push(this.get(deps[i], lvl + 1)); 40 | } 41 | return fn.apply(instance, args); 42 | } 43 | 44 | register(name: string, annotatedArray: any[]) { 45 | if (!isArray(annotatedArray)) { 46 | throw ERROR_ARRAY; 47 | } 48 | if (this.container[name]) { 49 | throw ERROR_REGISTRATION; 50 | } 51 | if (typeof annotatedArray[annotatedArray.length - 1] !== 'function') { 52 | throw ERROR_FUNCTION; 53 | } 54 | this.container[name] = (level: number) => { 55 | var lvl: number = level || 0, 56 | Template: any = function () {}, 57 | result: any = {}, 58 | instance: any, 59 | fn: Function = annotatedArray[annotatedArray.length - 1], 60 | deps: string[] = annotatedArray.length === 1 ? (annotatedArray[0].$$deps || []) : 61 | annotatedArray.slice(0, annotatedArray.length - 1), 62 | injected: any; 63 | Template.prototype = fn.prototype; 64 | instance = new Template(), 65 | injected = this.invoke(fn, deps, instance, lvl + 1); 66 | result = injected || instance; 67 | this.container[name] = () => result; 68 | return result; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /jsInjectTests.js: -------------------------------------------------------------------------------- 1 | describe("jsInject Inversion of Control", function () { 2 | 3 | var empty = function() {}; 4 | var $jsInject; 5 | 6 | beforeEach(function() { 7 | $jsInject = new $$jsInject(); 8 | $jsInject.register("echoFn", [function() { 9 | return { 10 | echo: function(msg) { 11 | return msg; 12 | } 13 | }; 14 | }]); 15 | }); 16 | 17 | it("Given ioc when $$jsInject requested then returns self.", function () { 18 | var actual = $jsInject.get('$$jsInject'); 19 | expect(actual).toBe($jsInject); 20 | }); 21 | 22 | describe("First-time registration", function() { 23 | 24 | it("Given self-invoking constructor function when registered then registers successfully", function() { 25 | 26 | var expected = "1"; 27 | 28 | var fn = (function() { 29 | function Fn(echo) { 30 | this.echo = echo; 31 | this.test = function() { 32 | return echo.echo(expected); 33 | }; 34 | } 35 | return Fn; 36 | })(); 37 | 38 | $jsInject.register("1", ["echoFn", fn]); 39 | var actual = $jsInject.get("1"); 40 | expect(actual.test()).toBe(expected); 41 | }); 42 | 43 | it("Given function constructor when registered then registers successfully", function() { 44 | 45 | var expected = "2"; 46 | 47 | function Fn(echo) { 48 | this.test = echo.echo(expected); 49 | } 50 | 51 | $jsInject.register("2", ["echoFn", Fn]); 52 | var actual = $jsInject.get("2"); 53 | expect(actual.test).toBe(expected); 54 | }); 55 | 56 | it("Given function factory method when registered then registers successfully", function() { 57 | 58 | var expected = "3"; 59 | 60 | var factory = function(echo) { 61 | return { 62 | test: echo.echo(expected) 63 | }; 64 | }; 65 | 66 | factory.$$deps = ["echoFn"]; 67 | 68 | $jsInject.register("3", [factory]); 69 | var actual = $jsInject.get("3"); 70 | expect(actual.test).toBe(expected); 71 | }); 72 | 73 | }); 74 | 75 | describe("Validity checks and contracts", function() { 76 | 77 | it("Given something other than an array passed then it throws an error", function() { 78 | expect(function() { 79 | $jsInject.register("4", "4"); 80 | }).toThrow($$jsInject.ERROR_ARRAY); 81 | }); 82 | 83 | it("Given lst item in array passed is not a function then it throws an error", function() { 84 | expect(function() { 85 | $jsInject.register("4", ["4"]); 86 | }).toThrow($$jsInject.ERROR_FUNCTION); 87 | }); 88 | 89 | it("Given a registration already exists when duplicate registration is attempted then it throws an error", function() 90 | { 91 | $jsInject.register("3", [empty]); 92 | 93 | expect(function() { 94 | $jsInject.register("3", [empty]); 95 | }).toThrow($$jsInject.ERROR_REGISTRATION); 96 | }); 97 | 98 | it("Given recursive dependencies when a dependency is requested then it throws an error", function () { 99 | 100 | $jsInject.register("depA", ["depB", empty]); 101 | $jsInject.register("depB", ["depA", empty]); 102 | 103 | expect(function () { 104 | var depA = $jsInject.get("depA"); 105 | }).toThrow($$jsInject.ERROR_RECURSION); 106 | }); 107 | 108 | it("Given get request on service that does not exist then it throws an error", function () { 109 | expect(function () { 110 | var nothing = $jsInject.get("nothing"); 111 | }).toThrow($$jsInject.ERROR_SERVICE); 112 | }); 113 | 114 | }); 115 | 116 | describe("Run-time annotations", function(){ 117 | 118 | function serviceA() { 119 | return { 120 | id: "a" 121 | }; 122 | } 123 | 124 | function serviceB(a) { 125 | return { 126 | id: a.id + "b" 127 | }; 128 | } 129 | 130 | function serviceC(a, b) { 131 | return { 132 | id: a.id + b.id + "c" 133 | }; 134 | } 135 | 136 | beforeEach(function() { 137 | $jsInject.register("serviceA", [serviceA]); 138 | $jsInject.register("serviceB", ["serviceA", serviceB]); 139 | $jsInject.register("serviceC", ["serviceA", serviceC]); 140 | }); 141 | 142 | it ("Given registration with proper annotation then returns properly configured instance", function() { 143 | var expected = "ab"; 144 | var actual = $jsInject.get("serviceB").id; 145 | expect(actual).toBe(expected); 146 | }); 147 | 148 | it ("Given registration with improper annotation then throws exception due to bad reference", function() { 149 | expect(function () { 150 | $jsInject.get("serviceC"); 151 | }).toThrow(); 152 | }); 153 | }); 154 | 155 | describe("Object annotations", function(){ 156 | 157 | function ServiceA() { 158 | this.id = "a"; 159 | } 160 | 161 | function ServiceB(serviceA) { 162 | this.id = serviceA.id + "b"; 163 | } 164 | 165 | ServiceB.$$deps = ["ServiceA"]; 166 | 167 | function ServiceC(serviceA, serviceB) { 168 | this.id = serviceA.id + serviceB.id + "b"; 169 | } 170 | 171 | ServiceC.$$deps = ["ServiceA"]; 172 | 173 | beforeEach(function() { 174 | $jsInject.register("ServiceA", [ServiceA]); 175 | $jsInject.register("ServiceB", [ServiceB]); 176 | $jsInject.register("ServiceC", [ServiceC]); 177 | }); 178 | 179 | it ("Given registration with properly annotated function then returns properly configured instance", function() { 180 | var expected = "ab"; 181 | var actual = $jsInject.get("ServiceB").id; 182 | expect(actual).toBe(expected); 183 | }); 184 | 185 | it ("Given registration with improperly annotated function then throws exception due to bad reference", function() { 186 | expect(function () { 187 | $jsInject.get("ServiceC"); 188 | }).toThrow(); 189 | }); 190 | }); 191 | }); -------------------------------------------------------------------------------- /jsTestDriver.conf: -------------------------------------------------------------------------------- 1 | load: 2 | - jsInject.js 3 | - jasmine-1.1.0.js 4 | - JasmineAdapter-1.1.2.js 5 | 6 | test: 7 | - jsInjectTests.js 8 | --------------------------------------------------------------------------------