├── .editorconfig ├── .gitignore ├── AvoidReactiveProgramming.md ├── ProblemsWithReactiveProgrammingInAngular.md ├── README.md ├── angular.json ├── avoid-leverage-observables.png ├── avoid-observables-cover.png ├── browserslist ├── draft └── handling-conditions.component.ts ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── ex1-http.png ├── ex2-route-params.png ├── ex3-store.png ├── ex4-store-and-http.png ├── karma.conf.js ├── mix-styles.png ├── package-lock.json ├── package.json ├── projects └── ng-re │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── core │ │ │ ├── get-property-subject.ts │ │ │ ├── invalid_pipe_argument_error.ts │ │ │ ├── isZoneLess.ts │ │ │ └── state-default.ts │ │ ├── hook$ │ │ │ ├── hook$.decorator.ts │ │ │ └── operators │ │ │ │ └── selectChange.ts │ │ ├── host-listener$ │ │ │ └── host-listener$.decorator.ts │ │ ├── input$ │ │ │ └── input$.decorator.ts │ │ ├── let │ │ │ └── let.directive.ts │ │ ├── local-state │ │ │ ├── local-state.ts │ │ │ └── operators │ │ │ │ └── selectSlice.ts │ │ ├── ng-re.module.ts │ │ └── push$ │ │ │ ├── async$.pipe.ts │ │ │ ├── operators │ │ │ └── detectChanges.ts │ │ │ ├── push$.pipe.spec.ts │ │ │ └── push$.pipe.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── src ├── app │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routes.ts │ └── components │ │ ├── avoid-reactivity-container │ │ ├── avoid-reactivity-container.component.ts │ │ ├── avoid-reactivity-rx-subscription.component.ts │ │ ├── avoid-reactivity-subscription.component.ts │ │ ├── avoid-reactivity-timing.component.ts │ │ ├── index.ts │ │ ├── routes.ts │ │ └── tick.service.ts │ │ ├── from-view-event-container │ │ ├── from-view-event-container.component.ts │ │ ├── from-view-event.component.ts │ │ ├── index.ts │ │ └── routes.ts │ │ ├── hook$-container │ │ ├── dummy.service.ts │ │ ├── full-example-container.component.ts │ │ ├── full-exmple.component.ts │ │ ├── hook$-container.component.ts │ │ ├── index.ts │ │ ├── routes.ts │ │ ├── select-change-container.component.ts │ │ ├── select-change.component.ts │ │ └── service-life-cycle-contaier.component.ts │ │ ├── host-listener-container │ │ ├── host-listener-container.component.ts │ │ ├── host-listener.component.ts │ │ ├── index.ts │ │ └── routes.ts │ │ ├── input-container │ │ ├── index.ts │ │ ├── input-container.component.ts │ │ ├── input.component.ts │ │ ├── input2.component.ts │ │ └── routes.ts │ │ ├── let-directive-container │ │ ├── full-example.component.ts │ │ ├── index.ts │ │ ├── let-directive-container.component.ts │ │ ├── observable-channels.component.ts │ │ ├── routes.ts │ │ ├── supported-syntax.component.ts │ │ └── value.component.ts │ │ ├── local-state-container │ │ ├── creation-and-clean-up │ │ │ └── creation-and-clean-up-container.component.ts │ │ ├── early-producer │ │ │ ├── early-producer-container.component.ts │ │ │ └── example.service.ts │ │ ├── full-example-container │ │ │ ├── child-local-state-container.component.ts │ │ │ ├── components │ │ │ │ ├── display-compoent-state.interface.ts │ │ │ │ ├── options-state.ts │ │ │ │ ├── options.component.ts │ │ │ │ └── table.component.ts │ │ │ ├── full-example-container.component.ts │ │ │ ├── local-state-container2.component.ts │ │ │ ├── map-to-Attendees-with-selection-filtered.ts │ │ │ └── services │ │ │ │ ├── local-state-component.facade.ts │ │ │ │ └── ng-rx-store.service.ts │ │ ├── index.ts │ │ ├── late-subscribers │ │ │ ├── example.service.ts │ │ │ ├── late-subscriber.component.ts │ │ │ └── late-subscribers-container.component.ts │ │ ├── local-state-container.component.ts │ │ ├── ng-for │ │ │ └── ng-for-container.component.ts │ │ ├── placeholder-content │ │ │ └── placeholder-content-container.component.ts │ │ ├── random.ts │ │ ├── routes.ts │ │ └── sharing-a-reference │ │ │ ├── sharing-a-reference-container.component.ts │ │ │ └── sharing-a-reference.component.ts │ │ ├── push-pipe-container │ │ ├── index.ts │ │ ├── push-pipe-channels.component.ts │ │ ├── push-pipe-container.component.ts │ │ ├── push-pipe.component.ts │ │ └── routes.ts │ │ └── star-rating │ │ ├── index.ts │ │ ├── routes.ts │ │ ├── star-rating-config.interface.ts │ │ ├── star-rating-container.component.ts │ │ ├── star-rating.component.ts │ │ └── star.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /AvoidReactiveProgramming.md: -------------------------------------------------------------------------------- 1 | # How to Avoid Observables in Angular 2 | 3 | ![](https://github.com/BioPhoton/ngRe/raw/master/avoid-observables-cover.png "How to Avoid Observables in Angular - Cover") 4 | 5 | Angular is an object-oriented framework. 6 | Even if there are a lot of things imperative some services and therefore also some third party libs, are reactive. 7 | This is great because it provides both approaches in one framework, which is at the moment a more or less unique thing. 8 | 9 | As reactive programming is hard for an imperative thinking mind, many people try to avoid reactive programming. 10 | This article will help you to understand how to avoid it and why it is even here at all. 11 | 12 | 13 | 14 | - [Comparing Basic Usecases](#comparing-basic-usecases) 15 | * [Retrieving data over HTTP](#retrieving-data-over-http) 16 | * [Retrieving values provided by Angular](#retrieving-values-provided-by-angular) 17 | * [Retrieving values provided by third parties](#retrieving-values-provided-by-third-parties) 18 | - [Patterns to avoid observables](#patterns-to-avoid-observables) 19 | * [Where to subscribe](#where-to-subscribe) 20 | * [Make it even easier](#make-it-even-easier) 21 | - [The reason for reactive programming](#the-reason-for-reactive-programming) 22 | * [Comparing composition](#comparing-composition) 23 | - [Summary](#summary) 24 | 25 | 26 | 27 | If you **DON'T** want to use a reactive approach in your component you 28 | should subscribe as soon as possible to the stream you want to get rid of and do the following things: 29 | - subscribe to a stream and assign incoming values to a component property 30 | - if necessary, unsubscribe the stream as soon as the component gets destroyed 31 | 32 | To elaborate with some more practical things we start with a part of angular that provides reactivity and try to avoid it. 33 | 34 | ## Comparing Basic Usecases 35 | 36 | In this section, we will get a good overview of some of the scenarios we get in touch with reactive programming in Angular. 37 | 38 | We will take a look at: 39 | - Reactive services provided by Angular 40 | - Cold and Hot Observables 41 | - Subscription handling 42 | 43 | And see the reactive and imperative approach in comparison. 44 | 45 | ### Retrieving values from cold observables 46 | 47 | Let's solve a very primitive example first. 48 | Retrieving data over HTTP and render it. 49 | 50 | ![](https://github.com/BioPhoton/ngRe/raw/master/ex1-http.png "Retrieving values from cold observables") 51 | 52 | 53 | We start with the reactive approach and then try to convert it into an imperative approach. 54 | 55 | **Leveraging Reactive Programming** 56 | ```typescript 57 | import { Component } from '@angular/core'; 58 | import { HttpClient } from '@angular/common/http'; 59 | import { map } from 'rxjs/operators'; 60 | 61 | @Component({ 62 | selector: 'example1-rx', 63 | template: ` 64 |

Example1 - Leverage Reactive Programming

65 | Http result: {{result | async}} 66 | ` 67 | }) 68 | export class Example1RxComponent { 69 | result; 70 | 71 | constructor(private http: HttpClient) { 72 | this.result = this.http.get('https://api.github.com/users/ReactiveX') 73 | .pipe(map((user: any) => user.login)); 74 | } 75 | 76 | } 77 | ``` 78 | 79 | Following things happen here: 80 | - subscribing to `http.get` by using the `async` pipe triggers: 81 | - a HTTP `get` request fires 82 | - we retrieve the result in the pipe and render it 83 | 84 | On the next change detection run, we will see the latest emitted value in the view. 85 | 86 | As observables from `HttpClient` are cold and they complete after the first emission we don't care about subscription handling here. 87 | 88 | **Avoid Reactive Programming** 89 | ```typescript 90 | import { Component } from '@angular/core'; 91 | import { HttpClient } from '@angular/common/http'; 92 | 93 | @Component({ 94 | selector: 'example1-im', 95 | template: ` 96 |

Example1 - Avoid Reactive Programming

