├── .gitignore ├── README.md ├── dist ├── radical.d.ts ├── radical.js └── radical.js.map ├── karma.conf.js ├── package.json ├── src └── radical.ts ├── test └── test.js ├── tsconfig.json └── typings.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | typings/ 3 | node_modules/ 4 | docs/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is Radical? 2 | ================ 3 | Radical is a client API framework, designed to simplify the creation and maintenance of React/Redux applications. 4 | Radical allows you to model your client API and state via the composition of Actions and Namespaces. 5 | 6 | * Actions are the basic unit of functionality; anything that influences state is an Action. 7 | * Namespaces provide a mechanism for organizing and grouping actions. Namespaces also define the state hierarchy, and 8 | provide state locality for actions. 9 | 10 | Why Radical? 11 | ============ 12 | Writing web applications with React and Redux offers a lot of advantages in terms of conceptual simplicity and 13 | testability. Unfortunately, the cost of this simplicity is a layer of indirection between initiation of an action and 14 | its resolution. Additionally, because state is non-local, you are forced to consider the entire state structure when 15 | performing updates; even worse, if you decide to change its structure, you have to update your all the reducers that 16 | traverse that portion of the state. 17 | 18 | Radical was designed solve these problems. 19 | 20 | Using Actions, you can group initiation and resolution functionality 21 | together. Even better, by default Actions only have to deal with localized state, and most of the initiation/reduction 22 | boilerplate has been handled for you. That doesn't mean Actions are limiting - most of the default behavior of Actions 23 | can be easily overridden in the event you need to do something unanticipated. 24 | 25 | Using Namespaces, you have the ability to compose the structure of your API and state from modules. Because state is 26 | modular and composable, you are free to design it from the bottom up, rather than having to plan your entire state 27 | structure ahead of time. If you decide to change how your state is organized, all you need to do is change how you 28 | compose Namespaces; all your reducers will just work. Of course, if you don't want your state model to match your 29 | Namespace structure, you can override the default Namespace sub-state resolution behavior easily, and Actions associated 30 | with that Namespace will still support state locality. 31 | 32 | Installation 33 | ============ 34 | 35 | Just `npm install radical` and you're up and running. The primary way to consume it is via webpack, or as an internal 36 | typescript module. If you want to consume it via AMD you can rebuild via `npm run-script build-amd`. 37 | 38 | Documentation can be generated using Typedoc, just do `npm run-script document` and it will be built in the docs 39 | directory. If something in the documentation is confusing or needs additional explanation feel free to create an issue 40 | and I will do my best to address it. 41 | 42 | There is a (meager) test suite, you can run it via `karma start karma.conf.js`. If you run into any bugs I would 43 | greatly appreciate if you could include code for a demonstration test case when creating an issue. 44 | 45 | Quick Start 46 | =========== 47 | 48 | Radical is authored in Typescript, and I've made every effort to maintain type safety and editor support for things like 49 | smart code completion while providing dynamic composability. There are a few cases where you have to make a choice 50 | between expressiveness and full type safety, or you have to include type hints, but these are limited (don't worry, I'll 51 | highlight them in the examples). 52 | 53 | The first thing you need to do is define a Namespace; there are several ways to do it, lets start off simply, using 54 | Javascript semantics: 55 | 56 | ```typescript 57 | var store = Redux.createStore(state => state); 58 | 59 | /* You can just create an instance of the Namespace class. If you do this, you 60 | * should provide a value for the name attribute, as this is used when 61 | * constructing Redux action types. Note that since this is going to be a "root" 62 | * Namespace, you must also specify a state retrieval function, and the action 63 | * dispatch function. This is not necessary for child Namespaces, by default 64 | * they recursively search ancestor Namespaces for the appropriate values. You 65 | * can also pass a defaultState object, which can house Namespace specific 66 | * configuration. 67 | * 68 | * Note: I'm creating and populating Namespace here in a step by step fashion to 69 | * ease you into Radical gradually, but you don't get type safety or IDE 70 | * auto-completion this way; I'll show you a better way in a bit. 71 | */ 72 | 73 | var apiRoot = Radical.Namespace.create({ 74 | name: "My root namespace", 75 | getState: store.getState, 76 | dispatch: store.dispatch, 77 | // Note, if you do not specify defaultState, an empty object is assumed 78 | defaultState: {greeting: "hello", target: "world"} 79 | }); 80 | 81 | /* Namespaces provide a reduce function that automatically dispatches relevant 82 | * portions of state to child components, so you only need to specify the root 83 | * reducer here. 84 | */ 85 | store.replaceReducer(apiRoot.reduce); 86 | ``` 87 | 88 | Now lets provide some actions on our Namespace: 89 | 90 | ```typescript 91 | /* If your Action only reads state, the configuration is very simple - just pass 92 | * it a function. The function being passed here is called the initiator. By 93 | * default, actions have access to their parent Namespace's portion of the state 94 | * tree. 95 | * 96 | * Important note: you cannot use the arrow notation when defining the 97 | * initiator. This is because the initiator is bound to the Namespace where it 98 | * is mounted. This is also why the function for greetTarget has action as an 99 | * argument; the first argument of Action initiators is bound to the Action 100 | * itself. 101 | */ 102 | var greetTarget = Radical.Action.create(function (action) { 103 | /* Generally, you are going to want to use the getState method of this. 104 | * The reason is that this is bound to the parent Namespace of the Action. 105 | */ 106 | let state = this.getState(); 107 | return state.greeting + " " + state.target + "!"; 108 | }); 109 | 110 | /* If your Action needs to modify state, you usually need to specify a reducer; 111 | * however, if all you want to do is set a value in state, you the default 112 | * reducer handles that case for you. The default Action reducer copies all 113 | * properties of the dispatched Redux action (besides type) to the state object. 114 | * 115 | * Note: I'm using an object argument to the create method here just to expose 116 | * you to more of the interface. 117 | */ 118 | var setGreeting = Radical.Action.create({ 119 | initiator: function (action, newGreeting) { 120 | /* Note that I am dispatching without an action type. The dispatch 121 | * method automatically adds a type property to the passed object 122 | * (if one is not already present) with the Action's name property 123 | * as a value. Actions that do not have an explicitly set name 124 | * property have one automatically generated via a combination of 125 | * the containing Namespace's name and the mount location for the 126 | * action. 127 | * 128 | * Note: An Action's dispatch method returns a reference to its 129 | * parent Namespace, to enable fluent-style method chaining. 130 | */ 131 | return action.dispatch({greeting: newGreeting}); 132 | } 133 | }) 134 | 135 | // For this action I'll specify the reducer manually. 136 | var setTarget = Radical.Action.create({ 137 | initiator: function (action, newTarget) { 138 | return action.dispatch({target: newTarget}); 139 | }, 140 | /* Note that you can directly mutate the passed state, since Radical 141 | * passes each reducer a shallow copy of the parent Namespace's state. Thus 142 | * as long as you don't directly alter any mutable children of the passed 143 | * state, any references to old versions of state remain pristine. 144 | * 145 | * Note: you can also specify an array of reducer functions, and they will 146 | * be applied sequentially. 147 | */ 148 | reducer: (state, action) => { 149 | state[target] = action.target; 150 | return state; 151 | } 152 | } 153 | 154 | var rootConfig = { 155 | components: { 156 | greetTarget: greetTarget, 157 | setGreeting: setGreeting 158 | } 159 | }; 160 | 161 | // You can attach Actions to a Namespace using the configure method 162 | apiRoot.configure(rootConfig); 163 | 164 | // You can also use the mount method 165 | apiRoot.mount("setTarget", setTarget); 166 | 167 | apiRoot.greetTarget(); // -> "hello world!" 168 | apiRoot.setTarget("hacker news").greetTarget(); // -> "hello hacker news!" 169 | ``` 170 | 171 | You might have noticed that I use a create factory function rather than the new keyword. This is the preferred method 172 | of creating new Radical components. The reason for this is that when we get into defining Namespaces and Actions using 173 | Typescript class semantics with instance properties, components created with the new keyword must have their configure 174 | method called *after* all constructor functions have resolved or they are not properly instrumented; the create method 175 | does this for you automatically. 176 | 177 | In Typescript, it is much better to use class semantics to define Namespaces and Actions. There are a couple of ways of 178 | going about this, depending on whether you value brevity and uncluttered code or full type safety. 179 | 180 | ```typescript 181 | /* First, with an emphasis on uncluttered brevity. This method will get you editor 182 | * autocomplete for names, but you won't have type safety on the arguments and 183 | * return value of actions. Since this Namespace is going to be a child of our 184 | * previously created apiRoot Namespace, we don't have to specify getState or 185 | * dispatch properties. 186 | * 187 | * Note: You don't need to specify a name for Namespaces defined this way unless 188 | * you plan to have more than one instance of it. If no name attribute is 189 | * specified, Namespaces will derive a name from their class name. 190 | */ 191 | class SpanishGreeter extends Radical.Namespace { 192 | 193 | defaultState = {greeting: "hola", target: "mundo"}; 194 | 195 | greetTarget = Radical.Action.create(function (action) { 196 | let state = this.getState(); 197 | return "¡" + state.greeting + " " + state.target + "!"; 198 | }); 199 | 200 | setTarget = Radical.Action.create(function (action, newTarget) { 201 | return action.dispatch({target: newTarget}); 202 | }); 203 | 204 | setGreeting = Radical.Action.create(function (action, newGreeting) { 205 | return action.dispatch({greeting: newGreeting}); 206 | }); 207 | } 208 | 209 | /* Now, with editor support for type safe usage and return values. Note that you 210 | * still don't get compiler assurances that the initiator function of the Action 211 | * you supplied to the Namespace matches matches the signature definition on the 212 | * class. 213 | */ 214 | class FrenchGreeter extends Radical.Namespace { 215 | 216 | defaultState = {greeting: "bonjour", target: "le monde"}; 217 | 218 | components = { 219 | greetTarget: Radical.Action.create(function (action) { 220 | let state = this.getState(); 221 | return state.greeting + " " + state.target + "!"; 222 | }), 223 | 224 | setTarget: Radical.Action.create(function (action, newTarget) { 225 | return action.dispatch({target: newTarget}); 226 | }), 227 | 228 | setGreeting: Radical.Action.create(function (action, newGreeting) { 229 | return action.dispatch({greeting: newGreeting}); 230 | }) 231 | } 232 | 233 | greetTarget: () => string; 234 | setTarget: (newTarget: string) => FrenchGreeter; 235 | setGreeting: (newGreeting: string) => FrenchGreeter; 236 | } 237 | 238 | /* You can attach Namespaces to other Namespaces in exactly the same way I attached 239 | * Actions previously. Note that the configure method performs an update rather 240 | * than completely replacing the components on the Namespace. 241 | * 242 | * Note: You still won't get full editor autocomplete and type-safety if you attach 243 | * new Namespaces to a pre-existing Namespace in this way - you need to use class 244 | * semantics all the way down. 245 | */ 246 | apiRoot.configure({components: {spanish: SpanishGreeter.create()}); 247 | apiRoot.mount("french", FrenchGreeter.create()); 248 | 249 | /* For best results in Typescript, just define a class. This will provide 250 | * autocomplete and type safety (if you used signature style definitions). 251 | * 252 | * Note: I specify names here for the greeters because otherwise their actions 253 | * would have the same dispatch type as the previously created instances. 254 | */ 255 | class GreeterContainer extends Radical.Namespace { 256 | spanish = SpanishGreeter.create({name: "alt spanish"}) as SpanishGreeter; 257 | french = FrenchGreeter.create({name: "alt french"}) as FrenchGreeter; 258 | } 259 | 260 | var newApiRoot = GreeterContainer.create({ 261 | getState: store.getState, 262 | dispatch: store.dispatch 263 | }); 264 | 265 | store.replaceReducer(newApiRoot.reduce); 266 | 267 | newApiRoot.getState(); 268 | /* -> { 269 | * spanish: {greeting: "hola", target: "mundo"}, 270 | * french: {greeting: "bonjour", target: "le monde"} 271 | * } 272 | */ 273 | ``` 274 | 275 | **Important**: Note that I type-cast SpanishGreeter and FrenchGreeter in the previous code. This is unfortunately 276 | necessary in order for the code to compile. The reason for this is that Typescript doesn't currently support 277 | returning polymorphic **this** from static methods. If this offends you, I suggest leaving a note on the relevant 278 | [Typescript Github Issue](https://github.com/Microsoft/TypeScript/issues/5863) mentioning how much you would like it if 279 | they made this feature a slightly higher priority. 280 | 281 | Radical includes a few more features for your development pleasure. For actions that need to make an ajax call to the 282 | server, I've included a declarative endpoint description interface. Additionally, since Immutable is commonly used 283 | with React/Redux, there are version of Namespace and Action that work with it (or any library that implements a basic 284 | collection interface) seamlessly. 285 | 286 | ```typescript 287 | 288 | /* CollectionNamespace supports Immutable (or any collection with get, set and 289 | * merge methods). 290 | */ 291 | class AnotherDemoNamespace extends Radical.CollectionNamespace { 292 | /* Note that you MUST set a defaultState for CollectionNamespaces. This is 293 | * because I don't assume anything about the type of collection you are 294 | * using. 295 | */ 296 | defaultState = Immutable.fromJS({}); 297 | 298 | actionWithGetEndpoint = Radical.CollectionAction({ 299 | /* If your endpoint is accessed using the GET method, and returns text 300 | * which doesn't need to be transformed (or you want to handle the 301 | * transformation yourself) you can specify it using just the URL. 302 | */ 303 | endpoint: "/get_endpoint_returning_text", 304 | initiator: function (action, arg1) { 305 | action.endpoint.execute({ 306 | arguments: {foo: arg1, bar: 2}, 307 | success: (data) => { 308 | action.dispatch({newData: data}); 309 | } 310 | }); 311 | } 312 | }); 313 | 314 | actionWithJsonPostEndpoint = Radical.CollectionAction({ 315 | endpoint: Radical.JsonEndpoint.create({ 316 | url: "/post_json_endpoint", 317 | method: "POST" 318 | }), 319 | initiator: function (action, arg1) { 320 | action.endpoint.execute({ 321 | data: {foo: arg1, bar: 2}, 322 | // The data passed to the success function has already been parsed 323 | success: (data) => { 324 | action.dispatch({newData: data}); 325 | }, 326 | /* Note that JsonEndpoint assumes the server is delivering JSON 327 | * error messages. If this is not the case (it really should be!) 328 | * you need to provide a function that returns its input unchanged 329 | * (e.g. r => r) as the errorParser argument to the JsonEndpoint. 330 | */ 331 | error: (data, status) => { 332 | // handle your business 333 | } 334 | }); 335 | } 336 | }); 337 | } 338 | ``` -------------------------------------------------------------------------------- /dist/radical.d.ts: -------------------------------------------------------------------------------- 1 | export interface IEndpointInput { 2 | converter?: Function; 3 | } 4 | export declare class EndpointInput implements IEndpointInput { 5 | converter: Function; 6 | constructor(config?: IEndpointInput | Function); 7 | } 8 | export interface IEndpointBodyInput extends IEndpointInput { 9 | contentType?: string; 10 | } 11 | export declare class JsonBodyInput implements IEndpointBodyInput { 12 | contentType: string; 13 | converter: { 14 | (value: any): string; 15 | (value: any, replacer: (key: string, value: any) => any): string; 16 | (value: any, replacer: any[]): string; 17 | (value: any, replacer: (key: string, value: any) => any, space: string | number): string; 18 | (value: any, replacer: any[], space: string | number): string; 19 | }; 20 | } 21 | export interface IEndpointArgumentContainer { 22 | [key: string]: IEndpointInput; 23 | } 24 | export declare class RequestArgument { 25 | argument: string; 26 | value: any; 27 | constructor(argument: string, value: any); 28 | } 29 | export interface IEndpoint { 30 | url?: string; 31 | method?: string; 32 | arguments?: IEndpointArgumentContainer; 33 | headers?: Object; 34 | body?: IEndpointBodyInput; 35 | } 36 | export interface IEndpointExecutionParameters { 37 | arguments?: Object; 38 | data?: Object; 39 | success?: Function; 40 | error?: Function; 41 | headers?: Object; 42 | } 43 | export declare class Endpoint implements IEndpoint { 44 | url: string; 45 | method: string; 46 | arguments: IEndpointArgumentContainer; 47 | headers: Object; 48 | body: IEndpointBodyInput; 49 | responseParser: (response: string) => any; 50 | errorParser: (error: string) => any; 51 | configure(config: IEndpoint): this; 52 | constructor(config?: IEndpoint); 53 | private convert(argument, value); 54 | private toQueryString; 55 | private setHeaders(request, headers?); 56 | execute(parameters?: IEndpointExecutionParameters): Promise<{}>; 57 | static create(config?: IEndpoint): Endpoint; 58 | } 59 | export declare class JsonEndpoint extends Endpoint { 60 | body: JsonBodyInput; 61 | responseParser: (text: string, reviver?: (key: any, value: any) => any) => any; 62 | errorParser: (text: string, reviver?: (key: any, value: any) => any) => any; 63 | } 64 | export interface IApiComponent { 65 | name?: string; 66 | parent?: Namespace; 67 | defaultState?: Object; 68 | dispatch?: Function; 69 | getState?: Function; 70 | } 71 | export interface IAction extends IApiComponent { 72 | endpoint?: Endpoint | string; 73 | initiator?: Function; 74 | reducer?: ((state, action) => Object) | ((state, action) => Object)[]; 75 | } 76 | export declare class ApiComponent { 77 | name: string; 78 | parent: Namespace; 79 | defaultState: Object; 80 | dispatch: Function; 81 | reduce: Function; 82 | getState: Function; 83 | protected getSubState(state: any, location: any): any; 84 | configure(config?: IApiComponent): this; 85 | static create(config?: IApiComponent): ApiComponent; 86 | } 87 | export declare class Action extends ApiComponent implements IAction, Function { 88 | endpoint: Endpoint; 89 | initiator: Function; 90 | reducer: ((state, action) => Object) | ((state, action) => Object)[]; 91 | apply: (thisArg: any, argArray?: any) => any; 92 | call: (thisArg: any, ...argArray: any[]) => any; 93 | bind: (thisArg: any, ...argArray: any[]) => any; 94 | prototype: any; 95 | length: number; 96 | arguments: any; 97 | caller: Function; 98 | dispatch: (action?: Object) => Namespace; 99 | configure(config?: IAction | Function): this; 100 | static create(config?: any): Action; 101 | reduce: Function; 102 | } 103 | export interface IComponentContainer { 104 | [key: string]: ApiComponent; 105 | } 106 | export interface INamespace extends IApiComponent { 107 | components?: IComponentMountConfiguration[] | IComponentContainer | Object; 108 | } 109 | export interface IComponentMountConfiguration { 110 | location: string; 111 | component: ApiComponent; 112 | stateLocation?: string; 113 | } 114 | export declare class Namespace extends ApiComponent implements INamespace { 115 | components: IComponentContainer | Object; 116 | defaultState: {}; 117 | protected _stateLocation: {}; 118 | protected nameSelf(): void; 119 | configure(config?: INamespace): this; 120 | protected updateDefaultState(stateLocation: any, state: any): Namespace; 121 | protected nameComponent(component: any, location: any): void; 122 | mount(location: string, component: ApiComponent, stateLocation?: string): Namespace; 123 | mountAll(components: IComponentMountConfiguration[] | IComponentContainer | Object): this; 124 | unmount(location: string): Namespace; 125 | mountLocation(component: ApiComponent): string; 126 | stateLocation(component: ApiComponent): string; 127 | reduce: (state: any, action: any) => {}; 128 | } 129 | export interface ICollection { 130 | get: (key: K) => V; 131 | set: (key: K, value: V) => ICollection; 132 | merge: (...iterables: ICollection[]) => ICollection; 133 | } 134 | export declare class CollectionAction extends Action { 135 | defaultState: ICollection; 136 | reducer: (state: ICollection, action) => ICollection | ((state: ICollection, action) => ICollection)[]; 137 | protected getSubState(state: any, location: any): any; 138 | } 139 | export declare class CollectionNamespace extends Namespace { 140 | defaultState: ICollection; 141 | protected getSubState(state: any, location: any): any; 142 | protected nameSelf(): void; 143 | protected updateDefaultState(stateLocation: any, state: ICollection): CollectionNamespace; 144 | reduce: (state: ICollection, action: any) => ICollection; 145 | } 146 | -------------------------------------------------------------------------------- /dist/radical.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || function (d, b) { 3 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 4 | function __() { this.constructor = d; } 5 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 6 | }; 7 | var es6_shim_1 = require('es6-shim'); 8 | var EndpointInput = (function () { 9 | function EndpointInput(config) { 10 | this.converter = function (argumentValue) { return argumentValue.toString(); }; 11 | if (config instanceof Function) { 12 | this.converter = config; 13 | } 14 | else if (config) { 15 | var config_ = config; 16 | if (config_.converter) 17 | this.converter = config_.converter; 18 | } 19 | } 20 | return EndpointInput; 21 | }()); 22 | exports.EndpointInput = EndpointInput; 23 | var JsonBodyInput = (function () { 24 | function JsonBodyInput() { 25 | this.contentType = "application/json; charset=utf-8"; 26 | this.converter = JSON.stringify; 27 | } 28 | return JsonBodyInput; 29 | }()); 30 | exports.JsonBodyInput = JsonBodyInput; 31 | var RequestArgument = (function () { 32 | function RequestArgument(argument, value) { 33 | this.argument = argument; 34 | this.value = value; 35 | this.value = value.toString(); 36 | } 37 | return RequestArgument; 38 | }()); 39 | exports.RequestArgument = RequestArgument; 40 | var Endpoint = (function () { 41 | function Endpoint(config) { 42 | var _this = this; 43 | this.method = "GET"; 44 | this.arguments = {}; 45 | this.body = { 46 | contentType: "application/x-www-form-urlencoded; charset=utf-8", 47 | converter: this.toQueryString 48 | }; 49 | this.responseParser = function (response) { return response; }; 50 | this.errorParser = function (error) { return error; }; 51 | this.toQueryString = function (data) { 52 | var key, arg, value, queryArgs = []; 53 | for (key in data) { 54 | if (data[key] instanceof RequestArgument) { 55 | arg = data[key].argument; 56 | value = _this.convert(data[key].argument, data[key].value); 57 | } 58 | else { 59 | arg = key; 60 | value = _this.convert(key, data[key]); 61 | } 62 | queryArgs.push(arg + "=" + encodeURIComponent(value)); 63 | } 64 | return queryArgs.join("&"); 65 | }; 66 | if (config) { 67 | this.configure(config); 68 | } 69 | } 70 | Endpoint.prototype.configure = function (config) { 71 | if (config.method) 72 | this.method = config.method; 73 | if (config.arguments) 74 | this.arguments = config.arguments; 75 | if (config.headers) 76 | this.headers = config.headers; 77 | if (config.body) 78 | this.body = config.body; 79 | if (config.url) 80 | this.url = config.url; 81 | return this; 82 | }; 83 | Endpoint.prototype.convert = function (argument, value) { 84 | if (this.arguments[argument] && this.arguments[argument].converter) { 85 | return this.arguments[argument].converter(value); 86 | } 87 | else { 88 | return value.toString(); 89 | } 90 | }; 91 | Endpoint.prototype.setHeaders = function (request, headers) { 92 | if (headers === void 0) { headers = {}; } 93 | for (var header in headers) { 94 | request.setRequestHeader(header, headers[header]); 95 | } 96 | }; 97 | Endpoint.prototype.execute = function (parameters) { 98 | var _this = this; 99 | var request = new XMLHttpRequest(), url = this.url, data = "", endpoint = this; 100 | return new es6_shim_1.Promise(function (resolve, reject) { 101 | if (parameters) { 102 | if (parameters.arguments) { 103 | url = _this.url + "?" + _this.toQueryString(parameters.arguments); 104 | } 105 | request.onload = function () { 106 | if (this.status >= 200 && this.status < 400) { 107 | if (parameters.success) 108 | parameters.success(endpoint.responseParser(this.response), this.status); 109 | resolve(this.response); 110 | } 111 | else { 112 | if (parameters.error) 113 | parameters.error(endpoint.errorParser(this.response), this.status); 114 | reject(this.response); 115 | } 116 | }; 117 | if (parameters.data) { 118 | data = _this.body.converter(parameters.data); 119 | } 120 | } 121 | request.open(_this.method, url, true); 122 | _this.setHeaders(request, _this.headers); 123 | _this.setHeaders(request, parameters.headers); 124 | request.setRequestHeader("Content-Type", _this.body.contentType); 125 | request.send(data); 126 | }); 127 | }; 128 | Endpoint.create = function (config) { 129 | return new this().configure(config); 130 | }; 131 | return Endpoint; 132 | }()); 133 | exports.Endpoint = Endpoint; 134 | var JsonEndpoint = (function (_super) { 135 | __extends(JsonEndpoint, _super); 136 | function JsonEndpoint() { 137 | _super.apply(this, arguments); 138 | this.body = new JsonBodyInput(); 139 | this.responseParser = JSON.parse; 140 | this.errorParser = JSON.parse; 141 | } 142 | return JsonEndpoint; 143 | }(Endpoint)); 144 | exports.JsonEndpoint = JsonEndpoint; 145 | var ApiComponent = (function () { 146 | function ApiComponent() { 147 | var _this = this; 148 | this.dispatch = function (action) { return _this.parent.dispatch(action); }; 149 | this.getState = function () { 150 | if (!_this.parent) 151 | return null; 152 | else 153 | return _this.getSubState(_this.parent.getState(), _this.parent.stateLocation(_this)); 154 | }; 155 | } 156 | ApiComponent.prototype.getSubState = function (state, location) { 157 | if (location) 158 | return state[location]; 159 | else 160 | return state; 161 | }; 162 | ApiComponent.prototype.configure = function (config) { 163 | if (config) { 164 | if (config.parent) 165 | this.parent = config.parent; 166 | if (config.getState) 167 | this.getState = config.getState; 168 | if (config.dispatch) 169 | this.dispatch = config.dispatch; 170 | if (config.defaultState) 171 | this.defaultState = config.defaultState; 172 | if (config.name) 173 | this.name = config.name; 174 | } 175 | return this; 176 | }; 177 | ApiComponent.create = function (config) { 178 | return new this().configure(config); 179 | }; 180 | return ApiComponent; 181 | }()); 182 | exports.ApiComponent = ApiComponent; 183 | var Action = (function (_super) { 184 | __extends(Action, _super); 185 | function Action() { 186 | var _this = this; 187 | _super.apply(this, arguments); 188 | this.initiator = function (action, data) { 189 | var reduxAction = {}, key; 190 | for (key in data) { 191 | reduxAction[key] = data[key]; 192 | } 193 | reduxAction["type"] = action.name; 194 | action.dispatch(reduxAction); 195 | return this; 196 | }; 197 | this.reducer = function (state, action) { 198 | for (var key in action) { 199 | if (key != "type") { 200 | state[key] = action[key]; 201 | } 202 | } 203 | return state; 204 | }; 205 | this.apply = function (thisArg, argArray) { return _this.initiator.apply(thisArg, argArray); }; 206 | this.call = function (thisArg) { 207 | var argArray = []; 208 | for (var _i = 1; _i < arguments.length; _i++) { 209 | argArray[_i - 1] = arguments[_i]; 210 | } 211 | return (_a = _this.initiator).call.apply(_a, [thisArg].concat(argArray)); 212 | var _a; 213 | }; 214 | this.bind = function (thisArg) { 215 | var argArray = []; 216 | for (var _i = 1; _i < arguments.length; _i++) { 217 | argArray[_i - 1] = arguments[_i]; 218 | } 219 | return (_a = _this.initiator).bind.apply(_a, [thisArg].concat(argArray)); 220 | var _a; 221 | }; 222 | this.dispatch = function (action) { 223 | if (action) { 224 | if (!action.hasOwnProperty("type")) 225 | action["type"] = _this.name; 226 | } 227 | else { 228 | action = { type: _this.name }; 229 | } 230 | _this.parent.dispatch(action); 231 | return _this.parent; 232 | }; 233 | this.reduce = function (state, action) { 234 | if (!state) { 235 | return _this.defaultState; 236 | } 237 | else if (action.type != _this.name || !_this.reducer) { 238 | return state; 239 | } 240 | else { 241 | if (_this.reducer instanceof Function) { 242 | return _this.reducer(state, action); 243 | } 244 | else { 245 | return _this.reducer.reduce(function (s, f) { return f(s, action); }, state); 246 | } 247 | } 248 | }; 249 | } 250 | Action.prototype.configure = function (config) { 251 | if (config) { 252 | if (config instanceof Function) { 253 | this.initiator = config; 254 | } 255 | else { 256 | var conf = config; 257 | _super.prototype.configure.call(this, conf); 258 | var endpoint = conf.endpoint; 259 | if (endpoint) { 260 | if (typeof endpoint === "string") { 261 | this.endpoint = new Endpoint({ url: endpoint }); 262 | } 263 | else { 264 | this.endpoint = endpoint; 265 | } 266 | } 267 | if (conf.reducer) 268 | this.reducer = conf.reducer; 269 | if (conf.initiator) 270 | this.initiator = conf.initiator; 271 | } 272 | } 273 | return this; 274 | }; 275 | Action.create = function (config) { 276 | return new this().configure(config); 277 | }; 278 | return Action; 279 | }(ApiComponent)); 280 | exports.Action = Action; 281 | var Namespace = (function (_super) { 282 | __extends(Namespace, _super); 283 | function Namespace() { 284 | var _this = this; 285 | _super.apply(this, arguments); 286 | this.components = {}; 287 | this.defaultState = {}; 288 | this._stateLocation = {}; 289 | this.reduce = function (state, action) { 290 | if (!state) 291 | return _this.defaultState; 292 | else { 293 | var newState = {}, location_1, stateLocation = void 0; 294 | for (var key in state) { 295 | newState[key] = state[key]; 296 | } 297 | for (location_1 in _this.components) { 298 | stateLocation = _this._stateLocation[location_1]; 299 | if (stateLocation) { 300 | newState[stateLocation] = _this.components[location_1].reduce(newState[stateLocation], action); 301 | } 302 | else { 303 | newState = _this.components[location_1].reduce(newState, action); 304 | } 305 | } 306 | return newState; 307 | } 308 | }; 309 | } 310 | Namespace.prototype.nameSelf = function () { 311 | if (this.constructor == Namespace) { 312 | throw new Error("Automatic Namespace naming is only supported for derived classes; you must specify a value for the name attribute"); 313 | } 314 | this.name = this.constructor.name; 315 | }; 316 | Namespace.prototype.configure = function (config) { 317 | _super.prototype.configure.call(this, config); 318 | if (!this.name) 319 | this.nameSelf(); 320 | var key; 321 | for (key in this) { 322 | if (this[key] instanceof ApiComponent && !this.mountLocation(this[key])) 323 | this.components[key] = this[key]; 324 | } 325 | if (config && config.components) { 326 | for (key in config.components) 327 | this.components[key] = config.components[key]; 328 | } 329 | this.mountAll(this.components); 330 | return this; 331 | }; 332 | Namespace.prototype.updateDefaultState = function (stateLocation, state) { 333 | if (stateLocation) { 334 | this.defaultState[stateLocation] = state; 335 | } 336 | else { 337 | for (var key in state) { 338 | this.defaultState[key] = state[key]; 339 | } 340 | } 341 | return this; 342 | }; 343 | Namespace.prototype.nameComponent = function (component, location) { 344 | component.configure({ name: this.name + ": " + location }); 345 | }; 346 | Namespace.prototype.mount = function (location, component, stateLocation) { 347 | component.parent = this; 348 | if (!this.components[location]) 349 | this.components[location] = component; 350 | if (component instanceof Action) { 351 | this._stateLocation[location] = stateLocation; 352 | this[location] = component.initiator.bind(this, component); 353 | if (!component.name) 354 | this.nameComponent(component, location); 355 | } 356 | else { 357 | this._stateLocation[location] = stateLocation || location; 358 | this[location] = component; 359 | } 360 | if (component.defaultState) 361 | this.updateDefaultState(this._stateLocation[location], component.defaultState); 362 | return this; 363 | }; 364 | Namespace.prototype.mountAll = function (components) { 365 | var key, component; 366 | for (key in components) { 367 | component = components[key]; 368 | if (component instanceof ApiComponent) { 369 | this.mount(key, component); 370 | } 371 | else { 372 | this.mount(component.location, component.component, component.stateLocation); 373 | } 374 | } 375 | return this; 376 | }; 377 | Namespace.prototype.unmount = function (location) { 378 | this.components[location].parent = undefined; 379 | delete this.components[location]; 380 | delete this.defaultState[location]; 381 | delete this[location]; 382 | return this; 383 | }; 384 | Namespace.prototype.mountLocation = function (component) { 385 | for (var location_2 in this.components) { 386 | if (component == this.components[location_2]) 387 | return location_2; 388 | } 389 | return null; 390 | }; 391 | Namespace.prototype.stateLocation = function (component) { 392 | var mountLocation = this.mountLocation(component); 393 | return this._stateLocation[mountLocation]; 394 | }; 395 | return Namespace; 396 | }(ApiComponent)); 397 | exports.Namespace = Namespace; 398 | var CollectionAction = (function (_super) { 399 | __extends(CollectionAction, _super); 400 | function CollectionAction() { 401 | _super.apply(this, arguments); 402 | this.reducer = function (state, action) { 403 | for (var key in action) { 404 | if (key != "type") { 405 | state = state.set(key, action[key]); 406 | } 407 | } 408 | return state; 409 | }; 410 | } 411 | CollectionAction.prototype.getSubState = function (state, location) { 412 | if (location) 413 | return state.get(location); 414 | else 415 | return state; 416 | }; 417 | return CollectionAction; 418 | }(Action)); 419 | exports.CollectionAction = CollectionAction; 420 | var CollectionNamespace = (function (_super) { 421 | __extends(CollectionNamespace, _super); 422 | function CollectionNamespace() { 423 | var _this = this; 424 | _super.apply(this, arguments); 425 | this.reduce = function (state, action) { 426 | if (!state) 427 | return _this.defaultState; 428 | else { 429 | var location_3, stateLocation = void 0, reducer = void 0; 430 | for (location_3 in _this.components) { 431 | stateLocation = _this._stateLocation[location_3]; 432 | reducer = _this.components[location_3].reduce; 433 | if (stateLocation) { 434 | state = state.set(stateLocation, reducer(state.get(stateLocation), action)); 435 | } 436 | else { 437 | state = reducer(state, action); 438 | } 439 | } 440 | return state; 441 | } 442 | }; 443 | } 444 | CollectionNamespace.prototype.getSubState = function (state, location) { 445 | if (location) 446 | return state.get(location); 447 | else 448 | return state; 449 | }; 450 | CollectionNamespace.prototype.nameSelf = function () { 451 | if (this.constructor == CollectionNamespace) { 452 | throw new Error("Automatic Namespace naming is only supported for derived classes; you must specify a value for the name attribute"); 453 | } 454 | this.name = this.constructor.name; 455 | }; 456 | CollectionNamespace.prototype.updateDefaultState = function (stateLocation, state) { 457 | if (stateLocation) { 458 | this.defaultState = this.defaultState.set(stateLocation, state); 459 | } 460 | else { 461 | this.defaultState = this.defaultState.merge(state); 462 | } 463 | return this; 464 | }; 465 | return CollectionNamespace; 466 | }(Namespace)); 467 | exports.CollectionNamespace = CollectionNamespace; 468 | //# sourceMappingURL=radical.js.map -------------------------------------------------------------------------------- /dist/radical.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"radical.js","sourceRoot":"","sources":["../src/radical.ts"],"names":[],"mappings":";;;;;;AAAA,yBAAsB,UAAU,CAAC,CAAA;AAajC;IAUI,uBAAY,MAAkC;QAF9C,cAAS,GAAa,UAAC,aAAa,IAAK,OAAA,aAAa,CAAC,QAAQ,EAAE,EAAxB,CAAwB,CAAC;QAG9D,EAAE,CAAC,CAAC,MAAM,YAAY,QAAQ,CAAC,CAAC,CAAC;YAC7B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;QAC5B,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YAChB,IAAI,OAAO,GAAmB,MAAM,CAAC;YACrC,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;gBAAC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QAC9D,CAAC;IACL,CAAC;IACL,oBAAC;AAAD,CAAC,AAlBD,IAkBC;AAlBY,qBAAa,gBAkBzB,CAAA;AAeD;IAAA;QACI,gBAAW,GAAG,iCAAiC,CAAC;QAChD,cAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAC/B,CAAC;IAAD,oBAAC;AAAD,CAAC,AAHD,IAGC;AAHY,qBAAa,gBAGzB,CAAA;AASD;IACI,yBAAmB,QAAgB,EAAS,KAAK;QAA9B,aAAQ,GAAR,QAAQ,CAAQ;QAAS,UAAK,GAAL,KAAK,CAAA;QAC7C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;IAClC,CAAC;IACL,sBAAC;AAAD,CAAC,AAJD,IAIC;AAJY,uBAAe,kBAI3B,CAAA;AA2BD;IAoDI,kBAAY,MAAkB;QApDlC,iBAoJC;QA/IG,WAAM,GAAW,KAAK,CAAC;QACvB,cAAS,GAA+B,EAAE,CAAC;QAW3C,SAAI,GAAuB;YACvB,WAAW,EAAE,kDAAkD;YAC/D,SAAS,EAAE,IAAI,CAAC,aAAa;SAChC,CAAC;QAQF,mBAAc,GAA8B,UAAC,QAAQ,IAAK,OAAA,QAAQ,EAAR,CAAQ,CAAC;QAOnE,gBAAW,GAA2B,UAAC,KAAK,IAAK,OAAA,KAAK,EAAL,CAAK,CAAC;QA6C/C,kBAAa,GAAG,UAAC,IAAgC;YACrD,IAAI,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,GAAG,EAAE,CAAC;YACpC,GAAG,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC;gBAEf,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,eAAe,CAAC,CAAC,CAAC;oBACvC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;oBACzB,KAAK,GAAG,KAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;gBAC9D,CAAC;gBAAC,IAAI,CAAC,CAAC;oBACJ,GAAG,GAAG,GAAG,CAAC;oBACV,KAAK,GAAG,KAAI,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;gBACzC,CAAC;gBACD,SAAS,CAAC,IAAI,CAAC,GAAG,GAAG,GAAG,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;YAE1D,CAAC;YACD,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/B,CAAC,CAAC;QA1CE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YACT,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC3B,CAAC;IACL,CAAC;IAbD,4BAAS,GAAT,UAAU,MAAiB;QACvB,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;YAAC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC/C,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;YAAC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QACxD,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;YAAC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAClD,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;YAAC,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACzC,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;YAAC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAeO,0BAAO,GAAf,UAAgB,QAAQ,EAAE,KAAK;QAC3B,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;YACjE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACrD,CAAC;QAAC,IAAI,CAAC,CAAC;YACJ,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC5B,CAAC;IAEL,CAAC;IA+BO,6BAAU,GAAlB,UAAmB,OAAuB,EAAE,OAAoB;QAApB,uBAAoB,GAApB,YAAoB;QAC5D,GAAG,CAAC,CAAC,IAAI,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC;YACzB,OAAO,CAAC,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACtD,CAAC;IACL,CAAC;IAOD,0BAAO,GAAP,UAAQ,UAAyC;QAAjD,iBA6BC;QA5BG,IAAI,OAAO,GAAG,IAAI,cAAc,EAAE,EAC9B,GAAG,GAAG,IAAI,CAAC,GAAG,EACd,IAAI,GAAG,EAAE,EACT,QAAQ,GAAG,IAAI,CAAC;QACpB,MAAM,CAAC,IAAI,kBAAO,CAAC,UAAC,OAAO,EAAE,MAAM;YAC/B,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;gBACb,EAAE,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;oBACvB,GAAG,GAAG,KAAI,CAAC,GAAG,GAAG,GAAG,GAAG,KAAI,CAAC,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;gBACpE,CAAC;gBACD,OAAO,CAAC,MAAM,GAAG;oBACb,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC;wBAC1C,EAAE,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC;4BAAC,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;wBAChG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBAC3B,CAAC;oBAAC,IAAI,CAAC,CAAC;wBACJ,EAAE,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC;4BAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;wBACzF,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBAC1B,CAAC;gBACL,CAAC,CAAC;gBACF,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;oBAClB,IAAI,GAAG,KAAI,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBAChD,CAAC;YACL,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,KAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;YACrC,KAAI,CAAC,UAAU,CAAC,OAAO,EAAE,KAAI,CAAC,OAAO,CAAC,CAAC;YACvC,KAAI,CAAC,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;YAC7C,OAAO,CAAC,gBAAgB,CAAC,cAAc,EAAE,KAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAChE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;IACP,CAAC;IAEM,eAAM,GAAb,UAAc,MAAkB;QAC5B,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IACL,eAAC;AAAD,CAAC,AApJD,IAoJC;AApJY,gBAAQ,WAoJpB,CAAA;AAKD;IAAkC,gCAAQ;IAA1C;QAAkC,8BAAQ;QACtC,SAAI,GAAG,IAAI,aAAa,EAAE,CAAC;QAC3B,mBAAc,GAAG,IAAI,CAAC,KAAK,CAAC;QAC5B,gBAAW,GAAG,IAAI,CAAC,KAAK,CAAC;IAC7B,CAAC;IAAD,mBAAC;AAAD,CAAC,AAJD,CAAkC,QAAQ,GAIzC;AAJY,oBAAY,eAIxB,CAAA;AAgBD;IAAA;QAAA,iBAyDC;QArCG,aAAQ,GAAa,UAAA,MAAM,IAAI,OAAA,KAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAA5B,CAA4B,CAAC;QAY5D,aAAQ,GAAa;YACjB,EAAE,CAAC,CAAC,CAAC,KAAI,CAAC,MAAM,CAAC;gBAAC,MAAM,CAAC,IAAI,CAAC;YAC9B,IAAI;gBAAC,MAAM,CAAC,KAAI,CAAC,WAAW,CAAC,KAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAI,CAAC,CAAC,CAAC;QAC1F,CAAC,CAAC;IAsBN,CAAC;IApBa,kCAAW,GAArB,UAAsB,KAAK,EAAE,QAAQ;QACjC,EAAE,CAAC,CAAC,QAAQ,CAAC;YAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI;YAAC,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC;IAED,gCAAS,GAAT,UAAU,MAAsB;QAC5B,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YACT,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;gBAAC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;YAC/C,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;gBAAC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;YACrD,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;gBAAC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;YACrD,EAAE,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;gBAAC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;YACjE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;gBAAC,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QAC7C,CAAC;QACD,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAEM,mBAAM,GAAb,UAAc,MAAsB;QAChC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAEL,mBAAC;AAAD,CAAC,AAzDD,IAyDC;AAzDY,oBAAY,eAyDxB,CAAA;AAED;IAA4B,0BAAY;IAAxC;QAAA,iBAmIC;QAnI2B,8BAAY;QAgBpC,cAAS,GAAa,UAAU,MAAc,EAAE,IAAI;YAChD,IAAI,WAAW,GAAG,EAAE,EAAE,GAAG,CAAC;YAC1B,GAAG,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC;gBACf,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACjC,CAAC;YAED,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;YAClC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC;QAChB,CAAC,CAAC;QAUF,YAAO,GAC6B,UAAC,KAAK,EAAE,MAAM;YAC9C,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC;gBACrB,EAAE,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC;oBAChB,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC7B,CAAC;YACL,CAAC;YACD,MAAM,CAAC,KAAK,CAAC;QACjB,CAAC,CAAC;QAKF,UAAK,GAAG,UAAC,OAAY,EAAE,QAAc,IAAK,OAAA,KAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAvC,CAAuC,CAAC;QAIlF,SAAI,GAAG,UAAC,OAAY;YAAE,kBAAkB;iBAAlB,WAAkB,CAAlB,sBAAkB,CAAlB,IAAkB;gBAAlB,iCAAkB;;YAAK,OAAA,MAAA,KAAI,CAAC,SAAS,EAAC,IAAI,YAAC,OAAO,SAAK,QAAQ,EAAC;;QAAzC,CAAyC,CAAC;QAIvF,SAAI,GAAG,UAAC,OAAY;YAAE,kBAAkB;iBAAlB,WAAkB,CAAlB,sBAAkB,CAAlB,IAAkB;gBAAlB,iCAAkB;;YAAK,OAAA,MAAA,KAAI,CAAC,SAAS,EAAC,IAAI,YAAC,OAAO,SAAK,QAAQ,EAAC;;QAAzC,CAAyC,CAAC;QAkBvF,aAAQ,GAAG,UAAC,MAAe;YACvB,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;gBACT,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;oBAAC,MAAM,CAAC,MAAM,CAAC,GAAG,KAAI,CAAC,IAAI,CAAC;YACnE,CAAC;YAAC,IAAI,CAAC,CAAC;gBACJ,MAAM,GAAG,EAAC,IAAI,EAAE,KAAI,CAAC,IAAI,EAAC,CAAC;YAC/B,CAAC;YACD,KAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC7B,MAAM,CAAC,KAAI,CAAC,MAAM,CAAC;QACvB,CAAC,CAAC;QAoCF,WAAM,GAAa,UAAC,KAAK,EAAE,MAAM;YAC7B,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;gBACT,MAAM,CAAC,KAAI,CAAC,YAAY,CAAC;YAC7B,CAAC;YAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,KAAI,CAAC,IAAI,IAAI,CAAC,KAAI,CAAC,OAAO,CAAC,CAAC,CAAC;gBACnD,MAAM,CAAC,KAAK,CAAC;YACjB,CAAC;YAAC,IAAI,CAAC,CAAC;gBACJ,EAAE,CAAC,CAAC,KAAI,CAAC,OAAO,YAAY,QAAQ,CAAC,CAAC,CAAC;oBACnC,MAAM,CAAE,KAAI,CAAC,OAAqC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;gBACtE,CAAC;gBAAC,IAAI,CAAC,CAAC;oBACJ,MAAM,CAAE,KAAI,CAAC,OAAyC,CAAC,MAAM,CAAC,UAAC,CAAC,EAAE,CAAC,IAAK,OAAA,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAZ,CAAY,EAAE,KAAK,CAAC,CAAC;gBACjG,CAAC;YACL,CAAC;QACL,CAAC,CAAA;IACL,CAAC;IA/CG,0BAAS,GAAT,UAAU,MAA2B;QACjC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YACT,EAAE,CAAC,CAAC,MAAM,YAAY,QAAQ,CAAC,CAAC,CAAC;gBAC7B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;YAC5B,CAAC;YACD,IAAI,CAAC,CAAC;gBACF,IAAI,IAAI,GAAY,MAAM,CAAC;gBAC3B,gBAAK,CAAC,SAAS,YAAC,IAAI,CAAC,CAAC;gBACtB,IAAI,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;gBAC7B,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;oBACX,EAAE,CAAC,CAAC,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC;wBAC/B,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,EAAC,GAAG,EAAE,QAAQ,EAAC,CAAC,CAAC;oBAClD,CAAC;oBAAC,IAAI,CAAC,CAAC;wBACJ,IAAI,CAAC,QAAQ,GAAG,QAAoB,CAAC;oBACzC,CAAC;gBACL,CAAC;gBACD,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;oBAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;gBAC9C,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;oBAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;YACxD,CAAC;QACL,CAAC;QACD,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAEM,aAAM,GAAb,UAAc,MAAO;QACjB,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAsBL,aAAC;AAAD,CAAC,AAnID,CAA4B,YAAY,GAmIvC;AAnIY,cAAM,SAmIlB,CAAA;AAgBD;IAA+B,6BAAY;IAA3C;QAAA,iBAmJC;QAnJ8B,8BAAY;QACvC,eAAU,GAAiC,EAAE,CAAC;QAC9C,iBAAY,GAAG,EAAE,CAAC;QACR,mBAAc,GAAG,EAAE,CAAC;QA8H9B,WAAM,GAAG,UAAC,KAAK,EAAE,MAAM;YACnB,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;gBAAC,MAAM,CAAC,KAAI,CAAC,YAAY,CAAC;YACrC,IAAI,CAAC,CAAC;gBACF,IAAI,QAAQ,GAAG,EAAE,EAAE,UAAQ,EAAE,aAAa,SAAA,CAAC;gBAC3C,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC;oBACpB,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;gBAC/B,CAAC;gBACD,GAAG,CAAC,CAAC,UAAQ,IAAI,KAAI,CAAC,UAAU,CAAC,CAAC,CAAC;oBAC/B,aAAa,GAAG,KAAI,CAAC,cAAc,CAAC,UAAQ,CAAC,CAAC;oBAC9C,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;wBAChB,QAAQ,CAAC,aAAa,CAAC,GAAG,KAAI,CAAC,UAAU,CAAC,UAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC;oBAChG,CAAC;oBAAC,IAAI,CAAC,CAAC;wBACJ,QAAQ,GAAG,KAAI,CAAC,UAAU,CAAC,UAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;oBACjE,CAAC;gBACL,CAAC;gBACD,MAAM,CAAC,QAAQ,CAAC;YACpB,CAAC;QACL,CAAC,CAAA;IACL,CAAC;IA9Ia,4BAAQ,GAAlB;QACI,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,IAAI,SAAS,CAAC,CAAC,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,mHAAmH,CAAC,CAAA;QACxI,CAAC;QACD,IAAI,CAAC,IAAI,GAAI,IAAI,CAAC,WAAmB,CAAC,IAAI,CAAC;IAC/C,CAAC;IAED,6BAAS,GAAT,UAAU,MAAmB;QACzB,gBAAK,CAAC,SAAS,YAAC,MAAM,CAAC,CAAC;QACxB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChC,IAAI,GAAG,CAAC;QACR,GAAG,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC;YAEf,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,YAAY,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9G,CAAC;QACD,EAAE,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;YAC9B,GAAG,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,UAAU,CAAC;gBAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/B,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IASS,sCAAkB,GAA5B,UAA6B,aAAa,EAAE,KAAK;QAC7C,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;YAChB,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,GAAG,KAAK,CAAC;QAC7C,CAAC;QAAC,IAAI,CAAC,CAAC;YACJ,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC;gBACpB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;YACxC,CAAC;QACL,CAAC;QACD,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAES,iCAAa,GAAvB,UAAwB,SAAS,EAAE,QAAQ;QACvC,SAAS,CAAC,SAAS,CAAC,EAAC,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,IAAI,GAAG,QAAQ,EAAC,CAAC,CAAA;IAC5D,CAAC;IAYD,yBAAK,GAAL,UAAM,QAAgB,EAAE,SAAuB,EAAE,aAAsB;QACnE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC;QACxB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;YAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC;QACtE,EAAE,CAAC,CAAC,SAAS,YAAY,MAAM,CAAC,CAAC,CAAC;YAE9B,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG,aAAa,CAAC;YAC9C,IAAI,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YAC3D,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC;gBAAC,IAAI,CAAC,aAAa,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QACjE,CAAC;QAAC,IAAI,CAAC,CAAC;YAEJ,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG,aAAa,IAAI,QAAQ,CAAC;YAC1D,IAAI,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC;QAC/B,CAAC;QACD,EAAE,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC;YAAC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;QAC3G,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAED,4BAAQ,GAAR,UAAS,UAAyE;QAC9E,IAAI,GAAG,EAAE,SAAS,CAAC;QACnB,GAAG,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC;YACrB,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;YAC5B,EAAE,CAAC,CAAC,SAAS,YAAY,YAAY,CAAC,CAAC,CAAC;gBACpC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC/B,CAAC;YAAC,IAAI,CAAC,CAAC;gBACJ,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC,aAAa,CAAC,CAAA;YAChF,CAAC;QACL,CAAC;QACD,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAED,2BAAO,GAAP,UAAQ,QAAgB;QACpB,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,SAAS,CAAC;QAC7C,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QACjC,OAAO,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtB,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAQD,iCAAa,GAAb,UAAc,SAAuB;QACjC,GAAG,CAAC,CAAC,IAAI,UAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;YACnC,EAAE,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC,UAAQ,CAAC,CAAC;gBAAC,MAAM,CAAC,UAAQ,CAAC;QAChE,CAAC;QACD,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IASD,iCAAa,GAAb,UAAc,SAAuB;QACjC,IAAI,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QAClD,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAC9C,CAAC;IA2BL,gBAAC;AAAD,CAAC,AAnJD,CAA+B,YAAY,GAmJ1C;AAnJY,iBAAS,YAmJrB,CAAA;AAcD;IAAsC,oCAAM;IAA5C;QAAsC,8BAAM;QAIxC,YAAO,GACmE,UAAC,KAAK,EAAE,MAAM;YACpF,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC;gBACrB,EAAE,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC;oBAChB,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;gBACxC,CAAC;YACL,CAAC;YACD,MAAM,CAAC,KAAK,CAAC;QACjB,CAAC,CAAC;IAMN,CAAC;IAJa,sCAAW,GAArB,UAAsB,KAAK,EAAE,QAAQ;QACjC,EAAE,CAAC,CAAC,QAAQ,CAAC;YAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI;YAAC,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC;IACL,uBAAC;AAAD,CAAC,AAlBD,CAAsC,MAAM,GAkB3C;AAlBY,wBAAgB,mBAkB5B,CAAA;AAKD;IAAyC,uCAAS;IAAlD;QAAA,iBA4CC;QA5CwC,8BAAS;QA2B9C,WAAM,GAAG,UAAC,KAA4B,EAAE,MAAM;YAC1C,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;gBAAC,MAAM,CAAC,KAAI,CAAC,YAAY,CAAC;YACrC,IAAI,CAAC,CAAC;gBAEF,IAAI,UAAQ,EAAE,aAAa,SAAA,EAAE,OAAO,SAAA,CAAC;gBACrC,GAAG,CAAC,CAAC,UAAQ,IAAI,KAAI,CAAC,UAAU,CAAC,CAAC,CAAC;oBAC/B,aAAa,GAAG,KAAI,CAAC,cAAc,CAAC,UAAQ,CAAC,CAAC;oBAC9C,OAAO,GAAG,KAAI,CAAC,UAAU,CAAC,UAAQ,CAAC,CAAC,MAAM,CAAC;oBAC3C,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;wBAChB,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,aAAa,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;oBAChF,CAAC;oBAAC,IAAI,CAAC,CAAC;wBACJ,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;oBACnC,CAAC;gBACL,CAAC;gBACD,MAAM,CAAC,KAAK,CAAC;YACjB,CAAC;QACL,CAAC,CAAA;IACL,CAAC;IAtCa,yCAAW,GAArB,UAAsB,KAAK,EAAE,QAAQ;QACjC,EAAE,CAAC,CAAC,QAAQ,CAAC;YAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI;YAAC,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC;IAES,sCAAQ,GAAlB;QACI,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,IAAI,mBAAmB,CAAC,CAAC,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,mHAAmH,CAAC,CAAA;QACxI,CAAC;QACD,IAAI,CAAC,IAAI,GAAI,IAAI,CAAC,WAAmB,CAAC,IAAI,CAAC;IAC/C,CAAC;IAES,gDAAkB,GAA5B,UAA6B,aAAa,EAAE,KAA4B;QACpE,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;YAChB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;QACpE,CAAC;QAAC,IAAI,CAAC,CAAC;YACJ,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACvD,CAAC;QACD,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAmBL,0BAAC;AAAD,CAAC,AA5CD,CAAyC,SAAS,GA4CjD;AA5CY,2BAAmB,sBA4C/B,CAAA"} -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | // Karma configuration 4 | // Generated on Mon Apr 04 2016 18:24:16 GMT-0400 (EDT) 5 | 6 | module.exports = function (config) { 7 | config.set({ 8 | 9 | // base path that will be used to resolve all patterns (eg. files, exclude) 10 | basePath: '', 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: ['jasmine'], 15 | 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 'test/test.js' 20 | ], 21 | 22 | 23 | // list of files to exclude 24 | exclude: [], 25 | 26 | // preprocess matching files before serving them to the browser 27 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 28 | preprocessors: { 29 | 'test/test.js': ['webpack'] 30 | }, 31 | 32 | plugins: ["karma-webpack", "karma-jasmine", "karma-chrome-launcher"], 33 | 34 | // test results reporter to use 35 | // possible values: 'dots', 'progress' 36 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 37 | reporters: ['progress'], 38 | 39 | 40 | // web server port 41 | port: 9876, 42 | 43 | 44 | // enable / disable colors in the output (reporters and logs) 45 | colors: true, 46 | 47 | 48 | // level of logging 49 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 50 | logLevel: config.LOG_INFO, 51 | 52 | 53 | // enable / disable watching file and executing tests whenever any file changes 54 | autoWatch: true, 55 | 56 | 57 | // start these browsers 58 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 59 | browsers: ['Chrome'], 60 | 61 | // Continuous Integration mode 62 | // if true, Karma captures browsers, runs the tests and exits 63 | singleRun: false, 64 | 65 | // Concurrency level 66 | // how many browser should be started simultaneous 67 | concurrency: Infinity, 68 | 69 | webpackServer: { 70 | noInfo: true 71 | } 72 | }) 73 | }; 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radical", 3 | "version": "0.1.16", 4 | "description": "A boilerplate-free, composable API framework for React/Redux", 5 | "main": "dist/radical.js", 6 | "typings": "dist/radical.d.ts", 7 | "scripts": { 8 | "test": "karma start karma.conf.js", 9 | "document": "node_modules/.bin/typedoc --name radical --readme ./README.md --module commonjs --target ES5 src/ --out docs", 10 | "build": "node_modules/.bin/tsc", 11 | "build-system": "node_modules/.bin/tsc --module system", 12 | "build-commonjs": "node_modules/.bin/tsc --module commonjs", 13 | "build-amd": "node_modules/.bin/tsc --module amd" 14 | }, 15 | "keywords": [ 16 | "API", 17 | "framework", 18 | "react", 19 | "redux" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/nathan-rice/radical.git" 24 | }, 25 | "author": "Nathan Rice", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "es6-promise": "^3.2.1", 29 | "immutable": "^3.7.6", 30 | "jasmine": "^2.4.1", 31 | "jasmine-core": "^2.4.1", 32 | "karma": "^0.13.22", 33 | "karma-chrome-launcher": "^0.2.3", 34 | "karma-commonjs": "0.0.13", 35 | "karma-jasmine": "^0.3.8", 36 | "redux": "^3.3.1", 37 | "typedoc": "^0.3.12", 38 | "typescript": "^1.8.10" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/radical.ts: -------------------------------------------------------------------------------- 1 | import {Promise} from 'es6-shim'; 2 | 3 | /** 4 | * This is the base for all inputs to a HTTP endpoint. This includes both URL arguments, and body data for 5 | * POST/PUT requests. 6 | */ 7 | export interface IEndpointInput { 8 | /** 9 | * This function should take a single argument of any type and return a string. 10 | */ 11 | converter?: Function; 12 | } 13 | 14 | export class EndpointInput implements IEndpointInput { 15 | 16 | /** 17 | * This function should take a single argument of any type and return a string. By default returns the result of 18 | * the toString method of argumentValue. 19 | * 20 | * @param argumentValue the value to be converted to a string. 21 | */ 22 | converter: Function = (argumentValue) => argumentValue.toString(); 23 | 24 | constructor(config?: IEndpointInput | Function) { 25 | if (config instanceof Function) { 26 | this.converter = config; 27 | } else if (config) { 28 | let config_: IEndpointInput = config; 29 | if (config_.converter) this.converter = config_.converter; 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * This specifies body data input for POST/PUT requests. 36 | */ 37 | export interface IEndpointBodyInput extends IEndpointInput { 38 | /** 39 | * The content type HTTP header that should be set for the endpoint request. 40 | */ 41 | contentType?: string; 42 | } 43 | 44 | /** 45 | * This specifies the correct content type and converter for JSON data. 46 | */ 47 | export class JsonBodyInput implements IEndpointBodyInput { 48 | contentType = "application/json; charset=utf-8"; 49 | converter = JSON.stringify; 50 | } 51 | 52 | export interface IEndpointArgumentContainer { 53 | [key: string]: IEndpointInput; 54 | } 55 | 56 | /** 57 | * This class is used to represent HTTP request url arguments. 58 | */ 59 | export class RequestArgument { 60 | constructor(public argument: string, public value) { 61 | this.value = value.toString(); 62 | } 63 | } 64 | 65 | export interface IEndpoint { 66 | url?: string; 67 | /** 68 | * The HTTP request method for the endpoint. 69 | */ 70 | method?: string; 71 | arguments?: IEndpointArgumentContainer; 72 | /** 73 | * This should be an object, where the keys are the header names, and the values are the header values to be set. 74 | */ 75 | headers?: Object; 76 | body?: IEndpointBodyInput; 77 | } 78 | 79 | export interface IEndpointExecutionParameters { 80 | arguments?: Object; 81 | data?: Object; 82 | success?: Function; 83 | error?: Function; 84 | headers?: Object; 85 | } 86 | 87 | /** 88 | * A declarative representation of a HTTP API endpoint. 89 | */ 90 | export class Endpoint implements IEndpoint { 91 | url: string; 92 | /** 93 | * The HTTP request method for the endpoint. Defaults to "GET" 94 | */ 95 | method: string = "GET"; 96 | arguments: IEndpointArgumentContainer = {}; 97 | /** 98 | * This should be an object, where the keys are the header names, and the values are the header values to be set. 99 | */ 100 | headers: Object; 101 | /** 102 | * The type of HTTP reqeust body input for this endpoint. By default, converts the input data to form-urlencode 103 | * format. 104 | * 105 | * @type {{contentType: string, converter: (function((RequestArgument[]|Object)): string)}} 106 | */ 107 | body: IEndpointBodyInput = { 108 | contentType: "application/x-www-form-urlencoded; charset=utf-8", 109 | converter: this.toQueryString 110 | }; 111 | 112 | /** 113 | * A function which is called to transform the success response body data before it is passed to the success 114 | * callback. 115 | * 116 | * @param response The HTTP response body contents. 117 | */ 118 | responseParser: (response: string) => any = (response) => response; 119 | 120 | /** 121 | * A function which is called to transform the error response body data before it is passed to the error callback. 122 | * 123 | * @param error The HTTP Error response body contents. 124 | */ 125 | errorParser: (error: string) => any = (error) => error; 126 | 127 | /** 128 | * 129 | * 130 | * @param config 131 | * @returns {Endpoint} 132 | */ 133 | configure(config: IEndpoint) { 134 | if (config.method) this.method = config.method; 135 | if (config.arguments) this.arguments = config.arguments; 136 | if (config.headers) this.headers = config.headers; 137 | if (config.body) this.body = config.body; 138 | if (config.url) this.url = config.url; 139 | return this; 140 | } 141 | 142 | constructor(config?: IEndpoint) { 143 | if (config) { 144 | this.configure(config); 145 | } 146 | } 147 | 148 | /** 149 | * Converts a URL query argument value to a string using the converter specified by the arguments configuration, or 150 | * the value's toString method if no converter was specified. 151 | * 152 | * @param argument The argument name 153 | * @param value The argument value 154 | */ 155 | private convert(argument, value): string { 156 | if (this.arguments[argument] && this.arguments[argument].converter) { 157 | return this.arguments[argument].converter(value); 158 | } else { 159 | return value.toString(); 160 | } 161 | 162 | } 163 | 164 | /** 165 | * Converts a set of URL query key, value pairs into an escaped query string. 166 | * 167 | * @param data The url query parameters 168 | * @returns {string} A URL-eescaped query string 169 | */ 170 | private toQueryString = (data: RequestArgument[] | Object) => { 171 | let key, arg, value, queryArgs = []; 172 | for (key in data) { 173 | // Handle array-like 174 | if (data[key] instanceof RequestArgument) { 175 | arg = data[key].argument; 176 | value = this.convert(data[key].argument, data[key].value); 177 | } else { 178 | arg = key; 179 | value = this.convert(key, data[key]); 180 | } 181 | queryArgs.push(arg + "=" + encodeURIComponent(value)); 182 | 183 | } 184 | return queryArgs.join("&"); 185 | }; 186 | 187 | /** 188 | * Sets the specified headers on the request object. 189 | * 190 | * @param request The request 191 | * @param headers The headers to set 192 | */ 193 | private setHeaders(request: XMLHttpRequest, headers: Object = {}) { 194 | for (let header in headers) { 195 | request.setRequestHeader(header, headers[header]); 196 | } 197 | } 198 | 199 | /** 200 | * Call an API endpoint via HTTP request. 201 | * 202 | * @param parameters The parameters to the HTTP request 203 | */ 204 | execute(parameters?: IEndpointExecutionParameters) { 205 | var request = new XMLHttpRequest(), 206 | url = this.url, 207 | data = "", 208 | endpoint = this; 209 | return new Promise((resolve, reject) => { 210 | if (parameters) { 211 | if (parameters.arguments) { 212 | url = this.url + "?" + this.toQueryString(parameters.arguments); 213 | } 214 | request.onload = function () { 215 | if (this.status >= 200 && this.status < 400) { 216 | if (parameters.success) parameters.success(endpoint.responseParser(this.response), this.status); 217 | resolve(this.response); 218 | } else { 219 | if (parameters.error) parameters.error(endpoint.errorParser(this.response), this.status); 220 | reject(this.response); 221 | } 222 | }; 223 | if (parameters.data) { 224 | data = this.body.converter(parameters.data); 225 | } 226 | } 227 | request.open(this.method, url, true); 228 | this.setHeaders(request, this.headers); 229 | this.setHeaders(request, parameters.headers); 230 | request.setRequestHeader("Content-Type", this.body.contentType); 231 | request.send(data); 232 | }); 233 | } 234 | 235 | static create(config?: IEndpoint) { 236 | return new this().configure(config); 237 | } 238 | } 239 | 240 | /** 241 | * Utility class for accessing API endpoints that send and receive data via JSON. 242 | */ 243 | export class JsonEndpoint extends Endpoint { 244 | body = new JsonBodyInput(); 245 | responseParser = JSON.parse; 246 | errorParser = JSON.parse; 247 | } 248 | 249 | export interface IApiComponent { 250 | name?: string; 251 | parent?: Namespace; 252 | defaultState?: Object; 253 | dispatch?: Function; 254 | getState?: Function; 255 | } 256 | 257 | export interface IAction extends IApiComponent { 258 | endpoint?: Endpoint | string; 259 | initiator?: Function; 260 | reducer?: ((state, action) => Object) | ((state, action) => Object)[]; 261 | } 262 | 263 | export class ApiComponent { 264 | /** 265 | * The name of this component. Used to automatically generate Redux action types. If specified, it must be unique 266 | * among all instantiated `ApiComponents`. 267 | */ 268 | name: string; 269 | 270 | /** 271 | * The parent `Namespace`, used for state and store resolution. 272 | */ 273 | parent: Namespace; 274 | 275 | /** 276 | * The defaultState required for this component to function properly. 277 | */ 278 | defaultState: Object; 279 | 280 | /** 281 | * The dispatch function. Optional for child nodes, required for root nodes. 282 | */ 283 | dispatch: Function = action => this.parent.dispatch(action); 284 | 285 | /** 286 | * For root nodes, the reduce function which should be passed to the Redux store. 287 | */ 288 | reduce: Function; 289 | 290 | /** 291 | * Returns the state object specified for this `ApiComponent` by its parent component. 292 | * 293 | * @returns {any} The localized state for this component 294 | */ 295 | getState: Function = () => { 296 | if (!this.parent) return null; 297 | else return this.getSubState(this.parent.getState(), this.parent.stateLocation(this)); 298 | }; 299 | 300 | protected getSubState(state, location) { 301 | if (location) return state[location]; 302 | else return state; 303 | } 304 | 305 | configure(config?: IApiComponent) { 306 | if (config) { 307 | if (config.parent) this.parent = config.parent; 308 | if (config.getState) this.getState = config.getState; 309 | if (config.dispatch) this.dispatch = config.dispatch; 310 | if (config.defaultState) this.defaultState = config.defaultState; 311 | if (config.name) this.name = config.name; 312 | } 313 | return this; 314 | } 315 | 316 | static create(config?: IApiComponent) { 317 | return new this().configure(config); 318 | } 319 | 320 | } 321 | 322 | export class Action extends ApiComponent implements IAction, Function { 323 | 324 | /** 325 | * The API HTTP `Endpoint` for this action, if any. 326 | */ 327 | endpoint: Endpoint; 328 | 329 | /** 330 | * The initiator function for this `Action`. This is the function that should be invoked when you call the `Action` 331 | * via your client API. 332 | * 333 | * @param action Bound parameter, references the current `Action` in place of this, which is bound to the parent 334 | * namespace. 335 | * @param data 336 | * @returns {Namespace} 337 | */ 338 | initiator: Function = function (action: Action, data) { 339 | let reduxAction = {}, key; 340 | for (key in data) { 341 | reduxAction[key] = data[key]; 342 | } 343 | // Set the type property of the reduxAction after copying data properties in case type is a data property 344 | reduxAction["type"] = action.name; 345 | action.dispatch(reduxAction); 346 | return this; 347 | }; 348 | 349 | /** 350 | * The function responsible for updating state as a result of an action. If not specified, defaults 351 | * to copying all properties of the action object except type to the state object. 352 | * 353 | * @param state 354 | * @param action 355 | * @returns {any} 356 | */ 357 | reducer: ((state, action) => Object) | 358 | ((state, action) => Object)[] = (state, action) => { 359 | for (let key in action) { 360 | if (key != "type") { 361 | state[key] = action[key]; 362 | } 363 | } 364 | return state; 365 | }; 366 | 367 | /** 368 | * Required in order to masquerade as a function for the purpose of type checking 369 | */ 370 | apply = (thisArg: any, argArray?: any) => this.initiator.apply(thisArg, argArray); 371 | /** 372 | * Required in order to masquerade as a function for the purpose of type checking 373 | */ 374 | call = (thisArg: any, ...argArray: any[]) => this.initiator.call(thisArg, ...argArray); 375 | /** 376 | * Required in order to masquerade as a function for the purpose of type checking 377 | */ 378 | bind = (thisArg: any, ...argArray: any[]) => this.initiator.bind(thisArg, ...argArray); 379 | /** 380 | * Required in order to masquerade as a function for the purpose of type checking 381 | */ 382 | prototype: any; 383 | /** 384 | * Required in order to masquerade as a function for the purpose of type checking 385 | */ 386 | length: number; 387 | /** 388 | * Required in order to masquerade as a function for the purpose of type checking 389 | */ 390 | arguments: any; 391 | /** 392 | * Required in order to masquerade as a function for the purpose of type checking 393 | */ 394 | caller: Function; 395 | 396 | dispatch = (action?: Object) => { 397 | if (action) { 398 | if (!action.hasOwnProperty("type")) action["type"] = this.name; 399 | } else { 400 | action = {type: this.name}; 401 | } 402 | this.parent.dispatch(action); 403 | return this.parent; 404 | }; 405 | 406 | configure(config?: IAction | Function) { 407 | if (config) { 408 | if (config instanceof Function) { 409 | this.initiator = config; 410 | } 411 | else { 412 | let conf: IAction = config; 413 | super.configure(conf); 414 | let endpoint = conf.endpoint; 415 | if (endpoint) { 416 | if (typeof endpoint === "string") { 417 | this.endpoint = new Endpoint({url: endpoint}); 418 | } else { 419 | this.endpoint = endpoint as Endpoint; 420 | } 421 | } 422 | if (conf.reducer) this.reducer = conf.reducer; 423 | if (conf.initiator) this.initiator = conf.initiator; 424 | } 425 | } 426 | return this; 427 | } 428 | 429 | static create(config?) { 430 | return new this().configure(config); 431 | } 432 | 433 | /** 434 | * This function is a pass-through for the reducer method that handles some common Redux boilerplate. 435 | * 436 | * @param state 437 | * @param action 438 | * @returns {any} 439 | */ 440 | reduce: Function = (state, action) => { 441 | if (!state) { 442 | return this.defaultState; 443 | } else if (action.type != this.name || !this.reducer) { 444 | return state; 445 | } else { 446 | if (this.reducer instanceof Function) { 447 | return (this.reducer as (state, action) => Object)(state, action); 448 | } else { 449 | return (this.reducer as ((state, action) => Object)[]).reduce((s, f) => f(s, action), state); 450 | } 451 | } 452 | } 453 | } 454 | 455 | export interface IComponentContainer { 456 | [key: string]: ApiComponent; 457 | } 458 | 459 | export interface INamespace extends IApiComponent { 460 | components?: IComponentMountConfiguration[] | IComponentContainer | Object; 461 | } 462 | 463 | export interface IComponentMountConfiguration { 464 | location: string; 465 | component: ApiComponent; 466 | stateLocation?: string; 467 | } 468 | 469 | export class Namespace extends ApiComponent implements INamespace { 470 | components: IComponentContainer | Object = {}; 471 | defaultState = {}; 472 | protected _stateLocation = {}; 473 | 474 | protected nameSelf() { 475 | if (this.constructor == Namespace) { 476 | throw new Error("Automatic Namespace naming is only supported for derived classes; you must specify a value for the name attribute") 477 | } 478 | this.name = (this.constructor as any).name; 479 | } 480 | 481 | configure(config?: INamespace) { 482 | super.configure(config); 483 | if (!this.name) this.nameSelf(); 484 | let key; 485 | for (key in this) { 486 | // This ensures consistent behavior for ApiComponents defined on a namespace as part of a class declaration 487 | if (this[key] instanceof ApiComponent && !this.mountLocation(this[key])) this.components[key] = this[key]; 488 | } 489 | if (config && config.components) { 490 | for (key in config.components) this.components[key] = config.components[key]; 491 | } 492 | this.mountAll(this.components); 493 | return this; 494 | } 495 | 496 | /** 497 | * Merges child `ApiComponent` defaultState into the current `Namespace` defaultState. 498 | * 499 | * @param stateLocation 500 | * @param state 501 | * @returns {Namespace} 502 | */ 503 | protected updateDefaultState(stateLocation, state): Namespace { 504 | if (stateLocation) { 505 | this.defaultState[stateLocation] = state; 506 | } else { 507 | for (let key in state) { 508 | this.defaultState[key] = state[key]; 509 | } 510 | } 511 | return this; 512 | } 513 | 514 | protected nameComponent(component, location) { 515 | component.configure({name: this.name + ": " + location}) 516 | } 517 | 518 | /** 519 | * Registers an `ApiComponent` as a child of this `Namespace`. Also names any unnamed child components, and 520 | * handles merging defaultState objects. 521 | * 522 | * @param location The bind point for 523 | * @param component The component to mount. 524 | * @param stateLocation An optional state location; used to provide an `Action` with an isolated state, or to 525 | * use a different name for a child `Namespace`'s sub-state tree. 526 | * @returns {Namespace} 527 | */ 528 | mount(location: string, component: ApiComponent, stateLocation?: string): Namespace { 529 | component.parent = this; 530 | if (!this.components[location]) this.components[location] = component; 531 | if (component instanceof Action) { 532 | // Actions with an undefined state location operate on the parent Namespace's entire state 533 | this._stateLocation[location] = stateLocation; 534 | this[location] = component.initiator.bind(this, component); 535 | if (!component.name) this.nameComponent(component, location); 536 | } else { 537 | // Namespaces *must* have a state location, we'll use the mount location if necessary 538 | this._stateLocation[location] = stateLocation || location; 539 | this[location] = component; 540 | } 541 | if (component.defaultState) this.updateDefaultState(this._stateLocation[location], component.defaultState); 542 | return this; 543 | } 544 | 545 | mountAll(components: IComponentMountConfiguration[] | IComponentContainer | Object) { 546 | let key, component; 547 | for (key in components) { 548 | component = components[key]; 549 | if (component instanceof ApiComponent) { 550 | this.mount(key, component); 551 | } else { 552 | this.mount(component.location, component.component, component.stateLocation) 553 | } 554 | } 555 | return this; 556 | } 557 | 558 | unmount(location: string): Namespace { 559 | this.components[location].parent = undefined; 560 | delete this.components[location]; 561 | delete this.defaultState[location]; 562 | delete this[location]; 563 | return this; 564 | } 565 | 566 | /** 567 | * Returns the property name for the specified `ApiComponent` on this `Namespace`. 568 | * 569 | * @param component 570 | * @returns {any} 571 | */ 572 | mountLocation(component: ApiComponent): string { 573 | for (let location in this.components) { 574 | if (component == this.components[location]) return location; 575 | } 576 | return null; 577 | } 578 | 579 | /** 580 | * Returns the property name for the portion of the state object specific to the specified `ApiComponent`, or 581 | * undefined for components that receive the entire local state object. 582 | * 583 | * @param component 584 | * @returns {any} 585 | */ 586 | stateLocation(component: ApiComponent): string { 587 | var mountLocation = this.mountLocation(component); 588 | return this._stateLocation[mountLocation]; 589 | } 590 | 591 | /** 592 | * Provides state locality by dispatching portions of the state tree to child `ApiComponent` reduce methods. 593 | * 594 | * @param state 595 | * @param action 596 | * @returns {{}} 597 | */ 598 | reduce = (state, action) => { 599 | if (!state) return this.defaultState; 600 | else { 601 | let newState = {}, location, stateLocation; 602 | for (let key in state) { 603 | newState[key] = state[key]; 604 | } 605 | for (location in this.components) { 606 | stateLocation = this._stateLocation[location]; 607 | if (stateLocation) { 608 | newState[stateLocation] = this.components[location].reduce(newState[stateLocation], action); 609 | } else { 610 | newState = this.components[location].reduce(newState, action) 611 | } 612 | } 613 | return newState; 614 | } 615 | } 616 | } 617 | 618 | /** 619 | * Lightweight interface to support Immutable collections without requiring Immutable. 620 | */ 621 | export interface ICollection { 622 | get: (key: K) => V; 623 | set: (key: K, value: V) => ICollection; 624 | merge: (...iterables: ICollection[]) => ICollection; 625 | } 626 | 627 | /** 628 | * Just a minor tweak of `Action` to support Immutable (or similar) state. 629 | */ 630 | export class CollectionAction extends Action { 631 | 632 | defaultState: ICollection; 633 | 634 | reducer: (state: ICollection, action) => ICollection | 635 | ((state: ICollection, action) => ICollection)[] = (state, action) => { 636 | for (let key in action) { 637 | if (key != "type") { 638 | state = state.set(key, action[key]); 639 | } 640 | } 641 | return state; 642 | }; 643 | 644 | protected getSubState(state, location) { 645 | if (location) return state.get(location); 646 | else return state; 647 | } 648 | } 649 | 650 | /** 651 | * Just a minor tweak of `Namespace to support Immutable (or similar) state. 652 | */ 653 | export class CollectionNamespace extends Namespace { 654 | /** 655 | * The `CollectionNamespace`'s default state. This property is **required**. 656 | */ 657 | defaultState: ICollection; 658 | 659 | protected getSubState(state, location) { 660 | if (location) return state.get(location); 661 | else return state; 662 | } 663 | 664 | protected nameSelf() { 665 | if (this.constructor == CollectionNamespace) { 666 | throw new Error("Automatic Namespace naming is only supported for derived classes; you must specify a value for the name attribute") 667 | } 668 | this.name = (this.constructor as any).name; 669 | } 670 | 671 | protected updateDefaultState(stateLocation, state: ICollection): CollectionNamespace { 672 | if (stateLocation) { 673 | this.defaultState = this.defaultState.set(stateLocation, state); 674 | } else { 675 | this.defaultState = this.defaultState.merge(state); 676 | } 677 | return this; 678 | } 679 | 680 | reduce = (state: ICollection, action): ICollection => { 681 | if (!state) return this.defaultState; 682 | else { 683 | // applying to a new object here to retain state set by actions with a non-standard getState 684 | let location, stateLocation, reducer; 685 | for (location in this.components) { 686 | stateLocation = this._stateLocation[location]; 687 | reducer = this.components[location].reduce; 688 | if (stateLocation) { 689 | state = state.set(stateLocation, reducer(state.get(stateLocation), action)); 690 | } else { 691 | state = reducer(state, action); 692 | } 693 | } 694 | return state; 695 | } 696 | } 697 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var Redux = require('redux'); 2 | var Immutable = require('immutable'); 3 | var Radical = require('../dist/radical.js'); 4 | 5 | describe("Namespace", function () { 6 | var store = Redux.createStore(function (s) { return s }); 7 | it("can mount components, get and set state properly", function () { 8 | var ns = Radical.Namespace.create({ 9 | name: "Greeter", 10 | defaultState: {greeting: "hello", target: "world"}, 11 | dispatch: store.dispatch, 12 | getState: store.getState 13 | }), 14 | greetTarget = Radical.Action.create(function (action) { 15 | var state = this.getState(); 16 | return state.greeting + " " + state.target; 17 | }), 18 | setTarget = Radical.Action.create(function (action, target) { 19 | return action.dispatch({target: target}); 20 | }), 21 | setGreeting = Radical.Action.create(function (action, greeting) { 22 | return action.dispatch({greeting: greeting}); 23 | }); 24 | store.replaceReducer(ns.reduce); 25 | ns.mount("greetTarget", greetTarget); 26 | ns.configure({components: {setTarget: setTarget, setGreeting: setGreeting}}); 27 | expect(ns.greetTarget()).toEqual("hello world"); 28 | expect(ns.setTarget("everybody").greetTarget()).toEqual("hello everybody"); 29 | expect(ns.setGreeting("hiya").greetTarget()).toEqual("hiya everybody"); 30 | var state = ns.getState(); 31 | expect(state.target).toEqual("everybody"); 32 | expect(state.greeting).toEqual("hiya"); 33 | }); 34 | }); 35 | 36 | describe("CollectionNamespace", function () { 37 | var store = Redux.createStore(function (s) { return s }); 38 | it("can mount components, get and set state properly", function () { 39 | var ns = Radical.CollectionNamespace.create({ 40 | name: "Collection Greeter", 41 | defaultState: Immutable.fromJS({greeting: "hello", target: "world"}), 42 | dispatch: store.dispatch, 43 | getState: store.getState 44 | }), 45 | greetTarget = Radical.CollectionAction.create(function (action) { 46 | var state = this.getState(); 47 | return state.get("greeting") + " " + state.get("target"); 48 | }), 49 | setTarget = Radical.CollectionAction.create(function (action, target) { 50 | return action.dispatch({target: target}); 51 | }), 52 | setGreeting = Radical.CollectionAction.create(function (action, greeting) { 53 | return action.dispatch({greeting: greeting}); 54 | }); 55 | store.replaceReducer(ns.reduce); 56 | ns.mount("greetTarget", greetTarget); 57 | ns.configure({components: {setTarget: setTarget, setGreeting: setGreeting}}); 58 | expect(ns.greetTarget()).toEqual("hello world"); 59 | expect(ns.setTarget("everybody").greetTarget()).toEqual("hello everybody"); 60 | expect(ns.setGreeting("hiya").greetTarget()).toEqual("hiya everybody"); 61 | var state = ns.getState(); 62 | expect(state.get("target")).toEqual("everybody"); 63 | expect(state.get("greeting")).toEqual("hiya"); 64 | }); 65 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "declaration": true, 5 | "outDir": "dist", 6 | "sourceMap": true, 7 | "removeComments": true, 8 | "jsx": "react", 9 | "moduleResolution": "node" 10 | }, 11 | "files": [ 12 | "src/radical.ts", 13 | "typings/index.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Radical", 3 | "version": false, 4 | "dependencies": {}, 5 | "devDependencies": {}, 6 | "ambientDevDependencies": { 7 | "react": "registry:dt/react#0.14.0+20160319053454", 8 | "react-dom": "registry:dt/react-dom#0.14.0+20160316155526", 9 | "react-redux": "registry:dt/react-redux#4.4.0+20160406115600", 10 | "redux": "registry:dt/redux#3.3.1+20160326112656" 11 | }, 12 | "globalDevDependencies": { 13 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160602141504" 14 | } 15 | } 16 | --------------------------------------------------------------------------------