├── 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 | --------------------------------------------------------------------------------