├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── angular │ ├── angular-component.js │ ├── angular-controller.js │ ├── angular-route.js │ ├── angular-url-manager.js │ ├── angular-url.js │ ├── base │ │ ├── directive-controller.js │ │ └── screen-controller.js │ ├── index.js │ ├── services │ │ ├── compiler.js │ │ ├── digest.js │ │ ├── directive-params.js │ │ ├── events.js │ │ ├── injector.js │ │ ├── scope.js │ │ └── watcher.js │ ├── translators │ │ ├── component.js │ │ ├── controller.js │ │ └── route.js │ └── validation │ │ └── structures.js ├── application-config.js ├── decorators │ ├── component.js │ └── controller.js ├── exceptions │ ├── exception.js │ ├── register.js │ └── runtime.js ├── serializers │ ├── coding-serializer.js │ ├── rename-serializer.js │ ├── serializer.js │ └── url-serializer.js ├── url-manager.js ├── url.js ├── utils │ ├── class-name.js │ ├── logger.js │ ├── object-difference.js │ ├── object-transition.js │ └── primitives.js ├── valent-component.js ├── valent-controller.js ├── valent-route.js ├── valent.js └── validation │ └── structures.js └── test ├── components ├── serializer.js ├── url-struct.js └── url.js ├── controller ├── controller-flow.js └── controller-model.js ├── index.js ├── manager.js ├── route ├── route-config.js ├── route-convert.js ├── route-flow.js └── route-url-struct.js ├── serializers ├── coding-serializer.js ├── rename-serializer.js ├── serializer.js └── url-serializer.js ├── test-utils.js └── utils ├── object-description.js └── object-transition.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "modules": true 4 | }, 5 | 6 | "env": { 7 | "es6": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /node_modules 5 | /src-native 6 | /temp 7 | 8 | /angular 9 | /exceptions 10 | /decorators 11 | /serializers 12 | /utils 13 | /validation 14 | /application-config.js 15 | /url.js 16 | /url-manager.js 17 | /valent.js 18 | /valent-component.js 19 | /valent-controller.js 20 | /valent-route.js 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /node_modules 5 | /src-native 6 | /temp 7 | .npmignore 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Frankland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Valentjs 2 | --- 3 | [![Join the chat at https://gitter.im/frankland/valent](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/frankland/valent?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Valentjs provide easier way to register framework components (directives / routing / controllers) with features. Registered components could be transalted into different frameworks but right now only [AngularJS](https://github.com/angular/angular.js) available. 6 | 7 | Valentjs - just the wrapper for frameworks and could be used together with default frameworks approach. 8 | 9 | 10 | - [Valentjs](#valentjs) 11 | - [Valentjs + AngularJS](#valentjs--angularjs) 12 | - [AngularJS bootstrap](#angularjs-bootstrap) 13 | - [Configuration](#configuration) 14 | - [Routing](#routing) 15 | - [Exceptins](#exceptins) 16 | - [Angular configuration (config/run)](#angular-configuration-configrun) 17 | - [Controllers](#controllers) 18 | - [Controller class](#controller-class) 19 | - [Controller options](#controller-options) 20 | - [controller.option.as](#controlleroptionas) 21 | - [controller.option.url](#controlleroptionurl) 22 | - [controller.option.params](#controlleroptionparams) 23 | - [controller.option.resolve](#controlleroptionresolve) 24 | - [controller.option.struct](#controlleroptionstruct) 25 | - [controller.option.template](#controlleroptiontemplate) 26 | - [controller.option.templateUrl](#controlleroptiontemplateurl) 27 | - [Controller.render()](#controllerrender) 28 | - [Route](#route) 29 | - [url](#url) 30 | - [Route options](#route-options) 31 | - [route.option.params](#routeoptionparams) 32 | - [route.option.resolve](#routeoptionresolve) 33 | - [route.option.template](#routeoptiontemplate) 34 | - [route.option.templateUrl](#routeoptiontemplateurl) 35 | - [route.option.struct](#routeoptionstruct) 36 | - [Directive](#directive) 37 | - [Directive Controller class](#directive-controller-class) 38 | - [Directive options](#directive-options) 39 | - [directive.option.as](#directiveoptionas) 40 | - [directive.option.template](#directiveoptiontemplate) 41 | - [directive.option.templateUrl](#directiveoptiontemplateurl) 42 | - [directive.option.restrict](#directiveoptionrestrict) 43 | - [directive.option.require](#directiveoptionrequire) 44 | - [directive.option.params](#directiveoptionparams) 45 | - [directive.option.interfaces](#directiveoptioninterfaces) 46 | - [directive.option.options (rename)](#directiveoptionoptions-rename) 47 | - [directive.option.pipes](#directiveoptionpipes) 48 | - [Directive Params](#directive-params) 49 | - [Defined structures](#defined-structures) 50 | - [Serializers](#serializers) 51 | - [Base serializer](#base-serializer) 52 | - [Rename serializer](#rename-serializer) 53 | - [Custom serializer](#custom-serializer) 54 | - [Url serializer](#url-serializer) 55 | - [Url](#url) 56 | - [Url Manager](#url-manager) 57 | - [Services](#services) 58 | - [Digest](#digest) 59 | - [Injector](#injector) 60 | - [Watcher](#watcher) 61 | - [Events](#events) 62 | - [Decorators](#decorators) 63 | - [Base Components](#base-components) 64 | - [Contributing](#contributing) 65 | - [TODO](#todo) 66 | 67 | 68 | TOC was generated using [doctoc](https://github.com/thlorenz/doctoc). 69 | 70 | # Valentjs + AngularJS 71 | 72 | Valentjs provide methods to register and validate: 73 | 74 | - directvies 75 | - controllers 76 | - routing 77 | 78 | and features form the box: 79 | 80 | - Writing code using ES6 81 | - Easier application configuration 82 | - URL manager 83 | - Configured URL serializer/unserializer 84 | - Custom serializers 85 | - Multiple routing for one controller 86 | - Enchanted [tcomb](https://github.com/gcanti/tcomb) structures 87 | - Interfaces / pipes for directives 88 | - Debounced safe digest 89 | - No access to **$scope** 90 | 91 | Simple examples (valentjs / angularjs) 92 | 93 | - [controller](https://gist.github.com/tuchk4/eed3b6e58d52dac1d51e) 94 | - [directive](https://gist.github.com/tuchk4/6c25e0fc25cb5eb5d31d) 95 | - [route](https://gist.github.com/tuchk4/12683667be66b562794c) 96 | - [configuration](https://gist.github.com/tuchk4/7ad1707ee0aed2df673a) 97 | 98 | # AngularJS bootstrap 99 | 100 | **valent** - same as **angular** - variable at the global namespace. 101 | 102 | ```js 103 | import Angular from 'valent/angular'; 104 | 105 | let framework = new Angular('your-application-name', { 106 | 107 | /** 108 | * if dependencies are defined - angular module 109 | * will be created automatically 110 | * 111 | * Otherwise - you should register angular module 112 | * manually 113 | */ 114 | dependencies: [ 115 | 'ngRoute' 116 | ] 117 | }); 118 | 119 | valent.bootstrap(framework); 120 | ``` 121 | 122 | ```html 123 | 124 | ``` 125 | # Configuration 126 | 127 | `valent.config` could be used as key/value storage. Keys with dots will be translated into nested objects. 128 | 129 | ```js 130 | valent.config.set('routing.otherwise', '/home'); 131 | valent.config.set('routing.html5', true); 132 | 133 | valent.config.get('routing'); 134 | // {otherwise: '/home', 'html5': true} 135 | valent.config.get('routing.otherwise') // '/home' 136 | ``` 137 | 138 | And there are number of shortctus 139 | 140 | ```js 141 | valent.config.route.otherwise('/home'); 142 | 143 | valent.config.route.onChangeStart(route => { 144 | // ... 145 | }); 146 | 147 | valent.config.route.addResolver('schema', () => { 148 | // ... 149 | }); 150 | 151 | valent.config.exception.handler((error, causedBy) => { 152 | // ... 153 | }); 154 | ``` 155 | List of config shorctus 156 | 157 | ## Routing 158 | - route.otherwise(url) - setup url for redirect if route does not exist 159 | - route.onChangeStart(callback) - add hook for event **$routeChangeStart** 160 | - route.onChangeError(callback) - add hook for event **$routeChangeError** 161 | - route.addResolver(key, resolver) - add [global resolver](http://i.imgur.com/eO43UR5.png) that will be applied for each route 162 | - resolver(name, params) - same as [local resolver](#routeoptionresolve). Takes 2 arguments - route name, route params. 163 | - route.enableHistoryApi() - enable html5 routing. By default - html5 routing is enabled 164 | - route.disableHistoryApi() - disable html5 routing 165 | - routing.requireBase(isBaseRequired) - is base tag requierd for application 166 | 167 | ## Exceptions 168 | 169 | - exception.handler(handler) - setup exception handler that will available for framework's context and window.on('error') 170 | 171 | 172 | # Angular configuration (config/run) 173 | ```js 174 | import Angular from 'valent/angular'; 175 | 176 | let framework = new Angular('your-application-name', { 177 | dependencies: [ 178 | 'ngRoute' 179 | ] 180 | }); 181 | 182 | // app - angular module 183 | let app = framework.getAngularModule(); 184 | 185 | // same as with native angular 186 | app.config(['$rootScope', $rootScope => { 187 | // ... 188 | }); 189 | 190 | // same as with native angular 191 | app.run(['$rootScope', $rootScope => { 192 | // ... 193 | }); 194 | ``` 195 | Or you can run config/run methods directly to angular.module 196 | 197 | ```js 198 | import Angular from 'valent/angular'; 199 | 200 | let framework = new Angular('your-application-name', { 201 | dependencies: [ 202 | 'ngRoute' 203 | ] 204 | }); 205 | 206 | angular.module('your-application-name') 207 | .config(['$rootScope', $rootScope => { 208 | // ... 209 | }); 210 | ``` 211 | # Controllers 212 | 213 | Simple configuration 214 | ```js 215 | class HomeController { 216 | // ... 217 | } 218 | 219 | valent.controller('home', HomeController); 220 | 221 | // or with already attached route 222 | valent.controller('home', HomeController, { 223 | url: '/home' 224 | }); 225 | ``` 226 | `valent.controller` takes 3 arguments 227 | 228 | - controller name 229 | - controller class 230 | - options 231 | 232 | ## Controller class 233 | 234 | ```js 235 | class HomeController { 236 | constructor(resolvers, url, logger) { 237 | // ... 238 | } 239 | 240 | destructor() { 241 | // ... 242 | } 243 | } 244 | ``` 245 | 246 | Constructor could take up to 3 arguments: 247 | 248 | - `resolvers` - if there any local or global resolver 249 | - `url` - if three is attached route 250 | - `logger` - configured logger. Always add colored controller's name to logs 251 | 252 | `destructor` method is called when controller's **$scope** is destroyed ($destroy event). 253 | 254 | ## Controller options 255 | 256 | ### controller.option.as 257 | An identifier name for a reference to the controller. By default - **controller**. 258 | 259 | ```html 260 |

{{ controller.greeting }}

261 | ``` 262 | 263 | If **as** defined: 264 | ```js 265 | { 266 | as: '_' 267 | } 268 | ``` 269 | Template should be 270 | ```html 271 |

{{ _.greeting }}

272 | ``` 273 | 274 | ### controller.option.url 275 | Route [url proxy](#url) 276 | 277 | ### controller.option.params 278 | Route [url proxy](#routeoptionparams) 279 | 280 | ### controller.option.resolve 281 | Route [resolve proxy](#routeoptionresolve) 282 | 283 | ### controller.option.struct 284 | Route [struct proxy](#routeoptionstruct) 285 | 286 | ### controller.option.template 287 | Route [template proxy](#routeoptiontemplate) 288 | 289 | ### controller.option.templateUrl 290 | Route [templateUrl proxy](#routeoptiontemplateurl) 291 | 292 | ### Controller.render() 293 | If there is static function **render()** at controller's class - it's result will be used as template. 294 | 295 | ```js 296 | class HomeController { 297 | constructor() { 298 | // ... 299 | } 300 | 301 | static render() { 302 | return '
Yo!
' 303 | } 304 | } 305 | ``` 306 | 307 | # Route 308 | 309 | ```js 310 | valent.route('home', '/home', { 311 | template: '
...
' 312 | }); 313 | ``` 314 | 315 | `valent.controller` takes 3 arguments 316 | 317 | - controller name 318 | - url 319 | - options 320 | 321 | ## url 322 | url for controller. Could be a string: 323 | ```js 324 | { 325 | url: '/home' 326 | } 327 | ``` 328 | or array of strings (means that controller will be available by different routes) 329 | ```js 330 | { 331 | url: ['/home', '/my/home'] 332 | } 333 | ``` 334 | 335 | ## Route options 336 | 337 | ### route.option.params 338 | Any params that will be passed to angular route config. Also route params are available in resolvers (local and global) as second argument. 339 | 340 | ```js 341 | valent.route('home', '/home', { 342 | params: { 343 | guest: true 344 | } 345 | }); 346 | ``` 347 | 348 | ### route.option.resolve 349 | Local resolvers. Function that will be executed before controller's constructor. Support promises. Resolved results will be passed to controller's constructor. Local resolvers will be executed [after global resolvers](http://i.imgur.com/eO43UR5.png). 350 | Here is [Angular documentation](https://docs.angularjs.org/api/ngRoute/provider/$routeProvider#when) about route resolvers. 351 | Each resolver take 2 arguments: 352 | 353 | - `name` - resolving route name 354 | - `params` - resolving [route params](#routeoptionparams) 355 | 356 | ```js 357 | { 358 | resolve: { 359 | 'permission': (name, params) => { 360 | return Users.me().catch(() => { 361 | 362 | return params.guest 363 | ? Promise.resolve('Allowed as guest') 364 | : Promise.reject('Denied for guests'); 365 | }); 366 | } 367 | } 368 | } 369 | ``` 370 | 371 | ### route.option.template 372 | ```js 373 | { 374 | template: '
Yo!
' 375 | } 376 | ``` 377 | 378 | ### route.option.templateUrl 379 | ```js 380 | { 381 | templateUrl: '/templates/home.html' 382 | } 383 | ``` 384 | 385 | ### route.option.struct 386 | Structure for url. 387 | ```js 388 | import * as primitives from 'valent/utils/primitives'; 389 | 390 | class HomeController { 391 | // ... 392 | } 393 | 394 | valent.controller(home, HomeController, { 395 | url: '/home', 396 | struct: { 397 | id: primitives.Num, 398 | tags: primitives.MaybeListStr, 399 | period: primitives.MaybeListDat 400 | } 401 | }); 402 | ``` 403 | 404 | # Directive 405 | ```js 406 | class GreetMeController { 407 | // ... 408 | } 409 | 410 | valent.component('greet-me', GreetMeController, { 411 | 412 | }); 413 | ``` 414 | 415 | ## Directive Controller class 416 | Full list of auto called methods. They are **NOT** required. 417 | 418 | ```js 419 | class GreetMeController { 420 | constructor(params, logger) { 421 | 422 | } 423 | 424 | destructor() { 425 | 426 | } 427 | 428 | link(element, compileResult) { 429 | 430 | } 431 | 432 | require(controllers) { 433 | 434 | } 435 | 436 | static compile(element) { 437 | return {}; 438 | } 439 | 440 | static render() { 441 | return '
....
'; 442 | } 443 | } 444 | ``` 445 | Constructor arguments are depends on options. By default constructor takes 2 arguments 446 | 447 | - [directive params](#directive-params) 448 | - logger - configured logger. Always add colored controller's name to logs 449 | 450 | if `interfaces` or `optionals (options)` are defined - they will passed before. 451 | 452 | TODO: Find better naming for this features. High prio :) 453 | 454 | - `destructor` method is called when controller's \$scope is destroyed (\$destroy event). 455 | 456 | - `link(element, compileResult)` method - same as [default angular's link](https://docs.angularjs.org/api/ng/service/$compile#-link-) function but do not take \$scope. `static compile()` result will be passed as second argument. 457 | 458 | - `require(controllers)` method - takes all required controllers. Returned content will be passed as second argument to `link()` method. 459 | 460 | - `static render()` - result of this method could be used as directive's template. 461 | 462 | - `static compile(element)` method could be used for template compilation. Same as [default angular's compile](https://docs.angularjs.org/api/ng/service/$compile#-compile-). Very useful if directive's templates are used as configuration. In this way - directive's template should **NOT** be defined. 463 | 464 | For example in this case "Applications" will be used for multi-select label. 465 | ```html 466 | 467 | Applications 468 | 469 | ``` 470 | 471 | For example in this case - cell templates are defined as a content of `grid` directive. In `static compile(element)` method this could be parsed and passed to directive controller. 472 | ```html 473 | 474 | 475 | 476 | 477 | 478 | {{ cell.value | date:"MM/dd/yyyy" }} 479 | 480 | 481 | ``` 482 | 483 | ## Directive options 484 | 485 | ### directive.option.as 486 | Same as valent.controller [as option](#controlleroptionas) 487 | 488 | ### directive.option.template 489 | Same as valent.controller [template option](#controlleroptiontemplate) but not proxy no route. 490 | 491 | ### directive.option.templateUrl 492 | Same as valent.controller [templateUrl option](#controlleroptiontemplateurl) but not proxy no route. 493 | 494 | ### directive.option.restrict 495 | Same as angular directive's options [restrict](https://docs.angularjs.org/guide/directive#template-expanding-directive). 496 | 497 | Recomment to use only 498 | - A - only matches attribute name 499 | - E - only matches element name 500 | 501 | ### directive.option.require 502 | Uses for [directive communications](https://docs.angularjs.org/guide/directive#creating-directives-that-communicate). 503 | ```js 504 | { 505 | require: ['ngModel', '^^plFilterBar'] 506 | } 507 | More details at official angular [doc](https://docs.angularjs.org/api/ng/service/$compile#-require-). 508 | ``` 509 | 510 | Relate controllers will be passed to `require(controllers)` method. 511 | 512 | ```js 513 | class GreetMeController { 514 | constructor(params, logger) { 515 | 516 | } 517 | 518 | require(controllers) { 519 | this.ngModel = controllers.ngModel; 520 | this.plFilterBar = controllers.plFilterBar; 521 | } 522 | } 523 | ``` 524 | 525 | ### directive.option.params 526 | Same as angular directive's options [scope](https://docs.angularjs.org/api/ng/service/$compile#-scope-). 527 | 528 | ### directive.option.interfaces 529 | ```js 530 | // app-connector.js 531 | class AppConnector { 532 | host = valent.config.get('app.server.host'); 533 | 534 | constructor(port) { 535 | //... 536 | } 537 | 538 | connect() { 539 | 540 | } 541 | 542 | // ... 543 | } 544 | ``` 545 | ```js 546 | // server-status-component.js 547 | import AppConnector from './app-connector'; 548 | 549 | class ServerStatusController { 550 | constructor(connector) { 551 | connector.connect().then(status => { 552 | this.status = status; 553 | }); 554 | } 555 | 556 | static render() { 557 | return '
{{ _.status }}
' 558 | } 559 | } 560 | 561 | valent.component('server-status', ServerStatusController, { 562 | interfaces: { 563 | connector: AppConnector 564 | } 565 | }); 566 | ``` 567 | 568 | ```js 569 | // home-screen.js 570 | import AppConnector from './app-connector'; 571 | 572 | class HomeController { 573 | connector = new AppConnector(9001); 574 | 575 | constructor() { 576 | this.connector.notify(); 577 | } 578 | 579 | static render() { 580 | return ``; 581 | } 582 | } 583 | 584 | valent.controller('home', HomeController); 585 | ``` 586 | 587 | So we defined for directive ``interface **connector** with specific class - **AppConnector**. So already created instance of this class should be passed to directive as attribute. If not - Exception. If wrong class - Exception. 588 | Bonuses: 589 | 590 | - Have control over same object in directive and in parent controller (no matter if it is screen's controller or another directive's controller). Could be useful to create relation between app components. 591 | - Interfaces are passed as first arguments to directitve's constructor 592 | - Inside directive you are sure that instance (or its parents) has correct class (type). 593 | - Useful for complex directives that works with **charts**, **grids**, **forms** etc. 594 | 595 | ### directive.option.options (rename) 596 | ```js 597 | valent.component('server-status', ServerStatusController, { 598 | interfaces: { 599 | connector: AppConnector 600 | }, 601 | option: { 602 | validator: AppValidator 603 | } 604 | }); 605 | ``` 606 | Same as interfaces but options are **NOT** required. 607 | If option's attribute is defined at directive - option instance will be passed to directive's constructor. 608 | ```html 609 | 610 | ``` 611 | ```js 612 | class ServerStatusController { 613 | constructor(connector, validator) { 614 | // 615 | } 616 | } 617 | ``` 618 | 619 | If option's attribute is **NOT** defined - null will be passed to directive's constructor. 620 | ```html 621 | 622 | ``` 623 | ```js 624 | class ServerStatusController { 625 | constructor(connector, validator) { 626 | equals(validator, null); 627 | } 628 | } 629 | ``` 630 | 631 | ### directive.option.pipes 632 | If defined and not passed to directive - will be created automatically. Available throw `DirectiveParams.get()`. 633 | 634 | ```js 635 | // toggler.js 636 | class Toggler extends Events { 637 | isVisible = false; 638 | 639 | open() { 640 | if (!this.isVisible) { 641 | this.isVisible = true; 642 | this.emit('open'); 643 | } 644 | } 645 | 646 | close() { 647 | if (this.isVisible) { 648 | this.isVisible = false; 649 | this.emit('close'); 650 | } 651 | } 652 | } 653 | ``` 654 | 655 | ```js 656 | // drop-down-component.js 657 | import Toggler from './toggler'; 658 | 659 | class DropDownController { 660 | constructor(params) { 661 | this.toggler = params.get('toggler'); 662 | } 663 | 664 | static render() { 665 | return ` 666 | 670 | 674 |
675 | ... 676 |
`; 677 | } 678 | } 679 | 680 | valent.component('drop-down', DropDownController, { 681 | pipes: { 682 | toggler: Toggler 683 | } 684 | }); 685 | ``` 686 | 687 | ```js 688 | // home-screen.js 689 | import Toggler from './toggler'; 690 | 691 | class HomeController { 692 | toggler = new Toggler(); 693 | 694 | constructor() { 695 | this.toggler.open(); 696 | this.toggler.on('open', () => { 697 | console.log('opened :)'); 698 | }); 699 | } 700 | } 701 | 702 | valent.controller('home', HomeController); 703 | ``` 704 | ```html 705 |
706 | 707 | `${user.id}:${user.name}`, 875 | decode: raw => { 876 | let splitted = raw.split(':'); 877 | return new UserStruct({ 878 | id: splitted[0], 879 | name: splitted[1] 880 | }); 881 | } 882 | }); 883 | }, 884 | 885 | /** 886 | * We should override encode/decode methods 887 | * because by default encode method takes object 888 | * same as struct that is defined in constructor 889 | */ 890 | encode(user) { 891 | return super.encode({user}); 892 | } 893 | 894 | decode(raw) { 895 | let decoded = super.decode(raw); 896 | return decoded.user; 897 | } 898 | } 899 | 900 | let serializer = new UserSerializer(); 901 | 902 | let user = new User({ 903 | id: 1, 904 | name: 'Lorem' 905 | }); 906 | 907 | // encode 908 | let encoded = serializer.encode(user); 909 | equal(encoded, '1:Lorem'); 910 | 911 | // decode 912 | let decoded = serializer.decode('2:Ipsum'); 913 | equal(decided, new User({ 914 | id: 2, 915 | name: Ipsum 916 | })); 917 | ``` 918 | 919 | ## Url serializer 920 | Extends rename serializer and contain rules for encode/decode. Does not contains URL instance. Rules will work only if struct attributes are references to primitives. 921 | 922 | NOTE: Serializers contains WeakMap of encode/decode rules. And keys - are objects from "primitives.js" module. Thats you need to make references to primitives. Primitives works with tcomb - so it also works as type validator. 923 | 924 | TODO: map strings to primitive's objects? 925 | { 926 | id: 'number', 927 | tags: 'maybe.listOfStrings' 928 | } 929 | 930 | Constructor takes 2 arguments 931 | 932 | - struct (same as at rename serializer) 933 | - encode/decode options 934 | 935 | Default options: 936 | ```js 937 | { 938 | list_delimiter: '~', 939 | matrix_delimiter: '!', 940 | date_format: 'YYYYMMDD' 941 | } 942 | ``` 943 | 944 | Example: 945 | ```js 946 | import primitives from 'valent/utils/primitives'; 947 | import UrlSerializer from 'valent/serializers/url-serializer'; 948 | 949 | let serializer = new UrlSerializer({ 950 | id: ['i', primitives.Num] 951 | tags: primitives.MaybeListStr 952 | }); 953 | 954 | let origin = { 955 | id: 1, 956 | tags: ['a', 'b', 'c'] 957 | }; 958 | 959 | let encoded = serializer.encode(origin); 960 | let decoded = serializer.decode(encoded); 961 | 962 | equals(origin, decoded); 963 | ``` 964 | 965 | Encode rules: 966 | 967 | Primitive | Origin | Encoded 968 | --------- | ------ | ------- 969 | primitives.Num, primitives.MaybeNum | 1 | 1 970 | primitives.Str, primitives.MaybeStr | 'a' | a 971 | primitives.Bool, primitives.MaybeBool | true | 1 972 | primitives.Bool, primitives.MaybeBool | false | 0 973 | primitives.Dat, primitives.MaybeDat | new Date() | 2015112 974 | primitives.ListNum, primitives.MaybeListNum | [1, 2, 3] | 1~2~3 975 | primitives.ListMaybeNum | [1, null, 2, 3] | 1~~2,3 976 | primitives.ListStr, primitives.MaybeListStr | ['a', 'b', 'c'] | a~b~c 977 | primitives.ListMaybeStr | ['a', null, 'b', 'c'] | a~~b~c 978 | primitives.ListBool, primitives.MaybeListBool | [true, false] | 1~0 979 | primitives.ListDat, primitives.MaybeListDat | [new Date(), new Date()] | 2015112~2015112 980 | primitives.MatrixNum, primitives.MaybeMatrixNum | [[1,2],[3,4]] | 1~2!3~4 981 | primitives.MatrixMaybeNum | [[1,2,null,3],[4,null,5]] | 1~2~~3!4~~5 982 | primitives.MatrixStr, primitives.MaybeMatrixStr | [['a','b'],['c','4']] | a~b!c~d 983 | primitives.MatrixBool, primitives.MaybeMatrixBool | [[true, false],[false, true]] | 1~0!0~1 984 | 985 | 986 | # Url 987 | If route is defined for controller url instance will be passed to controller's constructor. Also url instance could be created manually. 988 | ```js 989 | import Url from 'valent/angular/angular-url'; 990 | import * as primitives from 'valent/utils/primitives'; 991 | 992 | let url = new Url('/store/:id/:tags', { 993 | id: primitives.Num, 994 | tags: primitives.MaybeListStr, 995 | period: primitives.MaybeListDat, 996 | search: ['q', primitives.MaybeListStr 997 | }); 998 | ``` 999 | 1000 | Uses URL serializer. Constructor takes 2 arguments: 1001 | 1002 | - pattern - url pattern with placeholders. 1003 | - struct - url params structure 1004 | 1005 | ```js 1006 | import Url from 'valent/angular/angular-url'; 1007 | import * as primitives from 'valent/utils/primitives'; 1008 | 1009 | let url = new Url('/store/:id/:tags', { 1010 | id: primitives.Num, 1011 | tags: primitives.MaybeListStr, 1012 | period: primitives.MaybeListDat, 1013 | search: ['q', primitives.MaybeStr 1014 | }); 1015 | 1016 | let route = url.stringify({ 1017 | id: 1, 1018 | search: 'Hello', 1019 | tags: ['yellow', 'large'] 1020 | }); 1021 | 1022 | equal(route, '/store/1/yellow-large?q=Hello'); 1023 | 1024 | // And if current url is 1025 | // '/store/1/yellow-large?q=Hello?period=20151110-20151120' 1026 | 1027 | let params = url.parse(); 1028 | equal(parms, { 1029 | id: 1, 1030 | search: 'Hello', 1031 | tags: ['yellow', 'large'], 1032 | period: [ 1033 | // date objects. btw - not sure about correct timezones... 1034 | Wed Nov 10 2015 00:00:00 GMT+0200 (EET), 1035 | Wed Nov 20 2015 00:00:00 GMT+0200 (EET) 1036 | ] 1037 | }); 1038 | ``` 1039 | 1040 | Structures with **Maybe** prefix - means that this parameters are not required. If passed parameters have wrong type - will be exception. Parameters that are not described as placeholder at url pattern - will be added as GET parameter. 1041 | 1042 | Provide helpful methods to work with url. Available methods: 1043 | 1044 | - `go(params, options)` - replace current url with generating according to passed params. Works in angular context - all route events will be fired. **options** - event options that will be available at url watchers. 1045 | 1046 | - `stringify(params)` - return url according to passed params 1047 | 1048 | - `redirect(params)` - same as **go()** but with page reloading 1049 | 1050 | - `parse()` - parse current url and return decoded params 1051 | 1052 | - `watch(callback)` - listen url changes (\$scope event **\$routeUpdate**) and execute callback. Callback arguments - params, diff, options. 1053 | - params - current url params. 1054 | - diff - difference between previous url update. 1055 | - options - event options that were passed to **go()** method 1056 | 1057 | - `isEmpty()` - return true if there are no params in current url 1058 | 1059 | - `link(key, fn)` - describe setter for url param. 1060 | 1061 | - `linkTo(store)` - automatically link all structure params to store object 1062 | 1063 | - `apply()` - execute all added **link()** functions 1064 | 1065 | Url link and apply example. If url is changed (no matter how - back/forward browser buttons, url.go(params) method, page reload etc.) - each **link** function will be executed and take current value of binded param. 1066 | 1067 | ```js 1068 | import Url from 'valent/angular/angular-url'; 1069 | 1070 | class HomeController { 1071 | filters = {}; 1072 | 1073 | constructor(resovled, url) { 1074 | /** 1075 | * url params "search", "tags" 1076 | * will be linked this this.filters object 1077 | */ 1078 | url.linkTo(this.filters, [ 1079 | 'tags', 1080 | 'search' 1081 | ]); 1082 | 1083 | // add link for "id" param 1084 | url.link('id', id => { 1085 | this.id = id; 1086 | }); 1087 | 1088 | url.link('search', search => { 1089 | this.filters.search = search; 1090 | }); 1091 | 1092 | url.watch((params, diff, options) => { 1093 | /** 1094 | * We can not run apply automatically 1095 | * on route update because there are 1096 | * a lot of cases when developers should 1097 | * call apply() manually 1098 | */ 1099 | url.apply(); 1100 | }); 1101 | } 1102 | } 1103 | 1104 | valent.controller('store', StoreController, { 1105 | url: '/store/:id/:tags', 1106 | struct: { 1107 | id: primitives.Num, 1108 | search: ['q', primitives.MaybeStr, 1109 | tags: primitives.MaybeListStr, 1110 | } 1111 | }); 1112 | ``` 1113 | # Url Manager 1114 | ```js 1115 | let homeUrl = valent.url.get('home'); 1116 | homeUrl.go(params); 1117 | 1118 | valent.url.set('custom-url', ManualltCreatedUrlInstance); 1119 | ``` 1120 | 1121 | - valent.url.get(key) returns url instance for passed **key**. 1122 | - valent.url.set(key, instance) set url instance for **key**. 1123 | 1124 | For controller with attached url `valent.url.set(...)` will be called automatically. 1125 | 1126 | # Services 1127 | 1128 | ## Digest 1129 | ```js 1130 | import digest from 'valent/angular/services/digest'; 1131 | 1132 | class HomeController { 1133 | constructor() { 1134 | digest(this); 1135 | } 1136 | } 1137 | ``` 1138 | 1139 | Already [debounce](https://lodash.com/docs#debounce) digest (trailing = true, timeout = 50). Configurable. 1140 | Global configuration: 1141 | ```js 1142 | valent.config.set('angular.digest.timeout', 100); 1143 | ``` 1144 | Local configuration: 1145 | ```js 1146 | import digest from 'valent/angular/services/digest'; 1147 | let configured = digest.configure(100); 1148 | ``` 1149 | 1150 | ## Injector 1151 | ```js 1152 | import Injector from 'valent/angular/services/injector'; 1153 | ``` 1154 | AngularJS [\$injector](https://docs.angularjs.org/api/auto/service/$injector) service. Only method **get()** is available and only after application bootstrap (after angular run phase). 1155 | 1156 | ```js 1157 | import Injector from 'valent/angular/services/injector'; 1158 | 1159 | class HomeController { 1160 | constructor() { 1161 | let $parse = Injector.get('$parse'); 1162 | } 1163 | } 1164 | ``` 1165 | 1166 | ## Watcher 1167 | ```js 1168 | import Watcher from 'valent/angular/services/watcher'; 1169 | ``` 1170 | Service is using to create [watchers](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch). watchGroup, watchCollection and deep watch - are not available. 1171 | 1172 | NOTE: We highly recommend NOT to use watchers. No matter how watchers are created using this service or native $scope methods. 1173 | 1174 | ```js 1175 | import Watcher from 'valent/angular/services/watcher'; 1176 | 1177 | class HomeController { 1178 | title = 'Hello World!'; 1179 | 1180 | constructor() { 1181 | let watcher = new Watcher(this); 1182 | watcher.watch('controller.title', title => { 1183 | // ... 1184 | }); 1185 | } 1186 | } 1187 | ``` 1188 | 1189 | ` new Watcher(context)` - takes one argument - controller's context. Return watcher instance that setuped for controller's $scope. 1190 | 1191 | ` new Watcher()` - Return watcher instance that setuped for $rootScope. 1192 | 1193 | ## Events 1194 | ```js 1195 | import Events from 'valent/angular/services/events'; 1196 | ``` 1197 | 1198 | Provide access to [$scope events](https://docs.angularjs.org/guide/scope#scope-events-propagation). Constructor takes one argument - controller's context. 1199 | 1200 | ```js 1201 | import Events from 'valent/angular/services/events'; 1202 | 1203 | class HomeController { 1204 | constructor() { 1205 | let events = new Events(this); 1206 | events.on('$routeChangeStart', () => { 1207 | // ... 1208 | }); 1209 | 1210 | events.broadcast('my.custom.event', { 1211 | greeting: 'Yo' 1212 | }); 1213 | 1214 | events.emit('my.custom.event', { 1215 | greeting: 'Yo' 1216 | }); 1217 | } 1218 | } 1219 | ``` 1220 | 1221 | # Decorators 1222 | 1223 | From [PR#10](https://github.com/frankland/valentjs/pull/10). 1224 | 1225 | TODO: implement and add docs :) 1226 | 1227 | ```js 1228 | import { Template, Url } from 'valent/decorators'; 1229 | 1230 | @Template('home.html'); 1231 | @Url('/home', '/index'); 1232 | class HomeController { 1233 | // ... 1234 | } 1235 | ``` 1236 | 1237 | # Base Components 1238 | ```js 1239 | import BaseScreenController from 'valent/angular/base/screen-controller'; 1240 | 1241 | import BaseComponentController from 'valent/angular/base/component-controller'; 1242 | ``` 1243 | 1244 | TODO: implement and add docs :) 1245 | 1246 | # Contributing 1247 | 1248 | TODO: add docs :) 1249 | 1250 | # TODO 1251 | - [ ] Use [tcomb](https://github.com/gcanti/tcomb) for controller/route/component validation! 1252 | - [ ] Boilerplate 1253 | - [ ] Examples 1254 | - [ ] valentjs vs angularjs. Configuration diffs 1255 | - [ ] implement TODO application using valent 1256 | - [ ] Fix old and add new test 1257 | - [ ] redevelop angular-url. Kick extra dependencies (url-pattern) 1258 | - [ ] rename directive options - interfaces / optionals / pipes 1259 | - [ ] rename DirecitveParams into ComponentParams 1260 | - [ ] replace pathes - `/valent/..`. into `/valentjs/....` 1261 | - [ ] redevelop exception system. Right now RuntimeException and RegisterException are not comfortable for debugging. 1262 | - [ ] add more useful primitives 1263 | - [ ] do not register component's interfaces / options / pipes as angular directive option - **scope**. This is extra watchers. Use `DirectiveParams.parse()` method to get them. 1264 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valent", 3 | "version": "0.1.0-rc23", 4 | "repository": "https://github.com/frankland/valent", 5 | "description": "Helpfull ES6 runtime for angular", 6 | "scripts": { 7 | "test": "mocha test/index.js --compilers js:babel-core/register", 8 | "prepublish": "./node_modules/.bin/babel --stage=0 -d ./ ./src", 9 | "dev": "./node_modules/.bin/babel -d ./ ./src --watch", 10 | "format": "prettier --trailing-comma es5 --single-quote --write './src/**/*.js' './test/**/*.js'", 11 | "precommit": "lint-staged" 12 | }, 13 | "main": "./valent.js", 14 | "keywords": [ 15 | "angular", 16 | "es6", 17 | "controller", 18 | "directive", 19 | "magic" 20 | ], 21 | "license": "MIT", 22 | "dependencies": { 23 | "deep-diff": "^0.3.8", 24 | "lodash": "^4.17.4", 25 | "moment": "^2.18.1", 26 | "tcomb": "^3.2.20", 27 | "tcomb-validation": "^3.3.0", 28 | "url-pattern": "^1.0.3" 29 | }, 30 | "devDependencies": { 31 | "babel-core": "^6.25.0", 32 | "babel-preset-es2015": "^6.24.1", 33 | "babel-preset-stage-0": "^6.24.1", 34 | "chai": "^4.0.2", 35 | "expect.js": "^0.3.1", 36 | "husky": "^0.13.4", 37 | "lint-staged": "^3.6.1", 38 | "mocha": "^3.4.2", 39 | "prettier": "^1.4.4", 40 | "rimraf": "^2.6.1" 41 | }, 42 | "lint-staged": { 43 | "*.js": [ 44 | "prettier --trailing-comma es5 --single-quote --write", 45 | "git add" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/angular/angular-component.js: -------------------------------------------------------------------------------- 1 | import camelCase from 'lodash/camelCase'; 2 | 3 | import isObject from 'lodash/isPlainObject'; 4 | import isArray from 'lodash/isArray'; 5 | 6 | import ValentComponent from '../valent-component'; 7 | 8 | import * as validation from './validation/structures'; 9 | 10 | let normalize = options => { 11 | let require = options.require; 12 | 13 | let normalizedRequire = null; 14 | if (require) { 15 | if (isArray(require)) { 16 | normalizedRequire = require; 17 | } else { 18 | normalizedRequire = [require]; 19 | } 20 | } 21 | 22 | return Object.assign({}, options, { 23 | require: normalizedRequire, 24 | }); 25 | }; 26 | 27 | export default class AngularComponent extends ValentComponent { 28 | constructor(name, ComponentClass, options) { 29 | let normalized = normalize(options); 30 | super(name, ComponentClass, normalized); 31 | } 32 | 33 | static validate(name, ComponentClass, options) { 34 | let errors = super.validate(name, ComponentClass, options); 35 | 36 | let isValidRequire = validation.isValidRequire(options.require); 37 | let isValidTransclude = validation.isValidTransclude(options.transclude); 38 | let isValidModule = validation.isValidModule(options.module); 39 | let isValidNamespace = validation.isValidNamespace(options.as); 40 | 41 | if (!isValidRequire) { 42 | errors.push('require should string or array of strings'); 43 | } 44 | 45 | if (!isValidTransclude) { 46 | errors.push( 47 | 'transclude should be boolean value or object (for multi-slot transclude)' 48 | ); 49 | } 50 | 51 | if (!isValidModule) { 52 | errors.push('Module name should be a string'); 53 | } 54 | 55 | if (!isValidNamespace) { 56 | errors.push('Namespace should be a string'); 57 | } 58 | 59 | return errors; 60 | } 61 | 62 | getDirectiveName() { 63 | let name = this.getName(); 64 | return camelCase(name); 65 | } 66 | 67 | getModule() { 68 | return this.options.module || null; 69 | } 70 | 71 | getNamespace() { 72 | return this.options.as || 'controller'; 73 | } 74 | 75 | isIsolated() { 76 | let bindings = this.getBindings(); 77 | return isObject(bindings); 78 | } 79 | 80 | getTransclude() { 81 | return this.options.transclude; 82 | } 83 | 84 | getRequire() { 85 | return this.options.require; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/angular/angular-controller.js: -------------------------------------------------------------------------------- 1 | import ValentController from '../valent-controller'; 2 | 3 | import * as validation from './validation/structures'; 4 | 5 | export default class AngularController extends ValentController { 6 | constructor(name, ControllerClass, options) { 7 | super(name, ControllerClass, options); 8 | } 9 | 10 | static validate(name, ControllerClass, options) { 11 | let errors = super.validate(name, ControllerClass, options); 12 | 13 | let isValidNamespace = validation.isValidNamespace(options.as); 14 | 15 | if (!isValidNamespace) { 16 | errors.push('Namespace should be a string'); 17 | } 18 | 19 | return errors; 20 | } 21 | 22 | getModule() { 23 | return this.options.module || null; 24 | } 25 | 26 | getNamespace() { 27 | return this.options.as || 'controller'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/angular/angular-route.js: -------------------------------------------------------------------------------- 1 | import ValentRoute from '../valent-route'; 2 | import * as validation from './validation/structures'; 3 | 4 | export default class AngularRoute extends ValentRoute { 5 | constructor(name, url, options) { 6 | super(name, url, options); 7 | } 8 | 9 | static validate(name, url, options) { 10 | let errors = super.validate(name, url, options); 11 | 12 | let isValidModule = validation.isValidModule(options.module); 13 | 14 | if (!isValidModule) { 15 | errors.push('Module name should be a string'); 16 | } 17 | 18 | return errors; 19 | } 20 | 21 | getModule() { 22 | return this.options.module || null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/angular/angular-url-manager.js: -------------------------------------------------------------------------------- 1 | import UrlManager from '../url-manager'; 2 | import Injector from './services/injector'; 3 | 4 | let _contexts = Symbol('controller-contexts'); 5 | let _queue = Symbol('contexts-queue'); 6 | 7 | export default class AngularUrlManager extends UrlManager { 8 | constructor() { 9 | super(); 10 | this[_contexts] = new WeakMap(); 11 | this[_queue] = new Map(); 12 | } 13 | 14 | attach(name, context, $scope) { 15 | this[_queue].set(name, $scope); 16 | this[_contexts].set(context, name); 17 | } 18 | 19 | detach($scope) { 20 | if (!$scope.hasOwnProperty('$valent')) { 21 | throw new Error( 22 | 'can not attach $scope because there is not $valent attribute' 23 | ); 24 | } 25 | 26 | let info = $scope.$valent; 27 | let namespace = info.namespace; 28 | let context = $scope[namespace]; 29 | 30 | this[_contexts].delete(context); 31 | } 32 | 33 | get(name) { 34 | let url = super.get(name); 35 | 36 | if (!url.hasScope() && this[_queue].has(name)) { 37 | let $scope = this[_queue].get(name); 38 | 39 | if (!$scope) { 40 | // NOTES: seems this case is impossible 41 | throw new Error('there is not scope to attach to angular url'); 42 | } 43 | 44 | url.attachScope($scope); 45 | 46 | this[_queue].delete(name); 47 | } 48 | 49 | return url; 50 | } 51 | 52 | create(context) { 53 | if (!this[_contexts].has(context)) { 54 | throw new Error( 55 | `can not get url by context. Seems $scope is not attached yet` 56 | ); 57 | } 58 | 59 | let name = this[_contexts].get(context); 60 | 61 | return this.get(name); 62 | } 63 | 64 | reload() { 65 | let $route = Injector.get('$route'); 66 | $route.reload(); 67 | } 68 | 69 | getCurrentRoute() { 70 | let $route = Injector.get('$route'); 71 | 72 | let url = null; 73 | if ($route.hasOwnProperty('current') && $route.current.$$route) { 74 | url = this.get($route.current.$$route.controller); 75 | } 76 | 77 | return url; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/angular/angular-url.js: -------------------------------------------------------------------------------- 1 | import isObject from 'lodash/isObject'; 2 | import isEqual from 'lodash/isEqual'; 3 | import cloneDeep from 'lodash/cloneDeep'; 4 | 5 | import union from 'lodash/union'; 6 | import reduce from 'lodash/reduce'; 7 | 8 | import Url from '../url'; 9 | 10 | import Scope from './services/scope'; 11 | import Injector from './services/injector'; 12 | 13 | let _scope = Symbol('scope'); 14 | let _state = Symbol('state'); 15 | 16 | export default class AngularUrl extends Url { 17 | constructor(pattern, struct) { 18 | super(pattern, struct); 19 | 20 | this[_state] = {}; 21 | this[_scope] = null; 22 | } 23 | 24 | // $scope is using for url.watch() method 25 | attachScope($scope) { 26 | this[_scope] = $scope; 27 | } 28 | 29 | hasScope() { 30 | return !!this[_scope]; 31 | } 32 | 33 | parse() { 34 | let $location = Injector.get('$location'); 35 | let decoded = this.decode($location.$$url); 36 | 37 | this.cacheParams(decoded); 38 | 39 | return decoded; 40 | } 41 | 42 | go(params, options) { 43 | let $location = Injector.get('$location'); 44 | 45 | if (options) { 46 | let $rootScope = Injector.get('$rootScope'); 47 | 48 | let unsubscribe = $rootScope.$on('$routeUpdate', event => { 49 | Object.assign(event, { 50 | $valentEvent: options, 51 | }); 52 | 53 | unsubscribe(); 54 | }); 55 | } 56 | 57 | let url = this.stringify(params); 58 | $location.url(url); 59 | } 60 | 61 | // TODO: fix this method 62 | redirect(params = {}) { 63 | if (!isObject(params)) { 64 | throw new Error('params should be an object'); 65 | } 66 | 67 | let url = this.stringify(params); 68 | 69 | let isHtml5Mode = valent.config.get('routing.html5Mode'); 70 | 71 | if (isHtml5Mode) { 72 | window.location.href = url; 73 | } else { 74 | window.location.href = `/#/${url}`; 75 | } 76 | } 77 | 78 | stringify(params) { 79 | let url = super.stringify(params); 80 | 81 | let $browser = Injector.get('$browser'); 82 | let base = $browser.baseHref(); 83 | 84 | return base ? url.replace(/^\//, '') : url; 85 | } 86 | 87 | watch(callback) { 88 | let $scope = this[_scope]; 89 | 90 | this[_state] = this.parse(); 91 | 92 | return $scope.$on('$routeUpdate', event => { 93 | let valentEvent = event.$valentEvent || {}; 94 | let params = this.parse(); 95 | 96 | // TODO: remove diff feature 97 | let state = this[_state]; 98 | let allkeys = union(Object.keys(params), Object.keys(state)); 99 | 100 | let diff = reduce( 101 | allkeys, 102 | function(result, key) { 103 | if (!isEqual(params[key], state[key])) { 104 | result[key] = params[key]; 105 | } 106 | 107 | return result; 108 | }, 109 | {} 110 | ); 111 | 112 | this[_state] = cloneDeep(params); 113 | 114 | callback(params, diff, valentEvent); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/angular/base/directive-controller.js: -------------------------------------------------------------------------------- 1 | import Injector from '../services/injector'; 2 | import digest from '../services/digest'; 3 | 4 | export default class BaseComponentController { 5 | digest = () => digest(this); 6 | injector = Injector; 7 | 8 | constructor() {} 9 | } 10 | -------------------------------------------------------------------------------- /src/angular/base/screen-controller.js: -------------------------------------------------------------------------------- 1 | import Injector from '../services/injector'; 2 | import digest from '../services/digest'; 3 | 4 | let proxy = name => { 5 | console.info(`"${name}" could be implemented in child class`); 6 | }; 7 | 8 | export default class BaseScreenController { 9 | digest = () => digest(this); 10 | 11 | injector = Injector; 12 | 13 | constructor(url) { 14 | this.url = url; 15 | 16 | this.createUrlLinks(url); 17 | 18 | this.url.watch((params, diff, options) => { 19 | this.onUrlChange(params, diff, options); 20 | }); 21 | } 22 | 23 | onUrlChange(params, diff, options) { 24 | proxy('onUrlChange'); 25 | } 26 | 27 | createUrlLinks(url) { 28 | proxy('createUrlLinks'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/angular/index.js: -------------------------------------------------------------------------------- 1 | import isString from 'lodash/isString'; 2 | import isFunction from 'lodash/isFunction'; 3 | 4 | import Injector from './services/injector'; 5 | import Logger from '../utils/logger'; 6 | 7 | import componentTranslator from './translators/component'; 8 | import controllerTranslator from './translators/controller'; 9 | import routeTranslator from './translators/route'; 10 | 11 | import AngularComponent from './angular-component'; 12 | import AngularController from './angular-controller'; 13 | import AngularRoute from './angular-route'; 14 | 15 | import AngularUrl from './angular-url'; 16 | import AngularUrlManager from './angular-url-manager'; 17 | 18 | let _app = Symbol('angular-module'); 19 | let _urlManager = Symbol('url-manager'); 20 | 21 | let _routeModels = new Map(); 22 | 23 | export default class Angular { 24 | component = AngularComponent; 25 | controller = AngularController; 26 | route = AngularRoute; 27 | 28 | state = 'prepare'; 29 | 30 | translate = { 31 | component: (component, config) => { 32 | let translated = componentTranslator(component, config); 33 | let application = translated.module || this[_app]; 34 | 35 | if (this.state == 'started') { 36 | console.log('compile diretive', translated.name); 37 | this.compileProvider.directive( 38 | translated.name, 39 | () => translated.configuration 40 | ); 41 | } else { 42 | angular 43 | .module(application) 44 | .directive(translated.name, () => translated.configuration); 45 | } 46 | }, 47 | 48 | controller: (controller, config) => { 49 | let translated = controllerTranslator(controller, config); 50 | let application = translated.module || this[_app]; 51 | 52 | angular 53 | .module(application) 54 | .controller(translated.name, translated.configuration); 55 | }, 56 | 57 | route: (route, config) => { 58 | let name = route.getName(); 59 | let translated = routeTranslator(route, config); 60 | 61 | let urlManager = this[_urlManager]; 62 | urlManager.set(name, translated.url); 63 | 64 | let application = translated.module || this[_app]; 65 | 66 | _routeModels.set(name, route); 67 | 68 | angular.module(application).config([ 69 | '$routeProvider', 70 | $routeProvider => { 71 | for (let url of translated.routes) { 72 | $routeProvider.when(url, translated.configuration); 73 | } 74 | }, 75 | ]); 76 | }, 77 | }; 78 | 79 | constructor(app, options = {}) { 80 | if (!app || !isString(app)) { 81 | throw new Error('Angular module should be a string'); 82 | } 83 | 84 | this[_app] = app; 85 | this[_urlManager] = new AngularUrlManager(); 86 | 87 | if (options.dependencies) { 88 | angular.module(app, options.dependencies); 89 | } 90 | } 91 | 92 | getAngularModule() { 93 | let app = this[_app]; 94 | return angular.module(app); 95 | } 96 | 97 | getUrlManager() { 98 | return this[_urlManager]; 99 | } 100 | 101 | bootstrap(config) { 102 | this.state = 'bootstrap'; 103 | 104 | let module = this[_app]; 105 | 106 | // initialize exception handler 107 | let exceptionHandler = config.exception.getHandler(); 108 | if (isFunction(exceptionHandler)) { 109 | angular.module(module).config([ 110 | '$provide', 111 | $provide => { 112 | $provide.decorator('$exceptionHandler', $delegate => { 113 | return (exception, cause) => { 114 | exceptionHandler(exception, cause, $delegate); 115 | }; 116 | }); 117 | }, 118 | ]); 119 | } 120 | 121 | let app = this.getAngularModule(); 122 | 123 | /** 124 | * NOTE: used for controllers if there is no global or local resolver. 125 | * If remove this factory will be exception 126 | * "Unknown provider: valent.resolveProvider <- valent.resolve <- root" 127 | */ 128 | app.factory('valent.resolve', () => ({})); 129 | 130 | /** 131 | * TODO: validation otherwise 132 | */ 133 | let otherwise = config.get('routing.otherwise'); 134 | let redirectTo = null; 135 | let otherwiseConfig = null; 136 | if (otherwise) { 137 | if (otherwise.length == 1) { 138 | redirectTo = otherwise[0]; 139 | } else { 140 | // otherwise route 141 | // TODO: rework getting of renderMethod 142 | let renderMethod = otherwise[0].render; 143 | 144 | if (isFunction(renderMethod)) { 145 | otherwise[1].template = renderMethod; 146 | } 147 | 148 | let otherwiseRoute = new this.route( 149 | 'valent.otherwise', 150 | null, 151 | otherwise[1] 152 | ); 153 | let translatedRoute = routeTranslator(otherwiseRoute, config); 154 | 155 | otherwiseConfig = translatedRoute.configuration; 156 | _routeModels.set('valent.otherwise', otherwiseRoute); 157 | 158 | // otherwise controller 159 | let otherwiseController = new this.controller( 160 | 'valent.otherwise', 161 | otherwise[0] 162 | ); 163 | otherwiseController.otherwise = true; 164 | this.translate.controller(otherwiseController, config); 165 | } 166 | } 167 | 168 | app.config([ 169 | '$locationProvider', 170 | '$routeProvider', 171 | '$compileProvider', 172 | ($locationProvider, $routeProvider, $compileProvider) => { 173 | this.compileProvider = $compileProvider; 174 | this.state = 'config'; 175 | 176 | $locationProvider.html5Mode({ 177 | enabled: config.get('routing.html5Mode'), 178 | requireBase: config.get('routing.requireBase'), 179 | }); 180 | 181 | if (redirectTo) { 182 | $routeProvider.otherwise({ 183 | redirectTo, 184 | }); 185 | } else if (otherwiseConfig) { 186 | $routeProvider.otherwise(otherwiseConfig); 187 | } 188 | }, 189 | ]); 190 | 191 | app.run([ 192 | '$injector', 193 | '$rootScope', 194 | '$location', 195 | ($injector, $rootScope, $location) => { 196 | this.state = 'run'; 197 | 198 | // initialize injector 199 | Injector.setInjector($injector); 200 | 201 | // initialize route events 202 | let hooks = config.route.getHooks(); 203 | 204 | if (isFunction(hooks.error)) { 205 | $rootScope.$on( 206 | '$routeChangeError', 207 | (event, current, previous, rejection) => { 208 | let hasRoute = current.hasOwnProperty('$$route'); 209 | 210 | let currentRouteName = 'valent.otherwise'; 211 | if (hasRoute) { 212 | currentRouteName = current.$$route.controller; 213 | } 214 | 215 | let previousRouteName = previous && previous.$$route 216 | ? previous.$$route.controller 217 | : null; 218 | 219 | let currentRouteModel = _routeModels.get(currentRouteName); 220 | let previousRouteModel = null; 221 | 222 | if (previousRouteName) { 223 | previousRouteModel = _routeModels.get(previousRouteName); 224 | } 225 | 226 | hooks.error( 227 | currentRouteModel, 228 | previousRouteModel, 229 | rejection, 230 | () => { 231 | event.preventDefault(); 232 | } 233 | ); 234 | } 235 | ); 236 | } 237 | 238 | if (isFunction(hooks.success)) { 239 | $rootScope.$on('$routeChangeSuccess', (event, current, previous) => { 240 | let hasRoute = current.hasOwnProperty('$$route'); 241 | 242 | let currentRouteName = 'valent.otherwise'; 243 | if (hasRoute) { 244 | currentRouteName = current.$$route.controller; 245 | } 246 | 247 | let previousRouteName = previous && previous.$$route 248 | ? previous.$$route.controller 249 | : null; 250 | 251 | let currentRouteModel = _routeModels.get(currentRouteName); 252 | let previousRouteModel = null; 253 | 254 | if (previousRouteName) { 255 | previousRouteModel = _routeModels.get(previousRouteName); 256 | } 257 | 258 | hooks.success(currentRouteModel, previousRouteModel, () => { 259 | event.preventDefault(); 260 | }); 261 | }); 262 | } 263 | 264 | $rootScope.$on('$routeChangeStart', (event, current, previous) => { 265 | Logger.resetColors(); 266 | 267 | let hasRoute = current.hasOwnProperty('$$route'); 268 | let currentRouteName = 'valent.otherwise'; 269 | if (hasRoute) { 270 | currentRouteName = current.$$route.controller; 271 | } 272 | 273 | if (isFunction(hooks.start)) { 274 | let previousRouteName = previous && previous.$$route 275 | ? previous.$$route.controller 276 | : null; 277 | 278 | let currentRouteModel = _routeModels.get(currentRouteName); 279 | let previousRouteModel = null; 280 | 281 | if (previousRouteName) { 282 | previousRouteModel = _routeModels.get(previousRouteName); 283 | } 284 | 285 | hooks.start(currentRouteModel, previousRouteModel, () => { 286 | event.preventDefault(); 287 | }); 288 | } 289 | }); 290 | 291 | this.state = 'started'; 292 | }, 293 | ]); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/angular/services/compiler.js: -------------------------------------------------------------------------------- 1 | import Injector from './injector'; 2 | import Scope from './scope'; 3 | 4 | //let _scope = Symbol('$scope'); 5 | 6 | let compileTemplate = function($scope, template, params) { 7 | let $compile = Injector.get('$compile'); 8 | let compileTemplate = $compile(template); 9 | 10 | let compilingScope = $scope.$new(); 11 | Object.assign(compilingScope, params); 12 | 13 | return compileTemplate(compilingScope); 14 | }; 15 | 16 | let Compiler = function($scope) { 17 | let compile = function(template, params = {}) { 18 | return compileTemplate($scope, template, params); 19 | }; 20 | 21 | compile.parent = function(template, params = {}) { 22 | return compileTemplate($scope.$parent, template, params); 23 | }; 24 | 25 | return compile; 26 | }; 27 | 28 | Compiler.create = function(context) { 29 | return Scope.get(context).then($scope => { 30 | return Compiler($scope); 31 | }); 32 | }; 33 | 34 | export default Compiler; 35 | -------------------------------------------------------------------------------- /src/angular/services/digest.js: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce'; 2 | 3 | import Scope from './scope'; 4 | import Injector from './injector'; 5 | 6 | function safe(scope, fn) { 7 | if (scope.$root === null) { 8 | return; 9 | } 10 | 11 | let phase = scope.$root.$$phase; 12 | if (phase == '$apply' || phase == '$digest') { 13 | if (angular.isFunction(fn)) { 14 | fn(); 15 | } 16 | } else { 17 | scope.$apply(fn); 18 | } 19 | } 20 | 21 | let digest = (context, fn) => { 22 | if (context) { 23 | if (Scope.has(context)) { 24 | Scope.get(context).then(scope => { 25 | safe(scope, fn); 26 | }); 27 | } 28 | } else { 29 | let scope = Injector.get('$rootScope'); 30 | safe(scope, fn); 31 | } 32 | }; 33 | 34 | const DEBOUNCE_TIMEOUT = 50; 35 | 36 | const DEBOUNCE_CONFIG = { 37 | leading: false, 38 | trailing: true, 39 | }; 40 | 41 | let timeout = valent.config.get('angular.digest.timeout', DEBOUNCE_TIMEOUT); 42 | let debounced = debounce(digest, timeout, DEBOUNCE_CONFIG); 43 | 44 | debounced.configure = timeout => { 45 | return debounce(digest, timeout, DEBOUNCE_CONFIG); 46 | }; 47 | 48 | export default debounced; 49 | -------------------------------------------------------------------------------- /src/angular/services/directive-params.js: -------------------------------------------------------------------------------- 1 | import isObject from 'lodash/isPlainObject'; 2 | import camelCase from 'lodash/camelCase'; 3 | 4 | import Watcher from './watcher'; 5 | 6 | let getAvailableParams = componentModel => { 7 | let bindings = componentModel.getBindings(); 8 | let keys = []; 9 | 10 | if (isObject(bindings)) { 11 | keys = Object.keys(bindings); 12 | } 13 | 14 | if (componentModel.isAttributeComponent()) { 15 | let name = componentModel.getName(); 16 | keys.push(name); 17 | } 18 | 19 | if (componentModel.hasPipes()) { 20 | let pipes = componentModel.getPipes(); 21 | for (let key of Object.keys(pipes)) { 22 | let translatedKey = camelCase(key); 23 | keys.push(translatedKey); 24 | } 25 | } 26 | 27 | if (componentModel.getOptions()) { 28 | let options = componentModel.getOptions(); 29 | for (let key of Object.keys(options)) { 30 | let translatedKey = camelCase(key); 31 | keys.push(translatedKey); 32 | } 33 | } 34 | 35 | return keys; 36 | }; 37 | 38 | const processPipes = (componentModel, attrs, parse) => { 39 | let normalizedPipes = {}; 40 | 41 | if (componentModel.hasPipes()) { 42 | let pipes = componentModel.getPipes(); 43 | 44 | for (let key of Object.keys(pipes)) { 45 | let Pipe = pipes[key]; 46 | 47 | // parse pipe from attributes 48 | let value = parse(key); 49 | 50 | if (!attrs.hasOwnProperty(key)) { 51 | // if pipe's key is not exists in attributes - create it 52 | value = new Pipe(); 53 | } else { 54 | // if pipe's key is exists in attributes - use it 55 | if (!(value instanceof Pipe)) { 56 | throw new Error( 57 | `"${this[_name]}" - directive pipe "${key}" has wrong class` 58 | ); 59 | } 60 | } 61 | 62 | normalizedPipes[key] = value; 63 | } 64 | } 65 | 66 | return normalizedPipes; 67 | }; 68 | 69 | const processOptions = (componentModel, parse) => { 70 | let normalizedOptions = {}; 71 | 72 | if (componentModel.hasOptions()) { 73 | let options = componentModel.getOptions(); 74 | 75 | for (let key of Object.keys(options)) { 76 | let optionInstance = parse(key); 77 | 78 | if (optionInstance) { 79 | let OptionClass = options[key]; 80 | 81 | if (!(optionInstance instanceof OptionClass)) { 82 | throw Error(`options "${key}" has wrong class`); 83 | } 84 | 85 | normalizedOptions[key] = optionInstance; 86 | } else { 87 | normalizedOptions[key] = null; 88 | } 89 | } 90 | } 91 | 92 | return normalizedOptions; 93 | }; 94 | 95 | let _scope = Symbol('$scope'); 96 | let _attrs = Symbol('$attrs'); 97 | let _attrValues = Symbol('attributes-inline-values'); 98 | let _element = Symbol('$element'); 99 | 100 | let _isIsolated = Symbol('is-scope-isolated'); 101 | let _definitions = Symbol('definitions'); 102 | let _watcher = Symbol('$watcher'); 103 | let _name = Symbol('name'); 104 | let _pipes = Symbol('pipes'); 105 | let _options = Symbol('options'); 106 | 107 | export default class DirectiveParams { 108 | constructor($scope, $attrs, $element, componentModel) { 109 | this[_scope] = $scope; 110 | this[_element] = $element; 111 | 112 | this[_name] = componentModel.getName(); 113 | this[_isIsolated] = componentModel.isIsolated(); 114 | this[_definitions] = getAvailableParams(componentModel); 115 | this[_watcher] = new Watcher($scope); 116 | 117 | this[_attrs] = $attrs; 118 | this[_attrValues] = {}; 119 | 120 | for (let key of Object.keys(this[_attrs].$attr)) { 121 | this[_attrValues][key] = this[_attrs][key]; 122 | } 123 | 124 | // setup pipes 125 | this[_pipes] = processPipes(componentModel, $attrs, key => { 126 | return this.parse(key); 127 | }); 128 | 129 | // setup options 130 | this[_options] = processOptions(componentModel, key => { 131 | return this.parse(key); 132 | }); 133 | 134 | // initialize getters 135 | for (let key of this[_definitions]) { 136 | Object.defineProperty(this, key, { 137 | set: () => { 138 | throw new Error('Can not set directive params property'); 139 | }, 140 | get: () => { 141 | return this.get(key); 142 | }, 143 | }); 144 | } 145 | } 146 | 147 | getElement() { 148 | return this[_element]; 149 | } 150 | 151 | getAttributes() { 152 | return this[_attrValues]; 153 | } 154 | 155 | isAvailable(key) { 156 | return this[_definitions].indexOf(key) != -1; 157 | } 158 | 159 | get(key) { 160 | if (!this.isAvailable(key)) { 161 | throw new Error( 162 | `"${this[ 163 | _name 164 | ]}" - directive param "${key}" is not defined at directive config` 165 | ); 166 | } 167 | 168 | let value = null; 169 | 170 | if (this[_options].hasOwnProperty(key)) { 171 | value = this[_options][key]; 172 | } else if (this[_pipes].hasOwnProperty(key)) { 173 | value = this[_pipes][key]; 174 | } else { 175 | value = this[_scope][key]; 176 | } 177 | 178 | return value; 179 | } 180 | 181 | watch(key, cb) { 182 | if (!this.isAvailable(key)) { 183 | throw new Error( 184 | `"${this[ 185 | _name 186 | ]}" - can not initialize watcher for "${key}" because this params is not defined at directive config` 187 | ); 188 | } 189 | 190 | return this[_watcher].watch(key, cb); 191 | } 192 | 193 | attr(key) { 194 | return this[_attrValues][key]; 195 | } 196 | 197 | hasAttr(key) { 198 | return this[_attrValues].hasOwnProperty(key); 199 | } 200 | 201 | parse(key) { 202 | let $scope = this[_scope]; 203 | let $attrs = this[_attrs]; 204 | 205 | let parsed = undefined; 206 | 207 | // parse - means that we parse attribute value form parent scope 208 | if ($attrs.hasOwnProperty(key)) { 209 | let expression = $attrs[key]; 210 | let scopeToParse = this[_isIsolated] ? $scope.$parent : $scope; 211 | 212 | parsed = scopeToParse.$eval(expression); 213 | } else { 214 | //throw new Error(`"${this[_name]}" - can not parse "${key}" because this params is not passed to attributes`); 215 | } 216 | 217 | return parsed; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/angular/services/events.js: -------------------------------------------------------------------------------- 1 | import Scope from './scope'; 2 | 3 | let _context = Symbol('context'); 4 | 5 | export default class Events { 6 | constructor(controller) { 7 | this[_context] = controller; 8 | } 9 | 10 | on(event, fn) { 11 | let context = this[_context]; 12 | 13 | let off = null; 14 | 15 | Scope.get(context).then($scope => { 16 | if (!off) { 17 | off = $scope.$on(event, fn); 18 | } 19 | }); 20 | 21 | return () => { 22 | if (off) { 23 | off(); 24 | } else { 25 | off = true; 26 | } 27 | }; 28 | } 29 | 30 | broadcast(event, args) { 31 | let context = this[_context]; 32 | 33 | Scope.get(context).then($scope => $scope.$broadcast(event, args)); 34 | } 35 | 36 | emit(event, args) { 37 | let context = this[_context]; 38 | 39 | Scope.get(context).then($scope => $scope.$emit(event, args)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/angular/services/injector.js: -------------------------------------------------------------------------------- 1 | class Injector { 2 | setInjector($injector) { 3 | if (this.$injector) { 4 | throw new Error('Injector component: $injector already exists'); 5 | } 6 | 7 | this.$injector = $injector; 8 | } 9 | 10 | get(dependency) { 11 | let $injector = this.$injector; 12 | 13 | if (!$injector) { 14 | throw new Error('Injector is not defined'); 15 | } 16 | 17 | if (!$injector.has(dependency)) { 18 | throw new Error(`Dependency "${dependency}" does not exist`); 19 | } 20 | 21 | return $injector.get(dependency); 22 | } 23 | } 24 | 25 | export default new Injector(); 26 | -------------------------------------------------------------------------------- /src/angular/services/scope.js: -------------------------------------------------------------------------------- 1 | let scopes = new WeakMap(); 2 | let queue = new WeakMap(); 3 | 4 | export default class Scope { 5 | static attach(context, scope) { 6 | scopes.set(context, scope); 7 | 8 | if (queue.has(context)) { 9 | let resolve = queue.get(context); 10 | resolve(scope); 11 | queue.delete(context); 12 | } 13 | } 14 | 15 | static has(context) { 16 | return scopes.has(context); 17 | } 18 | 19 | static get(context) { 20 | return new Promise((resolve, reject) => { 21 | if (scopes.has(context)) { 22 | let scope = scopes.get(context); 23 | resolve(scope); 24 | } else { 25 | // TODO: fix for case if scope is not yet created but already deleted! 26 | queue.set(context, resolve); 27 | } 28 | }); 29 | } 30 | 31 | static delete(context) { 32 | scopes.delete(context); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/angular/services/watcher.js: -------------------------------------------------------------------------------- 1 | import Scope from './scope'; 2 | import Injector from './injector'; 3 | 4 | let _scope = Symbol('$scope'); 5 | let _queue = Symbol('queue'); 6 | 7 | /** 8 | * https://github.com/angular/angular.js/blob/f7b999703f4f3bdaea035ce692f1a656b0c1a933/src/Angular.js#L632 9 | * @param angularScope 10 | * @returns {*} 11 | */ 12 | function isValidScope(angularScope) { 13 | return angularScope && angularScope.$evalAsync && angularScope.$watch; 14 | } 15 | 16 | export default class Watcher { 17 | constructor(context) { 18 | this[_queue] = new Set(); 19 | 20 | if (context) { 21 | // TODO: remove context dep. Work only with valid scopes 22 | if (isValidScope(context)) { 23 | this[_scope] = context; 24 | } else { 25 | Scope.get(context).then($scope => { 26 | this[_scope] = $scope; 27 | 28 | for (let task of this[_queue]) { 29 | task.off = this.watch(...task.arguments); 30 | } 31 | }); 32 | } 33 | } else { 34 | this[_scope] = Injector.get('$rootScope'); 35 | } 36 | } 37 | 38 | watch() { 39 | let off = null; 40 | 41 | if (this[_scope]) { 42 | let $scope = this[_scope]; 43 | off = $scope.$watch.apply($scope, arguments); 44 | } else { 45 | let task = { 46 | arguments: Array.prototype.slice.call(arguments), 47 | }; 48 | 49 | this[_queue].add(task); 50 | 51 | off = () => { 52 | if (this[_scope]) { 53 | task.off(); 54 | } else { 55 | this[_queue].delete(task); 56 | } 57 | }; 58 | } 59 | 60 | return off; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/angular/translators/component.js: -------------------------------------------------------------------------------- 1 | import isFunction from 'lodash/isFunction'; 2 | import isArray from 'lodash/isArray'; 3 | 4 | import Logger from '../../utils/logger'; 5 | import Compiler from '../services/compiler'; 6 | 7 | import DirectiveParams from '../services/directive-params'; 8 | import Scope from '../services/scope'; 9 | 10 | let getValentInfo = componentModel => { 11 | return { 12 | type: 'component', 13 | name: componentModel.getName(), 14 | namespace: componentModel.getNamespace(), 15 | }; 16 | }; 17 | 18 | let translateRestrict = componentModel => { 19 | let restrict = componentModel.getRestrict(); 20 | 21 | if (!restrict) { 22 | // TODO: check if we need to compile directive with restrict A 23 | if ( 24 | componentModel.withoutTemplate() && 25 | !componentModel.hasCompileMethod() 26 | ) { 27 | restrict = 'A'; 28 | } else { 29 | restrict = 'E'; 30 | } 31 | } 32 | 33 | return restrict; 34 | }; 35 | 36 | let translateParams = componentModel => { 37 | let bindings = componentModel.getBindings(); 38 | let angularScope = null; 39 | 40 | if (componentModel.isIsolated()) { 41 | angularScope = Object.assign({}, bindings); 42 | } else { 43 | angularScope = false; 44 | } 45 | 46 | return angularScope; 47 | }; 48 | 49 | let getInterfaces = (directiveParams, componentModel) => { 50 | let instances = []; 51 | let interfaces = componentModel.getInterfaces(); 52 | 53 | for (let key of Object.keys(interfaces)) { 54 | let instance = directiveParams.parse(key); 55 | 56 | if (!instance) { 57 | throw Error(`directive should implements interface "${key}"`); 58 | } 59 | 60 | let InterfaceClass = interfaces[key]; 61 | 62 | if (!(instance instanceof InterfaceClass)) { 63 | throw Error(`interface "${key}" has wrong class`); 64 | } 65 | 66 | instances.push(instance); 67 | } 68 | 69 | return instances; 70 | }; 71 | 72 | let getRequiredControllers = (componentModel, require) => { 73 | let controllers = {}; 74 | let configure = componentModel.getRequire(); 75 | let index = 0; 76 | 77 | for (let required of require) { 78 | if (required) { 79 | let api = null; 80 | 81 | let key = configure[index]; 82 | let normalized = key.replace(/[\?\^]*/, ''); 83 | 84 | if (required.hasOwnProperty('$valent')) { 85 | let namespace = required.$valent.namespace; 86 | api = required[namespace]; 87 | 88 | if (!api) { 89 | throw new Error(`"${normalized}" component has no api`); 90 | } 91 | } else { 92 | // means that required component - is not valent component 93 | api = required; 94 | } 95 | 96 | controllers[normalized] = api; 97 | } 98 | 99 | index++; 100 | } 101 | 102 | return controllers; 103 | }; 104 | 105 | export default componentModel => { 106 | let module = componentModel.getModule(); 107 | 108 | let link = (compileResult, $scope, $element, $attrs, require) => { 109 | let directiveName = componentModel.getDirectiveName(); 110 | let controller = $scope.$valent[directiveName].controller; 111 | 112 | if (controller.link) { 113 | let attributes = {}; 114 | for (let key of Object.keys($attrs.$attr)) { 115 | attributes[key] = $attrs[key]; 116 | } 117 | 118 | let args = [$element, attributes]; 119 | 120 | if (isArray(require)) { 121 | let requiredControllers = getRequiredControllers( 122 | componentModel, 123 | require 124 | ); 125 | args.push(requiredControllers); 126 | } 127 | 128 | args.push({ 129 | template: Compiler($scope), 130 | result: compileResult, 131 | }); 132 | 133 | controller.link(...args); 134 | } 135 | }; 136 | 137 | let configuration = { 138 | replace: false, 139 | transclude: componentModel.getTransclude(), 140 | restrict: translateRestrict(componentModel), 141 | scope: translateParams(componentModel), 142 | require: componentModel.getRequire(), 143 | controller: [ 144 | '$scope', 145 | '$attrs', 146 | '$element', 147 | function($scope, $attrs, $element) { 148 | let instances = []; 149 | 150 | let directiveParams = new DirectiveParams( 151 | $scope, 152 | $attrs, 153 | $element, 154 | componentModel 155 | ); 156 | 157 | if (componentModel.hasInterfaces()) { 158 | let interfaces = getInterfaces(directiveParams, componentModel); 159 | instances = instances.concat(interfaces); 160 | } 161 | 162 | let Controller = componentModel.getController(); 163 | let name = componentModel.getName(); 164 | 165 | let logger = Logger.create(name); 166 | 167 | // controller - closed variable 168 | let controller = new Controller(...instances, directiveParams, logger); 169 | Scope.attach(controller, $scope); 170 | 171 | let valentInfo = getValentInfo(componentModel); 172 | valentInfo.controller = controller; 173 | let namespace = valentInfo.namespace; 174 | 175 | if (!$scope.hasOwnProperty('$valent')) { 176 | $scope.$valent = {}; 177 | } 178 | 179 | let directiveName = componentModel.getDirectiveName(); 180 | $scope.$valent[directiveName] = valentInfo; 181 | 182 | // used for requiring 183 | this[namespace] = controller.api; 184 | this.$valent = valentInfo; 185 | 186 | if (componentModel.isIsolated()) { 187 | $scope[namespace] = controller; 188 | } else { 189 | //console.log(`component "${name}" - is not isolated. Its controller will not be attached to scope (added in rc4)`); 190 | } 191 | 192 | // $scope events 193 | $scope.$on('$destroy', () => { 194 | if (isFunction(controller.destructor)) { 195 | controller.destructor(); 196 | } 197 | }); 198 | }, 199 | ], 200 | 201 | link: ($scope, element, attrs, require) => { 202 | link(null, $scope, element, attrs, require); 203 | }, 204 | }; 205 | 206 | let Controller = componentModel.getController(); 207 | 208 | if (isFunction(Controller.compile)) { 209 | configuration.compile = (element, attrs) => { 210 | let params = Controller.compile(element, attrs); 211 | 212 | return ($scope, element, attrs, require) => { 213 | link(params, $scope, element, attrs, require); 214 | }; 215 | }; 216 | } 217 | 218 | if (componentModel.hasTemplate()) { 219 | configuration.template = componentModel.getTemplate(); 220 | } else if (componentModel.hasTemplateUrl()) { 221 | configuration.templateUrl = componentModel.getTemplateUrl(); 222 | } 223 | 224 | return { 225 | name: componentModel.getDirectiveName(), 226 | module, 227 | configuration, 228 | }; 229 | }; 230 | -------------------------------------------------------------------------------- /src/angular/translators/controller.js: -------------------------------------------------------------------------------- 1 | import isFunction from 'lodash/isFunction'; 2 | 3 | import Logger from '../../utils/logger'; 4 | import Scope from '../services/scope'; 5 | 6 | let getValentInfo = controllerModel => { 7 | return { 8 | type: 'controller', 9 | name: controllerModel.getName(), 10 | namespace: controllerModel.getNamespace(), 11 | }; 12 | }; 13 | 14 | export default (controllerModel, config) => { 15 | let name = controllerModel.getName(); 16 | let module = controllerModel.getModule(); 17 | 18 | let configuration = [ 19 | '$scope', 20 | 'valent.resolve', 21 | ($scope, valentResolve) => { 22 | let Controller = controllerModel.getController(); 23 | 24 | let name = controllerModel.getName(); 25 | let logger = Logger.create(name); 26 | 27 | let args = []; 28 | 29 | if (valent.url.has(name)) { 30 | args.push(valentResolve); 31 | 32 | // attach scope to AngularUrl to allow use watch() method 33 | let url = valent.url.get(name); 34 | url.attachScope($scope); 35 | 36 | args.push(url); 37 | } else if (controllerModel.otherwise) { 38 | args.push(valentResolve); 39 | } 40 | 41 | args.push(logger); 42 | 43 | let controller = new Controller(...args); 44 | Scope.attach(controller, $scope); 45 | 46 | let namespace = controllerModel.getNamespace(); 47 | $scope[namespace] = controller; 48 | 49 | // $scope events 50 | $scope.$on('$destroy', () => { 51 | if (isFunction(controller.destructor)) { 52 | controller.destructor(); 53 | } 54 | }); 55 | 56 | // attach $scope to url. needs for url.watch and get url by context. useful at parent classes 57 | valent.url.attach(name, controller, $scope); 58 | 59 | $scope.$on('$destroy', () => { 60 | valent.url.detach($scope); 61 | }); 62 | 63 | if (!$scope.hasOwnProperty('$valent')) { 64 | $scope.$valent = {}; 65 | } 66 | 67 | let controllerName = `controller.${name}`; 68 | $scope.$valent[controllerName] = getValentInfo(controllerModel); 69 | }, 70 | ]; 71 | 72 | return { 73 | name, 74 | module, 75 | configuration, 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /src/angular/translators/route.js: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash/isArray'; 2 | import isFunction from 'lodash/isFunction'; 3 | 4 | import AngularUrl from '../angular-url'; 5 | 6 | let resolve = (resolvers, args = {}) => { 7 | let dependencies = Object.keys(resolvers); 8 | 9 | let tasks = []; 10 | for (let key of Object.keys(resolvers)) { 11 | let resolver = resolvers[key]; 12 | 13 | let task = resolver(...args); 14 | 15 | tasks.push(task); 16 | } 17 | 18 | return Promise.all(tasks).then(resolved => { 19 | let results = {}; 20 | let index = 0; 21 | 22 | for (let resolverResult of resolved) { 23 | let key = dependencies[index]; 24 | results[key] = resolverResult; 25 | 26 | index++; 27 | } 28 | 29 | return results; 30 | }); 31 | }; 32 | 33 | let getValentResolver = (config, routeModel) => ({ 34 | 'valent.resolve': () => { 35 | let globalResolvers = config.route.getResolvers(); 36 | 37 | let name = routeModel.getName(); 38 | let params = routeModel.getParams(); 39 | 40 | let resolverArguments = [name, params]; 41 | 42 | let result = {}; 43 | 44 | if (config.route.hasResolvers()) { 45 | result = resolve( 46 | globalResolvers, 47 | resolverArguments 48 | ).then(globalResult => { 49 | let resolveResult = null; 50 | 51 | if (routeModel.hasResolvers()) { 52 | let localResolvers = routeModel.getResolvers(); 53 | 54 | resolveResult = resolve( 55 | localResolvers, 56 | resolverArguments 57 | ).then(localResult => { 58 | return Object.assign({}, globalResult, localResult); 59 | }); 60 | } else { 61 | resolveResult = globalResult; 62 | } 63 | 64 | return resolveResult; 65 | }); 66 | } else if (routeModel.hasResolvers()) { 67 | let localResolvers = routeModel.getResolvers(); 68 | result = resolve(localResolvers, resolverArguments); 69 | } 70 | 71 | return result; 72 | }, 73 | }); 74 | 75 | export default (routeModel, config) => { 76 | let name = routeModel.getName(); 77 | let module = routeModel.getModule(); 78 | 79 | let params = routeModel.getParams(); 80 | let defaultRouteParams = { 81 | reloadOnSearch: false, 82 | }; 83 | 84 | let configuration = Object.assign(defaultRouteParams, params, { 85 | controller: name, 86 | }); 87 | 88 | let globalResolvers = config.route.getResolvers(); 89 | 90 | if (!!Object.keys(globalResolvers).length || routeModel.hasResolvers()) { 91 | configuration.resolve = getValentResolver(config, routeModel); 92 | } 93 | 94 | if (routeModel.hasTemplate()) { 95 | // set template 96 | let template = routeModel.getTemplate(); 97 | if (isFunction(template)) { 98 | configuration.template = template.bind(routeModel); 99 | } else { 100 | configuration.template = template; 101 | } 102 | } else if (routeModel.hasTemplateUrl()) { 103 | // set templateUrl 104 | configuration.templateUrl = routeModel.getTemplateUrl(); 105 | } 106 | 107 | let routes = null; 108 | let url = null; 109 | 110 | if (!routeModel.isOtherwise()) { 111 | routes = routeModel.getUrl(); 112 | if (!isArray(routes)) { 113 | routes = [routes]; 114 | } 115 | 116 | // create URL 117 | let structure = routeModel.getStructure(); 118 | let pattern = routes[0]; 119 | 120 | url = () => { 121 | // ? 122 | return new AngularUrl(pattern, structure); 123 | }; 124 | } 125 | 126 | return { 127 | name, 128 | module, 129 | routes, 130 | url, 131 | configuration, 132 | }; 133 | }; 134 | -------------------------------------------------------------------------------- /src/angular/validation/structures.js: -------------------------------------------------------------------------------- 1 | import t from 'tcomb-validation'; 2 | 3 | var validate = t.validate; 4 | 5 | /** 6 | * true/false 7 | * @param transclude 8 | */ 9 | export const isValidTransclude = transclude => 10 | validate( 11 | transclude, 12 | t.maybe(t.union([t.Bool, t.dict(t.Str, t.Str)])) 13 | ).isValid(); 14 | 15 | /** 16 | * String or array of strings 17 | * @param req 18 | */ 19 | export const isValidRequire = req => 20 | validate(req, t.maybe(t.union([t.list(t.Str), t.Str]))).isValid(); 21 | 22 | export const isValidModule = module => 23 | validate(module, t.maybe(t.Str)).isValid(); 24 | 25 | export const isValidNamespace = namespace => 26 | validate(namespace, t.maybe(t.Str)).isValid(); 27 | -------------------------------------------------------------------------------- /src/application-config.js: -------------------------------------------------------------------------------- 1 | import getter from 'lodash/get'; 2 | import setter from 'lodash/set'; 3 | 4 | let _config = Symbol('config'); 5 | 6 | export default class ApplicationConfig { 7 | route = { 8 | onChangeStart: handler => { 9 | this.set('routing.hooks.start', handler); 10 | }, 11 | 12 | onChangeSuccess: handler => { 13 | this.set('routing.hooks.success', handler); 14 | }, 15 | 16 | onChangeError: handler => { 17 | this.set('routing.hooks.error', handler); 18 | }, 19 | 20 | getHooks: () => { 21 | return this.get('routing.hooks', {}); 22 | }, 23 | 24 | otherwise: (...otherwise) => { 25 | this.set('routing.otherwise', otherwise); 26 | }, 27 | 28 | addResolver: (key, resolver) => { 29 | this.set(`routing.resolvers.${key}`, resolver); 30 | }, 31 | 32 | getResolvers: () => { 33 | return this.get('routing.resolvers', {}); 34 | }, 35 | 36 | hasResolvers: () => { 37 | return !!this.get('routing.resolvers'); 38 | }, 39 | 40 | requireBase: requireBase => { 41 | this.set('routing.requireBase', requireBase); 42 | }, 43 | 44 | enableHistoryApi: () => { 45 | this.set('routing.html5Mode', true); 46 | }, 47 | 48 | disableHistoryApi: () => { 49 | this.set('routing.html5Mode', false); 50 | }, 51 | }; 52 | 53 | exception = { 54 | handler: handler => { 55 | this.set('exception.handler', handler); 56 | }, 57 | 58 | getHandler: () => { 59 | return this.get('exception.handler'); 60 | }, 61 | }; 62 | 63 | constructor(config) { 64 | this[_config] = Object.assign(config, { 65 | routing: { 66 | html5Mode: true, 67 | }, 68 | }); 69 | } 70 | 71 | get(key, defaultValue = null) { 72 | return getter(this[_config], key, defaultValue); 73 | } 74 | 75 | set(key, value) { 76 | return setter(this[_config], key, value); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/decorators/component.js: -------------------------------------------------------------------------------- 1 | import { getControllerName } from '../utils/class-name'; 2 | 3 | export default options => target => { 4 | let controllerName = getControllerName(target); 5 | valent.component(controllerName, target, options); 6 | }; 7 | -------------------------------------------------------------------------------- /src/decorators/controller.js: -------------------------------------------------------------------------------- 1 | import { getControllerName } from '../utils/class-name'; 2 | 3 | export default options => target => { 4 | let controllerName = getControllerName(target); 5 | valent.controller(controllerName, target, options); 6 | }; 7 | -------------------------------------------------------------------------------- /src/exceptions/exception.js: -------------------------------------------------------------------------------- 1 | export default class Exception extends Error { 2 | constructor(message) { 3 | super(message); 4 | 5 | this.name = this.constructor.name; 6 | this.message = message; 7 | 8 | //Error.captureStackTrace(this, this.constructor.name) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/exceptions/register.js: -------------------------------------------------------------------------------- 1 | import Exception from './exception'; 2 | 3 | export default class RegisterException extends Exception { 4 | constructor(name, type, errors) { 5 | let message = `Could not register ${type} - "${name}".`; 6 | 7 | for (let error of errors) { 8 | message += '\n - ' + error; 9 | } 10 | 11 | message += '\n'; 12 | 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/exceptions/runtime.js: -------------------------------------------------------------------------------- 1 | import Exception from './exception'; 2 | 3 | export default class RuntimeException extends Exception { 4 | constructor(name, type, error) { 5 | super(`runtime error with "${name}" ${type} - ${error}`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/serializers/coding-serializer.js: -------------------------------------------------------------------------------- 1 | import isString from 'lodash/isString'; 2 | import isArray from 'lodash/isArray'; 3 | import isObject from 'lodash/isObject'; 4 | import isFunction from 'lodash/isFunction'; 5 | 6 | let _alias = {}; 7 | let _rules = {}; 8 | 9 | export default class CodingSerializer { 10 | static addAlias(alias, struct, rules) { 11 | if (!isString(alias)) { 12 | throw new Error('alias should be a String'); 13 | } 14 | 15 | if (!isObject(struct)) { 16 | throw new Error('struct must be an object'); 17 | } 18 | 19 | if ( 20 | rules !== undefined && 21 | (!isObject(rules) || 22 | isArray(rules) || 23 | isFunction(rules) || 24 | !isFunction(rules.encode) || 25 | !isFunction(rules.decode)) 26 | ) { 27 | throw new Error( 28 | 'Serialize rule should implement both @encode and @decode methods' 29 | ); 30 | } 31 | 32 | _alias[alias] = struct; 33 | if (rules) { 34 | _rules[alias] = rules; 35 | } 36 | } 37 | 38 | static getStruct(alias) { 39 | return _alias[alias]; 40 | } 41 | 42 | static getRule(alias) { 43 | return _rules[alias]; 44 | } 45 | 46 | constructor() { 47 | 'use strict'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/serializers/rename-serializer.js: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash/isArray'; 2 | import isString from 'lodash/isString'; 3 | import Serializer from './serializer'; 4 | 5 | export default class RenameSerializer extends Serializer { 6 | constructor(struct) { 7 | let normalizedStruct = {}; 8 | let renameOptions = {}; 9 | 10 | let props = struct; 11 | 12 | for (let key of Object.keys(props)) { 13 | let value = props[key]; 14 | let filedStruct = null; 15 | 16 | if (isArray(value)) { 17 | renameOptions[key] = value[0]; 18 | filedStruct = value[1]; 19 | } else { 20 | renameOptions[key] = key; 21 | filedStruct = value; 22 | } 23 | normalizedStruct[key] = filedStruct; 24 | } 25 | 26 | super(normalizedStruct); 27 | 28 | this.renameOptions = renameOptions; 29 | } 30 | 31 | encode(params) { 32 | let encoded = super.encode(params); 33 | 34 | let normalized = {}; 35 | for (let key of Object.keys(encoded)) { 36 | let value = encoded[key]; 37 | let rename = this.renameOptions[key]; 38 | 39 | if (rename) { 40 | normalized[rename] = value; 41 | } else { 42 | normalized[key] = value; 43 | } 44 | } 45 | 46 | return normalized; 47 | } 48 | 49 | hasRenameOption(key) { 50 | return this.renameOptions[key] !== key; 51 | } 52 | 53 | getOriginalName(renamed) { 54 | if (!isString(renamed)) { 55 | throw new Error('"rename" key must be a string'); 56 | } 57 | 58 | let original = null; 59 | 60 | for (let key of Object.keys(this.renameOptions)) { 61 | if (this.renameOptions[key] === renamed) { 62 | original = key; 63 | break; 64 | } 65 | } 66 | 67 | return original; 68 | } 69 | 70 | decode(params) { 71 | let normalized = {}; 72 | let struct = this.getStruct(); 73 | 74 | for (let key of Object.keys(params)) { 75 | let value = params[key]; 76 | let original = this.getOriginalName(key); 77 | 78 | if (!original) { 79 | // TODO: normalize exceptions 80 | if (!struct.hasOwnProperty(key)) { 81 | throw new Error(`Url param "${key}" is not described at struct`); 82 | } else { 83 | // NOTE: Seems this case is impossible 84 | throw new Error(`Can not find origin name for "${key}"`); 85 | } 86 | } 87 | 88 | if (original) { 89 | normalized[original] = value; 90 | } else { 91 | normalized[key] = value; 92 | } 93 | } 94 | 95 | return super.decode(normalized); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/serializers/serializer.js: -------------------------------------------------------------------------------- 1 | import isFunction from 'lodash/isFunction'; 2 | 3 | let _struct = Symbol('struct'); 4 | let _rules = Symbol('rules'); 5 | 6 | export default class Serializer { 7 | constructor(struct) { 8 | this[_struct] = struct; 9 | this[_rules] = new WeakMap(); 10 | } 11 | 12 | getRules() { 13 | return this[_rules]; 14 | } 15 | 16 | getStruct() { 17 | return this[_struct]; 18 | } 19 | 20 | addRule(namespace, serializer) { 21 | if (!namespace) { 22 | throw new Error('Namespace is required'); 23 | } 24 | 25 | if (!isFunction(serializer.encode) || !isFunction(serializer.decode)) { 26 | throw new Error( 27 | 'Serialize rule should implement both @encode and @decode methods' 28 | ); 29 | } 30 | this[_rules].set(namespace, serializer); 31 | } 32 | 33 | encode(params) { 34 | let struct = this.getStruct(); 35 | 36 | let rules = this.getRules(); 37 | let encodedObject = {}; 38 | 39 | for (let key of Object.keys(struct)) { 40 | if (params.hasOwnProperty(key)) { 41 | let structItem = struct[key]; 42 | 43 | if (false && !rules.has(structItem)) { 44 | throw new Error(`rule for struct with id "${key}" does not exist`); 45 | } 46 | 47 | let value = params[key]; 48 | let isEncodeAllowed = this.isEncodeAllowed(key, value); 49 | 50 | if (isEncodeAllowed) { 51 | if (!structItem.is(value)) { 52 | try { 53 | structItem(value); 54 | } catch (e) { 55 | throw new Error( 56 | `value with id "${key}" has wrong struct. Expected "${ 57 | structItem.displayName 58 | }", but value is "${value}"` 59 | ); 60 | } 61 | } 62 | 63 | let rule = rules.get(structItem); 64 | 65 | encodedObject[key] = rule.encode(value); 66 | } 67 | } 68 | } 69 | 70 | return encodedObject; 71 | } 72 | 73 | decode(params) { 74 | let struct = this.getStruct(); 75 | let rules = this.getRules(); 76 | let decodedObject = {}; 77 | 78 | //let props = struct; 79 | for (let key of Object.keys(struct)) { 80 | if (params.hasOwnProperty(key)) { 81 | let structItem = struct[key]; 82 | if (!rules.has(structItem)) { 83 | throw new Error(`rule for struct with id "${key}" does not exist`); 84 | } 85 | 86 | let value = params[key]; 87 | 88 | let rule = rules.get(structItem); 89 | decodedObject[key] = rule.decode(value); 90 | } 91 | } 92 | 93 | return decodedObject; 94 | } 95 | 96 | // should be overridden in child serializers 97 | isEncodeAllowed(key, value) { 98 | return true; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/serializers/url-serializer.js: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash/isArray'; 2 | import isString from 'lodash/isString'; 3 | 4 | import moment from 'moment'; 5 | 6 | import * as primitives from '../utils/primitives'; 7 | 8 | import RenameSerializer from './rename-serializer'; 9 | 10 | const datesToPeriod = dates => { 11 | return [ 12 | moment.utc(dates[0]).startOf('day').toDate(), 13 | moment.utc(dates[1]).endOf('day').toDate() 14 | ] 15 | }; 16 | 17 | let createDecoders = (options) => { 18 | let decoders = { 19 | // ------- NUMBER ------- 20 | num: raw => (raw === null ? null : parseFloat(raw)), 21 | listNum: raw => 22 | !raw || !raw.length 23 | ? null 24 | : raw.split(options.listDelimiter).map(decoders.num), 25 | matrixNum: raw => 26 | !raw || !raw.length 27 | ? null 28 | : raw.split(options.matrixDelimiter).map(decoders.listNum), 29 | 30 | // ------- STRING ------- 31 | str: raw => (raw === null ? null : '' + raw), 32 | listStr: raw => 33 | !raw || !raw.length 34 | ? null 35 | : raw.split(options.listDelimiter).map(decoders.str), 36 | matrixStr: raw => 37 | !raw || !raw.length 38 | ? null 39 | : raw.split(options.matrixDelimiter).map(decoders.listStr), 40 | 41 | // ------- DATE------- 42 | date: raw => moment.utc(raw, options.dateFormat).toDate(), 43 | listDate: raw => 44 | !raw || !raw.length 45 | ? null 46 | : raw.split(options.listDelimiter).map(decoders.date), 47 | matrixDate: raw => 48 | !raw || !raw.length 49 | ? null 50 | : raw.split(options.matrixDelimiter).map(decoders.listDate), 51 | 52 | // ------- PERIOD------- 53 | period: (raw) => { 54 | if (!raw || !raw.length) 55 | return null; 56 | let dates = raw.split(options.listDelimiter).map(decoders.date); 57 | return datesToPeriod(dates); 58 | }, 59 | comparePeriod: (raw) => { 60 | if (!raw || !raw.length) 61 | return null; 62 | 63 | let dates = raw.split(options.matrixDelimiter).map(decoders.listDate); 64 | return dates.map(datesToPeriod); 65 | }, 66 | 67 | // ------- BOOL------- 68 | bool: raw => raw !== '0', 69 | listBool: raw => 70 | !raw || !raw.length 71 | ? null 72 | : raw.split(options.listDelimiter).map(decoders.bool), 73 | matrixBool: raw => 74 | !raw || !raw.length 75 | ? null 76 | : raw.split(options.matrixDelimiter).map(decoders.listBool), 77 | }; 78 | 79 | return decoders; 80 | }; 81 | 82 | let createEncoders = options => { 83 | let encoders = { 84 | // ------- NUMBER ------- 85 | num: value => { 86 | let encoded = parseFloat(value).toString(10); 87 | return encoded === 'NaN' ? null : encoded; 88 | }, 89 | listNum: value => 90 | !isArray(value) 91 | ? null 92 | : value.map(encoders.num).join(options.listDelimiter), 93 | matrixNum: value => 94 | !isArray(value) 95 | ? null 96 | : value.map(encoders.listNum).join(options.matrixDelimiter), 97 | 98 | // ------- STRING ------- 99 | str: value => (value === null || value === undefined ? null : '' + value), 100 | listStr: value => 101 | !isArray(value) 102 | ? null 103 | : value.map(encoders.str).join(options.listDelimiter), 104 | matrixStr: value => 105 | !isArray(value) 106 | ? null 107 | : value.map(encoders.listStr).join(options.matrixDelimiter), 108 | 109 | // ------- DATE------- 110 | date: value => moment.utc(value).format(options.dateFormat), 111 | listDate: value => 112 | !isArray(value) 113 | ? null 114 | : value.map(encoders.date).join(options.listDelimiter), 115 | matrixDate: value => 116 | !isArray(value) 117 | ? null 118 | : value.map(encoders.listDate).join(options.matrixDelimiter), 119 | 120 | // ------- PERIOD------- 121 | period: (value) => !isArray(value) ? null : value.map(encoders.date).join(options.listDelimiter), 122 | comparePeriod: (value) => !isArray(value) ? null : value.map(encoders.listDate).join(options.matrixDelimiter), 123 | 124 | // ------- BOOL------- 125 | bool: value => (!!value ? '1' : '0'), 126 | listBool: value => 127 | !isArray(value) 128 | ? null 129 | : value.map(encoders.bool).join(options.listDelimiter), 130 | matrixBool: value => 131 | !isArray(value) 132 | ? null 133 | : value.map(encoders.listBool).join(options.matrixDelimiter), 134 | }; 135 | 136 | return encoders; 137 | }; 138 | 139 | let addUrlRules = (addRule, options) => { 140 | let decoders = createDecoders(options); 141 | let encoders = createEncoders(options); 142 | 143 | // ------ NUMBER ----- 144 | let num = { 145 | decode: decoders.num, 146 | encode: encoders.num, 147 | }; 148 | 149 | addRule(primitives.Num, num); 150 | addRule(primitives.MaybeNum, num); 151 | addRule(primitives.ListNum, num); 152 | 153 | addRule(primitives.Int, num); 154 | addRule(primitives.MaybeInt, num); 155 | addRule(primitives.ListInt, num); 156 | 157 | let numList = { 158 | decode: decoders.listNum, 159 | encode: encoders.listNum, 160 | }; 161 | 162 | addRule(primitives.ListNum, numList); 163 | addRule(primitives.MaybeListNum, numList); 164 | addRule(primitives.MaybeListInt, numList); 165 | 166 | let matrixNum = { 167 | encode: encoders.matrixNum, 168 | decode: decoders.matrixNum, 169 | }; 170 | 171 | addRule(primitives.MatrixNum, matrixNum); 172 | addRule(primitives.MatrixMaybeNum, matrixNum); 173 | 174 | // ------ STRING ----- 175 | let str = { 176 | decode: decoders.str, 177 | encode: encoders.str, 178 | }; 179 | 180 | addRule(primitives.Str, str); 181 | addRule(primitives.MaybeStr, str); 182 | 183 | let listStr = { 184 | decode: decoders.listStr, 185 | encode: encoders.listStr, 186 | }; 187 | 188 | addRule(primitives.ListStr, listStr); 189 | addRule(primitives.MaybeListStr, listStr); 190 | 191 | let matrixStr = { 192 | decode: decoders.matrixStr, 193 | encode: encoders.matrixStr, 194 | }; 195 | 196 | addRule(primitives.MatrixStr, matrixStr); 197 | addRule(primitives.MatrixMaybeStr, matrixStr); 198 | 199 | // ------ DATES ----- 200 | let date = { 201 | decode: decoders.date, 202 | encode: encoders.date, 203 | }; 204 | 205 | addRule(primitives.Dat, date); 206 | addRule(primitives.MaybeDat, date); 207 | 208 | let listDate = { 209 | decode: decoders.listDate, 210 | encode: encoders.listDate, 211 | }; 212 | 213 | addRule(primitives.ListDat, listDate); 214 | addRule(primitives.MaybeListDat, listDate); 215 | 216 | let matrixDate = { 217 | decode: decoders.matrixDate, 218 | encode: encoders.matrixDate, 219 | }; 220 | 221 | addRule(primitives.MatrixDate, matrixDate); 222 | addRule(primitives.MatrixMaybeDate, matrixDate); 223 | 224 | 225 | // ------- PERIOD------- 226 | 227 | addRule(primitives.Period, { 228 | decode: decoders.period, 229 | encode: encoders.period 230 | }); 231 | addRule(primitives.ComparePeriod, { 232 | decode: decoders.comparePeriod, 233 | encode: encoders.comparePeriod 234 | }); 235 | 236 | // ------ BOOL----- 237 | let bool = { 238 | decode: decoders.bool, 239 | encode: encoders.bool, 240 | }; 241 | 242 | addRule(primitives.Bool, bool); 243 | addRule(primitives.MaybeBool, bool); 244 | 245 | let listBool = { 246 | decode: decoders.listBool, 247 | encode: encoders.listBool, 248 | }; 249 | 250 | addRule(primitives.ListBool, listBool); 251 | addRule(primitives.MaybeListBool, listBool); 252 | 253 | let matrixBool = { 254 | decode: decoders.matrixBool, 255 | encode: encoders.matrixBool, 256 | }; 257 | 258 | addRule(primitives.MatrixBool, matrixBool); 259 | addRule(primitives.MatrixMaybeBool, matrixBool); 260 | }; 261 | 262 | export default class UrlSerializer extends RenameSerializer { 263 | constructor(struct, options = {}) { 264 | super(struct); 265 | 266 | let serializeOptions = { 267 | listDelimiter: options.listDelimiter || '~', 268 | matrixDelimiter: options.matrixDelimiter || '!', 269 | dateFormat: options.dateFormat || 'YYYYMMDDTHHmmss[Z]', 270 | conditionDelimiter: options.conditionDelimiter || ';', 271 | }; 272 | 273 | addUrlRules((struct, description) => { 274 | this.addRule(struct, description); 275 | }, serializeOptions); 276 | } 277 | 278 | isEncodeAllowed(key, value) { 279 | return true; 280 | //return value && value.length; 281 | } 282 | 283 | decode(params) { 284 | for (let key of Object.keys(params)) { 285 | if (!isString(params[key])) { 286 | throw new Error('URL param should be String'); 287 | } 288 | } 289 | return super.decode(params); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/url-manager.js: -------------------------------------------------------------------------------- 1 | import isFunction from 'lodash/isFunction'; 2 | 3 | let _map = Symbol('url-map'); 4 | let _currentRouteName = Symbol('current-route-name'); 5 | 6 | // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object] :( 7 | // export default class UrlManager extends Map { 8 | export default class UrlManager { 9 | constructor() { 10 | this[_map] = new Map(); 11 | } 12 | 13 | set(name, url) { 14 | this[_map].set(name, url); 15 | } 16 | 17 | has(name) { 18 | return this[_map].has(name); 19 | } 20 | 21 | get(name) { 22 | let url = this[_map].get(name); 23 | if (isFunction(url)) { 24 | url = url(); 25 | this.set(name, url); 26 | } 27 | 28 | return url; 29 | } 30 | 31 | getCurrentRoute() { 32 | throw new Error( 33 | 'depends on framework. Should be implemented at child class' 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/url.js: -------------------------------------------------------------------------------- 1 | import isObject from 'lodash/isObject'; 2 | import isEqual from 'lodash/isEqual'; 3 | import isArray from 'lodash/isArray'; 4 | 5 | import UrlPattern from 'url-pattern'; 6 | 7 | import UrlSerializer from './serializers/url-serializer'; 8 | 9 | let decodeSearchString = queryString => { 10 | let queryParams = {}; 11 | let queryPairs = queryString.split('&'); 12 | 13 | for (let pair of queryPairs) { 14 | let tuple = pair.split('='); 15 | let key = tuple[0]; 16 | let value = tuple.slice(1).join(''); 17 | 18 | queryParams[key] = decodeURIComponent(value); 19 | } 20 | 21 | return queryParams; 22 | }; 23 | 24 | let encodeSearchString = params => { 25 | let parts = []; 26 | 27 | for (let param of Object.keys(params)) { 28 | let value = params[param]; 29 | let part = `${param}=${encodeURIComponent(value)}`; 30 | 31 | parts.push(part); 32 | } 33 | 34 | return parts.join('&'); 35 | }; 36 | 37 | let _pattern = Symbol('pattern'); 38 | let _links = Symbol('mappings'); 39 | let _urlPattern = Symbol('url-pattern'); 40 | let _urlParamsKeys = Symbol('url-params-key'); 41 | let _searchParamsKeys = Symbol('search-params-key'); 42 | let _serializer = Symbol('url-serializer'); 43 | 44 | export default class Url { 45 | cache = {}; 46 | 47 | constructor(pattern, struct) { 48 | let serializer = new UrlSerializer(struct); 49 | this[_serializer] = serializer; 50 | 51 | let urlPattern = new UrlPattern(pattern); 52 | 53 | this[_pattern] = pattern; 54 | this[_urlPattern] = urlPattern; 55 | 56 | let urlParams = urlPattern.ast 57 | .filter(item => item.tag === 'named') 58 | .map(item => item.value); 59 | 60 | let searchParams = []; 61 | 62 | for (let key of Object.keys(struct)) { 63 | if (urlParams.indexOf(key) === -1) { 64 | searchParams.push(key); 65 | } else { 66 | if (serializer.hasRenameOption(key)) { 67 | throw new Error( 68 | 'url params with placeholders cant have rename config' 69 | ); 70 | } 71 | } 72 | } 73 | 74 | this[_searchParamsKeys] = searchParams; 75 | this[_urlParamsKeys] = urlParams; 76 | this[_links] = {}; 77 | } 78 | 79 | getSerializer() { 80 | return this[_serializer]; 81 | } 82 | 83 | getStruct() { 84 | return this[_serializer].getStruct(); 85 | } 86 | 87 | getPattern() { 88 | return this[_pattern]; 89 | } 90 | 91 | getUrlPattern() { 92 | return this[_urlPattern]; 93 | } 94 | 95 | getUrlParamKeys() { 96 | return this[_urlParamsKeys]; 97 | } 98 | 99 | getSearchParamKeys() { 100 | return this[_searchParamsKeys]; 101 | } 102 | 103 | // ----- 104 | 105 | // TODO: implement this method 106 | //isCurrentRoute() { 107 | // let pathname = this.getPathname(); 108 | // 109 | // var splittedPath = pathname.split('?'); 110 | // var url = splittedPath[0]; 111 | // 112 | // let urlPattern = this.getUrlPattern(); 113 | // return !!urlPattern.match(url); 114 | //} 115 | 116 | decode(path) { 117 | var splittedPath = path.split('?'); 118 | var search = splittedPath.slice(1).join(''); 119 | 120 | var url = splittedPath[0]; 121 | 122 | let urlPattern = this.getUrlPattern(); 123 | let urlParams = urlPattern.match(url); 124 | 125 | if (!urlParams) { 126 | let pattern = this.getPattern(); 127 | throw new Error( 128 | `Wrong url pattern. Expected "${pattern}", got "${path}"` 129 | ); 130 | } 131 | 132 | let searchParams = {}; 133 | 134 | //let search = window.location.search; 135 | if (search) { 136 | searchParams = decodeSearchString(search); 137 | } 138 | 139 | let params = Object.assign(urlParams, searchParams); 140 | let decoded = this[_serializer].decode(params); 141 | 142 | this.cacheParams(decoded); 143 | 144 | return decoded; 145 | } 146 | 147 | // ----- 148 | 149 | go(params = {}) { 150 | if (!isObject(params)) { 151 | throw new Error('params should be an object'); 152 | } 153 | 154 | let encoded = this.stringify(params); 155 | 156 | /** 157 | * TODO: check redirect method 158 | */ 159 | window.location.replace(encoded); 160 | } 161 | 162 | // -------------------------------- 163 | 164 | stringify(params) { 165 | let encodedParams = this[_serializer].encode(params); 166 | 167 | let urlParamKeys = this.getUrlParamKeys(); 168 | 169 | let searchParams = {}; 170 | let urlParams = {}; 171 | 172 | for (let key of Object.keys(encodedParams)) { 173 | if (urlParamKeys.indexOf(key) === -1) { 174 | searchParams[key] = encodedParams[key]; 175 | } else { 176 | urlParams[key] = encodedParams[key]; 177 | } 178 | } 179 | 180 | let urlPattern = this.getUrlPattern(); 181 | 182 | let url = urlPattern.stringify(urlParams); 183 | let search = encodeSearchString(searchParams); 184 | 185 | return search ? [url, search].join('?') : url; 186 | } 187 | 188 | parse() { 189 | /** 190 | * if there is no cached params - parse url. Otherwise - return cached params 191 | * 192 | * if (!Object.keys(this.cache)) { 193 | * 194 | * } 195 | * @type {string} 196 | */ 197 | var pathname = window.location.pathname; 198 | var search = window.location.search; 199 | 200 | var url = pathname; 201 | if (search) { 202 | url += `?${search}`; 203 | } 204 | 205 | return this.decode(url); 206 | } 207 | 208 | cacheParams(params) { 209 | this.cache = params; 210 | } 211 | 212 | getCachedParams() { 213 | return this.cache; 214 | } 215 | 216 | clearCachedParams() { 217 | this.cache = {}; 218 | } 219 | 220 | isEmpty() { 221 | // TODO: use cached params 222 | let params = this.parse(); 223 | return Object.keys(params).length == 0; 224 | } 225 | 226 | isEqual(params = {}) { 227 | if (!isObject(params)) { 228 | throw new Error('params should be an object'); 229 | } 230 | 231 | // TODO: use cached params 232 | let existingParams = this.parse(); 233 | return isEqual(existingParams, params); 234 | } 235 | 236 | // --------------------- 237 | 238 | link(key, link) { 239 | this[_links][key] = link; 240 | } 241 | 242 | linkTo(store, params = []) { 243 | if (!isArray(params)) { 244 | throw new Error('available params for linkTo should be an array'); 245 | } 246 | 247 | let struct = this.getStruct(); 248 | 249 | for (let key of Object.keys(struct)) { 250 | if (!params.length || params.indexOf(key) != -1) { 251 | this.link(key, value => { 252 | Object.assign(store, { 253 | key: value, 254 | }); 255 | }); 256 | } 257 | } 258 | } 259 | 260 | apply(defaults = {}) { 261 | // TODO: use cached params 262 | let params = this.parse(); 263 | let links = this[_links]; 264 | let tasks = []; 265 | 266 | let struct = this.getStruct(); 267 | for (let key of Object.keys(struct)) { 268 | if (links.hasOwnProperty(key)) { 269 | let value = params.hasOwnProperty(key) ? params[key] : defaults[key]; 270 | let link = links[key]; 271 | 272 | let task = link(value); 273 | tasks.push(task); 274 | } 275 | } 276 | 277 | return Promise.all(tasks); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/utils/class-name.js: -------------------------------------------------------------------------------- 1 | import isFunction from 'lodash/isFunction'; 2 | import endsWith from 'lodash/endsWith'; 3 | import Logger from '../utils/logger'; 4 | 5 | const CONSTRUCTOR_NAME_FUNC = 'Function'; 6 | const CONTROLLER_NAME_SUFFIX = 'Controller'; 7 | 8 | let logger = Logger.create('class-name'); 9 | 10 | /** 11 | * Looks for class constructor name 12 | * 13 | * @param {Object} 14 | * @return {string|null} 15 | * @throws {TypeError} 16 | */ 17 | export const getClassName = object => { 18 | if (!isFunction(object)) { 19 | throw new TypeError('Given argument is not a class'); 20 | } 21 | 22 | let constructorName = object.prototype.constructor.name; 23 | 24 | // anonymous functions don't have constructor name 25 | if (CONSTRUCTOR_NAME_FUNC === constructorName || !constructorName.length) { 26 | return null; 27 | } 28 | 29 | return constructorName; 30 | }; 31 | 32 | /** 33 | * Looks for a controller name 34 | * 35 | * @param {object} 36 | * @return {string} 37 | */ 38 | export const getControllerName = object => { 39 | let constructorName = getClassName(object); 40 | var controllerName = null; 41 | 42 | if (endsWith(constructorName, CONTROLLER_NAME_SUFFIX)) { 43 | logger.log( 44 | `Controller name should ends with "${CONTROLLER_NAME_SUFFIX}" suffix` 45 | ); 46 | controllerName = constructorName; 47 | } else { 48 | controllerName = constructorName.slice(0, CONTROLLER_NAME_SUFFIX.length); 49 | } 50 | 51 | return controllerName.toLowerCase(); 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | let colors = [ 2 | '#008EBA', 3 | '#99CC00', 4 | '#AA66CC', 5 | '#FD7400', 6 | '#723147', 7 | '#FF5F5F', 8 | '#AC59D6', 9 | '#6B5D99', 10 | '#FFBB33', 11 | '#FF4444', 12 | '#1F8A70', 13 | '#9BCF2E', 14 | '#004358', 15 | '#979C9C', 16 | '#962D3E', 17 | '#35478C', 18 | '#5F9C6D', 19 | '#FD7400', 20 | '#16193B', 21 | '#7FB2F0', 22 | ]; 23 | 24 | let counter = 0; 25 | 26 | let getColor = () => { 27 | if (counter == colors.length) { 28 | counter = 0; 29 | } 30 | 31 | return colors[counter++]; 32 | }; 33 | 34 | let formats = [ 35 | { 36 | regex: /\*([^\*]+)\*/, 37 | replacer: (m, p1) => `%c${p1}%c`, 38 | styles: () => ['font-style: italic', ''], 39 | }, 40 | { 41 | regex: /\_([^\_]+)\_/, 42 | replacer: (m, p1) => `%c${p1}%c`, 43 | styles: () => ['font-weight: bold', ''], 44 | }, 45 | { 46 | regex: /\`([^\`]+)\`/, 47 | replacer: (m, p1) => `%c${p1}%c`, 48 | styles: () => [ 49 | 'background: rgb(255, 255, 219); padding: 1px 5px; border: 1px solid rgba(0, 0, 0, 0.1)', 50 | '', 51 | ], 52 | }, 53 | { 54 | regex: /\[c\=(?:\"|\')?((?:(?!(?:\"|\')\]).)*)(?:\"|\')?\]((?:(?!\[c\]).)*)\[c\]/, 55 | replacer: (m, p1, p2) => `%c${p2}%c`, 56 | styles: match => [match[1], ''], 57 | }, 58 | ]; 59 | 60 | function hasMatches(str) { 61 | let hasMatches = false; 62 | 63 | for (let format of formats) { 64 | if (format.regex.test(str)) { 65 | hasMatches = true; 66 | break; 67 | } 68 | } 69 | 70 | return hasMatches; 71 | } 72 | 73 | function getOrderedMatches(str) { 74 | let matches = []; 75 | 76 | for (let format of formats) { 77 | let match = str.match(format.regex); 78 | 79 | if (match) { 80 | matches.push({ 81 | format: format, 82 | match: match, 83 | }); 84 | 85 | break; 86 | } 87 | } 88 | 89 | return matches; 90 | //return matches.sort(function(a, b) { 91 | // return a.match.index - b.match.index; 92 | //}); 93 | } 94 | 95 | function stringToArgs(str) { 96 | let firstMatch = null; 97 | let matches = null; 98 | let styles = []; 99 | 100 | while (hasMatches(str)) { 101 | matches = getOrderedMatches(str); 102 | firstMatch = matches[0]; 103 | str = str.replace(firstMatch.format.regex, firstMatch.format.replacer); 104 | styles = styles.concat(firstMatch.format.styles(firstMatch.match)); 105 | } 106 | 107 | return [str].concat(styles); 108 | } 109 | 110 | export default class Logger { 111 | static create(name) { 112 | let background = getColor(); 113 | let color = '#fff;'; 114 | 115 | return new Logger(name, color, background); 116 | } 117 | 118 | static resetColors() { 119 | counter = 0; 120 | } 121 | 122 | constructor(name, color, background) { 123 | this.color = color || '#000'; 124 | this.background = background || '#fff'; 125 | 126 | this.name = name; 127 | 128 | this.isEnabled = true; 129 | } 130 | 131 | enable() { 132 | this.isEnabled = true; 133 | } 134 | 135 | disable() { 136 | this.isEnabled = false; 137 | } 138 | 139 | error(message) { 140 | throw new Error(`${this.name}: ${message}`); 141 | } 142 | 143 | log(message, ...rest) { 144 | if (this.isEnabled) { 145 | let customized = stringToArgs(message); 146 | let completeMessage = customized[0]; 147 | let styles = customized.slice(1); 148 | 149 | console.log( 150 | `%c${this.name}%c ${completeMessage}`, 151 | `background:${this.background}; color:${this.color}; font-weight:bold`, 152 | '', 153 | ...styles, 154 | ...rest 155 | ); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/utils/object-difference.js: -------------------------------------------------------------------------------- 1 | import deepDiff from 'deep-diff'; 2 | 3 | let defaultOptions = { 4 | ignored: ['$$hashKey'], 5 | }; 6 | 7 | export default (left, right, callOptions) => { 8 | let options = Object.assign({}, defaultOptions, callOptions); 9 | 10 | let description = []; 11 | let diff = deepDiff(left, right); 12 | 13 | if (diff) { 14 | for (let chunk of diff) { 15 | let lastKey = chunk.path[chunk.path.length - 1]; 16 | 17 | if (options.ignored.indexOf(lastKey) == -1) { 18 | if (chunk.kind == 'A') { 19 | let path = chunk.path.join('.'); 20 | 21 | if (chunk.item.kind == 'N') { 22 | description.push({ 23 | description: `Added element to array _${path}_:`, 24 | value: chunk.item.rhs, 25 | }); 26 | } else if (chunk.item.kind == 'E') { 27 | description.push({ 28 | description: `Added element to array _${path}_ at _${ 29 | chunk.index 30 | }_ index:`, 31 | value: [chunk.item.lhs, chunk.item.rhs], 32 | }); 33 | } else if (chunk.item.kind == 'D') { 34 | description.push({ 35 | description: `Deleted element from array _${path}_ at _${ 36 | chunk.index 37 | }_ index:`, 38 | value: chunk.item.lhs, 39 | }); 40 | } 41 | } else if (chunk.kind == 'E') { 42 | let path = chunk.path.join('.'); 43 | 44 | description.push({ 45 | description: `Changed property _${path}_`, 46 | value: [chunk.lhs, chunk.rhs], 47 | }); 48 | } else if (chunk.kind == 'N') { 49 | let path = chunk.path ? chunk.path.join('.') : 'root'; 50 | 51 | description.push({ 52 | description: `Added property _${path}_`, 53 | value: chunk.rhs, 54 | }); 55 | } else if (chunk.kind == 'D') { 56 | let path = chunk.path ? chunk.path.join('.') : 'root'; 57 | 58 | description.push({ 59 | description: `Deleted property _${path}_`, 60 | value: chunk.lhs, 61 | }); 62 | } 63 | } 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/utils/object-transition.js: -------------------------------------------------------------------------------- 1 | import setter from 'lodash/set'; 2 | 3 | let _transition = Symbol('transition'); 4 | let _source = Symbol('source'); 5 | let _counter = Symbol('counter'); 6 | 7 | export default class ObjectTransition { 8 | constructor(obj) { 9 | this[_transition] = new Map(); 10 | this[_source] = obj; 11 | this[_counter] = 0; 12 | } 13 | 14 | /** 15 | * Scope methods 16 | */ 17 | has(key) { 18 | return this[_transition].has(key); 19 | } 20 | 21 | get(key) { 22 | return this[_transition].get(key); 23 | } 24 | 25 | clear() { 26 | this[_transition].clear(); 27 | } 28 | 29 | commit(key, value) { 30 | this[_transition].set(key, value); 31 | } 32 | 33 | push(...args) { 34 | if (args.length == 2) { 35 | this.commit(...args); 36 | } else if (!!args.length) { 37 | throw new Error('Wrong arguments for ObjectTransition.push'); 38 | } 39 | 40 | this[_transition].forEach((value, key) => { 41 | setter(this[_source], key, value); 42 | }); 43 | 44 | this[_counter]++; 45 | 46 | this.clear(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/primitives.js: -------------------------------------------------------------------------------- 1 | import t from 'tcomb'; 2 | const { Num, Str, Bool, Dat, list, maybe, subtype, struct } = t; 3 | 4 | // subtypes 5 | let Int = subtype(Num, number => number % 1 === 0, 'Int'); 6 | let DateStr = subtype( 7 | Str, 8 | date => !!date.match(/^\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{0,})?Z?$/), 9 | 'DateStr' 10 | ); 11 | 12 | export { Int, Num, Str, Bool, Dat }; 13 | 14 | export const ListNum = list(Num, 'ListNum'); 15 | export const ListInt = list(Int, 'ListInt'); 16 | export const ListStr = list(Str, 'ListStr'); 17 | export const ListBool = list(Bool, 'ListBool'); 18 | export const ListDat = list(Dat, 'ListDat'); 19 | 20 | export const MaybeNum = maybe(Num, 'MaybeNum'); 21 | export const MaybeInt = maybe(Int, 'MaybeInt'); 22 | export const MaybeStr = maybe(Str, 'MaybeStr'); 23 | export const MaybeBool = maybe(Bool, 'MaybeBool'); 24 | export const MaybeDat = maybe(Dat, 'MaybeDat'); 25 | 26 | export const MaybeListNum = maybe(ListNum, 'MaybeListNum'); 27 | export const MaybeListInt = maybe(ListInt, 'MaybeListInt'); 28 | export const MaybeListStr = maybe(ListStr, 'MaybeListStr'); 29 | export const MaybeListBool = maybe(ListBool, 'MaybeListBool'); 30 | export const MaybeListDat = maybe(ListDat, 'MaybeListDat'); 31 | 32 | export const Period = maybe(ListDat, 'MaybeListDat'); 33 | export const ComparePeriod = list(MaybeListDat, 'MatrixMaybeDate'); 34 | 35 | // [[1,..n],[1...m]...k] 36 | export const MatrixNum = list(ListNum, 'MatrixNum'); 37 | // [[1, undefined, .n],[1 undefined,..m]...k] 38 | export const MatrixMaybeNum = list(MaybeListNum, 'MatrixMaybeNum'); 39 | 40 | // [['a',..n],['a'...m]...k] 41 | export const MatrixStr = list(ListStr, 'MatrixStr'); 42 | // [['a', undefined, .n],['a' undefined,..m]...k] 43 | export const MatrixMaybeStr = list(MaybeListStr, 'MatrixMaybeStr'); 44 | 45 | export const MatrixDate = list(ListDat, 'MatrixDate'); 46 | export const MatrixMaybeDate = list(MaybeListDat, 'MatrixMaybeDate'); 47 | 48 | export const MatrixBool = list(ListBool, 'MatrixBool'); 49 | export const MatrixMaybeBool = list(MaybeListBool, 'MatrixMaybeBool'); 50 | 51 | export const Struct = params => struct(params); 52 | -------------------------------------------------------------------------------- /src/valent-component.js: -------------------------------------------------------------------------------- 1 | import isString from 'lodash/isString'; 2 | import isFunction from 'lodash/isFunction'; 3 | import uniq from 'lodash/uniq'; 4 | 5 | import * as validation from './validation/structures'; 6 | 7 | let normalize = (ComponentClass, options) => { 8 | let renderMethod = ComponentClass.render; 9 | let normalized = Object.assign({}, options); 10 | 11 | if (isFunction(renderMethod)) { 12 | normalized.template = renderMethod; 13 | } 14 | 15 | return normalized; 16 | }; 17 | 18 | export default class ValentComponent { 19 | constructor(name, ComponentClass, options) { 20 | this.name = name; 21 | this.options = normalize(ComponentClass, options); 22 | this.ComponentClass = ComponentClass; 23 | } 24 | 25 | static validate(name, ComponentClass, options) { 26 | let isValidName = validation.isValidName(name); 27 | let isValidController = validation.isValidConstructor(ComponentClass); 28 | let isValidTemplate = validation.isValidTemplate(options.template); 29 | let isValidTemplateUrl = validation.isValidTemplateUrl(options.templateUrl); 30 | let isValidRenderMethod = validation.isValidRenderMethod( 31 | ComponentClass.render 32 | ); 33 | let isValidBindings = validation.isValidBindings(options.bindings); 34 | 35 | let isValidInterfaces = validation.isValidInterfaces(options.interfaces); 36 | let isValidPipes = validation.isValidPipes(options.pipes); 37 | let isValidOptions = validation.isValidOptions(options.options); 38 | 39 | let isValidRestrict = validation.isValidRestrict(options.restrict); 40 | //let isValidCompileMethod = validation.isValidCompileMethod(ComponentClass.compile); 41 | 42 | let errors = []; 43 | 44 | if (!isValidName) { 45 | errors.push("Component's name could not be empty or with spaces"); 46 | } 47 | 48 | if (!isValidController) { 49 | errors.push("Component's class should be a constructor"); 50 | } 51 | 52 | // if al least two template options are defined 53 | if ( 54 | isValidTemplate == isValidTemplateUrl 55 | ? isValidTemplate 56 | : isValidRenderMethod 57 | ) { 58 | errors.push( 59 | 'Should have only one - template, templateUrl or static render() option' 60 | ); 61 | } 62 | 63 | if ( 64 | options.restrict == 'A' && 65 | (isValidTemplate || isValidTemplateUrl || isValidRenderMethod) 66 | ) { 67 | errors.push('Attribute directives should be without template'); 68 | } 69 | 70 | let keys = []; 71 | if (!isValidBindings) { 72 | errors.push('If bindings are defined - it should be an object'); 73 | } else if (options.bindings) { 74 | let bindingsKeys = Object.keys(options.bindings); 75 | keys.concat(bindingsKeys); 76 | } 77 | 78 | if (!isValidInterfaces) { 79 | errors.push('Interfaces should be an object with constructors at values'); 80 | } else if (options.interfaces) { 81 | let interfacesKeys = Object.keys(options.interfaces); 82 | keys.concat(interfacesKeys); 83 | } 84 | 85 | if (!isValidPipes) { 86 | errors.push('Pipes should be an object with constructors at values'); 87 | } else if (options.pipes) { 88 | let pipesKeys = Object.keys(options.pipes); 89 | keys.concat(pipesKeys); 90 | } 91 | 92 | if (!isValidOptions) { 93 | errors.push('Options should be an object with constructors at values'); 94 | } else if (options.options) { 95 | let optionsKeys = Object.keys(options.options); 96 | keys.concat(optionsKeys); 97 | } 98 | 99 | if (keys.length) { 100 | let uniqueKeys = uniq(keys); 101 | 102 | if (uniqueKeys.length == keys.length) { 103 | errors.push('Interfaces, options and pipes keys should not cross'); 104 | } 105 | } 106 | 107 | if (!isValidRestrict) { 108 | errors.push('Restrict should be any of "E" or "A"'); 109 | } 110 | 111 | return errors; 112 | } 113 | 114 | getName() { 115 | return this.name; 116 | } 117 | 118 | getController() { 119 | return this.ComponentClass; 120 | } 121 | 122 | hasTemplate() { 123 | return !!this.options.template; 124 | } 125 | 126 | getTemplate() { 127 | return this.options.template; 128 | } 129 | 130 | hasTemplateUrl() { 131 | return !!this.options.templateUrl; 132 | } 133 | 134 | getTemplateUrl() { 135 | return this.options.templateUrl; 136 | } 137 | 138 | withoutTemplate() { 139 | return !this.hasTemplate() && !this.hasTemplateUrl(); // && !this.hasRenderMethod(); 140 | } 141 | 142 | hasCompileMethod() { 143 | return isFunction(this.ComponentClass.compile); 144 | } 145 | 146 | getCompileMethod() { 147 | return this.ComponentClass.compile; 148 | } 149 | 150 | getBindings() { 151 | return this.options.bindings; 152 | } 153 | 154 | hasInterfaces() { 155 | return !!this.getInterfaces(); 156 | } 157 | 158 | getInterfaces() { 159 | return this.options.interfaces; 160 | } 161 | 162 | hasOptions() { 163 | return !!this.getOptions(); 164 | } 165 | 166 | getOptions() { 167 | return this.options.options; 168 | } 169 | 170 | hasPipes() { 171 | return !!this.getPipes(); 172 | } 173 | 174 | getPipes() { 175 | return this.options.pipes; 176 | } 177 | 178 | getRestrict() { 179 | return this.options.restrict; 180 | } 181 | 182 | isAttributeComponent() { 183 | let restrict = this.getRestrict(); 184 | return restrict === 'A'; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/valent-controller.js: -------------------------------------------------------------------------------- 1 | import isFunction from 'lodash/isFunction'; 2 | 3 | import * as validation from './validation/structures'; 4 | 5 | let normalize = (ControllerClass, options) => { 6 | let renderMethod = ControllerClass.render; 7 | let normalized = Object.assign({}, options); 8 | 9 | if (isFunction(renderMethod)) { 10 | normalized.template = renderMethod; 11 | } 12 | 13 | return normalized; 14 | }; 15 | 16 | export default class ValentController { 17 | constructor(name, ControllerClass, options) { 18 | this.name = name; 19 | this.options = normalize(ControllerClass, options); 20 | this.ControllerClass = ControllerClass; 21 | 22 | let url = this.options.url; 23 | 24 | if (url) { 25 | valent.route(this.name, url, this.options); 26 | } 27 | } 28 | 29 | static validate(name, ControllerClass) { 30 | let isValidName = validation.isValidName(name); 31 | let isValidController = validation.isValidConstructor(ControllerClass); 32 | 33 | let errors = []; 34 | 35 | if (!isValidName) { 36 | errors.push("Controller's name could not be empty or with spaces"); 37 | } 38 | 39 | if (!isValidController) { 40 | errors.push("Controller's class should be a constructor"); 41 | } 42 | 43 | return errors; 44 | } 45 | 46 | getName() { 47 | return this.name; 48 | } 49 | 50 | getController() { 51 | return this.ControllerClass; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/valent-route.js: -------------------------------------------------------------------------------- 1 | import * as validation from './validation/structures'; 2 | 3 | export default class ValentController { 4 | otherwise = false; 5 | 6 | constructor(name, url, options) { 7 | if (!url) { 8 | this.otherwise = true; 9 | } 10 | 11 | this.name = name; 12 | this.url = url; 13 | this.options = options; 14 | } 15 | 16 | static validate(name, url, options) { 17 | let otherwise = false; 18 | if (!url) { 19 | otherwise = true; 20 | } 21 | 22 | let isValidName = validation.isValidName(name); 23 | let isValidTemplate = validation.isValidTemplate(options.template); 24 | let isValidTemplateUrl = validation.isValidTemplateUrl(options.templateUrl); 25 | 26 | let isValidUrl = true; 27 | if (!otherwise) { 28 | isValidUrl = validation.isValidUrl(url); 29 | } 30 | 31 | let isValidStruct = validation.isValidStruct(options.structure); 32 | let isValidResolvers = validation.isValidResolvers(options.resolvers); 33 | 34 | let errors = []; 35 | 36 | if (!isValidName) { 37 | errors.push("Component's name could not be empty or with spaces"); 38 | } 39 | 40 | // if al least two template options are defined 41 | if ( 42 | (isValidTemplate && isValidTemplateUrl) || 43 | (!isValidTemplate && !isValidTemplateUrl) 44 | ) { 45 | errors.push( 46 | 'Should have only one - template, templateUrl or static render() option' 47 | ); 48 | } 49 | 50 | if (!otherwise && !isValidUrl) { 51 | errors.push('Url should be a string of list of strings'); 52 | } 53 | 54 | if (!isValidStruct) { 55 | errors.push('Struct should be an object of tcomb models'); 56 | } 57 | 58 | if (!isValidResolvers) { 59 | errors.push('Struct should be an object of functions'); 60 | } 61 | 62 | return errors; 63 | } 64 | 65 | getName() { 66 | return this.name; 67 | } 68 | 69 | isOtherwise() { 70 | return this.otherwise; 71 | } 72 | 73 | getController() { 74 | return this.Controller; 75 | } 76 | 77 | hasTemplate() { 78 | return !!this.options.template; 79 | } 80 | 81 | getTemplate() { 82 | return this.options.template; 83 | } 84 | 85 | hasTemplateUrl() { 86 | return !!this.options.templateUrl; 87 | } 88 | 89 | getTemplateUrl() { 90 | return this.options.templateUrl; 91 | } 92 | 93 | withoutTemplate() { 94 | return !this.hasTemplate() && !this.hasTemplateUrl(); 95 | } 96 | 97 | getUrl() { 98 | return this.url; 99 | } 100 | 101 | hasResolvers() { 102 | return !!this.options.resolve; 103 | } 104 | 105 | getResolvers() { 106 | return this.options.resolve; 107 | } 108 | 109 | getParams() { 110 | return this.options.params || {}; 111 | } 112 | 113 | getStructure() { 114 | return this.options.structure || {}; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/valent.js: -------------------------------------------------------------------------------- 1 | import ApplicationConfig from './application-config'; 2 | import RegisterException from './exceptions/register'; 3 | 4 | let _controllers = Symbol('controllers'); 5 | let _components = Symbol('components'); 6 | let _routes = Symbol('routes'); 7 | let _framework = Symbol('framework'); 8 | let _bootstrap = Symbol('bootstrap'); 9 | 10 | const translateComponents = (framework, components, config) => { 11 | let isDevEnvironment = config.get('valent.environment.dev', true); 12 | let FrameworkComponentClass = framework.component; 13 | 14 | for (let component of components) { 15 | let args = [component.name, component.controller, component.options]; 16 | 17 | if (isDevEnvironment) { 18 | let errors = FrameworkComponentClass.validate(...args); 19 | 20 | if (errors.length) { 21 | throw new RegisterException(component.name, 'valent-component', errors); 22 | } 23 | } 24 | 25 | let frameworkComponent = new FrameworkComponentClass(...args); 26 | framework.translate.component(frameworkComponent, config); 27 | } 28 | }; 29 | 30 | class Valent { 31 | version = '0.1.0'; 32 | 33 | config = new ApplicationConfig({ 34 | valent: { 35 | version: this.version, 36 | environment: { 37 | dev: true, 38 | debug: false, 39 | }, 40 | }, 41 | }); 42 | 43 | constructor() { 44 | this[_bootstrap] = false; 45 | 46 | this[_controllers] = new Set(); 47 | this[_components] = new Set(); 48 | this[_routes] = new Set(); 49 | } 50 | 51 | bootstrap(framework) { 52 | this[_framework] = framework; 53 | this.url = this[_framework].getUrlManager(); 54 | 55 | let isDevEnvironment = this.config.get('valent.environment.dev', true); 56 | 57 | // --- TRANSLATE COMPONENTS(DIRECTIVES) 58 | translateComponents(this[_framework], this[_components], this.config); 59 | 60 | // --- TRANSLATE CONTROLLERS 61 | let FrameworkControllerClass = this[_framework].controller; 62 | 63 | for (let controller of this[_controllers]) { 64 | let args = [controller.name, controller.controller, controller.options]; 65 | 66 | let frameworkController = new FrameworkControllerClass(...args); 67 | 68 | if (isDevEnvironment) { 69 | let errors = FrameworkControllerClass.validate(...args); 70 | 71 | if (errors.length) { 72 | throw new RegisterException( 73 | controller.name, 74 | 'valent-controller', 75 | errors 76 | ); 77 | } 78 | } 79 | 80 | this[_framework].translate.controller(frameworkController, this.config); 81 | } 82 | 83 | // --- TRANSLATE ROUTES 84 | let FrameworkRouteClass = this[_framework].route; 85 | 86 | for (let route of this[_routes]) { 87 | let args = [route.name, route.url, route.options]; 88 | 89 | let frameworkRoute = new FrameworkRouteClass(...args); 90 | 91 | if (isDevEnvironment) { 92 | let errors = FrameworkRouteClass.validate(...args); 93 | 94 | if (errors.length) { 95 | throw new RegisterException(route.name, 'valent-route', errors); 96 | } 97 | } 98 | 99 | this[_framework].translate.route(frameworkRoute, this.config); 100 | } 101 | 102 | this[_framework].bootstrap(this.config); 103 | this[_bootstrap] = true; 104 | } 105 | 106 | component(name, Component, options = {}) { 107 | const component = { 108 | name, 109 | controller: Component, 110 | options, 111 | }; 112 | 113 | if (this[_bootstrap]) { 114 | console.info(`register comopnent "${name}" as lazy component`); 115 | translateComponents(this[_framework], [component], this.config); 116 | // throw new Error('component could no be registered after bootstrap'); 117 | } else { 118 | this[_components].add(component); 119 | } 120 | } 121 | 122 | controller(name, Controller, options = {}) { 123 | if (this[_bootstrap]) { 124 | throw new Error('controller could no be registered after bootstrap'); 125 | } 126 | 127 | this[_controllers].add({ 128 | name, 129 | controller: Controller, 130 | options, 131 | }); 132 | } 133 | 134 | route(name, url, options = {}) { 135 | if (this[_bootstrap]) { 136 | throw new Error('route could no be registered after bootstrap'); 137 | } 138 | 139 | this[_routes].add({ 140 | name, 141 | url, 142 | options, 143 | }); 144 | } 145 | } 146 | 147 | let valent = null; 148 | let context = typeof window !== 'undefined' ? window : global; 149 | 150 | if (context.valent) { 151 | throw new Error('Seems there are multiple installations of Valent'); 152 | } else { 153 | context.valent = new Valent(); 154 | } 155 | -------------------------------------------------------------------------------- /src/validation/structures.js: -------------------------------------------------------------------------------- 1 | import t from 'tcomb-validation'; 2 | 3 | var validate = t.validate; 4 | 5 | const templateStruct = t.union([t.Str, t.Function]); 6 | 7 | /** 8 | * Should be string and without spaces 9 | * @param name 10 | * @returns {*|boolean} 11 | */ 12 | export const isValidName = name => { 13 | let isValid = validate(name, t.Str).isValid(); 14 | return isValid && name.indexOf(' ') == -1; 15 | }; 16 | 17 | /** 18 | * Static controller's method. Should return string 19 | * @param render 20 | */ 21 | export const isValidRenderMethod = render => 22 | validate(render, t.Function).isValid(); 23 | 24 | /** 25 | * Controller's Constructor == Function 26 | * @param constructor 27 | */ 28 | export const isValidConstructor = constructor => 29 | validate(constructor, t.Function).isValid(); 30 | 31 | /** 32 | * Template could be a String of Function that returns string :) 33 | * @param template 34 | */ 35 | export const isValidTemplate = template => 36 | validate(template, templateStruct).isValid(); 37 | 38 | /** 39 | * Url == String 40 | * @param templateUrl 41 | */ 42 | export const isValidTemplateUrl = templateUrl => 43 | validate(templateUrl, t.Str).isValid(); 44 | 45 | /** 46 | * Directive params 47 | * if Object - isolated scope 48 | * otherwise - new scope will not be created 49 | * @param bindings 50 | */ 51 | export const isValidBindings = bindings => 52 | validate(bindings, t.maybe(t.Obj)).isValid(); 53 | 54 | /** 55 | * Object with Constructors at values 56 | * @type {{value, errors}|*} 57 | */ 58 | const interfacesStructure = t.dict(t.Str, t.Function); 59 | export const isValidInterfaces = interfaces => { 60 | return validate(interfaces, t.maybe(interfacesStructure)).isValid(); 61 | }; 62 | 63 | export const isValidPipes = pipes => isValidInterfaces(pipes); 64 | export const isValidOptions = options => isValidInterfaces(options); 65 | 66 | /** 67 | * Do not recommend to use classes as components's restricts 68 | * @param restrict 69 | */ 70 | export const isValidRestrict = restrict => 71 | validate(restrict, t.maybe(t.enums.of(['A', 'E']))).isValid(); 72 | 73 | /** 74 | * Static controller's method 75 | * @param compile 76 | */ 77 | export const isValidCompileMethod = compile => 78 | validate(compile, t.maybe(t.Function)).isValid(); 79 | 80 | /** 81 | * String or array of strings 82 | * @param url 83 | */ 84 | export const isValidUrl = url => 85 | validate(url, t.union([t.list(t.Str), t.Str])).isValid(); 86 | 87 | const structField = t.union([t.tuple([t.Str, t.Function]), t.Function]); 88 | 89 | export const isValidStruct = struct => 90 | validate(struct, t.maybe(t.dict(t.Str, structField))).isValid(); 91 | 92 | export const isValidResolvers = resolvers => 93 | validate(resolvers, t.maybe(t.dict(t.Str, t.Function))).isValid(); 94 | -------------------------------------------------------------------------------- /test/components/serializer.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import Serializer from '../../src/serializers/serializer'; 4 | 5 | import * as primitives from '../../src/utils/primitives'; 6 | import t from 'tcomb'; 7 | 8 | var optionItem = t.struct({ 9 | id: t.Num, 10 | value: t.Str, 11 | }); 12 | 13 | var optionList = t.list(optionItem); 14 | 15 | describe('Seriazlier', () => { 16 | var struct = t.struct({ 17 | id: t.Num, 18 | options: optionList, 19 | }); 20 | 21 | var serializer = new Serializer(struct); 22 | 23 | serializer.addRule(t.Num, { 24 | encode: value => { 25 | return `@${value}`; 26 | }, 27 | decode: value => { 28 | return parseInt(value.slice(1)); 29 | }, 30 | }); 31 | 32 | serializer.addRule(optionList, { 33 | encode: value => { 34 | return value 35 | .map(item => { 36 | return item.id + '|' + item.value; 37 | }) 38 | .join('~'); 39 | }, 40 | decode: value => { 41 | return value.split('~').map(item => { 42 | var splited = item.split('|'); 43 | return { 44 | id: parseInt(splited[0]), 45 | value: splited[1], 46 | }; 47 | }); 48 | }, 49 | }); 50 | 51 | it('should correctly encode data', () => { 52 | var encoded = serializer.encode({ 53 | id: 1, 54 | options: [ 55 | { 56 | id: 1, 57 | value: 'active', 58 | }, 59 | { 60 | id: 2, 61 | value: 'opened', 62 | }, 63 | ], 64 | }); 65 | 66 | expect(encoded).to.be.eql({ 67 | id: '@1', 68 | options: '1|active~2|opened', 69 | }); 70 | }); 71 | 72 | it('should correctly decode data', () => { 73 | var encoded = serializer.decode({ 74 | id: '@1', 75 | options: '1|active~2|opened', 76 | }); 77 | 78 | expect(encoded).to.be.eql({ 79 | id: 1, 80 | options: [ 81 | { 82 | id: 1, 83 | value: 'active', 84 | }, 85 | { 86 | id: 2, 87 | value: 'opened', 88 | }, 89 | ], 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/components/url-struct.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | // import UrlStruct from '../../src/components/url-struct'; 4 | import UrlSerializer from '../../src/serializers/url-serializer'; 5 | 6 | import * as primitives from '../../src/utils/primitives'; 7 | import t from 'tcomb'; 8 | 9 | var UserStruct = t.struct({ 10 | id: t.Num, 11 | name: t.Str, 12 | nickname: t.maybe(t.Str), 13 | }); 14 | 15 | class CustomSerializer extends UrlSerializer { 16 | constructor(struct) { 17 | super(struct); 18 | 19 | this.addRule(UserStruct, { 20 | encode: () => {}, 21 | decode: () => {}, 22 | }); 23 | } 24 | } 25 | 26 | describe('Url struct', () => { 27 | it('should encode correctly', () => { 28 | var serializer = new UrlSerializer({ 29 | apple: ['apl', primitives.Str], 30 | orange: primitives.Num, 31 | mango: primitives.ListNum, 32 | octarine: primitives.ListStr, 33 | octopus: primitives.ListDat, 34 | cat: ['c', primitives.Dat], 35 | }); 36 | 37 | var encoded = serializer.encode({ 38 | apple: 'a', 39 | orange: 1, 40 | mango: [1, 2, 3], 41 | octarine: ['a', 'b', 'c'], 42 | //octopus: [new Date(), new Date()], 43 | //cat: new Date() 44 | }); 45 | 46 | expect(encoded).to.be.eql({ 47 | apl: 'a', 48 | orange: '1', 49 | mango: '1~2~3', 50 | octarine: 'a~b~c', 51 | //octopus: '20150602~20150602', 52 | //c: '20150602' 53 | }); 54 | }); 55 | 56 | it('should decode correctly', () => { 57 | var serializer = new UrlSerializer({ 58 | apple: ['apl', primitives.Str], 59 | orange: primitives.Num, 60 | mango: primitives.ListNum, 61 | octarine: primitives.ListStr, 62 | octopus: primitives.ListDat, 63 | cat: ['c', primitives.Dat], 64 | }); 65 | 66 | var decoded = serializer.decode({ 67 | apl: 'a', 68 | orange: '1', 69 | mango: '1~2~3', 70 | octarine: 'a~b~c', 71 | //octopus: '20150602~20150602', 72 | //c: '20150602' 73 | }); 74 | 75 | expect(decoded).to.be.eql({ 76 | apple: 'a', 77 | orange: 1, 78 | mango: [1, 2, 3], 79 | octarine: ['a', 'b', 'c'], 80 | }); 81 | }); 82 | // 83 | // it('custom serializer', () => { 84 | // var urlSerializer = new CustomSerializer({ 85 | // a: primitives.Str, 86 | // b: primitives.Num, 87 | // c: primitives.ListNum, 88 | // d: primitives.ListStr, 89 | // e: primitives.ListDat, 90 | // f: primitives.Dat 91 | // }); 92 | 93 | // var urlStruct = new UrlStruct('/home/index'); 94 | // urlStruct.setSerializer(urlSerializer); 95 | // }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/components/url.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import * as primitives from '../../src/utils/primitives'; 4 | import t from 'tcomb'; 5 | 6 | import Url from '../../src/url'; 7 | 8 | describe('Url params', () => { 9 | var urlStruct = { 10 | id: primitives.Num, 11 | tags: primitives.ListNum, 12 | q: primitives.Str, 13 | }; 14 | 15 | var url = new Url('/api/user/:id', urlStruct); 16 | 17 | it('should encode correctly', () => { 18 | var encodedUrl = url.stringify({ 19 | id: 1, 20 | tags: [1, 2, 3], 21 | q: 'Yo', 22 | }); 23 | 24 | expect(encodedUrl).to.be.equal('/api/user/1?tags=1~2~3&q=Yo'); 25 | }); 26 | 27 | it('should decode correctly', () => { 28 | var decodedUrl = url.decode('/api/user/1?tags=1~2~3&q=Yo'); 29 | 30 | expect(decodedUrl).to.eql({ 31 | id: 1, 32 | tags: [1, 2, 3], 33 | q: 'Yo', 34 | }); 35 | }); 36 | 37 | it('should throw error if key does not exist at url strcut', () => { 38 | expect(() => 39 | urlParams.decode('/api/user/1?UNEXISTING_AT_STRUCT=1~2~3&q=Yo') 40 | ).to.throw(Error); 41 | }); 42 | 43 | it('should throw error if url is not match pattern', () => { 44 | expect(() => urlParams.decode('/test')).to.throw(Error); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/controller/controller-flow.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import ControllerFlow from '../../src/controller/controller-flow'; 4 | 5 | var controllerName = 'valent.controller'; 6 | var applicationName = 'valent'; 7 | var template = '
Hello World
'; 8 | 9 | class ValentController {} 10 | 11 | describe('Controller flow and model getters', () => { 12 | it('flow should be a function', () => { 13 | expect(ControllerFlow).to.be.a('function'); 14 | }); 15 | 16 | it('flow without controller name should throw Error', () => { 17 | expect(() => new ControllerFlow()).to.throw(Error); 18 | }); 19 | 20 | it('flow should throw exception if template and templateUrl exists (RouteFlow behaviour)', () => { 21 | var controllerFlow = new ControllerFlow(controllerName); 22 | 23 | expect(() => 24 | controllerFlow.template(template).templateUrl('/template/url.html') 25 | ).to.throw(Error); 26 | }); 27 | 28 | it('flow and model', () => { 29 | var controllerFlow = new ControllerFlow(controllerName); 30 | 31 | controllerFlow 32 | .at(applicationName) 33 | .src(ValentController) 34 | .url('/home/dashboard') 35 | .resolver('access.guest', () => true) 36 | .resolver('access.admin', () => false) 37 | .template(template); 38 | 39 | var controllerModel = controllerFlow.model; 40 | var routeModel = controllerModel.getRoute(); 41 | 42 | expect(controllerModel.hasRoute()).to.equal(true); 43 | expect(controllerModel.getApplicationName()).to.equal(applicationName); 44 | expect(controllerModel.getSource()).to.eql(ValentController); 45 | 46 | //expect(controllerModel.getDependencies()).to.be.an('array'); 47 | //expect(controllerModel.getDependencies()).to.eql(['access.guest', 'access.admin']); 48 | 49 | expect(routeModel.getApplicationName()).to.equal(applicationName); 50 | expect(routeModel.getUrls()).to.be.an('array'); 51 | expect(routeModel.getUrls()).to.eql(['/home/dashboard']); 52 | 53 | expect(routeModel.getResolvers()).to.be.an('object'); 54 | expect(routeModel.getResolvers()).have.all.keys([ 55 | 'access.guest', 56 | 'access.admin', 57 | ]); 58 | 59 | expect(routeModel.getTemplate()).to.be.equal(template); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/controller/controller-model.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import ControllerModel from '../../src/controller/controller-model'; 4 | import RouteModel from '../../src/route/route-model'; 5 | 6 | var controllerName = 'valent.controller'; 7 | var applicationName = 'valent'; 8 | var applicationName2 = 'valent.route'; 9 | 10 | var template = '
Hello World
'; 11 | 12 | describe('Controller model', () => { 13 | it('controller and route application name (first set controller model)', () => { 14 | var controllerModel = new ControllerModel('home.dashboard'); 15 | controllerModel.setApplicationName(applicationName); 16 | 17 | controllerModel.addUrl('/home/dashboard'); 18 | 19 | var routeModel = controllerModel.getRoute(); 20 | 21 | expect(routeModel.getApplicationName()).to.be.equal(applicationName); 22 | expect(controllerModel.getApplicationName()).to.be.equal(applicationName); 23 | }); 24 | 25 | it('controller and route application name (first create route for controller)', () => { 26 | var controllerModel = new ControllerModel('home.dashboard'); 27 | 28 | controllerModel.addUrl('/home/dashboard'); 29 | 30 | var routeModel = controllerModel.getRoute(); 31 | 32 | controllerModel.setApplicationName(applicationName); 33 | 34 | expect(routeModel.getApplicationName()).to.be.equal(applicationName); 35 | expect(controllerModel.getApplicationName()).to.be.equal(applicationName); 36 | }); 37 | 38 | it('controller and route as separated model', () => { 39 | var controllerModel = new ControllerModel('home.dashboard'); 40 | var routeModel = new RouteModel('home.dashboard'); 41 | 42 | controllerModel.setRoute(routeModel); 43 | controllerModel.setApplicationName(applicationName); 44 | 45 | expect(routeModel.getApplicationName()).to.be.equal(applicationName); 46 | expect(controllerModel.getApplicationName()).to.be.equal(applicationName); 47 | }); 48 | 49 | it('controller and route as separated model', () => { 50 | var controllerModel = new ControllerModel('home.dashboard'); 51 | var routeModel = new RouteModel('home.dashboard'); 52 | 53 | controllerModel.setApplicationName(applicationName); 54 | routeModel.setApplicationName(applicationName2); 55 | 56 | controllerModel.setRoute(routeModel); 57 | 58 | expect(controllerModel.getApplicationName()).to.be.equal(applicationName); 59 | expect(routeModel.getApplicationName()).to.be.equal(applicationName2); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | //--------- SERIALIZERS --------- 2 | import './serializers/serializer'; 3 | import './serializers/rename-serializer'; 4 | import './serializers/url-serializer'; 5 | import './serializers/coding-serializer'; 6 | 7 | // 8 | 9 | // import './utils/object-description'; 10 | import './utils/object-transition'; 11 | // 12 | // import './components/serializer'; 13 | import './components/url-struct'; 14 | import './components/url'; 15 | // 16 | // import './manager'; 17 | 18 | // import './route/route-config'; 19 | // import './route/route-flow'; 20 | // import './route/route-convert'; 21 | // import './route/route-url-struct'; 22 | 23 | // import './controller/controller-flow'; 24 | // import './controller/controller-model'; 25 | -------------------------------------------------------------------------------- /test/manager.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import manager from '../src/index'; 4 | import { Manager } from '../src/index'; 5 | 6 | import ControllerModel from '../src/controller/controller-model'; 7 | 8 | class CustomControllerModel extends ControllerModel {} 9 | 10 | describe('Manager', () => { 11 | it('should be an object if import default', () => { 12 | expect(Manager).to.be.a('function'); 13 | }); 14 | 15 | it('should be a function ect if import { Manager }', () => { 16 | expect(Manager).to.be.a('function'); 17 | }); 18 | 19 | it('should have methods to add components', () => { 20 | expect(manager.addController).to.be.a('function'); 21 | expect(manager.addDirective).to.be.a('function'); 22 | expect(manager.addFactory).to.be.a('function'); 23 | expect(manager.addRoute).to.be.a('function'); 24 | }); 25 | 26 | it('should check component instances', () => { 27 | var manager = new Manager(); 28 | var controller = {}; 29 | expect(() => manager.addController(controller)).to.throw(Error); 30 | 31 | var controllerModel = new ControllerModel('home.dashboard'); 32 | expect(() => manager.addController(controllerModel)).to.be.ok; 33 | 34 | var customControllerModel = new CustomControllerModel('home.dashboard'); 35 | expect(() => manager.addController(customControllerModel)).to.be.ok; 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/route/route-config.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { RouteConfig } from '../../src/route/route-config'; 4 | import routeConfig from '../../src/route/route-config'; 5 | 6 | describe('Route Config', () => { 7 | it('should be an object', () => { 8 | expect(routeConfig).to.be.an('object'); 9 | }); 10 | 11 | it('should have default values', () => { 12 | var routeConfig = new RouteConfig(); 13 | 14 | expect(routeConfig.getBase()).to.equal(null); 15 | expect(routeConfig.isHtml5Mode()).to.equal(true); 16 | expect(routeConfig.getOtherwise()).to.equal(null); 17 | }); 18 | 19 | it('should return correct custom setting values', () => { 20 | var routeConfig = new RouteConfig(); 21 | 22 | routeConfig.setBase('/test'); 23 | routeConfig.disableHtml5Mode(); 24 | 25 | routeConfig.addResolver('access.guest', () => {}); 26 | routeConfig.addResolver('access.admin', () => {}); 27 | 28 | expect(routeConfig.getBase()).to.equal('/test'); 29 | expect(routeConfig.isHtml5Mode()).to.equal(false); 30 | 31 | expect(routeConfig.getResolvers()).to.be.an('object'); 32 | expect(routeConfig.getResolvers()).have.all.keys([ 33 | 'access.guest', 34 | 'access.admin', 35 | ]); 36 | }); 37 | 38 | it('should accept otherwise as string', () => { 39 | var routeConfig = new RouteConfig(); 40 | 41 | routeConfig.setOtherwise('/404.html'); 42 | 43 | expect(routeConfig.getOtherwise()).to.eql('/404.html'); 44 | }); 45 | 46 | it('should accept otherwise as RouteModel instance', () => { 47 | var routeConfig = new RouteConfig(); 48 | 49 | routeConfig.setOtherwise('/404.html'); 50 | 51 | expect(routeConfig.getOtherwise()).to.eql('/404.html'); 52 | }); 53 | 54 | it('should not accept otherwise as object', () => { 55 | var routeConfig = new RouteConfig(); 56 | 57 | expect(() => 58 | routeConfig.setOtherwise({ 59 | template: '404.html', 60 | controller: 'application.not-found.controller', 61 | }) 62 | ).to.throw(Error); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/route/route-convert.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import RouteFlow from '../../src/route/route-flow'; 4 | import RouteConvert from '../../src/angular/converters/route-converter'; 5 | import { RouteConfig } from '../../src/route/route-config'; 6 | 7 | var controllerName = 'valent.controller'; 8 | var template = '
Hello World
'; 9 | 10 | describe('Route converter', () => { 11 | it('should convert route config with template', () => { 12 | var routeFlow = new RouteFlow(controllerName); 13 | 14 | routeFlow 15 | .url('/home') 16 | .resolver('access.guest', () => 'granted') 17 | .template(template); 18 | 19 | var model = routeFlow.model; 20 | 21 | var config = RouteConvert.getConfig(model); 22 | expect(config).have.all.keys([ 23 | 'controller', 24 | 'reloadOnSearch', 25 | 'resolve', 26 | 'template', 27 | ]); 28 | 29 | expect(config.controller).to.equal(controllerName); 30 | expect(config.reloadOnSearch).to.equal(false); 31 | expect(config.resolve).to.be.an('object'); 32 | expect(config.resolve).to.have.all.keys(['access.guest']); 33 | expect(config.template).to.equal(template); 34 | }); 35 | 36 | it('should convert route config with templateUrl', () => { 37 | var routeFlow = new RouteFlow(controllerName); 38 | 39 | routeFlow 40 | .url('/home') 41 | .resolver('access.guest', () => 'granted') 42 | .templateUrl('/template/url.html'); 43 | 44 | var model = routeFlow.model; 45 | 46 | var config = RouteConvert.getConfig(model); 47 | expect(config).have.all.keys([ 48 | 'controller', 49 | 'reloadOnSearch', 50 | 'resolve', 51 | 'templateUrl', 52 | ]); 53 | 54 | expect(config.templateUrl).to.equal('/template/url.html'); 55 | }); 56 | 57 | it('should throw error if there are no template or templateUrl', () => { 58 | var routeFlow = new RouteFlow(controllerName); 59 | 60 | routeFlow.url('/home'); 61 | 62 | var model = routeFlow.model; 63 | 64 | expect(() => RouteConvert.getConfig(model)).to.throw(Error); 65 | }); 66 | 67 | it('should convert otherwise as string', () => { 68 | expect(RouteConvert.convertOtherwise('/404.html')).to.eql({ 69 | redirectTo: '/404.html', 70 | }); 71 | }); 72 | 73 | it('should throw exception if otherwise is in wrong format', () => { 74 | expect(() => RouteConvert.convertOtherwise([1, 2, 3])).to.throw(Error); 75 | expect(() => RouteConvert.convertOtherwise({ a: 1 })).to.throw(Error); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/route/route-flow.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import RouteFlow from '../../src/route/route-flow'; 4 | 5 | var controllerName = 'valent.controller'; 6 | var applicationName = 'valent'; 7 | var template = '
Hello World
'; 8 | 9 | describe('Route flow', () => { 10 | it('should be a function', () => { 11 | expect(RouteFlow).to.be.a('function'); 12 | }); 13 | 14 | it('should contain RouteModel', () => { 15 | var routeFlow = new RouteFlow(controllerName); 16 | expect(routeFlow.model).to.be.an('object'); 17 | }); 18 | 19 | it('should throw Error is controller name is not described', () => { 20 | expect(() => new RouteFlow()).to.throw(Error); 21 | }); 22 | 23 | it('should throw exception if both template and templateUrl are defined', () => { 24 | var routeFlow = new RouteFlow(controllerName); 25 | 26 | expect(() => 27 | routeFlow.template(template).templateUrl('/template/url.html') 28 | ).to.throw(Error); 29 | }); 30 | 31 | it('should contain RouteModel with correct values', () => { 32 | var routeFlow = new RouteFlow(controllerName); 33 | 34 | routeFlow 35 | .at(applicationName) 36 | .url('/home') 37 | .url('/home/:state') 38 | .resolver('access.guest', () => 'granted') 39 | .resolver('access.admin', () => 'rejected') 40 | .template(template); 41 | 42 | var model = routeFlow.model; 43 | 44 | expect(model.getUrls()).to.an('array'); 45 | expect(model.getUrls()).to.eql(['/home', '/home/:state']); 46 | expect(model.getResolvers()).to.be.an('object'); 47 | expect(model.getResolvers()).have.all.keys([ 48 | 'access.guest', 49 | 'access.admin', 50 | ]); 51 | expect(model.getTemplate()).to.equal(template); 52 | }); 53 | 54 | // it('should correctly setup custom urlBuilder', () => { 55 | // var routeFlow = new RouteFlow(controllerName); 56 | // 57 | // routeFlow 58 | // .url('/home') 59 | // .urlBuilder((name) => { 60 | // return `/user/${name}`; 61 | // }); 62 | // 63 | // var model = routeFlow.model; 64 | // 65 | // var urlBuilder = model.getUrlBuilder(); 66 | // 67 | // expect(urlBuilder('valent')).to.equal('/user/valent'); 68 | // }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/route/route-url-struct.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import * as primitives from '../../src/utils/primitives'; 4 | import t from 'tcomb'; 5 | 6 | import RouteFlow from '../../src/route/route-flow'; 7 | import RouteConvert from '../../src/angular/converters/route-converter'; 8 | 9 | import Url from '../../src/components/url'; 10 | 11 | describe('Route url struct', () => { 12 | var urlSerializer = new Url('/test/serializer', { 13 | id: primitives.Num, 14 | name: primitives.Str, 15 | }); 16 | 17 | Url.clear(); 18 | Url.add('test.route', urlSerializer); 19 | 20 | it('should work encode correctly', () => { 21 | var url = Url.get('test.route'); 22 | 23 | expect( 24 | url.stringify({ 25 | id: 1, 26 | name: 'sads', 27 | }) 28 | ).to.be.equal('/test/serializer?id=1&name=sads'); 29 | }); 30 | 31 | it('should work decode correctly', () => { 32 | var url = Url.get('test.route'); 33 | 34 | expect(url.decode('/test/serializer?id=1&name=sads')).to.be.eql({ 35 | id: 1, 36 | name: 'sads', 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/serializers/coding-serializer.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import testUtils from '../test-utils'; 3 | import CodingSerializer from '../../src/serializers/coding-serializer'; 4 | import * as primitives from '../../src/utils/primitives'; 5 | 6 | describe('CodingSerializer', () => { 7 | describe('addAlias', () => { 8 | it('should add alias', () => { 9 | CodingSerializer.addAlias('string', primitives.Str); 10 | expect(CodingSerializer.getStruct('string')).to.be.eql(primitives.Str); 11 | }); 12 | 13 | it('should add rules', () => { 14 | var rules = { 15 | encode: value => { 16 | return value; 17 | }, 18 | decode: value => { 19 | return value; 20 | }, 21 | }; 22 | CodingSerializer.addAlias('int', primitives.Int, rules); 23 | expect(CodingSerializer.getRule('int')).to.be.eql(rules); 24 | }); 25 | 26 | it('should throw error if alias is not String', () => { 27 | for (let key of testUtils.getTestDataFor('str')) { 28 | expect(() => { 29 | CodingSerializer.addAlias(key, primitives.Str, { 30 | encode: () => {}, 31 | decode: () => {}, 32 | }); 33 | }).to.throw(Error); 34 | } 35 | }); 36 | 37 | it('should throw error if struct is not an object', () => { 38 | for (let key of testUtils.getTestDataFor(['obj', 'array', 'fn'])) { 39 | expect(() => { 40 | CodingSerializer.addAlias('fakeAlias', key, { 41 | encode: () => {}, 42 | decode: () => {}, 43 | }); 44 | }).to.throw(Error); 45 | } 46 | }); 47 | 48 | it('should throw error if encode or decode rule is not specified', () => { 49 | var testRules = testUtils.getTestDataFor(['undef']); 50 | testRules.push({ encode: () => {} }); 51 | testRules.push({ decode: () => {} }); 52 | testRules.push({ encode: '', decode: () => {} }); 53 | for (let rules of testRules) { 54 | expect(() => { 55 | CodingSerializer.addAlias('fakeAlias', primitives.Str, rules); 56 | }).to.throw(Error); 57 | } 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/serializers/rename-serializer.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import testUtils from '../test-utils'; 3 | import RenameSerializer from '../../src/serializers/rename-serializer'; 4 | //import t from 'tcomb'; 5 | import * as primitives from '../../src/utils/primitives'; 6 | 7 | describe('RenameSerializer', () => { 8 | var struct = { 9 | someKey: ['s', primitives.ListNum], 10 | otherKey: ['o', primitives.Str], 11 | }; 12 | var serializer = new RenameSerializer(struct); 13 | 14 | serializer.addRule(primitives.ListNum, { 15 | encode: value => { 16 | value.push('!'); 17 | return value; 18 | }, 19 | decode: value => { 20 | value.pop(); 21 | return value; 22 | }, 23 | }); 24 | 25 | serializer.addRule(primitives.Str, { 26 | encode: value => { 27 | return '-' + value; 28 | }, 29 | decode: value => { 30 | return value.substr(1); 31 | }, 32 | }); 33 | 34 | it('should throw if called without params', () => { 35 | expect(() => { 36 | new RenameSerializer(); 37 | }).to.throw(Error); 38 | }); 39 | 40 | it('should have defaults', () => { 41 | expect(serializer.renameOptions).to.be.an('Object'); 42 | }); 43 | 44 | describe('#getOriginalName', () => { 45 | it('should get original name', () => { 46 | expect(serializer.getOriginalName('s')).to.be.eql('someKey'); 47 | expect(serializer.getOriginalName('sk')).to.be.eql(null); 48 | }); 49 | 50 | it('should throw if "renamed" is not a string', () => { 51 | for (let wrongKey of testUtils.getTestDataFor('str')) { 52 | expect(() => { 53 | serializer.getOriginalName(wrongKey); 54 | }).to.throw(Error); 55 | } 56 | }); 57 | }); 58 | 59 | describe('#encode', () => { 60 | it('should encode correctly', () => { 61 | var params = { 62 | someKey: [1, 2, 3], 63 | otherKey: 'Foo', 64 | }; 65 | 66 | expect(serializer.encode(params)).to.be.eql({ 67 | s: [1, 2, 3, '!'], 68 | o: '-Foo', 69 | }); 70 | }); 71 | 72 | it('should throw if struct is wrong', () => { 73 | expect(() => { 74 | serializer.encode({ someKey: [1, ''] }); 75 | }).to.throw(Error); 76 | }); 77 | }); 78 | 79 | describe('#decode', () => { 80 | it('should decode correctly', () => { 81 | expect( 82 | serializer.decode({ 83 | s: [1, 2, 3, '!'], 84 | o: '-Foo', 85 | }) 86 | ).to.be.eql({ 87 | someKey: [1, 2, 3], 88 | otherKey: 'Foo', 89 | }); 90 | }); 91 | 92 | it('should throw if struct is wrong', () => { 93 | expect(() => { 94 | serializer.encode({ someKey: [1, ''] }); 95 | }).to.throw(Error); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/serializers/serializer.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import testUtils from '../test-utils'; 3 | import Serializer from '../../src/serializers/serializer'; 4 | import * as primitives from '../../src/utils/primitives'; 5 | 6 | import t from 'tcomb'; 7 | 8 | describe('Serializer', () => { 9 | var struct = { 10 | id: primitives.Num, 11 | options: primitives.ListStr, 12 | }; 13 | 14 | let serializer = new Serializer(struct); 15 | 16 | serializer.addRule(t.Num, { 17 | encode: value => { 18 | return `@${value}`; 19 | }, 20 | decode: value => { 21 | return parseInt(value.slice(1)); 22 | }, 23 | }); 24 | 25 | serializer.addRule(primitives.ListStr, { 26 | encode: value => { 27 | return value 28 | .map(item => { 29 | return item; 30 | }) 31 | .join('~'); 32 | }, 33 | decode: value => { 34 | return value.split('~').map(item => { 35 | return item; 36 | }); 37 | }, 38 | }); 39 | 40 | it('should have defaults', () => { 41 | expect(serializer.getStruct()).to.be.eql(struct); 42 | expect(serializer.getRules()).to.instanceof(WeakMap); 43 | }); 44 | 45 | describe('addRule', () => { 46 | var srlz = new Serializer(struct); 47 | 48 | it('should add rule correctly', () => { 49 | srlz.addRule(t.Num, { 50 | encode: value => { 51 | return `@${value}`; 52 | }, 53 | decode: value => { 54 | return parseInt(value.slice(1)); 55 | }, 56 | }); 57 | }); 58 | 59 | it('should throw error if encode rule is not specified', () => { 60 | expect(() => { 61 | srlz.addRule(t.Str, { 62 | encode: value => { 63 | return `@${value}`; 64 | }, 65 | }); 66 | }).to.throw(Error); 67 | }); 68 | 69 | it('should throw error if decode rule is not specified', () => { 70 | expect(() => { 71 | srlz.addRule(t.Str, { 72 | decode: value => { 73 | return parseInt(value.slice(1)); 74 | }, 75 | }); 76 | }).to.throw(Error); 77 | }); 78 | 79 | it('should throw error if namespace is not specified', () => { 80 | expect(() => { 81 | srlz.addRule(); 82 | }).to.throw(Error); 83 | }); 84 | }); 85 | 86 | describe('encode', () => { 87 | it('should encode correctly', () => { 88 | var encoded = serializer.encode({ id: 5, options: ['a', 'b', 'c'] }); 89 | 90 | expect(encoded).to.be.eql({ 91 | id: '@5', 92 | options: 'a~b~c', 93 | }); 94 | }); 95 | 96 | it('should encode params with extra key correctly', () => { 97 | expect(serializer.encode({ id: 3, wrongKey: [] })).to.be.eql({ 98 | id: '@3', 99 | }); 100 | }); 101 | 102 | it('should throw error if struct param type is wrong', () => { 103 | expect(() => { 104 | serializer.encode({ id: 3, options: '' }); 105 | }).to.throw(Error); 106 | }); 107 | }); 108 | 109 | describe('decode', () => { 110 | it('should decode correctly', () => { 111 | expect( 112 | serializer.decode({ 113 | id: '@5', 114 | options: 'a~b~c', 115 | }) 116 | ).to.be.eql({ id: 5, options: ['a', 'b', 'c'] }); 117 | }); 118 | 119 | it('should decode string with extra key correctly', () => { 120 | expect(serializer.decode({ id: '@2', wrongKey: '@2' })).to.be.eql({ 121 | id: 2, 122 | }); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/serializers/url-serializer.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import cloneDeep from 'lodash/cloneDeep'; 3 | import testUtils from '../test-utils'; 4 | import UrlSerializer from '../../src/serializers/url-serializer'; 5 | import * as primitives from '../../src/utils/primitives'; 6 | 7 | describe('UrlSerializer', () => { 8 | var struct = { 9 | id: primitives.Num, 10 | foo: ['f', primitives.ListNum], 11 | bar: ['b', primitives.Str], 12 | }; 13 | 14 | var serializer = new UrlSerializer(struct); 15 | 16 | var original = { 17 | id: 42, 18 | foo: [1, 2, 3, 5, 8, 13, 21, 34], 19 | bar: 'param', 20 | }; 21 | 22 | var converted = { 23 | id: '42', 24 | f: '1~2~3~5~8~13~21~34', 25 | b: 'param', 26 | }; 27 | 28 | describe('encode', () => { 29 | it('should encode params to link correctly', () => { 30 | expect(serializer.encode(original)).to.be.eql(converted); 31 | }); 32 | 33 | it('should ignore not provided in struct keys', () => { 34 | var fakeOriginal = cloneDeep(original); 35 | fakeOriginal.extra = ''; 36 | expect(serializer.encode(fakeOriginal)).to.be.eql(converted); 37 | }); 38 | 39 | it('should throw error if struct is wrong', () => { 40 | var fakeOriginal = cloneDeep(original); 41 | fakeOriginal.id = 'WROOOONG!!'; 42 | 43 | expect(() => { 44 | serializer.encode(fakeOriginal); 45 | }).to.throw(Error); 46 | }); 47 | }); 48 | 49 | describe('decode', () => { 50 | it('should decode link correctly', () => { 51 | expect(serializer.decode(converted)).to.be.eql(original); 52 | }); 53 | 54 | it('should throw error if extra key', () => { 55 | var fakeConverted = cloneDeep(converted); 56 | fakeConverted.extra = ''; 57 | expect(() => { 58 | serializer.decode(fakeConverted); 59 | }).to.throw(Error); 60 | }); 61 | 62 | it('should throw error if struct is wrong', () => { 63 | var fakeConverted = cloneDeep(converted); 64 | fakeConverted.id = []; 65 | expect(() => { 66 | serializer.decode(fakeConverted); 67 | }).to.throw(Error); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | import without from 'lodash/without'; 2 | import isString from 'lodash/isString'; 3 | 4 | function getTestDataFor(types = '') { 5 | var typesArr = []; 6 | if (isString(types)) { 7 | typesArr = [types]; 8 | } else { 9 | typesArr = types; 10 | } 11 | 12 | var data = { 13 | array: ['a', 'b', 'c'], 14 | obj: { a: 'b' }, 15 | int: 1, 16 | bool: true, 17 | fn: () => {}, 18 | str: 'some', 19 | zero: 0, 20 | nl: null, 21 | undef: undefined, 22 | }; 23 | 24 | var testDataTypes = Object.values(data); 25 | for (let type of typesArr) { 26 | if (Object.keys(data).indexOf(type) > -1) { 27 | testDataTypes = without(testDataTypes, data[type]); 28 | } 29 | } 30 | return testDataTypes; 31 | } 32 | 33 | module.exports = { 34 | getTestDataFor: getTestDataFor, 35 | }; 36 | -------------------------------------------------------------------------------- /test/utils/object-description.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import ObjectDescription from '../../src/utils/object-description'; 4 | 5 | describe('Object description', () => { 6 | it('should create objects correctly', () => { 7 | var t1 = { 8 | 'foo.bar': 1, 9 | 'a.b.c.d': 2, 10 | }; 11 | var t2 = { 12 | 'bar.foo': 3, 13 | }; 14 | 15 | var t3 = { 16 | 'foo.bar': 2, 17 | }; 18 | 19 | var o1 = ObjectDescription.create(t1, t2); 20 | var o2 = ObjectDescription.create(t2, t3); 21 | var o3 = ObjectDescription.create(t1, t2, t3); 22 | 23 | expect(o1).to.eql({ 24 | foo: { 25 | bar: 1, 26 | }, 27 | a: { 28 | b: { 29 | c: { 30 | d: 2, 31 | }, 32 | }, 33 | }, 34 | bar: { 35 | foo: 3, 36 | }, 37 | }); 38 | 39 | expect(o2).to.eql({ 40 | bar: { 41 | foo: 3, 42 | }, 43 | foo: { 44 | bar: 2, 45 | }, 46 | }); 47 | 48 | expect(o3).to.eql({ 49 | foo: { 50 | bar: 2, 51 | }, 52 | a: { 53 | b: { 54 | c: { 55 | d: 2, 56 | }, 57 | }, 58 | }, 59 | bar: { 60 | foo: 3, 61 | }, 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/utils/object-transition.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import ObjectTransition from '../../src/utils/object-transition'; 4 | 5 | describe('Object transition', () => { 6 | it('should not change source object without .push()', () => { 7 | var o1 = {}; 8 | var objectTransition = new ObjectTransition(o1); 9 | 10 | objectTransition.commit('foo.bar', 1); 11 | objectTransition.commit('test', 2); 12 | 13 | expect(o1).to.eql({}); 14 | }); 15 | 16 | it('should change source object after .push()', () => { 17 | var o1 = {}; 18 | var objectTransition = new ObjectTransition(o1); 19 | 20 | objectTransition.commit('foo.bar', 1); 21 | objectTransition.commit('test', 2); 22 | 23 | objectTransition.push(); 24 | 25 | expect(o1).to.eql({ 26 | foo: { 27 | bar: 1, 28 | }, 29 | test: 2, 30 | }); 31 | }); 32 | 33 | it('should change source object after .push() with arguments', () => { 34 | var o1 = {}; 35 | var objectTransition = new ObjectTransition(o1); 36 | 37 | objectTransition.push('foo.bar', 3); 38 | 39 | expect(o1).to.eql({ 40 | foo: { 41 | bar: 3, 42 | }, 43 | }); 44 | }); 45 | 46 | it('should clear all commits before push if .clear() was called', () => { 47 | var o1 = {}; 48 | var objectTransition = new ObjectTransition(o1); 49 | 50 | objectTransition.commit('foo.bar', 1); 51 | objectTransition.commit('test', 2); 52 | 53 | objectTransition.clear(); 54 | 55 | objectTransition.push(); 56 | 57 | expect(o1).to.eql({}); 58 | }); 59 | 60 | it('should check if commits contains key and return boolean', () => { 61 | var o1 = {}; 62 | var objectTransition = new ObjectTransition(o1); 63 | 64 | objectTransition.commit('foo.bar', 1); 65 | objectTransition.commit('test', 2); 66 | 67 | expect(objectTransition.has('foo.bar')).to.equal(true); 68 | expect(objectTransition.has('test')).to.equal(true); 69 | expect(objectTransition.has('foo2')).to.equal(false); 70 | }); 71 | 72 | it('should return value by key in commits', () => { 73 | var o1 = {}; 74 | var objectTransition = new ObjectTransition(o1); 75 | 76 | objectTransition.commit('foo.bar', 1); 77 | objectTransition.commit('test', 2); 78 | 79 | expect(objectTransition.get('foo.bar')).to.equal(1); 80 | expect(objectTransition.get('test')).to.equal(2); 81 | expect(objectTransition.get('foo2')).to.equal(undefined); 82 | }); 83 | }); 84 | --------------------------------------------------------------------------------