97 | Http result: {{result}} 98 | ` 99 | }) 100 | export class Example1ImComponent { 101 | result; 102 | 103 | constructor(private http: HttpClient) { 104 | this.result = this.http.get('https://api.github.com/users/ReactiveX') 105 | .subscribe((user: any) => this.result = user.login); 106 | } 107 | 108 | } 109 | ``` 110 | 111 | Following things happen here: 112 | - subscribing to `http.get` in the constructor triggers: 113 | - a HTTP `get` request fires 114 | - we retrieve the result in subscribe function 115 | 116 | On the next change detection run, we will see the result in the view. 117 | 118 | As observables from `HttpClient` are cold and they complete after the first emission we don't care about subscription handling here. 119 | 120 | ### Retrieving values from hot observables provided by Angular 121 | 122 | Next, let's use a hot observable provided by angular the router params. 123 | 124 | Retrieving the route params, plucking out a single key and displaying its value in the view. 125 | 126 | ![](https://github.com/BioPhoton/ngRe/raw/master/ex2-router-params.png "Retrieving values from hot observables provided by Angular") 127 | 128 | Again we start with the reactive approach first. 129 | 130 | **Leveraging Reactive Programming** 131 | ```typescript 132 | import { Component} from '@angular/core'; 133 | import { ActivatedRoute} from '@angular/router'; 134 | import { map } from 'rxjs/operators'; 135 | 136 | @Component({ 137 | selector: 'example2-rx', 138 | template: ` 139 |

Example2 - Leverage Reactive Programming

140 | URL param: {{page | async}} 141 | ` 142 | }) 143 | export class Example2RxComponent { 144 | page; 145 | 146 | constructor(private route: ActivatedRoute) { 147 | this.page = this.route.params 148 | .pipe(map((params: any) => params.page)); 149 | } 150 | 151 | } 152 | ``` 153 | 154 | Following things happen here: 155 | - retrieving the new route params by using the `async` 156 | - deriving the values from the `page` param from `params` with a transformation operation using the `map` operator 157 | - by using the `async` pipe we: 158 | - subscribe to the observable on `AfterContentChecked` 159 | - applying the internal value to the next pipe return value 160 | 161 | On the next change detection run, we will see the latest emitted value in the view. 162 | 163 | If the component gets destroyed, 164 | the subscription that got set up in the `async` pipe before the first run of `AfterContentChecked` 165 | gets destroyed on the pipes `ngOnDestroy` [hook](https://github.com/angular/angular/blob/0119f46daf8f1efda00f723c5e329b0c8566fe07/packages/common/src/pipes/async_pipe.ts#L83). 166 | 167 | **Avoiding Reactive Programming** 168 | ```typescript 169 | import { Component} from '@angular/core'; 170 | import { ActivatedRoute} from '@angular/router'; 171 | 172 | @Component({ 173 | selector: 'example2-im', 174 | template: ` 175 |

Example2 - Avoid Reactive Programming

176 | URL param: {{page}} 177 | ` 178 | }) 179 | export class Example2ImComponent { 180 | page; 181 | 182 | constructor(private route: ActivatedRoute) { 183 | this.route.params 184 | .subscribe(params => this.page = params.page) 185 | } 186 | 187 | } 188 | ``` 189 | 190 | Following things happen here: 191 | - retrieving the new route params by subscribing in the constructor 192 | - deriving the values from the `page` param from `params` object directly 193 | 194 | On the next change detection run, we will see the latest emitted value in the view. 195 | 196 | Even if observables from `ActivatedRoute` are hot we don't care about subscription handling because this is managed by angular. 197 | 198 | ### Retrieving values from hot observables provided by third parties 199 | 200 | In this section, we take a look at a scenario not managed by the framework. 201 | For this example, I will use the `@ngrx/store` library and it's `Store` service. 202 | 203 | Retrieving state from the store and display its value in the view. 204 | 205 | ![](https://github.com/BioPhoton/ngRe/raw/master/ex3-store.png "Retrieving values from hot observables provided by third parties") 206 | 207 | **Leveraging Reactive Programming** 208 | ```typescript 209 | import { Component } from '@angular/core'; 210 | import { Store } from '@ngrx/store'; 211 | 212 | 213 | @Component({ 214 | selector: 'example3-rx', 215 | template: ` 216 |

Example3 - Leverage Reactive Programming

217 | Store value {{page | async}} 218 | ` 219 | }) 220 | export class Example3RxComponent { 221 | page; 222 | 223 | constructor(private store: Store) { 224 | this.page = this.store.select(s => s.page); 225 | } 226 | 227 | } 228 | ``` 229 | 230 | Following things happen here: 231 | - retrieving the new state by using the `async` pipe 232 | - deriving the values from the `page` param from `this.store` by using the `select` method 233 | - by using the `async` pipe we: 234 | - subscribe to the observable on `AfterContentChecked` 235 | - applying the internal value to the next pipe return value 236 | 237 | On the next change detection run, we will see the latest emitted value in the view. 238 | 239 | If the component gets destroyed angular manages the subscription over the `async` pipe. 240 | 241 | **Avoiding Reactive Programming** 242 | ```typescript 243 | import { Component, OnDestroy } from '@angular/core'; 244 | import { Store } from '@ngrx/store'; 245 | 246 | @Component({ 247 | selector: 'example3-im', 248 | template: ` 249 |

Example3 - Avoid Reactive Programming

250 | Store value {{page}} 251 | ` 252 | }) 253 | export class Example3ImComponent implements OnDestroy { 254 | subscription; 255 | page; 256 | 257 | constructor(private store: Store) { 258 | this.subscription = this.store.select(s => s.page) 259 | .subscribe(page => this.page = page); 260 | } 261 | 262 | ngOnDestroy() { 263 | this.subscription.unsubscribe(); 264 | } 265 | 266 | } 267 | ``` 268 | 269 | Following things happen here: 270 | - retrieving the new state by subscribing in the constructor 271 | - deriving the values from the `page` param from `this.store` by using the `select` method 272 | - we store the returned subscription from the `subscribe` call under `subscription` 273 | 274 | On the next change detection run, we will see the latest emitted value in the view. 275 | 276 | Here we have to manage the subscription in case the component gets destroyed. 277 | 278 | - when the component gets destroyed 279 | - we call `this.subscription.unsubscribe()` in the `ngOnDestroy` life-cycle hook. 280 | 281 | ## Patterns to avoid observables 282 | 283 | As these examples are very simple let me summarise the learning in with a broader view. 284 | 285 | ### Where to subscribe 286 | 287 | We saw that we subscribe to observables in different places. 288 | 289 | Let's get a quick overview of the different options where we could subscribe. 290 | 291 | 1. constructor 292 | 2. ngOnChanges 293 | 3. ngOnInit 294 | 4. ngAfterContentInit 295 | 5. ngAfterContentChecked 296 | 6. subscription over `async` pipe in the template 297 | 7. ngAfterViewInit 298 | 8. ngAfterViewChecked 299 | 9. ngOnDestroy 300 | 301 | If we take another look at the above code examples we realize that we put our subscription in the constructor to avoid reactive programming. 302 | And we put the subscription in the template when we leveraged reactive programming. 303 | 304 | This is the critical thing, the **subscription**. 305 | The subscription is the place where values "dripping" out of the observable. 306 | It's the moment we start to mutate the properties of a component in an imperative way. 307 | 308 | **In RxJS `.subscribe()` is where reactive programming ends.** 309 | 310 | So the worst thing you could do to avoid reactive programming is to use the `async` pipe. 311 | Let me give you a quick illustration of this learning: 312 | 313 | ![](https://github.com/BioPhoton/ngRe/raw/master/avoid-leverage-observables.png "Avoid Reactive Programming") 314 | 315 | So now we know the following: 316 | - If we want to **avoid** reactive programming we have to 317 | **subscribe as early as possible**, i. e. in the `constructor`. 318 | - If we want to **leverage** reactive programming we have to 319 | **subscribe as late as possible**, i. e. in the template. 320 | 321 | As the last thing to mention here is a thing that I discover a lot when I consult projects is mixing the styles. 322 | Until now I saw plenty of them and It was always a mess. 323 | 324 | So as a suggestion from my side tries to avoid mixing stales as good as possible. 325 | 326 | ![](https://github.com/BioPhoton/ngRe/raw/master/mix-styles.png "Mixing Styles") 327 | 328 | 329 | ### Make it even easier 330 | 331 | We realized that there is a bit of boilerplate to write to get the values out of the observable. 332 | In some cases, we also need to manage the subscription according to the component's lifetime. 333 | 334 | As this is annoying or even tricky, if we forget to unsubscribe, we could create some helper functions to do so. 335 | 336 | We could... But let's first look at some solutions out there. 337 | 338 | In recent times 2 people presented something that I call "binding an observable to a property", for angular components. 339 | 340 | [@MikeRyanDev](https://twitter.com/MikeRyanDev) presented the "connect" method in his presentation [Building with Ivy: rethinking reactive Angular](https://www.youtube.com/watch?v=rz-rcaGXhGk) 341 | and [@EliranEliassy](https://twitter.com/EliranEliassy) presented the "unsubscriber HOC" in his presentation [Everything you need to know about Ivy](https://www.youtube.com/watch?v=AKibI36WNhY) 342 | 343 | Both of them eliminate the need to assign incoming values to a component property as well as manage the subscription. 344 | The great thing about it is we can solve our problem with a one-liner and can switch to imperative programming without having any trouble. 345 | 346 | As both versions are written for `Ivy` I can inform you that there are also several other libs out there for `ViewEngine`. 347 | 348 | With this information, we could stop here and start avoiding reactive programming like a pro. ;) 349 | 350 | But let's have the last section to give a bit reason for reactive programming. 351 | 352 | ## The reason for reactive programming 353 | 354 | You may wonder why Angular introduced observables. 355 | Let me ask another question first, why do angular needs observables at all? 356 | 357 | Yes, that's right, why observables at all? 358 | 359 | I mean I have to admit I understand that the `Router` is observable based, but for example `HttpClient`, as single shot observable, or other things? 360 | Not really, right... But there is! 361 | 362 | Unified Subscription and Composition! 363 | 364 | **Observables are a unified interface for pull and push-based collections** 365 | 366 | 367 | Think about all the different APIs for asynchronous operations: 368 | - `setInterval` and `setInterval` 369 | - `addEventListener` and `removeEventListener` 370 | - `new Promise` and `your custom dispose logic` :) 371 | - `requestAnimationFrame` and `cancelAnimationFrame` 372 | 373 | When you start to use them together you will see that you and up soon in a big mess. 374 | If you start to compose them and have them be dependent on each other hell breaks lose. 375 | 376 | As this article is on avoiding reactive programming I keep the next example short and just show one **big benefit** of observables, **composition**. 377 | 378 | ### Comparing composition 379 | 380 | In this section, we will compose values from the `Store` with results from HTTP requests and render it in the template. 381 | As we want to avoid broken UI state we have to handle race-conditions. 382 | Also if the component gets destroyed while a request is pending we don't process the result anymore. 383 | 384 | ![](https://github.com/BioPhoton/ngRe/raw/master/ex4-store-and-http.png "Comparing composition") 385 | 386 | **Leveraging Reactive Programming** 387 | ```typescript 388 | import { Component } from '@angular/core'; 389 | import { HttpClient } from '@angular/common/http'; 390 | import { map, switchMap } from 'rxjs/operators'; 391 | import { Store } from '@ngrx/store'; 392 | 393 | @Component({ 394 | selector: 'example4-rx', 395 | template: ` 396 |

Example4 - Leverage Reactive Programming

397 | Repositories Page [{{page | async}}]: 398 | 401 | ` 402 | }) 403 | export class Example4RxComponent { 404 | page; 405 | names; 406 | 407 | constructor(private store: Store, private http: HttpClient) { 408 | this.page = this.store.select(s => s.page); 409 | this.names = this.page 410 | .pipe( 411 | switchMap(page => this.http.get(`https://api.github.com/orgs/ReactiveX/repos?page=${page}&per_page=5`)), 412 | map(res => res.map(i => i.name)) 413 | ); 414 | } 415 | 416 | } 417 | ``` 418 | 419 | Following things roughly happen here: 420 | - all values are retrieving by using the `async` pipe in the template 421 | - deriving the values from the `page` param from `this.store` by using the `select` method 422 | - deriving the HTTP result by combining the page observable with the HTTP observable 423 | - solving race conditions by using the `switchMap` operator 424 | - as all subscriptions are done by the `async` pipe we: 425 | - subscribe to all observables on `AfterContentChecked` 426 | - applying all arriving values to the pipes return value 427 | 428 | On the next change detection run, we will see the latest emitted value in the template. 429 | 430 | If the component gets destroyed angular manages the subscription over the `async` pipe. 431 | 432 | **Avoiding Reactive Programming** 433 | ```typescript 434 | import { Component, OnDestroy } from '@angular/core'; 435 | import { HttpClient } from '@angular/common/http'; 436 | import { Store } from '@ngrx/store'; 437 | import { Subscription } from 'rxjs'; 438 | 439 | @Component({ 440 | selector: 'example4-im', 441 | template: ` 442 |

Example4 - Avoid Reactive Programming

443 | Repositories Page [{{page}}]: 444 |
    445 |
  • {{name}}
  • 446 |
447 | ` 448 | }) 449 | export class Example4ImComponent implements OnDestroy { 450 | pageSub = new Subscription(); 451 | httpSub = new Subscription(); 452 | page; 453 | names; 454 | 455 | constructor(private store: Store, private http: HttpClient) { 456 | this.pageSub = this.store.select(s => s.page) 457 | .subscribe(page => { 458 | this.page = page; 459 | if(!this.httpSub.closed) { 460 | this.httpSub.unsubscribe(); 461 | } 462 | 463 | this.httpSub = this.http 464 | .get(`https://api.github.com/orgs/ReactiveX/repos?page=${this.page}&per_page=5`) 465 | .subscribe((res: any) => { 466 | this.names = res.map(i => i.name); 467 | }); 468 | }); 469 | } 470 | 471 | ngOnDestroy() { 472 | this.pageSub.unsubscribe(); 473 | this.httpSub.unsubscribe(); 474 | } 475 | 476 | } 477 | ``` 478 | 479 | Following things happen here: 480 | - retrieving the new state by subscribing in the constructor 481 | - deriving the values from the `page` param from `this.store` by using the `select` method call subscribe 482 | - we store the returned subscription from the `subscribe` call under `pageSub` 483 | - in the store subscription we: 484 | - assign the arriving value to the components `page` property 485 | - we take the page value and subscribe to `http.get` 486 | - a HTTP `get` request fires 487 | - we retrieve the result in subscribe function 488 | - to handle race conditions we: 489 | - check if a subscription is active we check the `httpSub.closed` property 490 | - if it is active we close it 491 | - if it is done, and the HTTP request already arrived, we do nothing 492 | - we store the returned subscription from the `subscribe` call under `httpSub` 493 | 494 | Here we have to manage the subscription in case the component gets destroyed. 495 | 496 | - when the component gets destroyed 497 | - we call `this.pageSub.unsubscribe()` in the `ngOnDestroy` life-cycle hook 498 | - we call `this.httpSub.unsubscribe()` in the `ngOnDestroy` life-cycle hook 499 | 500 | 501 | As we can see there is a difference in lines of code, indentations in the process description as well as differences in the complexity and maintainability of the code. 502 | 503 | When I would think well the whole subscription handling is a clutter of Observables and reactive programming I could do one thing. 504 | 505 | Not using it. 506 | 507 | Not using it and implementing the scenario without another **big benefit** of observables, **a unified API**. 508 | 509 | If we compare 510 | 511 | the reactive approach with the following different APIs: 512 | - `subscribe` and `unsubscribe` 513 | 514 | and the approach without observables with the following different APIs: 515 | - `addEventListener` and `removeEventListener` 516 | - `new Promise` and `your custom dispose of logic` 517 | 518 | and further, consider the **clean** implementation effort for those APIs in the race condition scenario 519 | it shows us 2 things: 520 | - how hard it is to compose asynchronous operations 521 | - and the benefit of a unified API and functional composition 522 | 523 | ## Summary 524 | 525 | What we learned about Reactive programming and observables is: 526 | - Avoid it by subscribing as early as possible (use helpers for easy-going) 527 | - Leverage it by subscribing as late as possible 528 | - **Don't mix it!** 529 | - Functional programming brings a lot of headaches and considers a leaning effort 530 | - JavaScript's APIs for asynchronous operations is inconsistent (lots of lines of code) 531 | - Imperative code gets complex with asynchronous operations (lots of how instructions) 532 | 533 | Condensed there are 2 main learning: 534 | - The **2 biggest benefits** of reactive programming are **a unified API** and **functional composition** 535 | - The **2 biggest constraints** of reactive programming are **a lot of headaches** and a **steep learning curve** 536 | -------------------------------------------------------------------------------- /ProblemsWithReactiveProgrammingInAngular.md: -------------------------------------------------------------------------------- 1 | # General Overview of Problems with Reactive Programming 2 | 3 | Angular in an object oriented framework with partially reactive approaches. 4 | It's one of the view frameworks that allow both, functional reactive and imperative programming. 5 | 6 | In the last years of working and using angular I encountered several problem people ran into when working with angular. 7 | 8 | - avoiding reactive programming 9 | - and leveraging reactive programming 10 | 11 | There is also a big teaching effort in showing people that they should choose, maybe on component level, if they want to create a reactive or a imperative component. 12 | But in any case not mixing those two approaches! 13 | 14 | In the next sections I wil point out general problems on which people ran into most. 15 | 16 | 17 | 18 | 19 | constructor 20 | ngOnChanges 21 | ngOnInit 22 | ngAfterContentInit 23 | ngAfterContentChecked 24 | subscription over `async` pipe in template 25 | ngAfterViewInit 26 | ngAfterViewChecked 27 | ngOnDestroy 28 | 29 | 30 | ## General Timing Issues 31 | 32 | As a lot of problems are related to timing issues this section is here to give a comülete overview of all the different types of issues. 33 | 34 | Two different problems are occurring in multiple different situations: 35 | - Late Subscriber Problem 36 | - Early Producer Problem 37 | 38 | ### The Late Subscriber Problem 39 | 40 | Incoming values arrive before the subscription has happened. 41 | 42 | For example state over `@Input()` decorators arrives before the view gets rendered and a used pipe could receive the value. 43 | 44 | ```typescript 45 | @Component({ 46 | selector: 'app-late-subscriber', 47 | template: ` 48 | {{state$ | async | json}} 49 | ` 50 | }) 51 | export class LateSubscriberComponent { 52 | state$ = new Subject(); 53 | 54 | @Input() 55 | set state(v) { 56 | this.state$.next(v); 57 | } 58 | 59 | } 60 | ``` 61 | 62 | We call this situation late subscriber problem. In this case, the view is a late subscribe to the values from '@Input()' properties. 63 | There are several situations from our previous explorations that have this problem: 64 | - [Input Decorators](Input-Decorators) 65 | - transporting values from `@Input` to `AfterViewInit` hook 66 | - transporting values from `@Input` to the view 67 | - transporting values from `@Input` to the constructor 68 | - [Component And Directive Life Cycle Hooks](Component-And-Directive-Life-Cycle-Hooks) 69 | - transporting `OnChanges` to the view 70 | - getting the state of any life cycle hook later in time (important when hooks are composed) 71 | - [Local State](Local-State) 72 | - transporting the current local state to the view 73 | - getting the current local state for other compositions 74 | 75 | **Solutions** 76 | 77 | All those problems boil down to 2 different solutions depending on the particular problem. 78 | - using `ReplaySubjects` with `bufferSize` of `1` to cache the latest sent value 79 | - using `shareReplay` for referential sharing as shown in [Sharing References over Observables](Sharing-References-over-Observables) 80 | 81 | 82 | ### The Early Producer Problem 83 | 84 | The subscription happens before any value can arrive. 85 | 86 | For example, subscriptions to view elements the constructor happen before they ever exist. 87 | We call this situation early subscriber problem. In this case, the component constructor is an early subscribe to the events from '(click)' bindings. 88 | 89 | All above decorators should rely on a generic way of wrapping a function or property as well as a way 90 | to configure the used Subject for multi-casting similar to [multicast](https://github.com/ReactiveX/rxjs/blob/a9fa9d421d69e6e07aec0fa835b273283f8a034c/src/internal/operators/multicast.ts#L34) 91 | 92 | In this way, it is easy to have a simplified public API but flexibility internally. 93 | 94 | **Decorators that:** 95 | - rely on configurable multi-casting similar to `multicast` operator. 96 | this provides a generic configurable way for all cases 97 | --- 98 | 99 | ## Sharing references 100 | 101 | TBD 102 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngRe-tests": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/ngRe-tests", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": false, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/styles.scss" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "fileReplacements": [ 38 | { 39 | "replace": "src/environments/environment.ts", 40 | "with": "src/environments/environment.prod.ts" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "extractCss": true, 47 | "namedChunks": false, 48 | "aot": true, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | } 58 | ] 59 | } 60 | } 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "options": { 65 | "browserTarget": "ngRe-tests:build" 66 | }, 67 | "configurations": { 68 | "production": { 69 | "browserTarget": "ngRe-tests:build:production" 70 | } 71 | } 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular-devkit/build-angular:extract-i18n", 75 | "options": { 76 | "browserTarget": "ngRe-tests:build" 77 | } 78 | }, 79 | "test": { 80 | "builder": "@angular-devkit/build-angular:karma", 81 | "options": { 82 | "main": "src/test.ts", 83 | "polyfills": "src/polyfills.ts", 84 | "tsConfig": "tsconfig.spec.json", 85 | "karmaConfig": "karma.conf.js", 86 | "assets": [ 87 | "src/favicon.ico", 88 | "src/assets" 89 | ], 90 | "styles": [ 91 | "src/styles.scss" 92 | ], 93 | "scripts": [] 94 | } 95 | }, 96 | "lint": { 97 | "builder": "@angular-devkit/build-angular:tslint", 98 | "options": { 99 | "tsConfig": [ 100 | "tsconfig.app.json", 101 | "tsconfig.spec.json", 102 | "e2e/tsconfig.json" 103 | ], 104 | "exclude": [ 105 | "**/node_modules/**" 106 | ] 107 | } 108 | }, 109 | "e2e": { 110 | "builder": "@angular-devkit/build-angular:protractor", 111 | "options": { 112 | "protractorConfig": "e2e/protractor.conf.js", 113 | "devServerTarget": "ngRe-tests:serve" 114 | }, 115 | "configurations": { 116 | "production": { 117 | "devServerTarget": "ngRe-tests:serve:production" 118 | } 119 | } 120 | } 121 | } 122 | }, 123 | "ngRe": { 124 | "projectType": "library", 125 | "root": "projects/ng-re", 126 | "sourceRoot": "projects/ng-re/src", 127 | "prefix": "ngrx", 128 | "architect": { 129 | "build": { 130 | "builder": "@angular-devkit/build-ng-packagr:build", 131 | "options": { 132 | "tsConfig": "projects/ng-re/tsconfig.lib.json", 133 | "project": "projects/ng-re/ng-package.json" 134 | } 135 | }, 136 | "test": { 137 | "builder": "@angular-devkit/build-angular:karma", 138 | "options": { 139 | "main": "projects/ng-re/src/test.ts", 140 | "tsConfig": "projects/ng-re/tsconfig.spec.json", 141 | "karmaConfig": "projects/ng-re/karma.conf.js" 142 | } 143 | }, 144 | "lint": { 145 | "builder": "@angular-devkit/build-angular:tslint", 146 | "options": { 147 | "tsConfig": [ 148 | "projects/ng-re/tsconfig.lib.json", 149 | "projects/ng-re/tsconfig.spec.json" 150 | ], 151 | "exclude": [ 152 | "**/node_modules/**" 153 | ] 154 | } 155 | } 156 | } 157 | }}, 158 | "defaultProject": "ngRe-tests" 159 | } 160 | -------------------------------------------------------------------------------- /avoid-leverage-observables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/ngRe/6c1cbd62f425d522eb36f172b7e64bb89a2a5e2f/avoid-leverage-observables.png -------------------------------------------------------------------------------- /avoid-observables-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/ngRe/6c1cbd62f425d522eb36f172b7e64bb89a2a5e2f/avoid-observables-cover.png -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /draft/handling-conditions.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {interval, Observable} from 'rxjs'; 3 | import {filter, map, share} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-let-directive-handling-conditions', 7 | template: ` 8 |

*ngrxLet Handle Conditions

9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 18 |
{{val1 | json}}
19 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | ` 34 | }) 35 | export class LetDirectiveHandlingConditionsComponent { 36 | 37 | boolean1$: Observable = this.getHotRandomBoolena(2000); 38 | 39 | constructor() { 40 | } 41 | 42 | getHotRandomBoolena(intVal: number = 1000): Observable { 43 | return interval(intVal) 44 | .pipe( 45 | map(_ => Math.random() < 0.5), 46 | share() 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to ngRe!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ex1-http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/ngRe/6c1cbd62f425d522eb36f172b7e64bb89a2a5e2f/ex1-http.png -------------------------------------------------------------------------------- /ex2-route-params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/ngRe/6c1cbd62f425d522eb36f172b7e64bb89a2a5e2f/ex2-route-params.png -------------------------------------------------------------------------------- /ex3-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/ngRe/6c1cbd62f425d522eb36f172b7e64bb89a2a5e2f/ex3-store.png -------------------------------------------------------------------------------- /ex4-store-and-http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/ngRe/6c1cbd62f425d522eb36f172b7e64bb89a2a5e2f/ex4-store-and-http.png -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/ngRe'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /mix-styles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/ngRe/6c1cbd62f425d522eb36f172b7e64bb89a2a5e2f/mix-styles.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-re", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "serve": "ng s", 7 | "serve-no-zone": "ng s -c=noZone", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e", 12 | "readme:gen-toc": "markdown-toc ./README.md -i" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~8.0.1", 17 | "@angular/common": "~8.0.1", 18 | "@angular/compiler": "~8.0.1", 19 | "@angular/core": "~8.0.1", 20 | "@angular/forms": "~8.0.1", 21 | "@angular/platform-browser": "~8.0.1", 22 | "@angular/platform-browser-dynamic": "~8.0.1", 23 | "@angular/router": "~8.0.1", 24 | "@ngrx/store": "^8.3.0", 25 | "faker": "^4.1.0", 26 | "rxjs": "~6.4.0", 27 | "tslib": "^1.9.0", 28 | "zone.js": "~0.9.1" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "~0.800.0", 32 | "@angular-devkit/build-ng-packagr": "~0.800.6", 33 | "@angular/cli": "~8.0.3", 34 | "@angular/compiler-cli": "~8.0.1", 35 | "@angular/language-service": "~8.0.1", 36 | "@types/node": "~8.9.4", 37 | "@types/jasmine": "~3.3.8", 38 | "@types/jasminewd2": "~2.0.3", 39 | "codelyzer": "^5.0.0", 40 | "jasmine-core": "~3.4.0", 41 | "jasmine-spec-reporter": "~4.2.1", 42 | "karma": "~4.1.0", 43 | "karma-chrome-launcher": "~2.2.0", 44 | "karma-coverage-istanbul-reporter": "~2.0.1", 45 | "karma-jasmine": "~2.0.1", 46 | "karma-jasmine-html-reporter": "^1.4.0", 47 | "markdown-toc": "^1.2.0", 48 | "ng-packagr": "^5.1.0", 49 | "protractor": "~5.4.0", 50 | "ts-node": "~7.0.0", 51 | "tsickle": "^0.35.0", 52 | "tslint": "~5.15.0", 53 | "typescript": "~3.4.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /projects/ng-re/README.md: -------------------------------------------------------------------------------- 1 | # NgRe 2 | 3 | This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.0.3. 4 | 5 | ## Code scaffolding 6 | 7 | Run `ng generate component component-name --project ngRe` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ngRe`. 8 | > Note: Don't forget to add `--project ngRe` or else it will be added to the default project in your `angular.json` file. 9 | 10 | ## Build 11 | 12 | Run `ng build ngRe` to build the project. The build artifacts will be stored in the `dist/` directory. 13 | 14 | ## Publishing 15 | 16 | After building your library with `ng build ngRe`, go to the dist folder `cd dist/ng-re` and run `npm publish`. 17 | 18 | ## Running unit tests 19 | 20 | Run `ng test ngRe` to execute the unit tests via [Karma](https://karma-runner.github.io). 21 | 22 | ## Further help 23 | 24 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 25 | -------------------------------------------------------------------------------- /projects/ng-re/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/ng-re'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/ng-re/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ng-re", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/ng-re/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-re", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^8.0.3", 6 | "@angular/core": "^8.0.3" 7 | } 8 | } -------------------------------------------------------------------------------- /projects/ng-re/src/lib/core/get-property-subject.ts: -------------------------------------------------------------------------------- 1 | import {AsyncSubject, BehaviorSubject, ReplaySubject, Subject} from 'rxjs'; 2 | 3 | export type subjectFactory = () => Subject; 4 | 5 | export function getSubjectFactory(): subjectFactory { 6 | return () => new Subject(); 7 | } 8 | 9 | export function behaviourSubjectFactory(init: T): subjectFactory { 10 | return () => new BehaviorSubject(init); 11 | } 12 | 13 | export function getReplaySubjectFactory(bufferSize = 1): subjectFactory { 14 | return () => new ReplaySubject(bufferSize); 15 | } 16 | 17 | export function getAsyncSubjectFactory(): subjectFactory { 18 | return () => new AsyncSubject(); 19 | } 20 | 21 | export function getPropertySubject( 22 | // tslint:disable-next-line 23 | objInstance: Object, 24 | property: PropertyKey, 25 | sFactory: () => Subject = getReplaySubjectFactory(1), 26 | subProperty: PropertyKey = '' 27 | ): Subject { 28 | if (subProperty === '') { 29 | return objInstance[property] || (objInstance[property] = sFactory()); 30 | } else { 31 | if (!objInstance[property]) { 32 | objInstance[property] = {}; 33 | } 34 | return objInstance[property][subProperty] || (objInstance[property][subProperty] = sFactory()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/core/invalid_pipe_argument_error.ts: -------------------------------------------------------------------------------- 1 | import {Type, ɵstringify as stringify} from '@angular/core'; 2 | 3 | export function invalidInputValueError(type: Type, value: Object) { 4 | return Error(`invalidInputValueError: '${value}' for directive '${stringify(type)}'`); 5 | } 6 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/core/isZoneLess.ts: -------------------------------------------------------------------------------- 1 | export function isZoneLess(c): boolean { 2 | return c.constructor.name === 'NoopNgZone'; 3 | } 4 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/core/state-default.ts: -------------------------------------------------------------------------------- 1 | export const STATE_DEFAULT = undefined; 2 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/hook$/hook$.decorator.ts: -------------------------------------------------------------------------------- 1 | import {ɵComponentDef as ComponentDef, ɵNG_COMPONENT_DEF as NG_COMPONENT_DEF} from '@angular/core'; 2 | import {getPropertySubject, getReplaySubjectFactory} from 'ng-re/lib/core/get-property-subject'; 3 | import {EMPTY, Observable, of, pipe, UnaryFunction} from 'rxjs'; 4 | import {catchError, take, takeUntil} from 'rxjs/operators'; 5 | 6 | export enum HookNames { 7 | afterContentChecked = 'afterContentChecked', 8 | afterContentInit = 'afterContentInit', 9 | afterViewChecked = 'afterViewChecked', 10 | afterViewInit = 'afterViewInit', 11 | doCheck = 'doCheck', 12 | onChanges = 'onChanges', 13 | onDestroy = 'onDestroy', 14 | onInit = 'onInit', 15 | } 16 | 17 | interface Hooks { 18 | afterContentChecked: string; 19 | afterContentInit: string; 20 | afterViewChecked: string; 21 | afterViewInit: string; 22 | doCheck: string; 23 | onChanges: string; 24 | onDestroy: string; 25 | onInit: string; 26 | } 27 | 28 | const hooksWrapped: { [x in keyof Hooks]: boolean } = { 29 | afterContentChecked: false, 30 | afterContentInit: false, 31 | afterViewChecked: false, 32 | afterViewInit: false, 33 | doCheck: false, 34 | onChanges: true, 35 | onDestroy: false, 36 | onInit: false 37 | }; 38 | 39 | const singleShotOperators = (destroy$: Observable): UnaryFunction => 40 | pipe(take(1), catchError(e => of()), takeUntil(destroy$)); 41 | const onGoingOperators = (destroy$: Observable): UnaryFunction => 42 | pipe(catchError(e => EMPTY), takeUntil(destroy$)); 43 | 44 | const getHooksOperatorsMap = (destroy$: Observable): { [x in keyof Hooks]: UnaryFunction } => ({ 45 | afterContentChecked: singleShotOperators(destroy$), 46 | afterContentInit: singleShotOperators(destroy$), 47 | afterViewChecked: onGoingOperators(destroy$), 48 | afterViewInit: singleShotOperators(destroy$), 49 | doCheck: onGoingOperators(destroy$), 50 | onChanges: onGoingOperators(destroy$), 51 | onDestroy: pipe(catchError(e => of()), take(1)), 52 | onInit: singleShotOperators(destroy$) 53 | }); 54 | 55 | export function Hook$(hookName: keyof Hooks): PropertyDecorator { 56 | return ( 57 | // tslint:disable-next-line 58 | component: Object, 59 | propertyKey: PropertyKey 60 | ) => { 61 | const keyUniquePerPrototype = Symbol('@ngRe-hook$'); 62 | const subjectFactory = getReplaySubjectFactory(1); 63 | 64 | const cDef: ComponentDef = component.constructor[NG_COMPONENT_DEF]; 65 | 66 | let target; 67 | let hook; 68 | let originalHook; 69 | 70 | 71 | if (cDef === undefined) { 72 | target = component; 73 | hook = getCompHookName(hookName); 74 | originalHook = target[hook]; 75 | } else { 76 | 77 | // @TODO I guess this is a miss conception that ngChanges is wrapped in a function. 78 | target = hooksWrapped[hookName] ? component : cDef; 79 | hook = hooksWrapped[hookName] ? getCompHookName(hookName) : hookName; 80 | originalHook = hooksWrapped[hookName] ? cDef[hook] : component[hook]; 81 | } 82 | 83 | target[hook] = function(args) { 84 | getPropertySubject(this, keyUniquePerPrototype, subjectFactory, hookName).next(args); 85 | // tslint:disable-next-line:no-unused-expression 86 | originalHook && originalHook.call(component, args); 87 | }; 88 | 89 | const propertyKeyDescriptor: TypedPropertyDescriptor> = { 90 | get() { 91 | const destroy$ = getPropertySubject(this, keyUniquePerPrototype, subjectFactory, HookNames.onDestroy).asObservable(); 92 | const hookOperators = getHooksOperatorsMap(destroy$)[hookName]; 93 | return getPropertySubject(this, keyUniquePerPrototype, subjectFactory, hookName).asObservable() 94 | .pipe( 95 | hookOperators 96 | ); 97 | } 98 | }; 99 | Object.defineProperty(target, propertyKey, propertyKeyDescriptor); 100 | 101 | }; 102 | } 103 | 104 | function getCompHookName(hookName: string): string { 105 | return 'ng' + hookName[0].toUpperCase() + hookName.slice(1); 106 | } 107 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/hook$/operators/selectChange.ts: -------------------------------------------------------------------------------- 1 | import {SimpleChanges} from '@angular/core'; 2 | import {pipe} from 'rxjs'; 3 | import {distinctUntilChanged, map} from 'rxjs/operators'; 4 | 5 | export function selectChange(prop: string) { 6 | return pipe( 7 | map((change: SimpleChanges) => change[prop].currentValue), 8 | distinctUntilChanged() 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/host-listener$/host-listener$.decorator.ts: -------------------------------------------------------------------------------- 1 | import {ElementRef} from '@angular/core'; 2 | import {fromEvent} from 'rxjs'; 3 | 4 | export function HostListener$(eventName: string): PropertyDecorator { 5 | return ( 6 | // tslint:disable-next-line 7 | target: Object, 8 | propertyKey: string | symbol 9 | ) => { 10 | Object.defineProperty(target, propertyKey, { 11 | get() { 12 | // @TODO investigate @ViewChild for usage 13 | const elementRef = this.injector.get(ElementRef); 14 | return fromEvent(elementRef.nativeElement, eventName); 15 | } 16 | }); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/input$/input$.decorator.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs'; 2 | import {getPropertySubject, getReplaySubjectFactory} from '../core/get-property-subject'; 3 | 4 | export function Input$(): PropertyDecorator { 5 | 6 | return ( 7 | // @TODO get better typing 8 | // tslint:disable-next-line 9 | component: Object, 10 | propertyKey: PropertyKey 11 | ) => { 12 | const keyUniquePerPrototype = Symbol('@ngRe-Input$'); 13 | 14 | const propertyKeyDescriptor: TypedPropertyDescriptor> = { 15 | set(newValue) { 16 | // @TODO: Get type of property instead of any 17 | getPropertySubject(this, keyUniquePerPrototype, getReplaySubjectFactory(1)).next(newValue); 18 | }, 19 | get() { 20 | return getPropertySubject(this, keyUniquePerPrototype, getReplaySubjectFactory(1)); 21 | } 22 | }; 23 | 24 | Object.defineProperty(component, propertyKey, propertyKeyDescriptor); 25 | 26 | 27 | }; 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/let/let.directive.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectorRef, Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef} from '@angular/core'; 2 | import {invalidInputValueError} from 'ng-re/lib/core/invalid_pipe_argument_error'; 3 | import { 4 | animationFrameScheduler, 5 | combineLatest, 6 | isObservable, 7 | NEVER, 8 | NextObserver, 9 | Observable, 10 | Observer, 11 | ReplaySubject, 12 | Subject 13 | } from 'rxjs'; 14 | import {isObject} from 'rxjs/internal-compatibility'; 15 | import {map, observeOn, startWith, switchAll, takeUntil, tap} from 'rxjs/operators'; 16 | 17 | const selector = 'ngrxLet'; 18 | 19 | export class LetContext { 20 | constructor( 21 | // to enable let we have to use $implicit 22 | public $implicit?: any, 23 | // to enable as we have to assign this 24 | public ngrxLet?: any, 25 | // value of error of undefined 26 | public $error?: any, 27 | // true or undefined 28 | public $complete?: any 29 | ) { 30 | } 31 | } 32 | 33 | @Directive({ 34 | selector: '[ngrxLet]' 35 | }) 36 | export class LetDirective implements OnInit, OnDestroy { 37 | private onDestroy$ = new Subject(); 38 | 39 | private ViewContext = new LetContext(); 40 | private af$ = new ReplaySubject(1); 41 | private observables$: ReplaySubject> = new ReplaySubject(1); 42 | 43 | @Input() 44 | set ngrxLet(o: Observable) { 45 | if (o === null || o === undefined) { 46 | this.observables$.next(NEVER); 47 | } else if (isObservable(o)) { 48 | this.observables$.next(o); 49 | } else { 50 | throw invalidInputValueError(LetDirective, selector); 51 | } 52 | } 53 | 54 | @Input() 55 | set ngrxLetUseAf(bool: boolean) { 56 | this.af$.next(bool); 57 | } 58 | 59 | resetContextObserver: NextObserver> = { 60 | // for every value reset context 61 | next: (_) => { 62 | // @TODO find out why we have to mutate the context object 63 | this.ViewContext.$implicit = undefined; 64 | this.ViewContext.ngrxLet = undefined; 65 | this.ViewContext.$error = undefined; 66 | this.ViewContext.$complete = undefined; 67 | } 68 | }; 69 | 70 | updateContextObserver: Observer = { 71 | next: (v) => { 72 | // @TODO find out why we have to mutate the context object 73 | // to enable `let` syntax we have to use $implicit (var; let v = var) 74 | this.ViewContext.$implicit = v; 75 | // to enable `as` syntax we have to assign the directives selector (var as v) 76 | this.ViewContext.ngrxLet = v; 77 | // @TODO Too much and remove? 78 | if (isObject(v)) { 79 | Object.entries(v).forEach(([key, value]) => this.ViewContext[key] = value); 80 | } 81 | }, 82 | error: (e) => { 83 | // set context var complete to true (var$; let v = $error) 84 | this.ViewContext.$error = e; 85 | }, 86 | complete: () => { 87 | // set context var complete to true (var$; let v = $complete) 88 | this.ViewContext.$complete = true; 89 | } 90 | }; 91 | 92 | constructor( 93 | private cd: ChangeDetectorRef, 94 | private readonly templateRef: TemplateRef, 95 | private readonly viewContainerRef: ViewContainerRef 96 | ) { 97 | combineLatest( 98 | this.observables$.asObservable() 99 | .pipe( 100 | tap(this.resetContextObserver), 101 | tap(_ => this.renderChange()) 102 | ), 103 | this.af$.pipe(startWith(false)) 104 | ) 105 | .pipe( 106 | map(([state$, af]) => { 107 | state$ = af ? 108 | // apply scheduling 109 | state$.pipe(observeOn(animationFrameScheduler)) : 110 | state$; 111 | 112 | return state$.pipe( 113 | // update context variables 114 | tap(this.updateContextObserver), 115 | tap(_ => this.renderChange()) 116 | ); 117 | } 118 | ), 119 | switchAll(), 120 | takeUntil(this.onDestroy$) 121 | ) 122 | .subscribe(); 123 | } 124 | 125 | renderChange() { 126 | // @TODO replace with `.detectChange()` after ivy fix 127 | // running zone-less with detectChange 128 | this.cd.markForCheck(); 129 | } 130 | 131 | ngOnInit() { 132 | // @TODO https://github.com/angular/angular/issues/15280#issuecomment-430479166 133 | // @TODO Also consider this.viewContainerRef.clear(); maybe in ngOnDestroy? 134 | this.viewContainerRef 135 | .createEmbeddedView(this.templateRef, this.ViewContext); 136 | } 137 | 138 | ngOnDestroy(): void { 139 | this.onDestroy$.next(); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/local-state/local-state.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Hook$} from '../hook$/hook$.decorator'; 3 | import {ConnectableObservable, merge, Observable, Subject} from 'rxjs'; 4 | import {endWith, map, mergeAll, publishReplay, scan, takeUntil, tap} from 'rxjs/operators'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class LocalStateService { 10 | 11 | @Hook$('onDestroy') 12 | onDestroy$; 13 | 14 | private commandObservable$$ = new Subject(); 15 | private command$$ = new Subject(); 16 | state$: Observable = 17 | merge( 18 | this.command$$, 19 | this.commandObservable$$.pipe(mergeAll()) 20 | ) 21 | .pipe( 22 | scan((s, c) => { 23 | const [keyToDelete, value]: [string, any] = Object.entries(c)[0]; 24 | const isKeyToDeletePresent = keyToDelete in s; 25 | // The key you want to delete is not stored :) 26 | if (!isKeyToDeletePresent && value === undefined) { 27 | return s; 28 | } 29 | // Delete slice 30 | if (value === undefined) { 31 | const {[keyToDelete]: v, ...newS} = s as any; 32 | return newS; 33 | } 34 | // update state 35 | return ({...s, ...c}); 36 | }, {}), 37 | takeUntil(this.onDestroy$), 38 | publishReplay(1) 39 | ); 40 | 41 | constructor() { 42 | // the local state service's `state$` observable should be hot on instantiation 43 | const subscription = (this.state$ as ConnectableObservable).connect(); 44 | this.onDestroy$.subscribe(_ => subscription.unsubscribe()); 45 | } 46 | 47 | // This breaks the functional programming style for the user. 48 | // We should avoid such things. It's recommended passing streams like with `connectSlice();` 49 | /** @deprecated This is an invitation for imperative client code */ 50 | setSlices(commands) { 51 | Object.entries(commands) 52 | .map(([key, value]) => ({[key]: value})) 53 | .forEach(command => this.command$$.next(command)); 54 | } 55 | 56 | // This should be the way to go. Functional style should be broken by the user. 57 | // Not like with `this.setSlice` 58 | connectSlices(config: { [key: string]: Observable }): void { 59 | // @TODO validation / typing params 60 | // @TODO consider multiple observables for the same key. Here I would suggest last one wins => switchAll 61 | Object.entries(config).map(([slice, state$]) => { 62 | const slice$ = state$.pipe( 63 | map(state => ({[slice]: state})), 64 | endWith({[slice]: undefined}) 65 | ); 66 | this.commandObservable$$.next(slice$); 67 | }); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/local-state/operators/selectSlice.ts: -------------------------------------------------------------------------------- 1 | import {pipe} from 'rxjs'; 2 | import {distinctUntilChanged, map} from 'rxjs/operators'; 3 | import {STATE_DEFAULT} from '../../core/state-default'; 4 | 5 | export function selectSlice(mapToSliceFn: (s: any) => any) { 6 | return pipe( 7 | map(s => { 8 | return (s !== STATE_DEFAULT) ? mapToSliceFn(s) : s; 9 | }), 10 | distinctUntilChanged() 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/ng-re.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {LetDirective} from './let/let.directive'; 3 | import {Async$Pipe} from './push$/async$.pipe'; 4 | import {PushPipe} from './push$/push$.pipe'; 5 | 6 | const DECLARATIONS = [ 7 | LetDirective, 8 | PushPipe, 9 | Async$Pipe 10 | ]; 11 | 12 | const EXPORTS = [ 13 | DECLARATIONS 14 | ]; 15 | 16 | @NgModule({ 17 | declarations: [ 18 | DECLARATIONS 19 | ], 20 | imports: [], 21 | exports: [ 22 | EXPORTS 23 | ] 24 | }) 25 | export class NgReModule { 26 | } 27 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/push$/async$.pipe.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectorRef, OnDestroy, Pipe, PipeTransform, WrappedValue, ɵisObservable, ɵisPromise, ɵlooseIdentical} from '@angular/core'; 2 | import {from, Observable, Subject, throwError} from 'rxjs'; 3 | import {distinctUntilChanged, switchAll, takeUntil, tap} from 'rxjs/operators'; 4 | 5 | // import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; 6 | 7 | @Pipe({name: 'async$', pure: false}) 8 | export class Async$Pipe implements OnDestroy, PipeTransform { 9 | private value: any = null; 10 | 11 | ngOnDestroy$$ = new Subject(); 12 | 13 | observablesToSubscribe$$ = new Subject>(); 14 | obs$ = this.observablesToSubscribe$$ 15 | .pipe(distinctUntilChanged()); 16 | 17 | handleIncomingObservables$ = this.obs$ 18 | .pipe( 19 | distinctUntilChanged(ɵlooseIdentical), 20 | switchAll(), 21 | tap(v => this.value = v), 22 | tap(_ => this.ref.markForCheck()) 23 | ); 24 | 25 | constructor(private ref: ChangeDetectorRef) { 26 | this.handleIncomingObservables$ 27 | .pipe(takeUntil(this.ngOnDestroy$$)) 28 | .subscribe(); 29 | } 30 | 31 | ngOnDestroy(): void { 32 | this.ngOnDestroy$$.next(true); 33 | } 34 | 35 | transform(obj: null): null; 36 | transform(obj: undefined): undefined; 37 | transform(obj: Observable | null | undefined): T | null; 38 | transform(obj: Promise | null | undefined): T | null; 39 | transform(obj: Observable | Promise | null | undefined): any { 40 | 41 | this.observablesToSubscribe$$.next(toObservable(obj)); 42 | return WrappedValue.wrap(this.value); 43 | 44 | function toObservable(obj) { 45 | if (ɵisObservable(obj) || ɵisPromise(obj)) 46 | return from(obj); 47 | else 48 | throwError(new Error('invalidPipeArgumentError')); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/push$/operators/detectChanges.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef } from '@angular/core'; 2 | import {pipe} from 'rxjs'; 3 | import { tap } from 'rxjs/operators'; 4 | 5 | export function detectChanges(cdr: ChangeDetectorRef) { 6 | return pipe(tap(_ => cdr.detectChanges())); 7 | } 8 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/push$/push$.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectorRef, Component, NgZone} from '@angular/core'; 2 | import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; 3 | import {EMPTY, NEVER, Observable, of, ReplaySubject} from 'rxjs'; 4 | import {PushPipe} from './push$.pipe'; 5 | 6 | @Component({ 7 | template: `

{{(value$ | push$ | json) || 'undefined'}}

` 8 | }) 9 | class BasicTestsComponent { 10 | value$: any = new ReplaySubject(1); 11 | } 12 | 13 | @Component({ 14 | template: `

{{(value$ | push$:onlyNewRef)?.value}}

` 15 | }) 16 | class FlagsTestsComponent { 17 | 18 | initialValue = ''; 19 | // forwardOnlyNewReferences option 20 | onlyNewRef = true; 21 | value$ = new ReplaySubject<{ value: string }>(1); 22 | 23 | 24 | constructor() { 25 | const a = {value: this.initialValue}; 26 | this.value$.next(a); 27 | a.value = 'mutation'; 28 | this.value$.next(a); 29 | } 30 | } 31 | 32 | describe('PushPipe', () => { 33 | 34 | let basicTestsComponent: BasicTestsComponent; 35 | let basicTestsFixture: ComponentFixture; 36 | 37 | let flagsTestsComponent: FlagsTestsComponent; 38 | let flagsTestsFixture: ComponentFixture; 39 | 40 | beforeEach(() => { 41 | TestBed.configureTestingModule({ 42 | declarations: [ 43 | BasicTestsComponent, 44 | FlagsTestsComponent, 45 | PushPipe 46 | ], 47 | providers: [ChangeDetectorRef, NgZone] 48 | }); 49 | 50 | basicTestsFixture = TestBed.createComponent(BasicTestsComponent); 51 | basicTestsComponent = basicTestsFixture.componentInstance; 52 | 53 | flagsTestsFixture = TestBed.createComponent(FlagsTestsComponent); 54 | flagsTestsComponent = flagsTestsFixture.componentInstance; 55 | }); 56 | 57 | it('create an instance', inject([ChangeDetectorRef, NgZone], (cd: ChangeDetectorRef, ngZone: NgZone) => { 58 | const pipe = new PushPipe(cd, ngZone); 59 | expect(pipe).toBeTruthy(); 60 | })); 61 | 62 | it('should create test-component for push pipe', () => { 63 | expect(basicTestsComponent).toBeDefined(); 64 | }); 65 | 66 | it('when initially passed `undefined` the pipe should **forward `undefined`** as value as on value ever was emitted', () => { 67 | const testValue = undefined; 68 | 69 | basicTestsComponent.value$.next(testValue); 70 | basicTestsFixture.detectChanges(); 71 | 72 | basicTestsComponent.value$ 73 | .subscribe(value => { 74 | expect(value).toBe(testValue); 75 | // See component view for reason of `+ ''` 76 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(testValue + ''); 77 | }); 78 | 79 | }); 80 | 81 | it('when initially passed `null` the pipe should **forward `null`** as value as `null` was emitted', () => { 82 | const testValue = null; 83 | 84 | basicTestsComponent.value$.next(testValue); 85 | basicTestsFixture.detectChanges(); 86 | 87 | basicTestsComponent.value$ 88 | .subscribe(value => { 89 | expect(value).toBe(testValue); 90 | // See component view for reason of `+ ''` 91 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(testValue + ''); 92 | }); 93 | 94 | }); 95 | 96 | it('when initially passed `of(undefined)` the pipe should **forward `undefined`** as value as `undefined` was emitted', () => { 97 | const testValue = undefined; 98 | 99 | basicTestsComponent.value$ = of(testValue); 100 | basicTestsFixture.detectChanges(); 101 | 102 | basicTestsComponent.value$ 103 | .subscribe(value => { 104 | expect(value).toBe(testValue); 105 | // See component view for reason of `+ ''` 106 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(testValue + ''); 107 | }); 108 | 109 | }); 110 | 111 | it('when initially passed `of(null)` the pipe should **forward `null`** as value as `null` was emitted', () => { 112 | const testValue = null; 113 | 114 | basicTestsComponent.value$ = of(testValue); 115 | basicTestsFixture.detectChanges(); 116 | 117 | basicTestsComponent.value$ 118 | .subscribe(value => { 119 | expect(value).toBe(testValue); 120 | // See component view for reason of `+ ''` 121 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(testValue + ''); 122 | }); 123 | 124 | }); 125 | 126 | it('when initially passed `EMPTY` the pipe should **forward `undefined`** as value as on value ever was emitted', () => { 127 | const testValue = undefined; 128 | 129 | basicTestsComponent.value$ = EMPTY; 130 | basicTestsFixture.detectChanges(); 131 | 132 | basicTestsComponent.value$ 133 | .subscribe(value => { 134 | expect(value).toBe(testValue); 135 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(testValue + ''); 136 | }); 137 | 138 | }); 139 | 140 | it('when initially passed `NEVER` the pipe should **forward `undefined`** as value as on value ever was emitted', () => { 141 | const testValue = undefined; 142 | 143 | basicTestsComponent.value$ = NEVER; 144 | basicTestsFixture.detectChanges(); 145 | 146 | basicTestsComponent.value$ 147 | .subscribe(value => { 148 | expect(value).toBe(testValue); 149 | // See component view for reason of `+ ''` 150 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(testValue + ''); 151 | }); 152 | 153 | }); 154 | 155 | it('when reassigned a new `Observable` the pipe should **forward `undefined`** as value as no value was emitted from the new Observable', () => { 156 | const testValue = undefined; 157 | 158 | basicTestsComponent.value$ = new Observable(() => { 159 | }); 160 | basicTestsFixture.detectChanges(); 161 | 162 | basicTestsComponent.value$ 163 | .subscribe(value => { 164 | expect(value).toBe(testValue); 165 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(testValue); 166 | }); 167 | 168 | }); 169 | 170 | it('when completed the pipe should **keep the last value** in the view until reassigned another observable', () => { 171 | const testValue = 'test'; 172 | const expectedViewValue = '"test"'; 173 | 174 | basicTestsComponent.value$ = of(testValue); 175 | basicTestsFixture.detectChanges(); 176 | 177 | basicTestsComponent.value$ 178 | .subscribe(value => { 179 | expect(value).toBe(testValue); 180 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(expectedViewValue); 181 | }); 182 | 183 | basicTestsFixture.detectChanges(); 184 | 185 | basicTestsComponent.value$ 186 | .subscribe(value => { 187 | expect(value).toBe(testValue); 188 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(expectedViewValue); 189 | }); 190 | 191 | basicTestsComponent.value$ = of(undefined); 192 | basicTestsFixture.detectChanges(); 193 | 194 | basicTestsComponent.value$ 195 | .subscribe(value => { 196 | expect(value).toBe(undefined); 197 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe('undefined'); 198 | }); 199 | 200 | }); 201 | 202 | it('when sending a value the pipe should **forward the value** without changing it', () => { 203 | const testValue = 'value'; 204 | const expectedViewValue = `"${testValue}"`; 205 | basicTestsComponent.value$.next(testValue); 206 | basicTestsFixture.detectChanges(); 207 | 208 | basicTestsComponent.value$ 209 | .subscribe(value => { 210 | expect(value).toBe(testValue); 211 | expect(getComponentElem(BasicTestsComponent).innerHTML).toBe(expectedViewValue); 212 | }); 213 | 214 | }); 215 | 216 | it('when the `forwardOnlyNewReferences` is set it should forward only new references', () => { 217 | 218 | }); 219 | 220 | // ==================================================== 221 | 222 | function getComponentElem(ClassName): HTMLElement { 223 | let debugEl: HTMLElement; 224 | if (ClassName === BasicTestsComponent) { 225 | debugEl = basicTestsFixture.debugElement.nativeElement; 226 | } else { 227 | debugEl = flagsTestsFixture.debugElement.nativeElement; 228 | } 229 | return debugEl.querySelector('p'); 230 | } 231 | 232 | }); 233 | -------------------------------------------------------------------------------- /projects/ng-re/src/lib/push$/push$.pipe.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectorRef, NgZone, OnDestroy, Pipe, PipeTransform} from '@angular/core'; 2 | import {isZoneLess} from 'ng-re/lib/core/isZoneLess'; 3 | import {combineLatest, isObservable, Observable, of, Subject} from 'rxjs'; 4 | import {distinctUntilChanged, map, switchAll, takeUntil} from 'rxjs/operators'; 5 | import {STATE_DEFAULT} from '../core/state-default'; 6 | 7 | /** 8 | * @description 9 | * 10 | * Unwraps a value from an asynchronous primitive. 11 | * 12 | * The `push` pipe subscribes to an `Observable` and returns the latest value it has 13 | * emitted. When a new value is emitted, the `push` pipe detects change in the component. 14 | * When the component gets destroyed, the `push` pipe unsubscribes automatically to avoid 15 | * potential memory leaks. 16 | * 17 | * it'S clean it's pure :) 18 | * 19 | */ 20 | // @TODO remove `pure: false` and experiment without zone 21 | @Pipe({name: 'push$', pure: false}) 22 | export class PushPipe implements PipeTransform, OnDestroy { 23 | private forwardOnlyNewReferences$$ = new Subject(); 24 | readonly cdFunction: () => void; 25 | private value: any = STATE_DEFAULT; 26 | 27 | ngOnDestroy$$ = new Subject(); 28 | 29 | // @TODO fix any types 30 | observablesToSubscribe$$ = new Subject>(); 31 | 32 | constructor(private cdRef: ChangeDetectorRef, private ngZone: NgZone) { 33 | if (isZoneLess(this.ngZone)) { 34 | this.cdFunction = () => this.cdRef.detectChanges(); 35 | } else { 36 | this.cdFunction = () => this.cdRef.markForCheck(); 37 | } 38 | 39 | const newObservables$ = this.observablesToSubscribe$$ 40 | .pipe( 41 | // only forward new references (avoids holding a local reference to the previous observable => this.currentObs !== obs) 42 | distinctUntilChanged(), 43 | ); 44 | combineLatest(newObservables$, 45 | // only forward distinct values => less executions 46 | this.forwardOnlyNewReferences$$.pipe(distinctUntilChanged())) 47 | .pipe( 48 | // Handle forwardOnlyNewReferences option 49 | map(([o, onlyNewRef]) => (onlyNewRef) ? o.pipe(distinctUntilChanged()) : o), 50 | // unsubscribe from previous observables 51 | // then flatten the latest internal observables into the output 52 | switchAll(), 53 | // unsubscribe if component gets destroyed 54 | takeUntil(this.ngOnDestroy$$) 55 | ) 56 | .subscribe(value => { 57 | // assign value that will get returned from the transform function on the next change detection 58 | this.value = value; 59 | // trigger change detection for the to get the newly assigned value rendered 60 | this.cdFunction(); 61 | }); 62 | } 63 | 64 | ngOnDestroy(): void { 65 | this.ngOnDestroy$$.next(true); 66 | } 67 | 68 | transform(obj: null | undefined, forwardOnlyNewReferences: boolean): null; 69 | transform(obj: Observable, forwardOnlyNewReferences: boolean): T; 70 | transform(obj: Observable | null | undefined, forwardOnlyNewReferences = true): T | null { 71 | this.forwardOnlyNewReferences$$.next(forwardOnlyNewReferences); 72 | 73 | this.observablesToSubscribe$$.next(!isObservable(obj) ? of(STATE_DEFAULT) : obj); 74 | return this.value; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /projects/ng-re/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ng-re 3 | */ 4 | 5 | // hook$ decorator 6 | export * from './lib/hook$/hook$.decorator'; 7 | export * from './lib/hook$/operators/selectChange'; 8 | 9 | // host-listener$ decorator 10 | export * from './lib/host-listener$/host-listener$.decorator'; 11 | 12 | // input$ decorator 13 | export * from './lib/input$/input$.decorator'; 14 | 15 | // local-state service 16 | export * from './lib/local-state/local-state'; 17 | export * from './lib/local-state/operators/selectSlice'; 18 | 19 | // push$ pipe 20 | export * from './lib/push$/push$.pipe'; 21 | export * from './lib/push$/async$.pipe'; 22 | export * from './lib/push$/operators/detectChanges'; 23 | 24 | // reLet directive 25 | export * from './lib/let/let.directive'; 26 | 27 | // modules 28 | export * from './lib/ng-re.module'; 29 | -------------------------------------------------------------------------------- /projects/ng-re/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting() 17 | ); 18 | // Then we find all the tests. 19 | const context = require.context('./', true, /\.spec\.ts$/); 20 | // And load the modules. 21 | context.keys().map(context); 22 | -------------------------------------------------------------------------------- /projects/ng-re/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true 21 | }, 22 | "exclude": [ 23 | "src/test.ts", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /projects/ng-re/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/ng-re/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, NgZone} from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | template: ` 7 |
8 | {{runningZoneLess ? 'Zone-Less' : 'Zone-Full'}} 9 |
10 |
11 | 45 |
46 | 47 |
48 |
49 | `, 50 | changeDetection: ChangeDetectionStrategy.OnPush 51 | }) 52 | export class AppComponent implements AfterViewInit { 53 | 54 | runningZoneLess: boolean; 55 | 56 | 57 | 58 | constructor(z: NgZone, private cd: ChangeDetectorRef, private router: Router) { 59 | this.runningZoneLess = z.constructor.name === 'NoopNgZone'; 60 | requestAnimationFrame(() => {}); 61 | cancelAnimationFrame(3); 62 | } 63 | 64 | ngAfterViewInit(): void { 65 | setTimeout(() => { 66 | // this.cd.detectChanges(); 67 | }, 1000); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {HttpClientModule} from '@angular/common/http'; 2 | import {NgModule} from '@angular/core'; 3 | import {ReactiveFormsModule} from '@angular/forms'; 4 | import {BrowserModule} from '@angular/platform-browser'; 5 | import {RouterModule} from '@angular/router'; 6 | import {NgReModule} from '../../projects/ng-re/src/lib/ng-re.module'; 7 | 8 | import {AppComponent} from './app.component'; 9 | import {APP_ROUTES} from './app.routes'; 10 | import {AVOID_REACTIVITY_DECLARATIONS} from './components/avoid-reactivity-container'; 11 | import {FROM_VIEW_EVENT$_DECLARATIONS} from './components/from-view-event-container'; 12 | import {HOOK_DECLARATIONS} from './components/hook$-container'; 13 | import {HOST_LISTENER$_DECLARATIONS} from './components/host-listener-container'; 14 | import {INPUT$_DECLARATIONS} from './components/input-container'; 15 | import {LET_DECLARATIONS} from './components/let-directive-container'; 16 | import {LOCAL_STATE_DECLARATIONS} from './components/local-state-container'; 17 | import {PUSH$_DECLARATIONS} from './components/push-pipe-container'; 18 | import {STAR_RATING_DECLARATIONS} from './components/star-rating'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | AppComponent, 23 | PUSH$_DECLARATIONS, 24 | HOOK_DECLARATIONS, 25 | HOST_LISTENER$_DECLARATIONS, 26 | INPUT$_DECLARATIONS, 27 | FROM_VIEW_EVENT$_DECLARATIONS, 28 | LOCAL_STATE_DECLARATIONS, 29 | LET_DECLARATIONS, 30 | STAR_RATING_DECLARATIONS, 31 | AVOID_REACTIVITY_DECLARATIONS 32 | ], 33 | imports: [ 34 | BrowserModule, 35 | HttpClientModule, 36 | ReactiveFormsModule, 37 | NgReModule, 38 | RouterModule.forRoot(APP_ROUTES) 39 | ], 40 | bootstrap: [AppComponent], 41 | exports: [] 42 | }) 43 | export class AppModule { 44 | } 45 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import {AVOID_REACTIVITY_DECLARATIONS, AVOIDING_REACTIVITY_ROUTES} from './components/avoid-reactivity-container'; 2 | import {FROM_VIEW_EVENT$_ROUTES} from './components/from-view-event-container'; 3 | import {HOOKS$_ROUTES} from './components/hook$-container'; 4 | import {HOST_LISTENER$_ROUTES} from './components/host-listener-container'; 5 | import {INPUT$_ROUTES} from './components/input-container'; 6 | import {LET_ROUTES} from './components/let-directive-container'; 7 | import {LOCAL_STATE_ROUTES} from './components/local-state-container'; 8 | import {PUSH$_ROUTES} from './components/push-pipe-container'; 9 | import {STAR_RATING_ROUTES} from './components/star-rating'; 10 | 11 | export const APP_ROUTES = [ 12 | // {path: '', redirectTo: 'readme', pathMath: 'full'}, 13 | {path: 'push-pipe', children: PUSH$_ROUTES}, 14 | {path: 'hook', children: HOOKS$_ROUTES}, 15 | {path: 'host-listener', children: HOST_LISTENER$_ROUTES}, 16 | {path: 'input', children: INPUT$_ROUTES}, 17 | {path: 'from-view-event', children: FROM_VIEW_EVENT$_ROUTES}, 18 | {path: 'local-state', children: LOCAL_STATE_ROUTES}, 19 | {path: 'let-directive', children: LET_ROUTES}, 20 | {path: 'star-rating', children: STAR_RATING_ROUTES}, 21 | {path: 'avoid-rx', children: AVOIDING_REACTIVITY_ROUTES} 22 | ]; 23 | -------------------------------------------------------------------------------- /src/app/components/avoid-reactivity-container/avoid-reactivity-container.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-avoid-rx-container', 5 | template: ` 6 |

Avoid Reactive Programming

7 | 12 | 16 | 17 | ` 18 | }) 19 | export class AvoidReactivityContainerComponent { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/avoid-reactivity-container/avoid-reactivity-rx-subscription.component.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import { 3 | AfterContentChecked, 4 | AfterContentInit, 5 | AfterViewChecked, 6 | AfterViewInit, 7 | ChangeDetectionStrategy, 8 | Component, 9 | Input, OnChanges, OnDestroy, OnInit, SimpleChanges 10 | } from '@angular/core'; 11 | import {ActivatedRoute} from '@angular/router'; 12 | import {interval, of, timer} from 'rxjs'; 13 | import {map, tap} from 'rxjs/operators'; 14 | import {TickService} from './tick.service'; 15 | 16 | @Component({ 17 | selector: 'app-avoid-rx-subscription', 18 | template: ` 19 |

Retrieving a single router-param and render it

20 |

Leverage Rx

21 |
Http result: {{result | async | json}}
22 |
TickService value: {{value | async | json}}
23 | `, 24 | changeDetection: ChangeDetectionStrategy.OnPush 25 | }) 26 | export class AvoidReactivityRxSubscriptionComponent { 27 | result; 28 | value; 29 | 30 | constructor(private tickService: TickService, private http: HttpClient) { 31 | this.result = this.http.get('https://api.github.com/users/octocat') 32 | .pipe(map((user: any) => user.login), tap(console.log)); 33 | this.value = tickService.tick$ 34 | .pipe( 35 | map(measure => measure.value), 36 | tap(v => console.log('leverage:', v)) 37 | ); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/app/components/avoid-reactivity-container/avoid-reactivity-subscription.component.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {ChangeDetectionStrategy, Component, OnInit,} from '@angular/core'; 3 | import {map, tap} from 'rxjs/operators'; 4 | import {TickService} from './tick.service'; 5 | 6 | @Component({ 7 | selector: 'app-avoid-subscription', 8 | template: ` 9 |

Retrieving a single router-param and render it

10 |

Avoid Rx

11 |
Http result: {{result | json}}
12 |
TickService value: {{value | json}}
13 | `, 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class AvoidReactivitySubscriptionComponent implements OnInit { 17 | result; 18 | value = 10; 19 | 20 | constructor(private tickService: TickService, private http: HttpClient) { 21 | 22 | } 23 | 24 | ngOnInit() { 25 | this.result = this.http.get('https://api.github.com/users/octocat') 26 | .pipe(map((user: any) => user.login)) 27 | .subscribe(result => this.result = result); 28 | 29 | this.tickService.tick$ 30 | .pipe( 31 | map(measure => measure.value), 32 | // tap(v => console.log('avoid:', v)) 33 | ) 34 | .subscribe(value => { 35 | console.log('sub v:', value); 36 | this.value = value; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/components/avoid-reactivity-container/avoid-reactivity-timing.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterContentChecked, 3 | AfterContentInit, 4 | AfterViewChecked, 5 | AfterViewInit, 6 | ChangeDetectionStrategy, 7 | Component, 8 | Input, 9 | OnChanges, 10 | OnDestroy, 11 | OnInit, 12 | SimpleChanges 13 | } from '@angular/core'; 14 | import {of} from 'rxjs'; 15 | import {tap} from 'rxjs/operators'; 16 | 17 | @Component({ 18 | selector: 'app-avoid-timing', 19 | template: ` 20 |

Timing

21 |
Input value: {{value}}
22 |
Component observable property: {{o$ | async}}
23 | `, 24 | changeDetection: ChangeDetectionStrategy.OnPush 25 | }) 26 | export class AvoidReactivityTimingComponent implements OnChanges, OnInit, AfterContentInit, AfterContentChecked, AfterViewInit, 27 | AfterViewChecked, OnDestroy { 28 | @Input() value; 29 | o$ = of(42).pipe(tap(console.log)); 30 | 31 | constructor() { 32 | } 33 | 34 | ngAfterContentChecked(): void { 35 | console.log('ngAfterContentChecked'); 36 | } 37 | 38 | ngOnDestroy(): void { 39 | console.log('ngOnDestroy'); 40 | } 41 | 42 | ngAfterContentInit(): void { 43 | console.log('ngAfterContentInit'); 44 | } 45 | 46 | ngOnInit(): void { 47 | console.log('ngOnInit'); 48 | } 49 | 50 | ngOnChanges(changes: SimpleChanges): void { 51 | console.log('ngOnChanges', changes); 52 | } 53 | 54 | ngAfterViewChecked(): void { 55 | console.log('ngAfterViewChecked'); 56 | } 57 | 58 | ngAfterViewInit(): void { 59 | console.log('ngAfterViewInit'); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/app/components/avoid-reactivity-container/index.ts: -------------------------------------------------------------------------------- 1 | import {AvoidReactivityContainerComponent} from './avoid-reactivity-container.component'; 2 | import {AvoidReactivityRxSubscriptionComponent} from './avoid-reactivity-rx-subscription.component'; 3 | import {AvoidReactivitySubscriptionComponent} from './avoid-reactivity-subscription.component'; 4 | import {AvoidReactivityTimingComponent} from './avoid-reactivity-timing.component'; 5 | 6 | export * from './routes'; 7 | export const AVOID_REACTIVITY_DECLARATIONS = [ 8 | AvoidReactivityContainerComponent, 9 | AvoidReactivitySubscriptionComponent, 10 | AvoidReactivityRxSubscriptionComponent, 11 | AvoidReactivityTimingComponent 12 | ]; 13 | -------------------------------------------------------------------------------- /src/app/components/avoid-reactivity-container/routes.ts: -------------------------------------------------------------------------------- 1 | import {AvoidReactivityContainerComponent} from './avoid-reactivity-container.component'; 2 | import {AvoidReactivitySubscriptionComponent} from './avoid-reactivity-subscription.component'; 3 | 4 | export const AVOIDING_REACTIVITY_ROUTES = [ 5 | { 6 | path: '', 7 | component: AvoidReactivityContainerComponent, 8 | children: [ 9 | { 10 | path: 'sync-with-class-property', 11 | component: AvoidReactivitySubscriptionComponent 12 | } 13 | ] 14 | } 15 | ]; 16 | -------------------------------------------------------------------------------- /src/app/components/avoid-reactivity-container/tick.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ConnectableObservable, interval} from 'rxjs'; 3 | import {multicast, publish, share, timeInterval} from 'rxjs/operators'; 4 | 5 | @Injectable({providedIn: 'root'}) 6 | export class TickService { 7 | tick$ = interval(600) 8 | .pipe(timeInterval(), share()); 9 | 10 | constructor() { 11 | console.log('TickService CTOR'); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/app/components/from-view-event-container/from-view-event-container.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterContentInit, ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {Subject} from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'app-output-container', 6 | template: ` 7 |

FromViewEvent$ Container

8 |

Last received output:

9 |
10 |       {{clicks$ | async | json}}
11 |     
12 | 16 |
17 | 20 | 21 | `, 22 | changeDetection: ChangeDetectionStrategy.OnPush 23 | }) 24 | export class FromViewEventContainerComponent implements AfterContentInit { 25 | 26 | clicks$ = new Subject(); 27 | 28 | constructor() { 29 | 30 | } 31 | 32 | ngAfterContentInit() { 33 | this.clicks$.subscribe(console.log); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/components/from-view-event-container/from-view-event.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Output} from '@angular/core'; 2 | import {interval} from 'rxjs'; 3 | import {share} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-from-view-event', 7 | template: ` 8 |

Observable as @Output() value

9 |

Last output emission:

10 |
11 |       {{interval$ | async$ | json}}
12 |     
13 | ` 14 | }) 15 | export class FromViewEventComponent { 16 | interval$ = interval(1000).pipe(share()); 17 | @Output() out = this.interval$; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/from-view-event-container/index.ts: -------------------------------------------------------------------------------- 1 | import {FromViewEventContainerComponent} from './from-view-event-container.component'; 2 | import {FromViewEventComponent} from './from-view-event.component'; 3 | 4 | 5 | export * from './routes'; 6 | export const FROM_VIEW_EVENT$_DECLARATIONS = [ 7 | FromViewEventContainerComponent, 8 | FromViewEventComponent 9 | ]; 10 | -------------------------------------------------------------------------------- /src/app/components/from-view-event-container/routes.ts: -------------------------------------------------------------------------------- 1 | import {FromViewEventContainerComponent} from './from-view-event-container.component'; 2 | 3 | export const FROM_VIEW_EVENT$_ROUTES = [ 4 | { 5 | path: '', 6 | component: FromViewEventContainerComponent 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /src/app/components/hook$-container/dummy.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, OnDestroy} from '@angular/core'; 2 | import {Hook$} from 'ng-re'; 3 | import {Observable} from 'rxjs'; 4 | 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class DummyService implements OnDestroy { 10 | 11 | @Hook$('onDestroy') onDestroy$: Observable; 12 | 13 | constructor() { 14 | this.onDestroy$.subscribe(v => console.log('Service onDestroy$', v)); 15 | 16 | } 17 | 18 | ngOnDestroy(): void { 19 | console.log('service original ngOnDestroy'); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/hook$-container/full-example-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {timer} from 'rxjs'; 3 | import {map} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-full-example-container', 7 | template: ` 8 |

Full Example Container

9 |

10 | Container state$: 11 |

12 |
13 |         {{state$ | async$ | json}}
14 |     
15 | 16 | 17 | `, 18 | changeDetection: ChangeDetectionStrategy.OnPush 19 | }) 20 | export class FullExampleContainerComponent { 21 | 22 | initialState = { 23 | value: 0, 24 | options: [1, 2, 3, 4, 5] 25 | }; 26 | 27 | state$ = timer(0, 1000) 28 | .pipe( 29 | map(v => ({ 30 | ...this.initialState, 31 | value: v 32 | })) 33 | ); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/app/components/hook$-container/full-exmple.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input, SimpleChanges} from '@angular/core'; 2 | import {Hook$} from 'ng-re'; 3 | import {Observable, Observer} from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-full-example', 7 | template: ` 8 |

FullExample

9 |

state:

10 |
{{onChanges$ | async | json}}
11 | `, 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | /*implements OnChanges, DoCheck, OnInit, AfterViewInit, 15 | AfterViewChecked, AfterContentInit, AfterContentChecked, OnDestroy */ 16 | export class FullExampleComponent { 17 | 18 | @Hook$('doCheck') doCheck$: Observable; 19 | @Hook$('onChanges') onChanges$: Observable; 20 | @Hook$('onInit') onInit$: Observable; 21 | @Hook$('afterContentChecked') afterContentChecked$: Observable; 22 | @Hook$('afterContentInit') afterContentInit$: Observable; 23 | @Hook$('afterViewChecked') afterViewChecked$: Observable; 24 | @Hook$('afterViewInit') afterViewInit$: Observable; 25 | @Hook$('onDestroy') onDestroy$: Observable; 26 | 27 | @Input() state; 28 | 29 | constructor() { 30 | this.doCheck$.subscribe(this.getHookObserver('onCheck$')); 31 | this.onChanges$.subscribe(this.getHookObserver('onChanges$')); 32 | this.onInit$.subscribe(this.getHookObserver('onInit$ next')); 33 | this.afterContentChecked$.subscribe(this.getHookObserver('afterContentChecked$')); 34 | this.afterContentInit$.subscribe(this.getHookObserver('afterContentInit$')); 35 | this.afterViewChecked$.subscribe(this.getHookObserver('afterViewChecked$')); 36 | this.afterViewInit$.subscribe(this.getHookObserver('afterViewInit$')); 37 | this.onDestroy$.subscribe(this.getHookObserver('onDestroy$')); 38 | 39 | } 40 | 41 | private getHookObserver(name: string): Observer { 42 | return { 43 | next(n) { 44 | console.log(name + ' next', n); 45 | }, 46 | error(e) { 47 | console.log(name + ' error', e); 48 | }, 49 | complete() { 50 | console.log(name + ' complete'); 51 | }, 52 | }; 53 | } 54 | 55 | /* 56 | ngDoCheck(): void { 57 | console.log('original ngDoCheck'); 58 | } 59 | 60 | ngOnChanges(changes: SimpleChanges): void { 61 | console.log('original ngOnChanges', changes); 62 | } 63 | 64 | ngOnInit(): void { 65 | console.log('original ngOnInit'); 66 | } 67 | 68 | ngAfterContentInit(): void { 69 | console.log('original ngAfterContentInit'); 70 | } 71 | 72 | ngAfterContentChecked(): void { 73 | console.log('original ngAfterContentChecked'); 74 | } 75 | 76 | ngAfterViewInit(): void { 77 | console.log('original ngAfterViewInit'); 78 | } 79 | 80 | ngAfterViewChecked(): void { 81 | console.log('original ngAfterViewChecked'); 82 | } 83 | 84 | ngOnDestroy(): void { 85 | console.log('original ngOnDestroy'); 86 | } 87 | */ 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/app/components/hook$-container/hook$-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {timer} from 'rxjs'; 3 | import {map} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-reactive-life-cycle-hooks-container', 7 | template: ` 8 |

Hook$(hookName) Container

9 | 26 | 27 | `, 28 | changeDetection: ChangeDetectionStrategy.OnPush 29 | }) 30 | export class HookContainerComponent { 31 | 32 | initialState = { 33 | value: 0, 34 | options: [1, 2, 3, 4, 5] 35 | }; 36 | 37 | state$ = timer(0, 1000) 38 | .pipe( 39 | map(v => ({ 40 | ...this.initialState, 41 | value: v 42 | })) 43 | ); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/app/components/hook$-container/index.ts: -------------------------------------------------------------------------------- 1 | import {FullExampleContainerComponent} from './full-example-container.component'; 2 | import {FullExampleComponent} from './full-exmple.component'; 3 | import {HookContainerComponent} from './hook$-container.component'; 4 | import {SelectChangeContainerComponent} from './select-change-container.component'; 5 | import {SelectChangeComponent} from './select-change.component'; 6 | import {ServiceLifeCycleContainerComponent} from './service-life-cycle-contaier.component'; 7 | 8 | export * from './routes'; 9 | export const HOOK_DECLARATIONS = [ 10 | HookContainerComponent, 11 | FullExampleContainerComponent, 12 | FullExampleComponent, 13 | SelectChangeContainerComponent, 14 | SelectChangeComponent, 15 | ServiceLifeCycleContainerComponent 16 | ]; 17 | -------------------------------------------------------------------------------- /src/app/components/hook$-container/routes.ts: -------------------------------------------------------------------------------- 1 | import {FullExampleContainerComponent} from './full-example-container.component'; 2 | import {HookContainerComponent} from './hook$-container.component'; 3 | import {SelectChangeContainerComponent} from './select-change-container.component'; 4 | import {ServiceLifeCycleContainerComponent} from './service-life-cycle-contaier.component'; 5 | 6 | export const HOOKS$_ROUTES = [ 7 | { 8 | path: '', 9 | component: HookContainerComponent, 10 | children: [ 11 | { 12 | path: 'full-example', 13 | component: FullExampleContainerComponent, 14 | }, 15 | { 16 | path: 'service-hooks', 17 | component: ServiceLifeCycleContainerComponent, 18 | }, 19 | { 20 | path: 'select-change', 21 | component: SelectChangeContainerComponent, 22 | }, 23 | ] 24 | } 25 | ]; 26 | -------------------------------------------------------------------------------- /src/app/components/hook$-container/select-change-container.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges} from '@angular/core'; 2 | import {Hook$, selectChange} from 'ng-re'; 3 | 4 | @Component({ 5 | selector: 'app-select-change-container', 6 | template: ` 7 |

selectChange Container

8 | 9 | 10 | `, 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class SelectChangeContainerComponent implements OnChanges, AfterViewInit { 14 | 15 | public state = 0; 16 | @Hook$('onChanges') 17 | onChanges$; 18 | 19 | @Input() 20 | value: { value: number }; 21 | state$ = this.onChanges$.pipe(selectChange('value')); 22 | 23 | constructor() { 24 | 25 | } 26 | 27 | // @TODO remove after fixed reactive hooks 28 | ngOnChanges(changes: SimpleChanges): void { 29 | } 30 | 31 | ngAfterViewInit(): void { 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/hook$-container/select-change.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges} from '@angular/core'; 2 | import {Hook$, selectChange} from 'ng-re'; 3 | 4 | @Component({ 5 | selector: 'app-select-change', 6 | template: ` 7 |

selectChange Child

8 |

state$:

9 |
{{state$ |async | json}}
10 | `, 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class SelectChangeComponent implements OnChanges, AfterViewInit { 14 | 15 | public state = 0; 16 | @Hook$('onChanges') 17 | onChanges$; 18 | 19 | @Input() 20 | value: { value: number }; 21 | state$ = this.onChanges$.pipe(selectChange('value')); 22 | 23 | constructor() { 24 | 25 | } 26 | 27 | // @TODO remove after fixed reactive hooks 28 | ngOnChanges(changes: SimpleChanges): void { 29 | } 30 | 31 | ngAfterViewInit(): void { 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/hook$-container/service-life-cycle-contaier.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterContentChecked, 3 | AfterContentInit, 4 | AfterViewChecked, 5 | AfterViewInit, 6 | ChangeDetectionStrategy, 7 | Component, 8 | DoCheck, 9 | Input, 10 | OnChanges, 11 | OnDestroy, 12 | OnInit, 13 | SimpleChanges 14 | } from '@angular/core'; 15 | import {Hook$} from 'ng-re'; 16 | import {Observable} from 'rxjs'; 17 | import {DummyService} from './dummy.service'; 18 | 19 | @Component({ 20 | selector: 'app-service-life-cycle', 21 | template: ` 22 |

Service Life-Cycle Container

23 | `, 24 | changeDetection: ChangeDetectionStrategy.OnPush, 25 | providers: [DummyService] 26 | }) 27 | export class ServiceLifeCycleContainerComponent { 28 | 29 | constructor(private dS: DummyService) {} 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/components/host-listener-container/host-listener-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-host-listener-container', 5 | template: ` 6 |

HostListener$(eventName) Container

7 | 8 | 9 | `, 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class HostListenerContainerComponent { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/app/components/host-listener-container/host-listener.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Injector} from '@angular/core'; 2 | import {HostListener$} from 'ng-re'; 3 | import {scan} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-host-listener', 7 | template: ` 8 |

HostListener$ Child 9 | Click me! 10 |

11 |

12 | Num clicks: 13 |

14 |
15 |       {{numClicks$ | async | json}}
16 |     
17 | `, 18 | changeDetection: ChangeDetectionStrategy.OnPush 19 | }) 20 | export class HostListenerComponent { 21 | 22 | @HostListener$('click') 23 | hostClick$; 24 | 25 | numClicks$ = this.hostClick$.pipe(scan(a => ++a, 0)); 26 | 27 | constructor(public injector: Injector) { 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/components/host-listener-container/index.ts: -------------------------------------------------------------------------------- 1 | import {HostListenerContainerComponent} from './host-listener-container.component'; 2 | import {HostListenerComponent} from './host-listener.component'; 3 | 4 | export * from './routes'; 5 | export const HOST_LISTENER$_DECLARATIONS = [ 6 | HostListenerContainerComponent, 7 | HostListenerComponent 8 | ]; 9 | -------------------------------------------------------------------------------- /src/app/components/host-listener-container/routes.ts: -------------------------------------------------------------------------------- 1 | import {HostListenerContainerComponent} from './host-listener-container.component'; 2 | 3 | export const HOST_LISTENER$_ROUTES = [ 4 | { 5 | path: '', 6 | component: HostListenerContainerComponent 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /src/app/components/input-container/index.ts: -------------------------------------------------------------------------------- 1 | import {InputContainerComponent} from './input-container.component'; 2 | import {InputComponent} from './input.component'; 3 | import {Input2Component} from './input2.component'; 4 | 5 | export * from './routes'; 6 | export const INPUT$_DECLARATIONS = [ 7 | InputContainerComponent, 8 | InputComponent, 9 | Input2Component 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/components/input-container/input-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {interval, Observable, timer} from 'rxjs'; 3 | import {map, share, take} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-input-container', 7 | template: ` 8 |

Input$() Container

9 |
10 |       value in container: {{state$ | push$ | json}}
11 |       value in container: {{state2$ | push$ | json}}
12 |     
13 | 14 | 16 | 17 | 20 | 21 | `, 22 | changeDetection: ChangeDetectionStrategy.OnPush 23 | }) 24 | export class InputContainerComponent { 25 | 26 | state$ = this.getHotRandomInterval('val1', 1).pipe(take(2)); 27 | state2$ = this.getHotRandomInterval('val2', 3).pipe(take(2)); 28 | 29 | constructor() { 30 | 31 | } 32 | 33 | getHotRandomInterval(name: string, intVal: number = 1000): Observable<{ [key: string]: number }> { 34 | return timer(0, intVal) 35 | .pipe( 36 | map(_ => ({[name]: Math.random()})), 37 | share() 38 | ); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/app/components/input-container/input.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; 2 | import {Input$} from 'ng-re'; 3 | 4 | @Component({ 5 | selector: 'app-input', 6 | template: ` 7 |

Input$ Child1

8 |
 9 |       state$: {{state$ | async | json}}
10 |     
11 | `, 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class InputComponent { 15 | 16 | @Input$() 17 | @Input('state') 18 | state$; 19 | 20 | constructor() { 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/app/components/input-container/input2.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; 2 | import {Input$} from 'ng-re'; 3 | import {merge, Subject} from 'rxjs'; 4 | import {map, scan, shareReplay} from 'rxjs/operators'; 5 | 6 | 7 | @Component({ 8 | selector: 'app-input2', 9 | template: ` 10 |

Input$ Child2

11 |
12 |       state$: {{state$ | async | json}}
13 | state2$: {{state2$ | async | json}} 14 |
15 |

Component composition

16 |
17 |       viewModelA$: {{viewModelA$ | async | json}}
18 |     
19 | `, 20 | changeDetection: ChangeDetectionStrategy.OnPush 21 | }) 22 | export class Input2Component { 23 | command$$ = new Subject(); 24 | 25 | @Input$() 26 | @Input('state') 27 | state$; 28 | 29 | @Input$() 30 | @Input('state2') 31 | state2$; 32 | 33 | viewModelA$ = merge( 34 | this.command$$, 35 | this.state$.pipe(map(state => ({state}))), 36 | this.state2$.pipe(map(state2 => ({state2}))), 37 | ) 38 | .pipe( 39 | scan((st, sl) => ({...st, ...sl}), {}) 40 | ); 41 | 42 | 43 | constructor() { 44 | console.log('CTRO2 input child', this.state$); 45 | const initialState = { 46 | state: null, 47 | state2: [], 48 | otherSlice: {} 49 | }; 50 | setTimeout(() => { 51 | console.log('intialState', initialState); 52 | this.command$$.next(initialState); 53 | }, 1000); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/app/components/input-container/routes.ts: -------------------------------------------------------------------------------- 1 | import {InputContainerComponent} from './input-container.component'; 2 | 3 | export const INPUT$_ROUTES = [ 4 | { 5 | path: '', 6 | component: InputContainerComponent, 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /src/app/components/let-directive-container/full-example.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {combineLatest, interval, Observable} from 'rxjs'; 3 | import {filter, map, share, take} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-let-directive-full-example', 7 | template: ` 8 |

*Full Example

9 | 10 | https://github.com/angular/angular/issues/15280#issuecomment-290913071 11 | 12 | 13 | 14 | ` 15 | }) 16 | export class LetDirectiveFullExampleComponent { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/let-directive-container/index.ts: -------------------------------------------------------------------------------- 1 | import {LetDirectiveFullExampleComponent} from './full-example.component'; 2 | import {LetDirectiveContainerComponent} from './let-directive-container.component'; 3 | import {LetDirectiveObservableChannelsComponent} from './observable-channels.component'; 4 | import {LetDirectiveSupportedSyntaxComponent} from './supported-syntax.component'; 5 | import {LetDirectiveValueComponent} from './value.component'; 6 | 7 | export * from './routes'; 8 | export const LET_DECLARATIONS = [ 9 | LetDirectiveContainerComponent, 10 | LetDirectiveFullExampleComponent, 11 | LetDirectiveValueComponent, 12 | LetDirectiveObservableChannelsComponent, 13 | LetDirectiveSupportedSyntaxComponent 14 | ]; 15 | -------------------------------------------------------------------------------- /src/app/components/let-directive-container/let-directive-container.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {interval, Observable} from 'rxjs'; 3 | import {filter, map, share} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-let-directive-container', 7 | template: ` 8 |

*ngrxLet Container

9 | 17 | ` 18 | }) 19 | export class LetDirectiveContainerComponent { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/let-directive-container/observable-channels.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {interval, Observable} from 'rxjs'; 3 | import {filter, map, share, take} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-let-directive-observable-channels', 7 | template: ` 8 |

*ngrxLet Observable Channels

9 | 10 | 11 | 12 | 13 | 15 |

next:

16 |
{{(val1 | json) || 'undefined'}}
17 |

error:

18 |
{{(error | json) || 'undefined'}}
19 |

complete:

20 |
{{(complete | json) || 'undefined'}}
21 |
22 | ` 23 | }) 24 | export class LetDirectiveObservableChannelsComponent { 25 | 26 | val1$; 27 | 28 | constructor() { 29 | } 30 | 31 | assignObservable() { 32 | this.val1$ = this.getHotRandomInterval('test$', 1000).pipe(take(5)); 33 | } 34 | 35 | assignUndefined() { 36 | this.val1$ = undefined; 37 | } 38 | 39 | getHotRandomInterval(name: string, intVal: number = 1000): Observable<{ [key: string]: number }> { 40 | return interval(intVal) 41 | .pipe( 42 | map(_ => ({[name]: Math.random()})), 43 | share() 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/components/let-directive-container/routes.ts: -------------------------------------------------------------------------------- 1 | import {LetDirectiveFullExampleComponent} from './full-example.component'; 2 | import {LetDirectiveContainerComponent} from './let-directive-container.component'; 3 | import {LetDirectiveObservableChannelsComponent} from './observable-channels.component'; 4 | 5 | export const LET_ROUTES = [ 6 | { 7 | path: '', 8 | component: LetDirectiveContainerComponent, 9 | children: [ 10 | { 11 | path: 'full-example', 12 | component: LetDirectiveFullExampleComponent 13 | }, 14 | { 15 | path: 'observable-channels', 16 | component: LetDirectiveObservableChannelsComponent 17 | } 18 | ] 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /src/app/components/let-directive-container/supported-syntax.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {combineLatest, interval, Observable} from 'rxjs'; 3 | import {filter, map, share, take} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-let-directive-supported-syntax', 7 | template: ` 8 |

*ngrxLet Supported Syntax

9 |

10 | Binding with as syntax *ngrxLet="val1$ as o" 11 |

12 | 14 |
{{(o | json) || 'undefined'}}
15 | 16 |
17 | 18 |

19 | Binding composed object *ngrxLet="combinedInComponent$ as o" 20 |

21 | 23 |
{{o | json}}
24 | 25 |
26 | 27 |

28 | Binding an object of single values *ngrxLet="combinedInComponent$ as o; val1 as val1; val2 as val2" 29 |

30 | 32 |
{{val1 | json}}
33 |
{{val2 | json}}
34 | 35 | 36 |
37 | 38 |

39 | Binding an object of single values *ngrxLet="combinedInComponent$; let val1 = val1; let val2 = val2" 40 |

41 | 43 |
{{val1 | json}}
44 |
{{val2 | json}}
45 | 46 | 47 |
48 | 49 |

50 | Use animationFrameScheduler*ngrxLet="val1$ as o; useAf:true" 51 |

52 | 54 |
{{(o | json) || 'undefined'}}
55 | 56 |
57 | ` 58 | }) 59 | export class LetDirectiveSupportedSyntaxComponent { 60 | 61 | val1 = Math.random() * 100; 62 | val2 = Math.random() * 100; 63 | 64 | val1$ = this.getHotRandomInterval('val1', 1000).pipe(take(2)); 65 | val2$ = this.getHotRandomInterval('val2', 1000).pipe(take(2)); 66 | combinedInComponent$ = combineLatest( 67 | this.val1$, 68 | this.val2$, 69 | (val1, val2) => ({...val1, ...val2})); 70 | 71 | constructor() { 72 | } 73 | 74 | getHotRandomInterval(name: string, intVal: number = 1000): Observable<{ [key: string]: string }> { 75 | return interval(intVal) 76 | .pipe( 77 | map((_, i) => ({[name]: i + ':' + Math.random()})), 78 | share(), 79 | filter((v, i) => i < 1) 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/components/let-directive-container/value.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | import {interval, Observable} from 'rxjs'; 3 | import {filter, map, share} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-let-directive-value', 7 | template: `{{value | json}} ` 8 | }) 9 | export class LetDirectiveValueComponent { 10 | @Input() 11 | value; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/creation-and-clean-up/creation-and-clean-up-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {LocalStateService, selectSlice} from 'ng-re'; 3 | import {interval} from 'rxjs'; 4 | import {take} from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'app-creation-and-clean-up-container', 8 | template: ` 9 | 10 | 11 | 12 |
state$: {{localState.state$ | push$ | json}}
13 |
num$: {{num$ | push$ | json}}
14 | `, 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | providers: [ 17 | LocalStateService 18 | ] 19 | }) 20 | export class CreationAndCleanUpContainerComponent { 21 | 22 | num$ = this.localState.state$ 23 | .pipe(selectSlice(s => s.num)); 24 | 25 | constructor(public localState: LocalStateService) { 26 | this.localState.setSlices({num: 777}); 27 | } 28 | 29 | setNum() { 30 | this.localState.setSlices({num: 3}); 31 | } 32 | 33 | deleteNum() { 34 | this.localState.setSlices({num: undefined}); 35 | } 36 | 37 | setRandomState() { 38 | this.localState 39 | .connectSlices({['num' + Math.random()]: interval(500).pipe(take(10))}); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/early-producer/early-producer-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {Subject} from 'rxjs'; 3 | import {share, shareReplay} from 'rxjs/operators'; 4 | import {LocalStateService} from './example.service'; 5 | 6 | @Component({ 7 | selector: 'app-early-producer-container', 8 | template: ` 9 |

Early Producer Container

10 |

fromLocalState$

11 |
{{fromLocalState$ | async | json}}
12 |

v

13 |
{{v | async | json}}
14 | `, 15 | changeDetection: ChangeDetectionStrategy.OnPush 16 | }) 17 | export class EarlyProducerContainerComponent { 18 | 19 | notUnderControl = new Subject(); 20 | v = this.notUnderControl 21 | .pipe( 22 | shareReplay(1) 23 | ); 24 | private localState = new LocalStateService(); 25 | fromLocalState$ = this.localState.state$; 26 | 27 | constructor() { 28 | this.notUnderControl.subscribe(value => { 29 | this.localState.set({value}); 30 | 31 | this.localState.set({timestamp: Date.now()}); 32 | }); 33 | 34 | this.fromLocalState$.subscribe(console.log); 35 | 36 | this.notUnderControl.next(1); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/early-producer/example.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ReplaySubject} from 'rxjs'; 3 | import {scan, shareReplay} from 'rxjs/operators'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class LocalStateService { 9 | private command = new ReplaySubject(1); 10 | state$ = this.command.asObservable() 11 | .pipe(scan((a, c) => ({...a, ...c}), {}), 12 | // shareReplay(1) 13 | ); 14 | 15 | constructor() { 16 | this.state$.subscribe(console.log); 17 | } 18 | 19 | set(command) { 20 | this.command.next(command); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/child-local-state-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; 2 | import {combineLatest, Subject} from 'rxjs'; 3 | import {map, startWith, withLatestFrom} from 'rxjs/operators'; 4 | import {Input$, LocalStateService, selectSlice} from 'ng-re'; 5 | import {mapToAttendeesWithSelectionFiltered} from './map-to-Attendees-with-selection-filtered'; 6 | import {LocalStateComponentFacade} from './services/local-state-component.facade'; 7 | 8 | @Component({ 9 | selector: 'app-child-local-state-container', 10 | template: ` 11 |

Manage Attendees

12 | 15 | 18 | 23 | 24 | 27 |

Hide entries with properties false

28 |
29 | 30 | 32 |

Filtered and joined attendees

33 |
34 | `, 35 | changeDetection: ChangeDetectionStrategy.OnPush, 36 | providers: [LocalStateService 37 | ] 38 | }) 39 | export class ChildLocalStateContainerComponent { 40 | // INCOMING ========================== 41 | // INPUT DATA 42 | @Input$() 43 | @Input('selectedAttendeesIds') 44 | selectedAttendeesIdsFromInput$; 45 | 46 | // VIEW EVENTS 47 | showAllClick$$ = new Subject(); 48 | refreshAttendeesClick$$ = new Subject(); 49 | refreshCitiesClick$$ = new Subject(); 50 | filtersComponentStateChange$$ = new Subject(); 51 | 52 | // STATE ========================== 53 | // STATE SLICES 54 | filtersSlice$ = this.localState.state$.pipe(selectSlice(s => s.filters)); 55 | showAllSlice$ = this.localState.state$.pipe(selectSlice(s => s.showAll)); 56 | attendeesWithCitySlice$ = this.localState.state$.pipe(selectSlice(s => s.attendeesWithCity)); 57 | selectedAttendeesIdsSlice$ = this.localState.state$.pipe(selectSlice(s => s.selectedAttendeesIds)); 58 | 59 | // RENDERED STATE 60 | optionComponentState$ = this.filtersSlice$ 61 | .pipe( 62 | map(s => (!s ? null : {state: s, config: Object.keys(s)})) 63 | ); 64 | attendeesWithSelectionFiltered$ = combineLatest( 65 | this.attendeesWithCitySlice$, 66 | this.selectedAttendeesIdsSlice$.pipe(map(v => v ? v : [])), 67 | this.showAllSlice$.pipe(startWith(true)), 68 | this.filtersSlice$ 69 | ) 70 | .pipe( 71 | mapToAttendeesWithSelectionFiltered() 72 | ); 73 | 74 | 75 | // COMMANDS ================================= 76 | showAllCommand$ = this.showAllClick$$ 77 | .pipe( 78 | withLatestFrom(this.showAllSlice$, (_, isNew) => isNew), 79 | // toggle showAll state 80 | map((isNew: boolean) => !isNew) 81 | ); 82 | 83 | constructor( 84 | private localState: LocalStateService, 85 | public ngRxFacade: LocalStateComponentFacade 86 | ) { 87 | this.ngRxFacade.connectUpdateAttendees$(this.refreshAttendeesClick$$); 88 | this.ngRxFacade.connectUpdateCities$(this.refreshCitiesClick$$); 89 | 90 | this.localState.setSlices({filters: {paymentDone: false, specialMember: false}}); 91 | this.localState.connectSlices( 92 | { 93 | filters: this.filtersComponentStateChange$$, 94 | showAll: this.showAllCommand$, 95 | attendeesWithCity: this.ngRxFacade.attendeesWithCity$, 96 | selectedAttendeesIds: this.selectedAttendeesIdsFromInput$ 97 | }); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/components/display-compoent-state.interface.ts: -------------------------------------------------------------------------------- 1 | export interface DisplayComponentState { 2 | state: T; 3 | config: I; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/components/options-state.ts: -------------------------------------------------------------------------------- 1 | import {DisplayComponentState} from './display-compoent-state.interface'; 2 | 3 | export interface OptionsState extends DisplayComponentState<{ [key: string]: boolean }, string[]> { 4 | state: { [key: string]: boolean }; 5 | config: string[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/components/options.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input, Output} from '@angular/core'; 2 | import {FormBuilder, FormGroup} from '@angular/forms'; 3 | import {Input$, selectSlice} from 'ng-re'; 4 | import {combineLatest, Observable} from 'rxjs'; 5 | import {filter, map, shareReplay, switchMap} from 'rxjs/operators'; 6 | 7 | @Component({ 8 | selector: 'app-options', 9 | template: ` 10 | 11 |
14 | 22 |
`, 23 | changeDetection: ChangeDetectionStrategy.OnPush 24 | }) 25 | export class OptionsComponent { 26 | 27 | // localState$$ = new ReplaySubject(1); 28 | 29 | @Input$() 30 | @Input('state') 31 | localState$; 32 | 33 | config$ = this.localState$.pipe(selectSlice(v => v.config)); 34 | state$ = this.localState$.pipe(selectSlice(v => v.state)); 35 | 36 | formGroup$: Observable = combineLatest(this.state$, this.config$) 37 | .pipe( 38 | map(arr => { 39 | // @TODO a lot of small adoptions are needed for the undefined case 40 | if (arr.some(i => i === undefined)) { 41 | return undefined; 42 | } else { 43 | // we derive a formGroup from the passed state 44 | const config = this.toFormGroupConfig(arr); 45 | return this.fb.group(config); 46 | } 47 | }), 48 | // we want a single instance fo formGroup for all subscribers 49 | shareReplay(1) 50 | ); 51 | 52 | @Output() stateChange = this.formGroup$ 53 | .pipe(filter(f => !!f), switchMap(f => f.valueChanges)); 54 | 55 | constructor(private fb: FormBuilder) { 56 | } 57 | 58 | toFormGroupConfig([st, ops]) { 59 | // map state changes to form config 60 | return ops 61 | .reduce((c, o) => ({...c, [o]: [st[o]]}), {}); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/components/table.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; 2 | import {Input$} from 'ng-re'; 3 | import {map} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-table', 7 | template: ` 8 | 9 | 10 | 11 | 14 | 15 | 16 | 18 | 21 | 22 | 23 |
12 | {{heading}} 13 |
19 | {{row[key]}} 20 |
24 | `, 25 | changeDetection: ChangeDetectionStrategy.OnPush 26 | }) 27 | export class TableComponent { 28 | @Input$() 29 | @Input('state') 30 | state$; 31 | 32 | headings$ = this.state$.pipe( 33 | map(a => a ? Object.keys(a[0]) : []) 34 | ); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/full-example-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {LocalStateService, selectSlice} from 'ng-re'; 3 | import {Subject} from 'rxjs'; 4 | import {map, share, switchMapTo} from 'rxjs/operators'; 5 | import {NgRxStoreService} from './services/ng-rx-store.service'; 6 | 7 | @Component({ 8 | selector: 'app-full-example-container', 9 | template: ` 10 |

Full Example Local State Management

11 |
12 | 16 |
17 | 20 |

Selected Attendees

21 |
22 | 25 | 26 | `, 27 | changeDetection: ChangeDetectionStrategy.OnPush, 28 | providers: [ 29 | LocalStateService 30 | ] 31 | }) 32 | export class FullExampleContainerComponent { 33 | updateSelectedAttendees$ = new Subject(); 34 | 35 | selectedAttendees$ = this.updateSelectedAttendees$ 36 | .pipe( 37 | switchMapTo(this.store.storeState$.pipe(selectSlice((s) => s.attendees))), 38 | map((a: any[]) => { 39 | const items = Array.from({length: 4}).map(_ => a[Math.floor(Math.random() * a.length)]); 40 | return items; 41 | }), 42 | // share the same reference of items 43 | share() 44 | ); 45 | 46 | selectedAttendeesIds$ = this.selectedAttendees$ 47 | .pipe(map(a => a.map(i => i.id))); 48 | 49 | constructor(private store: NgRxStoreService) { 50 | 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/local-state-container2.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; 2 | import {LocalStateService, selectSlice} from 'ng-re'; 3 | import {combineLatest, ReplaySubject, Subject} from 'rxjs'; 4 | import {map, startWith, withLatestFrom} from 'rxjs/operators'; 5 | import {mapToAttendeesWithSelectionFiltered} from './map-to-Attendees-with-selection-filtered'; 6 | import {LocalStateComponentFacade} from './services/local-state-component.facade'; 7 | 8 | @Component({ 9 | selector: 'app-local-state-container2', 10 | template: ` 11 |

Manage Attendees

12 | 15 | 18 | 23 | 24 | 27 |

Hide entries with properties false

28 |
29 | 30 | 32 |

Filtered and joined attendees

33 |
34 | `, 35 | changeDetection: ChangeDetectionStrategy.OnPush, 36 | providers: [ 37 | LocalStateService 38 | ] 39 | }) 40 | export class LocalStateContainer2Component { 41 | // INCOMING ========================== 42 | // INPUT DATA 43 | selectedAttendeesIdsFromInput$ = new ReplaySubject(1); 44 | 45 | @Input() 46 | set selectedAttendeesIds(v) { 47 | this.selectedAttendeesIdsFromInput$.next(v); 48 | } 49 | 50 | // VIEW EVENTS 51 | showAllClick$$ = new Subject(); 52 | refreshAttendeesClick$$ = new Subject(); 53 | refreshCitiesClick$$ = new Subject(); 54 | filtersComponentStateChange$$ = new Subject(); 55 | 56 | // STATE ========================== 57 | // STATE SLICES 58 | filtersSlice$ = this.localState.state$.pipe(selectSlice(s => s.filters)); 59 | showAllSlice$ = this.localState.state$.pipe(selectSlice(s => s.showAll)); 60 | attendeesWithCitySlice$ = this.localState.state$.pipe(selectSlice(s => s.attendeesWithCity)); 61 | selectedAttendeesIdsSlice$ = this.localState.state$.pipe(selectSlice(s => s.selectedAttendeesIds)); 62 | 63 | // RENDERED STATE 64 | optionComponentState$ = this.filtersSlice$ 65 | .pipe( 66 | map(s => (!s ? null : {state: s, config: Object.keys(s)})) 67 | ); 68 | attendeesWithSelectionFiltered$ = combineLatest( 69 | this.attendeesWithCitySlice$, 70 | this.selectedAttendeesIdsSlice$.pipe(map(v => v ? v : [])), 71 | this.showAllSlice$.pipe(startWith(true)), 72 | this.filtersSlice$ 73 | ) 74 | .pipe( 75 | mapToAttendeesWithSelectionFiltered() 76 | ); 77 | 78 | 79 | // COMMANDS ================================= 80 | showAllCommand$ = this.showAllClick$$ 81 | .pipe( 82 | withLatestFrom(this.showAllSlice$, (_, isNew) => isNew), 83 | // toggle showAll state 84 | map((isNew: boolean) => !isNew) 85 | ); 86 | 87 | constructor( 88 | private localState: LocalStateService, 89 | public ngRxFacade: LocalStateComponentFacade 90 | ) { 91 | this.ngRxFacade.connectUpdateAttendees$(this.refreshAttendeesClick$$); 92 | this.ngRxFacade.connectUpdateCities$(this.refreshCitiesClick$$); 93 | 94 | this.localState.setSlices({filters: {paymentDone: false, specialMember: false}}); 95 | this.localState.connectSlices({filters: this.filtersComponentStateChange$$}); 96 | this.localState.connectSlices({showAll: this.showAllCommand$}); 97 | this.localState.connectSlices({attendeesWithCity: this.ngRxFacade.attendeesWithCity$}); 98 | this.localState.connectSlices({selectedAttendeesIds: this.selectedAttendeesIdsFromInput$}); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/map-to-Attendees-with-selection-filtered.ts: -------------------------------------------------------------------------------- 1 | import {pipe} from 'rxjs'; 2 | import {map} from 'rxjs/operators'; 3 | 4 | export function mapToAttendeesWithSelectionFiltered() { 5 | return pipe( 6 | map(([all, ids, showAll, filters]) => { 7 | 8 | if (!(all && ids)) { 9 | return undefined; 10 | } 11 | 12 | let withSelectedState = all 13 | .map(a => ({...a, selected: ids.includes(a.id)})); 14 | 15 | const filterKeysSet = Object.keys(filters) 16 | .filter(filterProp => filters[filterProp] === true); 17 | 18 | if (filterKeysSet.length) { 19 | withSelectedState = withSelectedState.filter( 20 | i => { 21 | const isItemPropFalse = filterKeysSet 22 | .some(filterProp => i[filterProp] === false); 23 | return !isItemPropFalse; 24 | } 25 | ); 26 | } 27 | 28 | const visibleItems = showAll ? withSelectedState : withSelectedState.slice(0, 10); 29 | 30 | return visibleItems; 31 | }) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/services/local-state-component.facade.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Hook$, selectSlice} from 'ng-re'; 3 | import {combineLatest, merge, Subject} from 'rxjs'; 4 | import {filter, map, mergeAll, takeUntil, tap} from 'rxjs/operators'; 5 | import {NgRxStoreService} from './ng-rx-store.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class LocalStateComponentFacade { 11 | 12 | @Hook$('onDestroy') 13 | onDestroy$; 14 | 15 | cities$ = this.ngRxStore.storeState$ 16 | .pipe(selectSlice(s => s.cities)); 17 | attendees$ = this.ngRxStore.storeState$ 18 | .pipe(selectSlice(s => s.attendees)); 19 | 20 | attendeesWithCity$ = combineLatest( 21 | this.attendees$.pipe(filter(v => !!v)), 22 | this.cities$.pipe(filter(v => !!v)) 23 | ) 24 | .pipe( 25 | map(([attendees, cities]) => { 26 | return (attendees as any[]) 27 | .map(a => { 28 | const city = (cities as any[]).find(c => c.id === a.cid); 29 | // remove cid property; 30 | const {cid, ...withoutCityId} = a; 31 | return { 32 | ...withoutCityId, 33 | city: city ? city.name : 'none', 34 | paymentDone: Math.random() < 0.5 35 | }; 36 | }); 37 | }) 38 | ); 39 | 40 | triggerUpdateAttendees$ = new Subject(); 41 | triggerUpdateCities$ = new Subject(); 42 | 43 | constructor(private ngRxStore: NgRxStoreService) { 44 | merge( 45 | this.triggerUpdateAttendees$.pipe(mergeAll(), tap(_ => this.ngRxStore.updateAttendees())), 46 | this.triggerUpdateCities$.pipe(mergeAll(), tap(_ => this.ngRxStore.updateCities())), 47 | ).pipe( 48 | takeUntil(this.onDestroy$) 49 | ) 50 | .subscribe(); 51 | } 52 | 53 | connectUpdateAttendees$(t) { 54 | this.triggerUpdateAttendees$.next(t); 55 | } 56 | 57 | connectUpdateCities$(t) { 58 | this.triggerUpdateCities$.next(t); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/full-example-container/services/ng-rx-store.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {Injectable} from '@angular/core'; 3 | import {LocalStateService} from 'ng-re'; 4 | import {getRandomAttendees, getRandomCity} from '../../random'; 5 | 6 | // Requires a model 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class NgRxStoreService { 12 | 13 | private store = new LocalStateService(); 14 | storeState$ = this.store.state$; 15 | 16 | constructor(private http: HttpClient) { 17 | this.updateAttendees(); 18 | this.updateCities(); 19 | } 20 | 21 | updateAttendees() { 22 | this.http.get('https://my-json-server.typicode.com/BioPhoton/reactiveAddons/posts') 23 | .subscribe(console.log); 24 | // https://my-json-server.typicode.com/BioPhoton/reactiveAddons 25 | /*this.http.get('https://swapi.co/api/people/') 26 | .pipe( 27 | expand((r: any) => 'next' in r ? this.http.get(r.next) : EMPTY), 28 | map(r => r.results), 29 | map(a => a.map(i => ( 30 | ({name, hair_color, skin_color, created}) => 31 | ({name, hair_color, skin_color, id: Math.random()}))(i) 32 | ) 33 | ), 34 | // catchError(_ => of(getRandomAttendees(30))) 35 | ) 36 | .subscribe( 37 | attendees => { 38 | this.store.setSlice({attendees}); 39 | } 40 | );*/ 41 | setTimeout(() => { 42 | this.store.setSlices({attendees: getRandomAttendees(30)}); 43 | }, 2000); 44 | } 45 | 46 | updateCities() { 47 | setTimeout(() => { 48 | console.log('updateCities'); 49 | this.store.setSlices({cities: getRandomCity(5, 10)}); 50 | }, 2000); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/index.ts: -------------------------------------------------------------------------------- 1 | import {CreationAndCleanUpContainerComponent} from './creation-and-clean-up/creation-and-clean-up-container.component'; 2 | import {EarlyProducerContainerComponent} from './early-producer/early-producer-container.component'; 3 | import {ChildLocalStateContainerComponent} from './full-example-container/child-local-state-container.component'; 4 | import {OptionsComponent} from './full-example-container/components/options.component'; 5 | import {TableComponent} from './full-example-container/components/table.component'; 6 | import {FullExampleContainerComponent} from './full-example-container/full-example-container.component'; 7 | import {LocalStateContainer2Component} from './full-example-container/local-state-container2.component'; 8 | import {LateSubscriberComponent} from './late-subscribers/late-subscriber.component'; 9 | import {LateSubscribersContainerComponent} from './late-subscribers/late-subscribers-container.component'; 10 | import {LocalStateContainerComponent} from './local-state-container.component'; 11 | import {NgForContainerComponent} from './ng-for/ng-for-container.component'; 12 | import {PlaceholderContentContainerComponent} from './placeholder-content/placeholder-content-container.component'; 13 | import {SharingAReferenceContainerComponent} from './sharing-a-reference/sharing-a-reference-container.component'; 14 | import {SharingAReferenceComponent} from './sharing-a-reference/sharing-a-reference.component'; 15 | 16 | export * from './routes'; 17 | export const LOCAL_STATE_DECLARATIONS = [ 18 | CreationAndCleanUpContainerComponent, 19 | 20 | LateSubscribersContainerComponent, 21 | LateSubscriberComponent, 22 | 23 | EarlyProducerContainerComponent, 24 | 25 | SharingAReferenceContainerComponent, 26 | SharingAReferenceComponent, 27 | 28 | PlaceholderContentContainerComponent, 29 | 30 | NgForContainerComponent, 31 | 32 | ChildLocalStateContainerComponent, 33 | LocalStateContainer2Component, 34 | OptionsComponent, 35 | LocalStateContainerComponent, 36 | FullExampleContainerComponent, 37 | TableComponent 38 | 39 | ]; 40 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/late-subscribers/example.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ReplaySubject} from 'rxjs'; 3 | import {scan, share, shareReplay} from 'rxjs/operators'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class LocalStateService { 9 | private command = new ReplaySubject(1); 10 | state$ = this.command.asObservable() 11 | .pipe( 12 | scan((a, c) => ({...a, ...c}), {}), 13 | shareReplay() 14 | ); 15 | 16 | constructor() { 17 | } 18 | 19 | set(command) { 20 | this.command.next(command); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/late-subscribers/late-subscriber.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; 2 | import {ReplaySubject, Subject} from 'rxjs'; 3 | import {LocalStateService} from './example.service'; 4 | 5 | @Component({ 6 | selector: 'app-late-subscriber', 7 | template: ` 8 |

Late Subscriber Child

9 |

default$:

10 |
{{default$ | async | json}}
11 |

replayed$

12 |
{{replayed$ | async | json}}
13 |

fromLocalState$

14 |
{{fromLocalState$ | async | json}}
15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class LateSubscriberComponent { 19 | 20 | private localState = new LocalStateService(); 21 | 22 | default$ = new Subject(); 23 | replayed$ = new ReplaySubject(1); 24 | fromLocalState$ = this.localState.state$; 25 | 26 | @Input() 27 | set state(value) { 28 | this.default$.next({value}); 29 | this.replayed$.next({value}); 30 | 31 | this.localState.set({value}); 32 | this.localState.set({timestamp: Date.now()}); 33 | } 34 | 35 | constructor() { 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/late-subscribers/late-subscribers-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {of} from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'app-late-subscribers-container', 6 | template: ` 7 |

state$:

8 |
{{num$ | async | json}}
9 | 10 | 11 | `, 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class LateSubscribersContainerComponent { 15 | 16 | num$ = of(1); 17 | 18 | constructor() { 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/local-state-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {timer} from 'rxjs'; 3 | import {map} from 'rxjs/operators'; 4 | import {randomName} from './random'; 5 | 6 | @Component({ 7 | selector: 'app-local-state-container', 8 | template: ` 9 |

LocalStateService Container

10 | 43 | 44 | `, 45 | changeDetection: ChangeDetectionStrategy.OnPush 46 | }) 47 | export class LocalStateContainerComponent { 48 | prefilledData$ = timer(0, 2000) 49 | .pipe( 50 | map(i => i % 2 ? { 51 | name: randomName(), 52 | age: parseInt(Math.random() * 100 + '', 10) 53 | } : {} 54 | ) 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/ng-for/ng-for-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {LocalStateService, selectSlice} from 'ng-re'; 3 | 4 | @Component({ 5 | selector: 'app-ng-for-container', 6 | template: ` 7 |

ngFor Container

8 |
9 | 14 |
15 |
{{v | async | json}}
16 | `, 17 | changeDetection: ChangeDetectionStrategy.OnPush, 18 | providers: [LocalStateService] 19 | }) 20 | export class NgForContainerComponent { 21 | 22 | buttons$ = this.localState.state$.pipe(selectSlice(s => s.buttons)); 23 | 24 | constructor(private localState: LocalStateService) { 25 | this.localState.setSlices({ 26 | buttons: [1, 2, 3, 4, 5] 27 | }); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/placeholder-content/placeholder-content-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {of, Subject} from 'rxjs'; 3 | import {delay} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-placeholder-content-container', 7 | template: ` 8 |

Placeholder Content Container

9 |
10 | from http request{{data | json}} 11 |
12 | 13 |
Placeholder Content Here
14 |
15 |
16 | 17 | {{data | json}} 18 |
19 | from http request{{data | json}} 20 |
21 |
22 | Placeholder Content Here 23 |
24 |
25 | `, 26 | changeDetection: ChangeDetectionStrategy.OnPush 27 | }) 28 | export class PlaceholderContentContainerComponent { 29 | 30 | httpData$ = of({name: 'test name'}) 31 | .pipe( 32 | delay(3000) 33 | ); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/random.ts: -------------------------------------------------------------------------------- 1 | import * as faker from 'faker'; 2 | 3 | export function getRandomAttendees(num, range?) { 4 | return Array.from({length: num}) 5 | .map((_, index) => ( 6 | { 7 | id: faker.random.number(range || num), 8 | cid: faker.random.number(5), 9 | name: faker.name.findName(), 10 | specialMember: Math.random() < 0.5 11 | }) 12 | ); 13 | } 14 | 15 | export function getRandomCity(num, range?) { 16 | return Array.from({length: num}) 17 | .map((_, index) => ( 18 | { 19 | id: faker.random.number(range || num), 20 | name: faker.address.city() 21 | }) 22 | ); 23 | } 24 | 25 | export function randomName() { 26 | return faker.name.findName(); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/routes.ts: -------------------------------------------------------------------------------- 1 | import {CreationAndCleanUpContainerComponent} from './creation-and-clean-up/creation-and-clean-up-container.component'; 2 | import {EarlyProducerContainerComponent} from './early-producer/early-producer-container.component'; 3 | import {FullExampleContainerComponent} from './full-example-container/full-example-container.component'; 4 | import {LateSubscribersContainerComponent} from './late-subscribers/late-subscribers-container.component'; 5 | import {LocalStateContainerComponent} from './local-state-container.component'; 6 | import {NgForContainerComponent} from './ng-for/ng-for-container.component'; 7 | import {PlaceholderContentContainerComponent} from './placeholder-content/placeholder-content-container.component'; 8 | import {SharingAReferenceContainerComponent} from './sharing-a-reference/sharing-a-reference-container.component'; 9 | 10 | export const LOCAL_STATE_ROUTES = [ 11 | { 12 | path: '', 13 | component: LocalStateContainerComponent, 14 | children: [ 15 | { 16 | path: 'state-clean-up', 17 | component: CreationAndCleanUpContainerComponent 18 | }, 19 | { 20 | path: 'late-subscriber', 21 | component: LateSubscribersContainerComponent 22 | }, 23 | { 24 | path: 'early-producer', 25 | component: EarlyProducerContainerComponent 26 | }, 27 | { 28 | path: 'sharing-a-reference', 29 | component: SharingAReferenceContainerComponent 30 | }, 31 | { 32 | path: 'placeholder-content', 33 | component: PlaceholderContentContainerComponent 34 | }, 35 | { 36 | path: 'ng-for', 37 | component: NgForContainerComponent 38 | }, 39 | { 40 | path: 'full-example', 41 | component: FullExampleContainerComponent 42 | } 43 | ] 44 | } 45 | ]; 46 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/sharing-a-reference/sharing-a-reference-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {of, Subject} from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'app-sharing-a-reference-container', 6 | template: ` 7 |

formGroupModel$:

8 |
{{formGroupModel$ | async | json}}
9 |

formValue$:

10 |
{{formValue$ | async | json}}
11 | 14 | 15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class SharingAReferenceContainerComponent { 19 | 20 | formValue$ = new Subject(); 21 | 22 | formGroupModel$ = of({ 23 | name: '', 24 | age: 0 25 | }); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/components/local-state-container/sharing-a-reference/sharing-a-reference.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; 2 | import {FormBuilder, FormGroup} from '@angular/forms'; 3 | import {ActivatedRoute} from '@angular/router'; 4 | import {combineLatest, Observable, ReplaySubject} from 'rxjs'; 5 | import {map, shareReplay, switchMap} from 'rxjs/operators'; 6 | 7 | @Component({ 8 | selector: 'app-sharing-a-reference', 9 | template: ` 10 |

Sharing a reference

11 |

default$:

12 |
13 |
14 | 15 | 16 |
17 |
18 | `, 19 | changeDetection: ChangeDetectionStrategy.OnPush, 20 | }) 21 | export class SharingAReferenceComponent { 22 | state$ = new ReplaySubject(1); 23 | formGroup$: Observable = combineLatest(this.state$, this.router.params) 24 | .pipe( 25 | map(this.preparingFormGroupConfig), 26 | map(formGroupConfig => this.fb.group(formGroupConfig)), 27 | shareReplay(1) 28 | ); 29 | 30 | @Input() 31 | set formGroupModel(value) { 32 | this.state$.next(value); 33 | } 34 | 35 | @Output() formValueChange = new EventEmitter(); 36 | 37 | constructor( 38 | private fb: FormBuilder, 39 | private router: ActivatedRoute 40 | ) { 41 | this.formGroup$ 42 | .pipe( 43 | switchMap((fg: FormGroup) => fg.valueChanges) 44 | ) 45 | .subscribe(v => this.formValueChange.emit(v)); 46 | } 47 | 48 | preparingFormGroupConfig([modelFromInput, modelFromRouterParams]) { 49 | // override defaults with router params if exist 50 | return Object.entries({...modelFromInput, ...modelFromRouterParams}) 51 | .reduce((c, [name, initialValue]) => ({...c, [name]: [initialValue]}), {}); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/app/components/push-pipe-container/index.ts: -------------------------------------------------------------------------------- 1 | import {PushPipeChannelsComponent} from './push-pipe-channels.component'; 2 | import {PushPipeContainerComponent} from './push-pipe-container.component'; 3 | import {PushPipeComponent} from './push-pipe.component'; 4 | 5 | export * from './routes'; 6 | export const PUSH$_DECLARATIONS = [ 7 | PushPipeContainerComponent, 8 | PushPipeComponent, 9 | PushPipeChannelsComponent 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/components/push-pipe-container/push-pipe-channels.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; 2 | import {interval} from 'rxjs'; 3 | import {map} from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-push-pipe-channel', 7 | template: ` 8 |

Channels

9 |

10 | value: 11 |

12 |
13 |       
value: {{value}}
14 |
15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class PushPipeChannelsComponent { 19 | value$ = interval(1000) 20 | .pipe( 21 | map((v, i) => { 22 | if (i === 4) { 23 | throw new Error('asdfsda'); 24 | } 25 | return v; 26 | }) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/app/components/push-pipe-container/push-pipe-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | import {timer} from 'rxjs'; 3 | import {shareReplay} from 'rxjs/operators'; 4 | 5 | 6 | @Component({ 7 | selector: 'app-push-pipe-container', 8 | template: ` 9 |

push$ pipe Container

10 |

primitiveInterval$ | push:

11 |
{{primitiveInterval$ | push$ | json}}
12 |
13 | 15 | 16 | `, 17 | changeDetection: ChangeDetectionStrategy.OnPush 18 | }) 19 | export class PushPipeContainerComponent { 20 | primitiveInterval$ = timer(0, 2000).pipe(shareReplay(1)); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/push-pipe-container/push-pipe.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-push-pipe', 5 | template: ` 6 |

push$ Child

7 |

8 | value: 9 |

10 |
11 |       {{value | json}}
12 |     
13 | `, 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class PushPipeComponent { 17 | @Input() value; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/push-pipe-container/routes.ts: -------------------------------------------------------------------------------- 1 | import {PushPipeChannelsComponent} from './push-pipe-channels.component'; 2 | import {PushPipeContainerComponent} from './push-pipe-container.component'; 3 | 4 | export const PUSH$_ROUTES = [ 5 | { 6 | path: '', 7 | component: PushPipeContainerComponent, 8 | children: [ 9 | { 10 | path: 'pipe-channels', 11 | component: PushPipeChannelsComponent 12 | } 13 | ] 14 | } 15 | ]; 16 | -------------------------------------------------------------------------------- /src/app/components/star-rating/index.ts: -------------------------------------------------------------------------------- 1 | import {StarRatingContainerComponent} from './star-rating-container.component'; 2 | import {StarRatingComponent} from './star-rating.component'; 3 | import {StarComponent} from './star.component'; 4 | 5 | export * from './routes'; 6 | export const STAR_RATING_DECLARATIONS = [ 7 | StarRatingContainerComponent, 8 | StarRatingComponent, 9 | StarComponent 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/components/star-rating/routes.ts: -------------------------------------------------------------------------------- 1 | import {StarRatingContainerComponent} from './star-rating-container.component'; 2 | 3 | export const STAR_RATING_ROUTES = [ 4 | { 5 | path: '', 6 | component: StarRatingContainerComponent, 7 | children: [ 8 | 9 | ] 10 | } 11 | ]; 12 | -------------------------------------------------------------------------------- /src/app/components/star-rating/star-rating-config.interface.ts: -------------------------------------------------------------------------------- 1 | export type starRatingSizes = 'small' | 'medium' | 'large'; 2 | export type starRatingColor = 'default' | 'negative' | 'ok' | 'positive'; 3 | export type starRatingSpeed = 'immediately' | 'noticeable' | 'slow'; 4 | export type starRatingLabelPosition = 'left' | 'right' | 'top' | 'bottom'; 5 | export type starRatingStarTypes = 'svg' | 'icon' | 'custom-icon'; 6 | export type starRatingStarSpace = 'no' | 'between' | 'around'; 7 | export type starRatingDirection = 'rtl' | 'ltr'; 8 | 9 | export class StarRatingConfig { 10 | // binding defaults 11 | numOfStars?: number; 12 | size?: starRatingSizes; 13 | speed?: starRatingSpeed; 14 | labelPosition?: starRatingLabelPosition; 15 | starType?: starRatingStarTypes; 16 | staticColor: starRatingColor; 17 | getColor?: ( 18 | rating: number, 19 | numOfStars: number, 20 | staticColor?: starRatingColor 21 | ) => starRatingColor; 22 | getHalfStarVisible?: (rating: number) => boolean; 23 | // statics 24 | classEmpty?: string; 25 | classHalf?: string; 26 | classFilled?: string; 27 | assetsPath?: string; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/components/star-rating/star-rating-container.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | 3 | 4 | @Component({ 5 | selector: 'app-star-rating-container', 6 | template: ` 7 |

StarRating Container

8 | 9 | `, 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class StarRatingContainerComponent { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/app/components/star-rating/star-rating.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | 3 | 4 | @Component({ 5 | selector: 'app-star-rating', 6 | template: ` 7 |
10 |
{{labelText}}
11 |
12 | 16 | 17 |
18 |
19 | `, 20 | changeDetection: ChangeDetectionStrategy.OnPush 21 | }) 22 | export class StarRatingComponent { 23 | id; 24 | labelText; 25 | stars = [0, 1, 2, 3, 4]; 26 | 27 | enterStar(starNumber: number) { 28 | 29 | } 30 | 31 | leaveStarContainer() { 32 | 33 | } 34 | 35 | selectStar(starNumber: number) { 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/app/components/star-rating/star.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-star', 5 | template: ` 6 |
7 | 8 | 9 | 10 |
11 | `, 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class StarComponent { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/ngRe/6c1cbd62f425d522eb36f172b7e64bb89a2a5e2f/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/ngRe/6c1cbd62f425d522eb36f172b7e64bb89a2a5e2f/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgRe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | .spin { 4 | animation-name: spin; 5 | animation-duration: 5000ms; 6 | animation-iteration-count: infinite; 7 | animation-timing-function: linear; 8 | width: 10px; 9 | text-align: center; 10 | font-size: 20px; 11 | line-height: 20px; 12 | &::after{ 13 | content: '↻'; 14 | } 15 | } 16 | 17 | @keyframes spin { 18 | from { 19 | transform:rotate(0deg); 20 | } 21 | to { 22 | transform:rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "src/test.ts", 12 | "src/**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "target": "es2015", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ], 22 | "paths": { 23 | "ng-re": [ 24 | "dist/ng-re", 25 | "projects/ng-re/src/public-api.ts" 26 | ], 27 | "ng-re/*": [ 28 | "dist/ng-re/*", 29 | "projects/ng-re/src/*" 30 | ] 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warn" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-use-before-declare": true, 64 | "no-var-requires": false, 65 | "object-literal-key-quotes": [ 66 | true, 67 | "as-needed" 68 | ], 69 | "object-literal-sort-keys": false, 70 | "ordered-imports": false, 71 | "quotemark": [ 72 | true, 73 | "single" 74 | ], 75 | "trailing-comma": false, 76 | "no-conflicting-lifecycle": true, 77 | "no-host-metadata-property": true, 78 | "no-input-rename": true, 79 | "no-inputs-metadata-property": true, 80 | "no-output-native": true, 81 | "no-output-on-prefix": true, 82 | "no-output-rename": true, 83 | "no-outputs-metadata-property": true, 84 | "template-banana-in-box": true, 85 | "template-no-negated-async": true, 86 | "use-lifecycle-interface": true, 87 | "use-pipe-transform-interface": true 88 | }, 89 | "rulesDirectory": [ 90 | "codelyzer" 91 | ] 92 | } --------------------------------------------------------------------------------