├── README.md
├── index.html
├── package.json
├── 00_start.ts
├── _index.ts
├── 01_implement-interval.ts
├── 02_maintain-interval-state.ts
├── 03-0_WRONG_implemented.ts
├── style.scss
├── readme.md
├── solution-no-comments.ts
├── 03-1_micro-architecture.ts
├── 03-4_intermediate-observables.ts
├── 03-2_event-sourcing.ts
├── 03-3_cqrs.ts
├── 03-5_interval-process.ts
├── 04_reset.ts
├── 05_countUp.ts
├── 06_dynamic-tickSpeed.ts
├── 07_dynamic-countDiff.ts
├── index.ts
├── 08_performance-optimisation-n-refactoring.ts
└── counter.ts
/README.md:
--------------------------------------------------------------------------------
1 | # rxjs-operating-heavily-dynamic-uis
2 |
3 | [Edit on StackBlitz ⚡️](https://stackblitz.com/edit/rxjs-operating-heavily-dynamic-uis)
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 | Operating Heavily Dynamic UI's
3 | Event Sourcing & CQRS in th frontend
4 |
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typescript-5i4anz",
3 | "version": "0.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "rxjs": "^6.3.3"
7 | }
8 | }
--------------------------------------------------------------------------------
/00_start.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys} from './counter'
2 | import { merge} from 'rxjs';
3 | import { mapTo} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 |
25 | const initialConterState: CountDownState = {
26 | isTicking: false,
27 | count: 0,
28 | countUp: true,
29 | tickSpeed: 200,
30 | countDiff:1
31 | };
32 |
33 | const counterUI = new Counter(
34 | document.body,
35 | {
36 | initialSetTo: initialConterState.count + 10,
37 | initialTickSpeed: initialConterState.tickSpeed,
38 | initialCountDiff: initialConterState.countDiff,
39 | }
40 | );
41 |
42 | merge(
43 | counterUI.btnStart$.pipe(mapTo(1)),
44 | counterUI.btnPause$.pipe(mapTo(0)),
45 | )
46 | .subscribe(
47 | s => counterUI.renderCounterValue(s)
48 | );
49 |
--------------------------------------------------------------------------------
/_index.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys} from './counter'
2 | import { merge} from 'rxjs';
3 | import { mapTo} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 |
25 | const initialConterState: CountDownState = {
26 | isTicking: false,
27 | count: 0,
28 | countUp: true,
29 | tickSpeed: 200,
30 | countDiff:1
31 | };
32 |
33 | const counterUI = new Counter(
34 | document.body,
35 | {
36 | initialSetTo: initialConterState.count + 10,
37 | initialTickSpeed: initialConterState.tickSpeed,
38 | initialCountDiff: initialConterState.countDiff,
39 | }
40 | );
41 |
42 | merge(
43 | counterUI.btnStart$.pipe(mapTo(1)),
44 | counterUI.btnPause$.pipe(mapTo(0)),
45 | )
46 | .subscribe(
47 | s => counterUI.renderCounterValue(s)
48 | );
49 |
--------------------------------------------------------------------------------
/01_implement-interval.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys} from './counter'
2 | import { merge, timer, NEVER} from 'rxjs';
3 | import { mapTo, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 | const initialConterState: CountDownState = {
25 | isTicking: false,
26 | count: 0,
27 | countUp: true,
28 | tickSpeed: 200,
29 | countDiff:1
30 | };
31 |
32 | const counterUI = new Counter(
33 | document.body,
34 | {
35 | initialSetTo: initialConterState.count + 10,
36 | initialTickSpeed: initialConterState.tickSpeed,
37 | initialCountDiff: initialConterState.countDiff,
38 | }
39 | );
40 |
41 | merge(
42 | counterUI.btnStart$.pipe(mapTo(true)),
43 | counterUI.btnPause$.pipe(mapTo(false)),
44 | )
45 | .pipe(
46 | switchMap(isTicking => (isTicking) ? timer(0, initialConterState.tickSpeed): NEVER)
47 | )
48 | .subscribe(
49 | s => counterUI.renderCounterValue(s)
50 | );
51 |
--------------------------------------------------------------------------------
/02_maintain-interval-state.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys} from './counter'
2 | import { merge, timer, NEVER} from 'rxjs';
3 | import { mapTo, switchMap, scan} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 | const initialConterState: CountDownState = {
25 | isTicking: false,
26 | count: 0,
27 | countUp: true,
28 | tickSpeed: 200,
29 | countDiff:1
30 | };
31 |
32 | const counterUI = new Counter(
33 | document.body,
34 | {
35 | initialSetTo: initialConterState.count + 10,
36 | initialTickSpeed: initialConterState.tickSpeed,
37 | initialCountDiff: initialConterState.countDiff,
38 | }
39 | );
40 |
41 | merge(
42 | counterUI.btnStart$.pipe(mapTo(true)),
43 | counterUI.btnPause$.pipe(mapTo(false)),
44 | )
45 | .pipe(
46 | switchMap(isTicking => (isTicking) ? timer(0, initialConterState.tickSpeed): NEVER),
47 | scan((conut: number, _) => ++conut)
48 | )
49 | .subscribe(
50 | n => counterUI.renderCounterValue(n)
51 | );
52 |
--------------------------------------------------------------------------------
/03-0_WRONG_implemented.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys} from './counter'
2 | import { merge, timer, NEVER} from 'rxjs';
3 | import { mapTo, switchMap, scan, tap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 | const initialConterState: CountDownState = {
25 | isTicking: false,
26 | count: 0,
27 | countUp: true,
28 | tickSpeed: 200,
29 | countDiff:1
30 | };
31 |
32 | const counterUI = new Counter(
33 | document.body,
34 | {
35 | initialSetTo: initialConterState.count + 10,
36 | initialTickSpeed: initialConterState.tickSpeed,
37 | initialCountDiff: initialConterState.countDiff,
38 | }
39 | );
40 |
41 | // WRONG SOLUTION ===================================================
42 | // Never maintain state by mutating variables outside of streams
43 |
44 | let actualCount = initialConterState.count;
45 |
46 | counterUI.btnSetTo$
47 | .pipe(
48 | tap(n => actualCount = n)
49 | )
50 | .subscribe(
51 | _ => counterUI.renderCounterValue(actualCount)
52 | );
53 |
54 | merge(
55 | counterUI.btnStart$.pipe(mapTo(true)),
56 | counterUI.btnPause$.pipe(mapTo(false)),
57 | )
58 | .pipe(
59 | switchMap(isTicking => isTicking ? timer(0, initialConterState.tickSpeed): NEVER),
60 | tap(_ => ++actualCount)
61 | )
62 | .subscribe(
63 | _ => counterUI.renderCounterValue(actualCount)
64 | );
65 |
--------------------------------------------------------------------------------
/style.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font: 16px/1 'Roboto', sans-serif;
3 | color: #000;
4 | background: #eee;
5 | -webkit-font-smoothing: antialiased;
6 | text-align: center;
7 | }
8 |
9 | button,
10 | input {
11 | font-family: inherit;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | border: 0;
15 | outline: 0;
16 | color: #fff;
17 | }
18 |
19 | label {
20 | color: rgb(0,0,0);
21 | }
22 |
23 | input[type="number"],
24 | input[type="text"] {
25 | width: 400px;
26 | font-weight: bold;
27 | margin: 0 0 20px;
28 | padding: 8px 12px 10px 12px;
29 | border: 0px solid rgb(0,0,0);
30 | border-bottom-width: 2px;
31 | background: none;
32 | color: rgb(0,0,0);
33 | }
34 |
35 | button,
36 | input[type="submit"] {
37 | width: 150px;
38 | margin: 0 0 20px;
39 | padding: 8px 0 10px 0;
40 | text-align: center;
41 | border: 1px solid rgba(0,0,0,0.9);
42 | background: rgba(0,0,0,.75);
43 | }
44 |
45 | .countdownHolder{
46 | display: flex;
47 | align-items: center;
48 | justify-content: center;
49 | min-height: 240px;
50 | width:100%;
51 | margin:0 auto;
52 | font: 120px/1.5 'Open Sans Condensed',sans-serif;
53 | text-align:center;
54 | letter-spacing:-3px;
55 |
56 | .position{
57 | display: inline-block;
58 | height: 1.6em;
59 | overflow: hidden;
60 | position: relative;
61 | width: 1.05em;
62 |
63 | .digit{
64 | position:absolute;
65 | display:block;
66 | width:1em;
67 | background-color:#444;
68 | border-radius:0.2em;
69 | text-align:center;
70 | color:#fff;
71 | letter-spacing:-1px;
72 |
73 | &.static{
74 | box-shadow:1px 1px 1px rgba(4, 4, 4, 0.35);
75 |
76 | background-image: linear-gradient(bottom, #3A3A3A 50%, #444444 50%);
77 | background-image: -o-linear-gradient(bottom, #3A3A3A 50%, #444444 50%);
78 | background-image: -moz-linear-gradient(bottom, #3A3A3A 50%, #444444 50%);
79 | background-image: -webkit-linear-gradient(bottom, #3A3A3A 50%, #444444 50%);
80 | background-image: -ms-linear-gradient(bottom, #3A3A3A 50%, #444444 50%);
81 |
82 | background-image: -webkit-gradient(
83 | linear,
84 | left bottom,
85 | left top,
86 | color-stop(0.5, #3A3A3A),
87 | color-stop(0.5, #444444)
88 | );
89 | }
90 | }
91 |
92 | }
93 |
94 | .countDiv{
95 | display:inline-block;
96 | width:16px;
97 | height:1.6em;
98 | position:relative;
99 |
100 | &:before,
101 | &:after{
102 | position:absolute;
103 | width:5px;
104 | height:5px;
105 | background-color:#444;
106 | border-radius:50%;
107 | left:50%;
108 | margin-left:-3px;
109 | top:0.5em;
110 | box-shadow:1px 1px 1px rgba(4, 4, 4, 0.5);
111 | content:'';
112 | }
113 |
114 | &:after{
115 | top:0.9em;
116 | }
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # STATE MANAGEMENT
2 |
3 | 1. Create a `command$` observable of all inputs (counterUI.btnStart$, counterUI.btnPause$, counterUI.inputTickSpeed, etc..)
4 | and map them to state updates i.e. counterUI.btnStart$.pipe(mapTo({isTicking: true}))
5 | 2. Create a `state$` observable.
6 | Start with initialConterState, use scan to merge updates from command$ in.
7 | Use shareReplay(1) to retrieve the last value emitted whenever you subscribe.
8 | 3. Subscribe to state$ and use console.log to test it.
9 |
10 | # RENDERING
11 |
12 | 1. Create a `renderCountValue$` observable in section "SIDE EFFECTS" - "Input".
13 | Use `tap` to execute counterUI.renderDisplayText(). To optimize performance use the `queryChange` custom operator.
14 | 2. Place the new observable in the "SUBSCRUPTIONS" section under "Input" to test it.
15 |
16 | # TIMER
17 |
18 | 1. Create a `timerProcessChange$` observable in the section "OBSERVABLES".
19 | 2. Use the `state$` to get the isTicking value. Use the "switchMap NEVER" pattern from before to start a timer.
20 | 3. Create a `programmaticCommands` subject in section "STATE" - "Command"
21 | 4. Create a `handleTimerProcessChange$` observable in section "SIDE EFFECTS" - "Outputs".
22 | Use the `tap` operator to call `next()` on `programmaticCommands`
23 |
24 | # BONUS
25 |
26 | Explore the counterUI API by typing `counterUI.` somewhere in the index.ts file. ;)
27 |
28 | Implement all the features of the counter:
29 | - Start, pause the counter. Then restart the counter (+)
30 | - Start it again from the paused number (++)
31 | - If Set to button is clicked set counter value to input value while counting (+++)
32 | - Reset to initial state if the reset button is clicked (+)
33 | - Is count up button is clicked count up (+)
34 | - Is count down button is clicked count down (+)
35 | - Change interval if input tickSpeed input changes (++)
36 | - Change count up if input countDiff changes (++)
37 | - Take care of rendering execution and other performance optimizations (+)
38 |
39 |
40 |
41 | Some structure recommendations
42 |
43 |
44 | // == CONSTANTS ===========================================================
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 | // === STATE OBSERVABLES ==================================================
48 | // === INTERACTION OBSERVABLES ============================================
49 | // == INTERMEDIATE OBSERVABLES ============================================
50 | // = SIDE EFFECTS =========================================================
51 | // == UI INPUTS ===========================================================
52 | // == UI OUTPUTS ==========================================================
53 | // == SUBSCRIPTION ========================================================
54 | // === INPUTs =============================================================
55 | // === OUTPUTS ============================================================
56 | // = HELPER ===============================================================
57 | // = CUSTOM OPERATORS =====================================================
58 | // == CREATION METHODS ====================================================
59 | // == OPERATORS ===========================================================
60 |
--------------------------------------------------------------------------------
/solution-no-comments.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys, PartialCountDownState} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge, UnaryFunction} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay,distinctUntilKeyChanged, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | const initialConterState: CountDownState = {
6 | count: 0,
7 | isTicking: false,
8 | tickSpeed: 200,
9 | countUp: true,
10 | countDiff:1
11 | };
12 |
13 | const counterUI = new Counter(
14 | document.body,
15 | {
16 | initialSetTo: initialConterState.count + 10,
17 | initialTickSpeed: initialConterState.tickSpeed,
18 | initialCountDiff: initialConterState.countDiff,
19 | }
20 | );
21 |
22 | const programmaticCommandSubject = new Subject();
23 | const counterCommands$ = merge(
24 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
25 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
26 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
27 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
28 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
29 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
30 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
31 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n}))),
32 | programmaticCommandSubject.asObservable()
33 | );
34 |
35 | const counterState$: Observable = counterCommands$
36 | .pipe(
37 | startWith(initialConterState),
38 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
39 | shareReplay(1)
40 | );
41 |
42 | const count$ = counterState$.pipe(pluck(ConterStateKeys.count));
43 | const isTicking$ = counterState$.pipe(queryChange(ConterStateKeys.isTicking));
44 | const tickSpeed$ = counterState$.pipe(queryChange(ConterStateKeys.tickSpeed));
45 | const countDiff$ = counterState$.pipe(queryChange(ConterStateKeys.countDiff));
46 |
47 | const counterUpdateTrigger$ = combineLatest([isTicking$, tickSpeed$])
48 | .pipe(switchMap(([isTicking, tickSpeed]) => isTicking ? timer(0, tickSpeed) : NEVER));
49 |
50 | const renderCountChange$ = count$.pipe(tap(n => counterUI.renderCounterValue(n)));
51 | const renderTickSpeedChange$ = tickSpeed$.pipe(tap(n => counterUI.renderTickSpeedInputValue(n)));
52 | const renderCountDiffChange$ = countDiff$.pipe(tap(n => counterUI.renderCountDiffInputValue(n)));
53 | const renderSetToChange$ = counterUI.btnReset$.pipe(tap(_ => { counterUI.renderSetToInputValue('10');}));
54 | const commandFromTick$ = counterUpdateTrigger$
55 | .pipe(
56 | withLatestFrom(counterState$, (_, counterState) => ({
57 | [ConterStateKeys.count]: counterState.count,
58 | [ConterStateKeys.countUp]: counterState.countUp,
59 | [ConterStateKeys.countDiff]: counterState.countDiff
60 | }) ),
61 | tap(({count, countUp, countDiff}) => programmaticCommandSubject.next( {count: count + countDiff * (countUp ? 1 : -1)}) )
62 | );
63 |
64 | merge(
65 | renderCountChange$,
66 | renderTickSpeedChange$,
67 | renderCountDiffChange$,
68 | renderSetToChange$,
69 | commandFromTick$
70 | ).subscribe();
71 |
72 | function queryChange(key: string): UnaryFunction, Observable> {
73 | return pipe(pluck(key), distinctUntilChanged() );
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/03-1_micro-architecture.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys} from './counter'
2 | import { Observable, NEVER, timer, merge} from 'rxjs';
3 | import { mapTo, tap, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 | // == CONSTANTS ==
25 | // Setup conutDown state
26 | const initialConterState: CountDownState = {
27 | isTicking: false,
28 | count: 0,
29 | countUp: true,
30 | tickSpeed: 200,
31 | countDiff:1
32 | };
33 |
34 | // Init CountDown counterUI
35 | const counterUI = new Counter(
36 | document.body,
37 | {
38 | initialSetTo: initialConterState.count + 10,
39 | initialTickSpeed: initialConterState.tickSpeed,
40 | initialCountDiff: initialConterState.countDiff,
41 | }
42 | );
43 |
44 | // = BASE OBSERVABLES ====================================================
45 | // == SOURCE OBSERVABLES ==================================================
46 | // === STATE OBSERVABLES ==================================================
47 | // === INTERACTION OBSERVABLES ============================================
48 | // == INTERMEDIATE OBSERVABLES ============================================
49 | // = SIDE EFFECTS =========================================================
50 |
51 |
52 | // WRONG SOLUTION !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
53 | // Never maintain state by mutating variables outside of streams
54 |
55 | let actualCount = initialConterState.count;
56 |
57 | // == UI INPUTS ===========================================================
58 | const renderCountChangeFromTick$ = merge(
59 | counterUI.btnStart$.pipe(mapTo(true)),
60 | counterUI.btnPause$.pipe(mapTo(false)),
61 | )
62 | .pipe(
63 | switchMap(isTicking => isTicking ? timer(0, initialConterState.tickSpeed): NEVER),
64 | tap(_ => ++actualCount),
65 | tap(_ => counterUI.renderCounterValue(actualCount))
66 | );
67 |
68 | const renderCountChangeFromSetTo$ = counterUI.btnSetTo$
69 | .pipe(
70 | tap(n => actualCount = n),
71 | tap(_ => counterUI.renderCounterValue(actualCount))
72 | );
73 |
74 | // == UI OUTPUTS ==========================================================
75 |
76 |
77 |
78 | // == SUBSCRIPTION ========================================================
79 |
80 | merge(
81 | // Input side effect
82 | renderCountChangeFromTick$,
83 | // Outputs side effect
84 | renderCountChangeFromSetTo$
85 | )
86 | .subscribe();
87 |
88 | // = HELPER ===============================================================
89 | // = CUSTOM OPERATORS =====================================================
90 | // == CREATION METHODS ====================================================
91 | // == OPERATORS ===========================================================
92 |
93 |
94 |
--------------------------------------------------------------------------------
/03-4_intermediate-observables.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay,distinctUntilKeyChanged, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 |
25 | // == CONSTANTS ===========================================================
26 | // Setup conutDown state
27 | const initialConterState: CountDownState = {
28 | isTicking: false,
29 | count: 0,
30 | countUp: true,
31 | tickSpeed: 200,
32 | countDiff:1
33 | };
34 |
35 | // Init CountDown counterUI
36 | const counterUI = new Counter(
37 | document.body,
38 | {
39 | initialSetTo: initialConterState.count + 10,
40 | initialTickSpeed: initialConterState.tickSpeed,
41 | initialCountDiff: initialConterState.countDiff,
42 | }
43 | );
44 |
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 |
48 | // All our source observables are extracted into Counter class
49 |
50 | // === STATE OBSERVABLES ==================================================
51 | const counterCommands$ = merge(
52 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
53 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
54 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
55 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
56 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
57 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
58 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
59 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n})))
60 | );
61 |
62 | const counterState$: Observable = counterCommands$
63 | .pipe(
64 | startWith(initialConterState),
65 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
66 | shareReplay(1)
67 | );
68 |
69 | // === INTERACTION OBSERVABLES ============================================
70 |
71 | // == INTERMEDIATE OBSERVABLES ============================================
72 | const count$ = counterState$.pipe(pluck(ConterStateKeys.count));
73 | const isTicking$ = counterState$.pipe(pluck(ConterStateKeys.isTicking), distinctUntilChanged());
74 |
75 | // = SIDE EFFECTS =========================================================
76 | // == UI INPUTS ===========================================================
77 | const renderCountChange$ = count$
78 | .pipe(
79 | tap(n => counterUI.renderCounterValue(n))
80 | );
81 |
82 | // WRONG SOLUTION REMOVED !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
83 |
84 | // == UI OUTPUTS ==========================================================
85 |
86 | // == SUBSCRIPTION ========================================================
87 |
88 | merge(
89 | // Input side effect
90 | renderCountChange$
91 | // Outputs side effect
92 | )
93 | .subscribe();
94 |
95 | // = HELPER ===============================================================
96 | // = CUSTOM OPERATORS =====================================================
97 | // == CREATION METHODS ====================================================
98 | // == OPERATORS ===========================================================
99 |
--------------------------------------------------------------------------------
/03-2_event-sourcing.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 | // == CONSTANTS ===========================================================
25 | // Setup conutDown state
26 | const initialConterState: CountDownState = {
27 | isTicking: false,
28 | count: 0,
29 | countUp: true,
30 | tickSpeed: 200,
31 | countDiff:1
32 | };
33 |
34 | // Init CountDown counterUI
35 | const counterUI = new Counter(
36 | document.body,
37 | {
38 | initialSetTo: initialConterState.count + 10,
39 | initialTickSpeed: initialConterState.tickSpeed,
40 | initialCountDiff: initialConterState.countDiff,
41 | }
42 | );
43 |
44 | // = BASE OBSERVABLES ====================================================
45 | // == SOURCE OBSERVABLES ==================================================
46 | // === STATE OBSERVABLES ==================================================
47 | const counterCommands$ = merge(
48 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
49 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
50 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
51 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
52 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
53 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
54 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
55 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n})))
56 | );
57 |
58 | // !!! REMOVE LATER !!! JUST FOR TESTING
59 | counterCommands$
60 | .subscribe(console.log);
61 |
62 | // === INTERACTION OBSERVABLES ============================================
63 | // == INTERMEDIATE OBSERVABLES ============================================
64 | // = SIDE EFFECTS =========================================================
65 |
66 | // WRONG SOLUTION !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
67 | // Never maintain state by mutating variables outside of streams
68 |
69 | let actualCount = initialConterState.count;
70 |
71 | // == UI INPUTS ===========================================================
72 | const renderCountChangeFromTick$ = merge(
73 | counterUI.btnStart$.pipe(mapTo(true)),
74 | counterUI.btnPause$.pipe(mapTo(false)),
75 | )
76 | .pipe(
77 | switchMap(isTicking => isTicking ? timer(0, initialConterState.tickSpeed): NEVER),
78 | tap(_ => ++actualCount),
79 | tap(_ => counterUI.renderCounterValue(actualCount))
80 | );
81 |
82 | const renderCountChangeFromSetTo$ = counterUI.btnSetTo$
83 | .pipe(
84 | tap(n => actualCount = n),
85 | tap(_ => counterUI.renderCounterValue(actualCount))
86 | );
87 |
88 | // == UI OUTPUTS ==========================================================
89 |
90 |
91 |
92 | // == SUBSCRIPTION ========================================================
93 |
94 | merge(
95 | // Input side effect
96 | renderCountChangeFromTick$,
97 | // Outputs side effect
98 | renderCountChangeFromSetTo$
99 | )
100 | .subscribe();
101 |
102 | // = HELPER ===============================================================
103 | // = CUSTOM OPERATORS =====================================================
104 | // == CREATION METHODS ====================================================
105 | // == OPERATORS ===========================================================
106 |
--------------------------------------------------------------------------------
/03-3_cqrs.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 |
25 | // == CONSTANTS ===========================================================
26 | // Setup conutDown state
27 | const initialConterState: CountDownState = {
28 | isTicking: false,
29 | count: 0,
30 | countUp: true,
31 | tickSpeed: 200,
32 | countDiff:1
33 | };
34 |
35 | // Init CountDown counterUI
36 | const counterUI = new Counter(
37 | document.body,
38 | {
39 | initialSetTo: initialConterState.count + 10,
40 | initialTickSpeed: initialConterState.tickSpeed,
41 | initialCountDiff: initialConterState.countDiff,
42 | }
43 | );
44 |
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 | // === STATE OBSERVABLES ==================================================
48 | const counterCommands$ = merge(
49 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
50 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
51 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
52 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
53 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
54 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
55 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
56 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n})))
57 | );
58 |
59 | const counterState$: Observable = counterCommands$
60 | .pipe(
61 | startWith(initialConterState),
62 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
63 | shareReplay(1)
64 | );
65 |
66 | // JUST FOR TESTING
67 | counterState$
68 | .subscribe(console.log);
69 |
70 | // === INTERACTION OBSERVABLES ============================================
71 | // == INTERMEDIATE OBSERVABLES ============================================
72 | // = SIDE EFFECTS =========================================================
73 |
74 | // WRONG SOLUTION !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
75 | // Never maintain state by mutating variables outside of streams
76 |
77 | let actualCount = initialConterState.count;
78 |
79 | // == UI INPUTS ===========================================================
80 | const renderCountChangeFromTick$ = merge(
81 | counterUI.btnStart$.pipe(mapTo(true)),
82 | counterUI.btnPause$.pipe(mapTo(false)),
83 | )
84 | .pipe(
85 | switchMap(isTicking => isTicking ? timer(0, initialConterState.tickSpeed): NEVER),
86 | tap(_ => ++actualCount),
87 | tap(_ => counterUI.renderCounterValue(actualCount))
88 | );
89 |
90 | const renderCountChangeFromSetTo$ = counterUI.btnSetTo$
91 | .pipe(
92 | tap(n => actualCount = n),
93 | tap(_ => counterUI.renderCounterValue(actualCount))
94 | );
95 |
96 | // == UI OUTPUTS ==========================================================
97 |
98 |
99 |
100 | // == SUBSCRIPTION ========================================================
101 |
102 | merge(
103 | // Input side effect
104 | renderCountChangeFromTick$,
105 | // Outputs side effect
106 | renderCountChangeFromSetTo$
107 | )
108 | .subscribe();
109 |
110 | // = HELPER ===============================================================
111 | // = CUSTOM OPERATORS =====================================================
112 | // == CREATION METHODS ====================================================
113 | // == OPERATORS ===========================================================
114 |
--------------------------------------------------------------------------------
/03-5_interval-process.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys, PartialCountDownState} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay,distinctUntilKeyChanged, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 |
25 | // == CONSTANTS ===========================================================
26 | // Setup conutDown state
27 | const initialConterState: CountDownState = {
28 | isTicking: false,
29 | count: 0,
30 | countUp: true,
31 | tickSpeed: 200,
32 | countDiff:1
33 | };
34 |
35 | // Init CountDown counterUI
36 | const counterUI = new Counter(
37 | document.body,
38 | {
39 | initialSetTo: initialConterState.count + 10,
40 | initialTickSpeed: initialConterState.tickSpeed,
41 | initialCountDiff: initialConterState.countDiff,
42 | }
43 | );
44 |
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 |
48 | // All our source observables are extracted into Counter class
49 |
50 | // === STATE OBSERVABLES ==================================================
51 | const programmaticCommandSubject = new Subject();
52 | const counterCommands$ = merge(
53 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
54 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
55 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
56 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
57 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
58 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
59 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
60 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n}))),
61 | programmaticCommandSubject.asObservable()
62 | );
63 |
64 | const counterState$: Observable = counterCommands$
65 | .pipe(
66 | startWith(initialConterState),
67 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
68 | shareReplay(1)
69 | );
70 |
71 | // === INTERACTION OBSERVABLES ============================================
72 |
73 | // == INTERMEDIATE OBSERVABLES ============================================
74 | const count$ = counterState$.pipe(pluck(ConterStateKeys.count));
75 | const isTicking$ = counterState$.pipe(pluck(ConterStateKeys.isTicking), distinctUntilChanged());
76 |
77 | const intervalTick$ = isTicking$
78 | .pipe(
79 | switchMap(isTicking => isTicking ? timer(0, initialConterState.tickSpeed) : NEVER)
80 | );
81 |
82 | // = SIDE EFFECTS =========================================================
83 | // == UI INPUTS ===========================================================
84 | const renderCountChange$ = count$
85 | .pipe(
86 | tap(n => counterUI.renderCounterValue(n))
87 | );
88 |
89 | // == UI OUTPUTS ==========================================================
90 | const commandFromTick$ = intervalTick$
91 | .pipe(
92 | withLatestFrom(count$, (_, count) => count),
93 | tap(count => programmaticCommandSubject.next({count: ++count}))
94 | );
95 |
96 | // == SUBSCRIPTION ========================================================
97 |
98 | merge(
99 | // Input side effect
100 | renderCountChange$,
101 | // Outputs side effect
102 | commandFromTick$
103 | )
104 | .subscribe();
105 |
106 | // = HELPER ===============================================================
107 | // = CUSTOM OPERATORS =====================================================
108 | // == CREATION METHODS ====================================================
109 | // == OPERATORS ===========================================================
110 |
--------------------------------------------------------------------------------
/04_reset.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys, PartialCountDownState} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay,distinctUntilKeyChanged, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ==============================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ==================================================================
23 |
24 |
25 | // == CONSTANTS ===========================================================
26 | // Setup conutDown state
27 | const initialConterState: CountDownState = {
28 | count: 0,
29 | isTicking: false,
30 | tickSpeed: 200,
31 | countUp: true,
32 | countDiff:1
33 | };
34 |
35 | // Init CountDown counterUI
36 | const counterUI = new Counter(
37 | document.body,
38 | {
39 | initialSetTo: initialConterState.count + 10,
40 | initialTickSpeed: initialConterState.tickSpeed,
41 | initialCountDiff: initialConterState.countDiff,
42 | }
43 | );
44 |
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 |
48 | // All our source observables are extracted into Counter class to hide away all the low leven bindings.
49 |
50 | // === STATE OBSERVABLES ==================================================
51 | const programmaticCommandSubject = new Subject();
52 | const counterCommands$ = merge(
53 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
54 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
55 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
56 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
57 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
58 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
59 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
60 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n}))),
61 | programmaticCommandSubject.asObservable()
62 | );
63 |
64 | const counterState$: Observable = counterCommands$
65 | .pipe(
66 | startWith(initialConterState),
67 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
68 | shareReplay(1)
69 | );
70 |
71 | // === INTERACTION OBSERVABLES ============================================
72 |
73 | // == INTERMEDIATE OBSERVABLES ============================================
74 | const count$ = counterState$.pipe(pluck(ConterStateKeys.count));
75 | const isTicking$ = counterState$.pipe(pluck(ConterStateKeys.isTicking), distinctUntilChanged());
76 | const tickSpeed$ = counterState$.pipe(pluck(ConterStateKeys.tickSpeed), distinctUntilChanged());
77 | const countDiff$ = counterState$.pipe(pluck(ConterStateKeys.countDiff), distinctUntilChanged());
78 |
79 | const counterUpdateTrigger$ = isTicking$
80 | .pipe(
81 | switchMap(isTicking => isTicking ? timer(0, initialConterState.tickSpeed) : NEVER)
82 | );
83 |
84 | // = SIDE EFFECTS =========================================================
85 | // == UI INPUTS ===========================================================
86 | const renderCountChange$ = count$.pipe(tap(n => counterUI.renderCounterValue(n)));
87 | const renderTickSpeedChange$ = tickSpeed$.pipe(tap(n => counterUI.renderTickSpeedInputValue(n)));
88 | const renderCountDiffChange$ = countDiff$.pipe(tap(n => counterUI.renderCountDiffInputValue(n)));
89 | const renderSetToChange$ = counterUI.btnReset$.pipe(tap(_ => { counterUI.renderSetToInputValue('10');}));
90 |
91 | // == UI OUTPUTS ==========================================================
92 | const commandFromTick$ = counterUpdateTrigger$
93 | .pipe(
94 | withLatestFrom(count$, (_, count) => count),
95 | tap(count => programmaticCommandSubject.next({count: ++count}))
96 | );
97 |
98 | // == SUBSCRIPTION ========================================================
99 |
100 | merge(
101 | // Input side effect
102 | renderCountChange$,
103 | renderTickSpeedChange$,
104 | renderCountDiffChange$,
105 | renderSetToChange$,
106 | // Outputs side effect
107 | commandFromTick$
108 | )
109 | .subscribe();
110 |
111 | // = HELPER ===============================================================
112 | // = CUSTOM OPERATORS =====================================================
113 | // == CREATION METHODS ====================================================
114 | // == OPERATORS ===========================================================
115 |
--------------------------------------------------------------------------------
/05_countUp.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys, PartialCountDownState} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay,distinctUntilKeyChanged, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ===================================================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ========================================================================
23 |
24 |
25 | // == CONSTANTS ===========================================================
26 | // Setup conutDown state
27 | const initialConterState: CountDownState = {
28 | count: 0,
29 | isTicking: false,
30 | tickSpeed: 200,
31 | countUp: true,
32 | countDiff:1
33 | };
34 |
35 | // Init CountDown counterUI
36 | const counterUI = new Counter(
37 | document.body,
38 | {
39 | initialSetTo: initialConterState.count + 10,
40 | initialTickSpeed: initialConterState.tickSpeed,
41 | initialCountDiff: initialConterState.countDiff,
42 | }
43 | );
44 |
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 |
48 | // All our source observables are extracted into Counter class to hide away all the low leven bindings.
49 |
50 | // === STATE OBSERVABLES ==================================================
51 | const programmaticCommandSubject = new Subject();
52 | const counterCommands$ = merge(
53 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
54 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
55 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
56 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
57 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
58 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
59 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
60 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n}))),
61 | programmaticCommandSubject.asObservable()
62 | );
63 |
64 | const counterState$: Observable = counterCommands$
65 | .pipe(
66 | startWith(initialConterState),
67 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
68 | shareReplay(1)
69 | );
70 |
71 | // === INTERACTION OBSERVABLES ============================================
72 |
73 | // == INTERMEDIATE OBSERVABLES ============================================
74 | const count$ = counterState$.pipe(pluck(ConterStateKeys.count));
75 | const isTicking$ = counterState$.pipe(pluck(ConterStateKeys.isTicking), distinctUntilChanged());
76 | const tickSpeed$ = counterState$.pipe(pluck(ConterStateKeys.tickSpeed), distinctUntilChanged());
77 | const countDiff$ = counterState$.pipe(pluck(ConterStateKeys.countDiff), distinctUntilChanged());
78 |
79 | const counterUpdateTrigger$ = isTicking$
80 | .pipe(
81 | switchMap(isTicking => isTicking ? timer(0, initialConterState.tickSpeed) : NEVER)
82 | );
83 |
84 | // = SIDE EFFECTS =========================================================
85 |
86 | // == UI INPUTS ===========================================================
87 | const renderCountChange$ = count$.pipe(tap(n => counterUI.renderCounterValue(n)));
88 | const renderTickSpeedChange$ = tickSpeed$.pipe(tap(n => counterUI.renderTickSpeedInputValue(n)));
89 | const renderCountDiffChange$ = countDiff$.pipe(tap(n => counterUI.renderCountDiffInputValue(n)));
90 | const renderSetToChange$ = counterUI.btnReset$.pipe(tap(_ => { counterUI.renderSetToInputValue('10');}));
91 |
92 | // == UI OUTPUTS ==========================================================
93 | const commandFromTick$ = counterUpdateTrigger$
94 | .pipe(
95 | withLatestFrom(counterState$, (_, counterState) => ({
96 | [ConterStateKeys.count]: counterState.count,
97 | [ConterStateKeys.countUp]: counterState.countUp
98 | }) ),
99 | tap(({count, countUp}) => programmaticCommandSubject.next({count: count + 1 * (countUp ? 1 : -1)}))
100 | );
101 |
102 | // == SUBSCRIPTION ========================================================
103 |
104 | merge(
105 | // Input side effect
106 | renderCountChange$,
107 | renderTickSpeedChange$,
108 | renderCountDiffChange$,
109 | renderSetToChange$,
110 | // Outputs side effect
111 | commandFromTick$
112 | )
113 | .subscribe();
114 |
115 | // = HELPER ===============================================================
116 | // = CUSTOM OPERATORS =====================================================
117 | // == CREATION METHODS ====================================================
118 | // == OPERATORS ===========================================================
119 |
--------------------------------------------------------------------------------
/06_dynamic-tickSpeed.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys, PartialCountDownState} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay,distinctUntilKeyChanged, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ===================================================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ========================================================================
23 |
24 |
25 | // == CONSTANTS ===========================================================
26 | // Setup conutDown state
27 | const initialConterState: CountDownState = {
28 | count: 0,
29 | isTicking: false,
30 | tickSpeed: 200,
31 | countUp: true,
32 | countDiff:1
33 | };
34 |
35 | // Init CountDown counterUI
36 | const counterUI = new Counter(
37 | document.body,
38 | {
39 | initialSetTo: initialConterState.count + 10,
40 | initialTickSpeed: initialConterState.tickSpeed,
41 | initialCountDiff: initialConterState.countDiff,
42 | }
43 | );
44 |
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 |
48 | // All our source observables are extracted into Counter class to hide away all the low leven bindings.
49 |
50 | // === STATE OBSERVABLES ==================================================
51 | const programmaticCommandSubject = new Subject();
52 | const counterCommands$ = merge(
53 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
54 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
55 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
56 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
57 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
58 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
59 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
60 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n}))),
61 | programmaticCommandSubject.asObservable()
62 | );
63 |
64 | const counterState$: Observable = counterCommands$
65 | .pipe(
66 | startWith(initialConterState),
67 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
68 | shareReplay(1)
69 | );
70 |
71 | // === INTERACTION OBSERVABLES ============================================
72 |
73 | // == INTERMEDIATE OBSERVABLES ============================================
74 | const count$ = counterState$.pipe(pluck(ConterStateKeys.count));
75 | const isTicking$ = counterState$.pipe(pluck(ConterStateKeys.isTicking), distinctUntilChanged());
76 | const tickSpeed$ = counterState$.pipe(pluck(ConterStateKeys.tickSpeed), distinctUntilChanged());
77 | const countDiff$ = counterState$.pipe(pluck(ConterStateKeys.countDiff), distinctUntilChanged());
78 |
79 | const counterUpdateTrigger$ = combineLatest([isTicking$, tickSpeed$])
80 | .pipe(
81 | switchMap(([isTicking, tickSpeed]) => isTicking ? timer(0, tickSpeed) : NEVER)
82 | );
83 |
84 | // = SIDE EFFECTS =========================================================
85 |
86 | // == UI INPUTS ===========================================================
87 | const renderCountChange$ = count$.pipe(tap(n => counterUI.renderCounterValue(n)));
88 | const renderTickSpeedChange$ = tickSpeed$.pipe(tap(n => counterUI.renderTickSpeedInputValue(n)));
89 | const renderCountDiffChange$ = countDiff$.pipe(tap(n => counterUI.renderCountDiffInputValue(n)));
90 | const renderSetToChange$ = counterUI.btnReset$.pipe(tap(_ => { counterUI.renderSetToInputValue('10');}));
91 |
92 | // == UI OUTPUTS ==========================================================
93 | const commandFromTick$ = counterUpdateTrigger$
94 | .pipe(
95 | withLatestFrom(counterState$, (_, counterState) => ({
96 | [ConterStateKeys.count]: counterState.count,
97 | [ConterStateKeys.countUp]: counterState.countUp
98 | }) ),
99 | tap(({count, countUp}) => programmaticCommandSubject.next({count: count + 1 * (countUp ? 1 : -1)}))
100 | );
101 |
102 | // == SUBSCRIPTION ========================================================
103 |
104 | merge(
105 | // Input side effect
106 | renderCountChange$,
107 | renderTickSpeedChange$,
108 | renderCountDiffChange$,
109 | renderSetToChange$,
110 | // Outputs side effect
111 | commandFromTick$
112 | )
113 | .subscribe();
114 |
115 | // = HELPER ===============================================================
116 | // = CUSTOM OPERATORS =====================================================
117 | // == CREATION METHODS ====================================================
118 | // == OPERATORS ===========================================================
119 |
--------------------------------------------------------------------------------
/07_dynamic-countDiff.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys, PartialCountDownState} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay,distinctUntilKeyChanged, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ===================================================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ========================================================================
23 |
24 |
25 | // == CONSTANTS ===========================================================
26 | // Setup conutDown state
27 | const initialConterState: CountDownState = {
28 | count: 0,
29 | isTicking: false,
30 | tickSpeed: 200,
31 | countUp: true,
32 | countDiff:1
33 | };
34 |
35 | // Init CountDown counterUI
36 | const counterUI = new Counter(
37 | document.body,
38 | {
39 | initialSetTo: initialConterState.count + 10,
40 | initialTickSpeed: initialConterState.tickSpeed,
41 | initialCountDiff: initialConterState.countDiff,
42 | }
43 | );
44 |
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 |
48 | // All our source observables are extracted into Counter class to hide away all the low leven bindings.
49 |
50 | // === STATE OBSERVABLES ==================================================
51 | const programmaticCommandSubject = new Subject();
52 | const counterCommands$ = merge(
53 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
54 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
55 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
56 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
57 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
58 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
59 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
60 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n}))),
61 | programmaticCommandSubject.asObservable()
62 | );
63 |
64 | const counterState$: Observable = counterCommands$
65 | .pipe(
66 | startWith(initialConterState),
67 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
68 | shareReplay(1)
69 | );
70 |
71 | // === INTERACTION OBSERVABLES ============================================
72 |
73 | // == INTERMEDIATE OBSERVABLES ============================================
74 | const count$ = counterState$.pipe(pluck(ConterStateKeys.count));
75 | const isTicking$ = counterState$.pipe(pluck(ConterStateKeys.isTicking), distinctUntilChanged());
76 | const tickSpeed$ = counterState$.pipe(pluck(ConterStateKeys.tickSpeed), distinctUntilChanged());
77 | const countDiff$ = counterState$.pipe(pluck(ConterStateKeys.countDiff), distinctUntilChanged());
78 |
79 | const counterUpdateTrigger$ = combineLatest([isTicking$, tickSpeed$])
80 | .pipe(
81 | switchMap(([isTicking, tickSpeed]) => isTicking ? timer(0, tickSpeed) : NEVER)
82 | );
83 |
84 | // = SIDE EFFECTS =========================================================
85 |
86 | // == UI INPUTS ===========================================================
87 | const renderCountChange$ = count$.pipe(tap(n => counterUI.renderCounterValue(n)));
88 | const renderTickSpeedChange$ = tickSpeed$.pipe(tap(n => counterUI.renderTickSpeedInputValue(n)));
89 | const renderCountDiffChange$ = countDiff$.pipe(tap(n => counterUI.renderCountDiffInputValue(n)));
90 | const renderSetToChange$ = counterUI.btnReset$.pipe(tap(_ => { counterUI.renderSetToInputValue('10');}));
91 |
92 | // == UI OUTPUTS ==========================================================
93 | const commandFromTick$ = counterUpdateTrigger$
94 | .pipe(
95 | withLatestFrom(counterState$, (_, counterState) => ({
96 | [ConterStateKeys.count]: counterState.count,
97 | [ConterStateKeys.countUp]: counterState.countUp,
98 | [ConterStateKeys.countDiff]: counterState.countDiff
99 | }) ),
100 | tap(({count, countUp, countDiff}) => programmaticCommandSubject.next({count: count + countDiff * (countUp ? 1 : -1)}))
101 | );
102 |
103 | // == SUBSCRIPTION ========================================================
104 |
105 | merge(
106 | // Input side effect
107 | renderCountChange$,
108 | renderTickSpeedChange$,
109 | renderCountDiffChange$,
110 | renderSetToChange$,
111 | // Outputs side effect
112 | commandFromTick$
113 | )
114 | .subscribe();
115 |
116 | // = HELPER ===============================================================
117 | // = CUSTOM OPERATORS =====================================================
118 | // == CREATION METHODS ====================================================
119 | // == OPERATORS ===========================================================
120 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys, PartialCountDownState} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge, UnaryFunction} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay,distinctUntilKeyChanged, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ===================================================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ========================================================================
23 |
24 |
25 | // == CONSTANTS ===========================================================
26 | // Setup conutDown state
27 | const initialConterState: CountDownState = {
28 | count: 0,
29 | isTicking: false,
30 | tickSpeed: 200,
31 | countUp: true,
32 | countDiff:1
33 | };
34 |
35 | // Init CountDown counterUI
36 | const counterUI = new Counter(
37 | document.body,
38 | {
39 | initialSetTo: initialConterState.count + 10,
40 | initialTickSpeed: initialConterState.tickSpeed,
41 | initialCountDiff: initialConterState.countDiff,
42 | }
43 | );
44 |
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 | // All our source observables are extracted into Counter class to hide away all the low leven bindings.
48 | // === STATE OBSERVABLES ==================================================
49 | const programmaticCommandSubject = new Subject();
50 | const counterCommands$ = merge(
51 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
52 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
53 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
54 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
55 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
56 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
57 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
58 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n}))),
59 | programmaticCommandSubject.asObservable()
60 | );
61 |
62 | const counterState$: Observable = counterCommands$
63 | .pipe(
64 | startWith(initialConterState),
65 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
66 | shareReplay(1)
67 | );
68 |
69 | // === INTERACTION OBSERVABLES ============================================
70 | // == INTERMEDIATE OBSERVABLES ============================================
71 | const count$ = counterState$.pipe(pluck(ConterStateKeys.count));
72 | const isTicking$ = counterState$.pipe(queryChange(ConterStateKeys.isTicking));
73 | const tickSpeed$ = counterState$.pipe(queryChange(ConterStateKeys.tickSpeed));
74 | const countDiff$ = counterState$.pipe(queryChange(ConterStateKeys.countDiff));
75 |
76 | const counterUpdateTrigger$ = combineLatest([isTicking$, tickSpeed$])
77 | .pipe(
78 | switchMap(([isTicking, tickSpeed]) => isTicking ? timer(0, tickSpeed) : NEVER)
79 | );
80 |
81 | // = SIDE EFFECTS =========================================================
82 |
83 | // == UI INPUTS ===========================================================
84 | const renderCountChange$ = count$.pipe(tap(n => counterUI.renderCounterValue(n)));
85 | const renderTickSpeedChange$ = tickSpeed$.pipe(tap(n => counterUI.renderTickSpeedInputValue(n)));
86 | const renderCountDiffChange$ = countDiff$.pipe(tap(n => counterUI.renderCountDiffInputValue(n)));
87 | const renderSetToChange$ = counterUI.btnReset$.pipe(tap(_ => { counterUI.renderSetToInputValue('10');}));
88 |
89 | // == UI OUTPUTS ==========================================================
90 | const commandFromTick$ = counterUpdateTrigger$
91 | .pipe(
92 | withLatestFrom(counterState$, (_, counterState) => ({
93 | [ConterStateKeys.count]: counterState.count,
94 | [ConterStateKeys.countUp]: counterState.countUp,
95 | [ConterStateKeys.countDiff]: counterState.countDiff
96 | }) ),
97 | tap(({count, countUp, countDiff}) => programmaticCommandSubject.next( {count: count + countDiff * (countUp ? 1 : -1)}) )
98 | );
99 |
100 | // == SUBSCRIPTION ========================================================
101 |
102 | merge(
103 | // Input side effect
104 | renderCountChange$,
105 | renderTickSpeedChange$,
106 | renderCountDiffChange$,
107 | renderSetToChange$,
108 | // Outputs side effect
109 | commandFromTick$
110 | )
111 | .subscribe();
112 |
113 | // = HELPER ===============================================================
114 | // = CUSTOM OPERATORS =====================================================
115 | // == CREATION METHODS ====================================================
116 | // == OPERATORS ===========================================================
117 | function queryChange(key: string): UnaryFunction, Observable> {
118 | return pipe(
119 | pluck(key),
120 | distinctUntilChanged()
121 | );
122 | }
123 |
124 |
--------------------------------------------------------------------------------
/08_performance-optimisation-n-refactoring.ts:
--------------------------------------------------------------------------------
1 | import {Counter, CountDownState, ConterStateKeys, PartialCountDownState} from './counter'
2 | import { Observable, Observer, NEVER, Subject, pipe, timer, combineLatest, merge, UnaryFunction} from 'rxjs';
3 | import { map, mapTo, withLatestFrom,tap, distinctUntilChanged, shareReplay,distinctUntilKeyChanged, startWith, scan, pluck, switchMap} from 'rxjs/operators';
4 |
5 | // EXERCISE DESCRIPTION ===================================================
6 |
7 | /**
8 | * Use `ConterStateKeys` for property names.
9 | * Explort the counterUI API by typing `counterUI.` somewhere. ;)
10 | *
11 | * Implement all features of the counter:
12 | * 1. Start, pause the counter. Then restart the counter with 0 (+)
13 | * 2. Start it again from paused number (++)
14 | * 3. If Set to button is clicked set counter value to input value while counting (+++)
15 | * 4. Reset to initial state if reset button is clicked (+)
16 | * 5. If count up button is clicked count up, if count down button is clicked count down (+)
17 | * 6. Change interval if input tickSpeed input changes (++)
18 | * 7. Change count up if input countDiff changes (++)
19 | * 8. Take care of rendering execution and other performance optimisations as well as refactoring (+)
20 | */
21 |
22 | // ========================================================================
23 |
24 |
25 | // == CONSTANTS ===========================================================
26 | // Setup conutDown state
27 | const initialConterState: CountDownState = {
28 | count: 0,
29 | isTicking: false,
30 | tickSpeed: 200,
31 | countUp: true,
32 | countDiff:1
33 | };
34 |
35 | // Init CountDown counterUI
36 | const counterUI = new Counter(
37 | document.body,
38 | {
39 | initialSetTo: initialConterState.count + 10,
40 | initialTickSpeed: initialConterState.tickSpeed,
41 | initialCountDiff: initialConterState.countDiff,
42 | }
43 | );
44 |
45 | // = BASE OBSERVABLES ====================================================
46 | // == SOURCE OBSERVABLES ==================================================
47 | // All our source observables are extracted into Counter class to hide away all the low leven bindings.
48 | // === STATE OBSERVABLES ==================================================
49 | const programmaticCommandSubject = new Subject();
50 | const counterCommands$ = merge(
51 | counterUI.btnStart$.pipe(mapTo({isTicking: true})),
52 | counterUI.btnPause$.pipe(mapTo({isTicking: false})),
53 | counterUI.btnSetTo$.pipe(map(n => ({count: n}))),
54 | counterUI.btnUp$.pipe(mapTo({countUp: true})),
55 | counterUI.btnDown$.pipe(mapTo({countUp: false})),
56 | counterUI.btnReset$.pipe(mapTo({...initialConterState})),
57 | counterUI.inputTickSpeed$.pipe(map ( n => ({tickSpeed: n}))),
58 | counterUI.inputCountDiff$.pipe(map ( n => ({countDiff: n}))),
59 | programmaticCommandSubject.asObservable()
60 | );
61 |
62 | const counterState$: Observable = counterCommands$
63 | .pipe(
64 | startWith(initialConterState),
65 | scan( (counterState: CountDownState, command): CountDownState => ( {...counterState, ...command} ) ),
66 | shareReplay(1)
67 | );
68 |
69 | // === INTERACTION OBSERVABLES ============================================
70 | // == INTERMEDIATE OBSERVABLES ============================================
71 | const count$ = counterState$.pipe(pluck(ConterStateKeys.count));
72 | const isTicking$ = counterState$.pipe(queryChange(ConterStateKeys.isTicking));
73 | const tickSpeed$ = counterState$.pipe(queryChange(ConterStateKeys.tickSpeed));
74 | const countDiff$ = counterState$.pipe(queryChange(ConterStateKeys.countDiff));
75 |
76 | const counterUpdateTrigger$ = combineLatest([isTicking$, tickSpeed$])
77 | .pipe(
78 | switchMap(([isTicking, tickSpeed]) => isTicking ? timer(0, tickSpeed) : NEVER)
79 | );
80 |
81 | // = SIDE EFFECTS =========================================================
82 |
83 | // == UI INPUTS ===========================================================
84 | const renderCountChange$ = count$.pipe(tap(n => counterUI.renderCounterValue(n)));
85 | const renderTickSpeedChange$ = tickSpeed$.pipe(tap(n => counterUI.renderTickSpeedInputValue(n)));
86 | const renderCountDiffChange$ = countDiff$.pipe(tap(n => counterUI.renderCountDiffInputValue(n)));
87 | const renderSetToChange$ = counterUI.btnReset$.pipe(tap(_ => { counterUI.renderSetToInputValue('10');}));
88 |
89 | // == UI OUTPUTS ==========================================================
90 | const commandFromTick$ = counterUpdateTrigger$
91 | .pipe(
92 | withLatestFrom(counterState$, (_, counterState) => ({
93 | [ConterStateKeys.count]: counterState.count,
94 | [ConterStateKeys.countUp]: counterState.countUp,
95 | [ConterStateKeys.countDiff]: counterState.countDiff
96 | }) ),
97 | tap(({count, countUp, countDiff}) => programmaticCommandSubject.next( {count: count + countDiff * (countUp ? 1 : -1)}) )
98 | );
99 |
100 | // == SUBSCRIPTION ========================================================
101 |
102 | merge(
103 | // Input side effect
104 | renderCountChange$,
105 | renderTickSpeedChange$,
106 | renderCountDiffChange$,
107 | renderSetToChange$,
108 | // Outputs side effect
109 | commandFromTick$
110 | )
111 | .subscribe();
112 |
113 | // = HELPER ===============================================================
114 | // = CUSTOM OPERATORS =====================================================
115 | // == CREATION METHODS ====================================================
116 | // == OPERATORS ===========================================================
117 | function queryChange(key: string): UnaryFunction, Observable> {
118 | return pipe(
119 | pluck(key),
120 | distinctUntilChanged()
121 | );
122 | }
123 |
124 |
--------------------------------------------------------------------------------
/counter.ts:
--------------------------------------------------------------------------------
1 | import './style.scss';
2 |
3 | import { Subject, Observable, fromEvent} from 'rxjs';
4 | import { mapTo, map, withLatestFrom, startWith, shareReplay} from 'rxjs/operators';
5 |
6 | export interface CounterConfig {
7 | initialSetTo?: number;
8 | initialTickSpeed?: number;
9 | initialCountDiff?: number;
10 | }
11 |
12 | export interface CountDownState {
13 | isTicking: boolean;
14 | count: number;
15 | countUp: boolean;
16 | tickSpeed: number;
17 | countDiff:number;
18 | }
19 |
20 | export type PartialCountDownState =
21 | { isTicking: boolean } |
22 | { count: number } |
23 | { countUp: boolean } |
24 | { tickSpeed: number } |
25 | { countDiff:number};
26 |
27 | export enum ConterStateKeys {
28 | isTicking = 'isTicking',
29 | count = 'count',
30 | countUp = 'countUp',
31 | tickSpeed = 'tickSpeed',
32 | countDiff = 'countDiff'
33 | }
34 |
35 | export enum ActionNames {
36 | Start,
37 | Pause,
38 | Reset,
39 | SetTo,
40 | Down,
41 | Up,
42 | TickSpeed,
43 | CountDiff
44 | }
45 |
46 | enum ElementIds {
47 | TimerDisplay = 'timer-display',
48 | BtnStart = 'btn-start',
49 | BtnPause = 'btn-pause',
50 | BtnUp = 'btn-up',
51 | BtnDown = 'btn-down',
52 | BtnReset = 'btn-reset',
53 | BtnSetTo = 'btn-set-to',
54 | InputSetTo = 'set-to-input',
55 | InputTickSpeed = 'tick-speed-input',
56 | InputCountDiff = 'count-diff-input'
57 | }
58 |
59 | export class Counter {
60 | private initialSetTo: number;
61 | private initialTickSpeed: number;
62 | private initialCountDiff: number;
63 |
64 | private viewHtml = (): string =>`
65 |
66 |
67 | Press Start
68 |
69 | Have fun! :)
70 |
71 | read the comments
72 |
73 |
74 |
75 |
76 |
77 |
80 |
81 |
84 |
85 |
86 |
87 |
90 |
91 |
92 |
93 |
94 |
97 |
98 |
99 |
100 |
103 |
104 |
107 |
108 |
109 |
110 |
113 |
114 |
115 |
116 |
117 |
118 |
121 |
122 | `;
123 |
124 | private display: HTMLParagraphElement;
125 |
126 | public renderCounterValue(count: number) {
127 | if(this.display) {
128 | this.display.innerHTML = count.toString()
129 | .split('')
130 | .map(this.getDigit)
131 | .join('');
132 | }
133 |
134 | }
135 |
136 | private setToInput: HTMLInputElement;
137 | public renderSetToInputValue = (value: string) => {
138 | if(this.setToInput) {
139 | this.setToInput.value = value.toString();
140 | }
141 | }
142 |
143 |
144 | private tickSpeedInput;
145 | public renderTickSpeedInputValue = (value: number): void => {
146 | if(this.tickSpeedInput) {
147 | this.tickSpeedInput.value = value.toString();
148 | }
149 | }
150 |
151 | private countDiffInput;
152 | public renderCountDiffInputValue = (value: number): void => {
153 | if(this.countDiffInput) {
154 | this.countDiffInput.value = value.toString();
155 | }
156 | }
157 |
158 | public btnStart$: Observable;
159 | public btnPause$: Observable;
160 | public btnUp$: Observable;
161 | public btnDown$: Observable;
162 | public btnReset$: Observable;
163 | public btnSetTo$: Observable;
164 |
165 | public inputTickSpeed$: Observable;
166 | public inputCountDiff$: Observable;
167 | public inputSetTo$: Observable
168 |
169 | constructor(parent: HTMLElement, config?: CounterConfig) {
170 | this.initialTickSpeed = config && config.initialTickSpeed || 1000;
171 | this.initialSetTo = config && config.initialSetTo || 0;
172 | this.initialCountDiff = config && config.initialCountDiff || 1;
173 |
174 | this.init(parent);
175 | }
176 |
177 | private init(parent: HTMLElement) {
178 | parent.innerHTML = parent.innerHTML + this.viewHtml();
179 |
180 | // getElements
181 | this.display = document.getElementById(ElementIds.TimerDisplay) as HTMLParagraphElement;
182 | this.setToInput = document.getElementById(ElementIds.InputSetTo) as HTMLInputElement;
183 | this.tickSpeedInput = document.getElementById(ElementIds.InputTickSpeed) as HTMLInputElement;
184 | this.countDiffInput = document.getElementById(ElementIds.InputCountDiff) as HTMLInputElement;
185 |
186 | // setup observables
187 | this.btnStart$ = getCommandObservableByElem(ElementIds.BtnStart, 'click', ActionNames.Start);
188 | this.btnPause$ = getCommandObservableByElem(ElementIds.BtnPause, 'click', ActionNames.Pause);
189 | this.btnUp$ = getCommandObservableByElem(ElementIds.BtnUp, 'click',ActionNames.Up);
190 | this.btnDown$ = getCommandObservableByElem(ElementIds.BtnDown, 'click', ActionNames.Down);
191 | this.btnReset$ = getCommandObservableByElem(ElementIds.BtnReset, 'click', ActionNames.Reset);
192 |
193 | this.inputSetTo$ = getValueObservable(ElementIds.InputSetTo, 'input').pipe(startWith(this.initialSetTo));
194 | this.inputTickSpeed$ = getValueObservable(ElementIds.InputTickSpeed, 'input').pipe(startWith(this.initialTickSpeed));
195 | this.inputCountDiff$ = getValueObservable(ElementIds.InputCountDiff, 'input').pipe(startWith(this.initialCountDiff));
196 |
197 | this.btnSetTo$ = getCommandObservableByElem(ElementIds.BtnSetTo, 'click', ActionNames.SetTo)
198 | .pipe(withLatestFrom(this.inputSetTo$, (_, i$) => i$));
199 |
200 | }
201 |
202 |
203 | private getDigit(d): string {
204 | return `
205 |
206 | ${d}
207 |
208 | `;
209 | }
210 |
211 | private getDigitDivider(): string {
212 | return ''
213 | }
214 |
215 |
216 | }
217 |
218 | function getCommandObservableByElem(elemId: string, eventName: string, command: ActionNames) {
219 | return fromEvent(document.getElementById(elemId), eventName).pipe(mapTo(command));
220 | }
221 |
222 | function getValueObservable (elemId: string, eventName: string): Observable {
223 | const elem = document.getElementById(elemId);
224 | return fromEvent(elem, eventName)
225 | .pipe(
226 | map(v => v.target.value),
227 | map(v => parseInt(v, 10)),
228 | shareReplay(1)
229 | );
230 | }
231 |
--------------------------------------------------------------------------------