├── src
├── assets
│ ├── .gitkeep
│ └── research-ephemeral-state-dramatic-title.png
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── app
│ ├── examples
│ │ ├── demo-basics
│ │ │ ├── 1
│ │ │ │ └── demo-basics-1.component.ts
│ │ │ ├── 2
│ │ │ │ └── demo-basics-2.component.ts
│ │ │ ├── 3
│ │ │ │ └── demo-basics-3.component.ts
│ │ │ ├── 4
│ │ │ │ ├── demo-basics.base-model.interface.ts
│ │ │ │ ├── demo-basics.view.interface.ts
│ │ │ │ ├── demo-basics.view-model.service.ts
│ │ │ │ ├── demo-basics-4.view.html
│ │ │ │ └── demo-basics-4.component.ts
│ │ │ ├── demo-basics-item.interface.ts
│ │ │ ├── demo-basics.container.component.ts
│ │ │ ├── demo-basics.module.ts
│ │ │ └── rx-ephemeral-state.ts
│ │ ├── examples.container.component.ts
│ │ └── problems
│ │ │ ├── cold-composition
│ │ │ ├── cold-composition.container.component.ts
│ │ │ ├── some-bad.service.ts
│ │ │ ├── some-good.service.ts
│ │ │ ├── cold-composition.module.ts
│ │ │ ├── cold-composition-bad.component.ts
│ │ │ └── cold-composition-good.component.ts
│ │ │ ├── declarative-interaction
│ │ │ ├── declarative-interaction.container.component.ts
│ │ │ ├── declarative-side-effects-good.component.ts
│ │ │ ├── declarative-interaction-bad.component.ts
│ │ │ ├── declarative-side-effects-good.service.ts
│ │ │ ├── declarative-interaction-good.component.ts
│ │ │ ├── declarative-interaction.module.ts
│ │ │ ├── declarative-interaction-bad.service.ts
│ │ │ └── declarative-interaction-good.service.ts
│ │ │ ├── subscription-handling
│ │ │ ├── subscription-handling.service.ts
│ │ │ ├── subscription-handling.module.ts
│ │ │ ├── subscription-handling.component.ts
│ │ │ └── subscription-handling-bad.component.ts
│ │ │ ├── late-subscriber
│ │ │ ├── late-subscriber.display.component.ts
│ │ │ ├── late-subscriber-fix.display.component.ts
│ │ │ ├── late-subscriber.container.component.ts
│ │ │ └── late-subscriber.module.ts
│ │ │ └── sharing-a-reference
│ │ │ ├── sharing-a-reference.module.ts
│ │ │ ├── sharing-a-reference-bad.display.component.ts
│ │ │ ├── sharing-a-reference.container.component.ts
│ │ │ ├── sharing-a-reference-good.display.component.ts
│ │ │ ├── sharing-a-reference-imp.display.component.ts
│ │ │ └── sharing-a-reference-basics.display.component.ts
│ ├── data-access
│ │ └── github
│ │ │ ├── +state
│ │ │ ├── repository-list.model.ts
│ │ │ ├── selectors.ts
│ │ │ ├── actions.ts
│ │ │ ├── effects.ts
│ │ │ └── reducer.ts
│ │ │ ├── index.ts
│ │ │ ├── github.module.ts
│ │ │ └── github.service.ts
│ ├── common
│ │ ├── index.ts
│ │ ├── local-effects.service.ts
│ │ ├── component-state.service.ts
│ │ └── local-state.service.ts
│ ├── app-component
│ │ ├── app.component.scss
│ │ ├── app.component.ts
│ │ ├── app.view.model.ts
│ │ └── app.component.html
│ ├── app.routes.ts
│ └── app.module.ts
├── favicon.ico
├── styes
│ ├── _general.scss
│ ├── theme.scss
│ └── _logger.scss
├── styles.scss
├── main.ts
├── index.html
└── polyfills.ts
├── .idea
├── .gitignore
├── misc.xml
├── vcs.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
└── blog-crafting-reactive-ephemeral-state.iml
├── images
├── angular-timeline__michael-hladky.png
├── researc-ephemeral-state-dramatic-title.png
├── cover-reactive-local-state__michael-hladky.png
├── reactive-local-state-intro__michael-hladky.png
├── reactive-local-state-hot-cold_unicast-multicast.png
├── reactive-local-quote-gang-of-four2__michael-hladky.png
├── reactive-local-quote-gang-of-four__michael-hladky.png
├── reactive-local-state-first-draft__michael-hladky.png
├── reactive-local-state_ephemeral-state__michael-hladky.png
├── reactive-local-state_layers-of-state__michael-hladky.png
├── reactive-local-state_global-accessible__michael-hladky.png
├── reactive-local-state_local-accessible__michael-hladky.png
├── reactive-local-state-sate-late-subscriber__michael-hladky.png
├── reactive-local-state_lifetime-async-pipe__michael-hladky.png
├── reactive-local-state-sate-subscriber-problem__michael-hladky.png
├── reactive-local-state_subscription-handling__michael-hladky.png
├── reactive-local-state_uni-case-vs-multi-cast__michael-hladky.png
├── reactive-local-state-sate-subscriber-solution__michael-hladky.png
├── reactive-local-changes_processing-global-sources__michael-hladky.png
├── reactive-local-changes_processing-local-sources__michael-hladky.png
├── reactive-local-state_timing-component-lifecycle__michael-hladky.png
├── reactive-local-state_uni-case-vs-multi-cast-work__michael-hladky.png
├── reactive-local-state-declarative-interaction-setter__michael-hladky.png
├── reactive-local-state-declarative-interaction-connector__michael-hladky.png
├── reactive-local-state_lifetime-angular-building-blocks__michael-hladky.png
├── reactive-local-state_lifetime-global-singleton-service__michael-hladky.png
├── reactive-local-state_uni-case-vs-multi-cast-instance__michael-hladky.png
├── reactive-local-state_uni-case-vs-multi-cast-operators__michael-hladky.png
├── reactive-local-state_uni-case-vs-multi-cast-observables__michael-hladky.png
├── reactive-local-state-declarative-interaction-breaking-flow__michael-hladky.png
├── reactive-local-state-declarative-interaction-connector-code__michael-hladky.png
├── reactive-local-state-sate-subscriber-replay-caveat-workload__michael-hladky.png
├── reactive-local-state-declarative-interaction-connector-and-state__michael-hladky.png
├── reactive-local-state-sate-subscriber-replay-caveat-cold-composition__michael-hladky.png
├── reactive-local-state-sate-subscriber-replay-cold-composition-problem__michael-hladky.png
├── reactive-local-state-sate-subscriber-replay-cold-composition-solution__michael-hladky.png
└── reactive-local-state_timing-lifecycl-hooks-and-subscriptions-hello-world__michael-hladky.png
├── tsconfig.app.json
├── browserslist
├── .gitignore
├── tsconfig.json
├── package.json
├── angular.json
└── README.md
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Default ignored files
3 | /workspace.xml
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/app/examples/demo-basics/demo-basics-item.interface.ts:
--------------------------------------------------------------------------------
1 | export interface DemoBasicsItem {
2 | id: string;
3 | name:string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BioPhoton/research-reactive-ephemeral-state-in-component-oriented-frontend-frameworks/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/app/data-access/github/+state/repository-list.model.ts:
--------------------------------------------------------------------------------
1 | export interface RepositoryListItem {
2 | id: string,
3 | name: string,
4 | created: string
5 | }
6 |
--------------------------------------------------------------------------------
/src/styes/_general.scss:
--------------------------------------------------------------------------------
1 | body {
2 |
3 | }
4 | .row {
5 | display: flex;
6 | margin: 0 -15px;
7 | .col {
8 | width: 50%;
9 | padding: 0 15px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 | `
8 | })
9 | export class ExampleContainerComponent {
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/app-component/app.component.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 15px;
3 | }
4 | .sidenav-container {
5 | height: 100%;
6 | }
7 |
8 | .sidenav {
9 | width: 250px;
10 | }
11 |
12 | .sidenav .mat-toolbar {
13 | background: inherit;
14 | }
15 |
16 | .mat-toolbar.mat-primary {
17 | position: sticky;
18 | top: 0;
19 | z-index: 1;
20 | }
21 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
child state$:
9 |{{state$ | async | json}}
10 | `,
11 | changeDetection: ChangeDetectionStrategy.OnPush,
12 | })
13 | export class LateSubscriberDisplayComponent {
14 |
15 |
16 | state$ = new Subject();
17 |
18 | constructor() {
19 | }
20 |
21 | @Input()
22 | set state(value) {
23 | this.state$.next({value});
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/examples/problems/late-subscriber/late-subscriber-fix.display.component.ts:
--------------------------------------------------------------------------------
1 | import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
2 | import {ReplaySubject} from 'rxjs';
3 |
4 | @Component({
5 | selector: 'late-subscriber-fix-display',
6 | template: `
7 | child state$:
9 |{{state$ | async | json}}
10 | `,
11 | changeDetection: ChangeDetectionStrategy.OnPush,
12 | })
13 | export class LateSubscriberFixDisplayComponent {
14 |
15 |
16 | state$ = new ReplaySubject(1);
17 |
18 | constructor() {
19 | }
20 |
21 | @Input()
22 | set state(value) {
23 | this.state$.next({value});
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/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/app/examples/problems/subscription-handling/subscription-handling.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {timer} from 'rxjs';
3 | import {tap} from 'rxjs/operators';
4 | import {SubscriptionHandlingService} from './subscription-handling.service';
5 |
6 | @Component({
7 | selector: 'subscription-handling',
8 | template: `
9 | parent state$:
8 |{{num$ | async | json}}
9 | Declarative SideEffects
10 | `, 11 | providers: [DeclarativeSideEffectsGoodService] 12 | }) 13 | export class DeclarativeSideEffectsGoodComponent { 14 | constructor(private stateService: DeclarativeSideEffectsGoodService) { 15 | this.stateService.connectEffect(interval(1000) 16 | .pipe(tap(_ => ({count: ~~(Math.random() * 100)})))); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | 4 | # compiled output 5 | /dist 6 | /tmp 7 | /out-tsc 8 | # Only exists if Bazel was run 9 | /bazel-out 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # profiling files 15 | chrome-profiler-events*.json 16 | speed-measure-plugin*.json 17 | 18 | # IDEs and editors 19 | .idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | .history/* 34 | 35 | # misc 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /src/app/examples/problems/late-subscriber/late-subscriber.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {LateSubscribersContainerComponent} from "./late-subscriber.container.component"; 4 | import {LateSubscriberDisplayComponent} from "./late-subscriber.display.component"; 5 | import {LateSubscriberFixDisplayComponent} from "./late-subscriber-fix.display.component"; 6 | 7 | export const ROUTES = [ 8 | { 9 | path: '', 10 | component: LateSubscribersContainerComponent 11 | } 12 | ]; 13 | const DECLARATIONS = [ 14 | LateSubscribersContainerComponent, LateSubscriberDisplayComponent, LateSubscriberFixDisplayComponent]; 15 | @NgModule({ 16 | declarations: [DECLARATIONS], 17 | imports: [ 18 | CommonModule 19 | ], 20 | exports: [DECLARATIONS] 21 | }) 22 | export class LateSubscriberModule { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/data-access/github/github.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {of} from 'rxjs'; 4 | import {RepositoryListItem} from "./+state/repository-list.model"; 5 | import {delay} from "rxjs/operators"; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class GitHubService { 11 | 12 | constructor(private http: HttpClient) { 13 | } 14 | 15 | getData = (arg?: any) => of(getData(arg)).pipe(delay(~~(Math.random()*5000))); 16 | 17 | } 18 | 19 | export function getData(cfg = {num: 5}): RepositoryListItem[] { 20 | const randId = (s: string) => s + ~~(Math.random() * 100); 21 | return new Array(cfg.num) 22 | .fill(cfg.num) 23 | .map(_ => ({ 24 | id: randId('id'), 25 | name: randId('name'), 26 | created: Date.now() / 1000 + '' 27 | })); 28 | } 29 | -------------------------------------------------------------------------------- /src/app/examples/problems/declarative-interaction/declarative-interaction-bad.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {DeclarativeInteractionBadService} from "./declarative-interaction-bad.service"; 3 | 4 | @Component({ 5 | selector: 'declarative-interaction-bad', 6 | template: ` 7 |Imperative Interaction
8 |{{state$ | async | json}}
9 |
12 | `,
13 |
14 | providers: [DeclarativeInteractionBadService]
15 | })
16 | export class DeclarativeInteractionBadComponent {
17 | state$ = this.stateService.state$;
18 |
19 | constructor(private stateService: DeclarativeInteractionBadService) {
20 |
21 | }
22 |
23 | updateCount() {
24 | this.stateService
25 | .dispatch(({count: ~~(Math.random() * 100)}));
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/examples/problems/cold-composition/some-good.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable, OnDestroy} from '@angular/core';
2 | import {ConnectableObservable, Subject, Subscription} from 'rxjs';
3 | import {publishReplay, scan, tap} from "rxjs/operators";
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class SomeGoodService implements OnDestroy {
9 | commands$ = new Subject();
10 | serviceSubscription = new Subscription();
11 | composedState$ = this.commands$
12 | .pipe(
13 | tap(v => console.log('compute state ', v)),
14 | scan((acc, i) => {
15 | return {sum : acc['sum'] + i['sum']};
16 | }, {sum: 0}),
17 | publishReplay(1)
18 | ) as ConnectableObservableDeclarative Interaction
10 |{{state$ | async | json}}
11 |
14 | `,
15 | providers: [DeclarativeInteractionGoodService]
16 | })
17 | export class DeclarativeInteractionGoodComponent {
18 | state$ = this.stateService.state$;
19 | update$ = new Subject();
20 |
21 | constructor(private stateService: DeclarativeInteractionGoodService) {
22 | this.stateService.connectSlice(this.update$
23 | .pipe(map(_ => ({count: ~~(Math.random() * 100)}))));
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/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 | "experimentalDecorators": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "es2015",
14 | "typeRoots": [
15 | "node_modules/@types"
16 | ],
17 | "lib": [
18 | "es2018",
19 | "dom"
20 | ],
21 | "paths": {
22 | "@data-access/github": [
23 | "src/app/data-access/github"
24 | ],
25 | "@data-access/github/*": [
26 | "src/app/data-access/github/"
27 | ],
28 | "@data-access/meetings": [
29 | "src/app/data-access/meetings"
30 | ],
31 | "@data-access/meetings/*": [
32 | "src/app/data-access/meetings/*"
33 | ],
34 | "@common": [
35 | "src/app/common"
36 | ]
37 | }
38 | },
39 | "angularCompilerOptions": {
40 | "fullTemplateTypeCheck": true,
41 | "strictInjectionParameters": true
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/examples/problems/cold-composition/cold-composition.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {ColdCompositionContainerComponent} from "./cold-composition.container.component";
4 | import {ColdCompositionBadComponent} from "./cold-composition-bad.component";
5 | import {ColdCompositionGoodComponent} from "./cold-composition-good.component";
6 | import {FormsModule} from "@angular/forms";
7 | import {MatButtonModule, MatExpansionModule, MatSlideToggleModule} from "@angular/material";
8 |
9 | const DECLARATIONS = [ColdCompositionContainerComponent, ColdCompositionBadComponent, ColdCompositionGoodComponent];
10 | const MATERIAL_MODULES = [MatButtonModule, MatSlideToggleModule, MatExpansionModule];
11 | export const ROUTES = [{
12 | path: '',
13 | component: ColdCompositionContainerComponent
14 | }];
15 |
16 | @NgModule({
17 | declarations: [DECLARATIONS],
18 | imports: [
19 | CommonModule,
20 | FormsModule,
21 | MATERIAL_MODULES
22 | ],
23 | exports: [DECLARATIONS]
24 | })
25 | export class ColdCompositionModule {
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/data-access/github/+state/effects.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Actions, createEffect, ofType} from '@ngrx/effects';
3 | import {of} from 'rxjs';
4 | import {catchError, map, switchMap, tap} from 'rxjs/operators';
5 |
6 | import {GitHubService} from '../github.service';
7 | import {fetchRepositoryList, repositoryListFetchError, repositoryListFetchSuccess} from './actions';
8 |
9 | @Injectable({
10 | providedIn: 'root'
11 | })
12 | export class GitHubEffects {
13 |
14 | fetchGithubRepositoriesList$ = createEffect(() =>
15 | this.actions$.pipe(
16 | ofType(fetchRepositoryList.type),
17 | switchMap(action =>
18 | this.gitHubService.getData(action).pipe(
19 | tap(v => console.log('EFFECT fetch Data', v)),
20 | map(list => repositoryListFetchSuccess({list})),
21 | catchError(error => of(repositoryListFetchError({error})))
22 | )
23 | )
24 | )
25 | );
26 |
27 | constructor(private actions$: Actions, private gitHubService: GitHubService) {
28 |
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/data-access/github/+state/reducer.ts:
--------------------------------------------------------------------------------
1 | import {createReducer, on} from '@ngrx/store';
2 | import {repositoryListFetchSuccess} from './actions';
3 | import {RepositoryListItem} from './repository-list.model';
4 | import {getData} from '../github.service';
5 |
6 | export const GITHUB_FEATURE_KEY = 'github';
7 |
8 | export interface GitHubState {
9 | user: string,
10 | list: RepositoryListItem[]
11 | }
12 | const initialGitHubState = {
13 | user: 'ReactiveX',
14 | list: getData()
15 | };
16 |
17 | export interface GitHubFeatureState {
18 | [GITHUB_FEATURE_KEY]: GitHubState
19 | }
20 |
21 | const _gitHubReducer = createReducer(
22 | initialGitHubState,
23 | on(repositoryListFetchSuccess, (state, action) => ({
24 | ...state,
25 | list: uniteItemArrays(state.list, action.list)
26 | })
27 | )
28 | );
29 |
30 | export const gitHubReducer = (state, action) => _gitHubReducer(state, action);
31 |
32 | function uniteItemArrays(...arrs: RepositoryListItem[][]) {
33 | return Array.from(
34 | new Map(arrs
35 | .reduce((arr: any, a: any): any => arr.concat(a), [])
36 | .map(i => [i.id, i])
37 | ).entries()
38 | ).map(e => e[1])
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/examples/problems/declarative-interaction/declarative-interaction.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 |
4 | import {DeclarativeInteractionContainerComponent} from "./declarative-interaction.container.component";
5 | import {DeclarativeInteractionGoodComponent} from "./declarative-interaction-good.component";
6 | import {DeclarativeInteractionBadComponent} from "./declarative-interaction-bad.component";
7 | import {DeclarativeSideEffectsGoodComponent} from "./declarative-side-effects-good.component";
8 | import {MatButtonModule} from "@angular/material";
9 |
10 | export const ROUTES = [
11 | {
12 | path: '',
13 | component: DeclarativeInteractionContainerComponent
14 | }
15 | ];
16 | const DECLARATIONS = [
17 | DeclarativeInteractionContainerComponent,
18 | DeclarativeInteractionGoodComponent,
19 | DeclarativeInteractionBadComponent,
20 | DeclarativeSideEffectsGoodComponent
21 | ];
22 |
23 | @NgModule({
24 | declarations: [DECLARATIONS],
25 | imports: [
26 | CommonModule,
27 | MatButtonModule
28 | ],
29 | exports: [DECLARATIONS]
30 | })
31 | export class DeclarativeInteractionModule {
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/examples/problems/cold-composition/cold-composition-bad.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {SomeBadService} from "./some-bad.service";
3 |
4 | @Component({
5 | selector: 'cold-composition-bad',
6 | template: `
7 | someService.composedState$: {{someBadService.composedState$ | async | json}}
16 | someService.composedState$: {{someGoodService.composedState$ | async | json}}
16 | formGroupModel$:
8 |{{formGroupModel$ | async | json}}
9 | imperative version:
11 |{{imp$ | async | json}}
12 | reactive bad version:
20 |{{reactiveBad$ | async | json}}
21 | reactiveGood$:
27 |{{reactiveGood$ | async | json}}
28 | Imperative Interaction
1033 |{{state$ | async | json}}
1034 |
1037 | `,
1038 |
1039 | providers: [StateService]
1040 | })
1041 | export class AnyComponent implements OnDestroy {
1042 | state$ = this.stateService.state$;
1043 |
1044 | constructor(private stateService: StateService) {
1045 |
1046 | }
1047 |
1048 | updateState() {
1049 | this.stateService
1050 | .dispatch(({key: value}));
1051 | }
1052 |
1053 | }
1054 | ```
1055 |
1056 | Why is this imperative? Imperative programming means working with instances and mutating state.
1057 | Whenever you write a `setter` or `getter` your code is imperative, It's not compose-able.
1058 |
1059 | If we now think about the `dispatch` method of `@ngrx/store`, we realize that it is similar to working with `setter`.
1060 |
1061 | While in this example it sits inside another un-compose-able thing, the instance method and therefore is ok.
1062 | Everything else would resolve in more refactoring.
1063 |
1064 | However, we can not use it to work with compose-able sources.
1065 |
1066 | Let's think about connection RouterState or any other source like `ngrx/store` to the local state:
1067 |
1068 | **Imperative Interaction Component**
1069 | ```typescript
1070 | @Component({
1071 | selector: 'component',
1072 | template: `
1073 | Imperative Interaction
1074 |{{state$ | async | json}}
1075 | `,
1076 |
1077 | providers: [StateService]
1078 | })
1079 | export class AnyComponent implements OnDestroy {
1080 | subscription = new Subscription();
1081 | state$ = this.stateService.state$;
1082 |
1083 | constructor(private stateService: StateService,
1084 | private store: Store) {
1085 |
1086 | this.subscription.add(
1087 | this.store.select(getStateSlice)
1088 | .subscribe(value => this.stateService
1089 | .setState({key: value})
1090 | )
1091 | );
1092 |
1093 | }
1094 |
1095 | ngOnDestroy() {
1096 | this.subscription.unsubscribe();
1097 | }
1098 |
1099 | }
1100 | ```
1101 |
1102 | As we can see as soon as we deal with something compose-able setters don't work anymore.
1103 | We end up in a very ugly code. We break the reactive flow and we have to take care of subscriptions.
1104 |
1105 | 
1106 |
1107 | But how can we go more declarative or even reactive?
1108 | **By providing something compose-able** :)
1109 |
1110 | Like an observable itself. :)
1111 |
1112 | By adding a single line of code we can go **fully declarative** as well as **fully subscription-less**.
1113 |
1114 | 
1115 |
1116 | **Declarative Interaction Service**
1117 | ```typescript
1118 | export class StateService implements OnDestroy {
1119 | ...
1120 | private stateSubject = new SubjectImperative Interaction
1143 |{{state$ | async | json}}
1144 | `,
1145 |
1146 | providers: [StateService]
1147 | })
1148 | export class AnyComponent {
1149 | state$ = this.stateService.state$;
1150 |
1151 | constructor(private stateService: StateService,
1152 | private store: Store) {
1153 | this.stateService.connectState(
1154 | this.store.select(getStateSlice)
1155 | .pipe(map(value => ({key: value})))
1156 | );
1157 |
1158 | }
1159 | }
1160 | ```
1161 |
1162 | Let's take a detailed look at the introduced changes:
1163 |
1164 | 1. In `StateService` we changed
1165 | `stateSubject = new Subject<{ [key: string]: any }>();`
1166 | to
1167 | `stateSubject = new SubjectDeclarative SideEffects
1229 | `, 1230 | providers: [StateAndEffectService] 1231 | }) 1232 | export class AnyComponent { 1233 | constructor(private stateService: StateAndEffectService) { 1234 | this.stateService.connectEffect(interval(1000) 1235 | .pipe(tap(_ => ({key: value})))); 1236 | } 1237 | 1238 | } 1239 | ``` 1240 | _(used RxJS parts: [publish](https://rxjs.dev/api/operator/publish) ))_ 1241 | 1242 | Note that the side-effect is now placed in a `tap` operator and the whole observable is handed over. 1243 | 1244 | ## Recap Problems 1245 | 1246 | So far we encountered the following problems: 1247 | - sharing work and references 1248 | - subscription handling 1249 | - late subscriber 1250 | - cold composition 1251 | - moving primitive tasks as subscription handling and state composition into another layer 1252 | - Subscription-less components and declarative interaction 1253 | 1254 | If you may already realize all the above problems naturally collapse into a single piece of code. 1255 | :) 1256 | 1257 | Also if you remember from the beginning this is what "the gang of four" quote says about Object-Oriented Design Patterns. 1258 | :) 1259 | 1260 | We can be happy as we did a great job so far. 1261 | We focused on understanding the problems, we used the language specific possibilities the right way, 1262 | and naturally, we ended up with a solution that is compact, robust and solves all related problems in an elegant way. 1263 | 1264 | Let's see how the local state service looks like. 1265 |  1266 | 1267 | 1268 | # Basic Usage 1269 | 1270 | ## Service Design 1271 | 1272 | **State Logic** 1273 | ```typescript 1274 | import {ConnectableObservable, merge, noop, Observable, OperatorFunction, Subject, Subscription, UnaryFunction} from 'rxjs'; 1275 | import {map, mergeAll, pluck, publishReplay, scan, tap} from 'rxjs/operators'; 1276 | 1277 | export function statefulComponent
1454 | ... 1455 | ` 1456 | }) 1457 | export class AnyComponent extends LocalState { 1458 | 1459 | constructor() { 1460 | this.super(); 1461 | } 1462 | 1463 | } 1464 | ``` 1465 | 1466 | **Injecting the service** 1467 | ```typescript 1468 | @Component({ 1469 | selector: 'component', 1470 | template: ` 1471 |Component
1472 | ... 1473 | `, 1474 | providers: [LocalState] 1475 | }) 1476 | export class AnyComponent { 1477 | 1478 | constructor(private stateService: LocalState) { 1479 | } 1480 | 1481 | } 1482 | ``` 1483 | 1484 | ## Service Usage 1485 | 1486 | Now let's see some basic usage: 1487 | 1488 | **Connecting Input-Bindings** 1489 | ```typescript 1490 | @Component({ 1491 | selector: 'component', 1492 | template: ` 1493 |Component
1494 | ... 1495 | ` 1496 | }) 1497 | export class AnyComponent extends LocalState { 1498 | 1499 | @Input() 1500 | set value(value) { 1501 | this.setState({slice: value}) 1502 | } 1503 | 1504 | constructor() { 1505 | this.super(); 1506 | } 1507 | 1508 | } 1509 | ``` 1510 | 1511 | **Connecting GlobalState** 1512 | ```typescript 1513 | @Component({ 1514 | selector: 'component', 1515 | template: ` 1516 |Component
1517 | ... 1518 | ` 1519 | }) 1520 | export class AnyComponent extends LocalState { 1521 | 1522 | @Input() 1523 | set value(value) { 1524 | this.setState({slice: value}) 1525 | } 1526 | 1527 | constructor(private store: Store) { 1528 | this.connectState( 1529 | this.store.select(getStateSlice) 1530 | .pipe(transformation) 1531 | ); 1532 | } 1533 | 1534 | } 1535 | ``` 1536 | 1537 | **Selecting LocalState** 1538 | ```typescript 1539 | @Component({ 1540 | selector: 'component', 1541 | template: ` 1542 |Component
1543 | 1544 | {{state$ | async}} 1545 | ` 1546 | }) 1547 | export class AnyComponent extends LocalState { 1548 | input$ = new Subject(); 1549 | 1550 | state$ = this.select( 1551 | withLatestFrom(input$), 1552 | map(([state, _]) => state.slice) 1553 | ); 1554 | 1555 | } 1556 | ``` 1557 | _(used RxJS parts: [withLatestFrom](https://rxjs.dev/api/operator/withLatestFrom))_ 1558 | 1559 | **Handling LocalSideEffects** 1560 | 1561 | ```typescript 1562 | @Component({ 1563 | selector: 'component', 1564 | template: ` 1565 |Component
1566 | ... 1567 | ` 1568 | }) 1569 | export class AnyComponent extends LocalState { 1570 | 1571 | constructor(private store: Store) { 1572 | this.connectEffect(interval(10000) 1573 | .pipe(tap(this.store.dispatch(loadDataAction))) 1574 | ); 1575 | } 1576 | 1577 | } 1578 | ``` 1579 | 1580 | This example shows a material design list that is collapsable. 1581 | It refreshed data every n seconds of if we click the button. 1582 | Also, it displays the fetched items. 1583 | 1584 | 1585 | *Basic Example - Stateful Component**: 1586 | ```typescript 1587 | @Component({ 1588 | selector: 'basic-list', 1589 | template: ` 1590 |