Slide 1

Slide 1 text

REACTIVE POWERED: RXJS WITH ANGULAR FORMS Shmuela Jacobs

Slide 2

Slide 2 text

Shmuela Jacobs Cloud Advocate @ Microsoft shmool @ShmuelaJ

Slide 3

Slide 3 text

Shmuela Jacobs Cloud Advocate @ Microsoft shmool @ShmuelaJ

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

DEMO

Slide 6

Slide 6 text

SERVER-CLIENT ARCHITECTURE @ShmuelaJ Server Client Room Occupied Seats Is Occupied? Save selection Room Id

Slide 7

Slide 7 text

SERVER-CLIENT ARCHITECTURE @ShmuelaJ Server Client Room Occupied Seats Is Occupied? Save selection Room Id

Slide 8

Slide 8 text

SERVER-CLIENT ARCHITECTURE @ShmuelaJ Server Client Room Occupied Seats Is Occupied? Save selection Room Id

Slide 9

Slide 9 text

SERVER-CLIENT ARCHITECTURE @ShmuelaJ Server Client Room Occupied Seats Is Occupied? Save selection Room Id

Slide 10

Slide 10 text

SERVER-CLIENT ARCHITECTURE @ShmuelaJ Server Client Room Occupied Seats Is Occupied? Save selection Room Id

Slide 11

Slide 11 text

WHY FORM? ➤ User input ➤ Validation: ➤ async - is seat occupied? ➤ sync - select adjacent seats ➤ Disable selecting occupied seats ➤ Get data from server ➤ when building the form ➤ patch /set value ➤ Submit @ShmuelaJ

Slide 12

Slide 12 text

REACTIVE FORMS ➤ How reactive are the Reactive Forms? ➤ Where can we use observables? ➤ How can we utilize RxJS? ➤ How can we use other observables with the form? @ShmuelaJ

Slide 13

Slide 13 text

REACTIVE FORMS ➤ Create form - get room data ➤ Get realtime updates ➤ Selection count ➤ Async validation ➤ Errors - ➤ display message ➤ update seat @ShmuelaJ

Slide 14

Slide 14 text

INITIALIZE

Slide 15

Slide 15 text

constructor( private db: DbService, private route: ActivatedRoute) { this.roomId$ = this.route.params.pipe( map(params => params.id) ); this.roomConfig$ = this.roomId$.pipe( switchMap((id) => this.db.getRoom(id)) ); this.seatsSelectForm$ = this.roomConfig$.pipe( map((room: Room) => this.createSeatsForm(room)) ); this.roomName$ = this.room$.pipe( map((value: Room) => value.roomName) ); } @ShmuelaJ Route Params ONE SOURCE, MULTIPLE STREAMS

Slide 16

Slide 16 text

constructor( private db: DbService, private route: ActivatedRoute) { this.roomId$ = this.route.params.pipe( map(params => params.id) ); this.roomConfig$ = this.roomId$.pipe( switchMap((id) => this.db.getRoom(id)) ); this.seatsSelectForm$ = this.roomConfig$.pipe( map((room: Room) => this.createSeatsForm(room)) ); this.roomName$ = this.room$.pipe( map((value: Room) => value.roomName) ); } @ShmuelaJ Route Params Room ID ONE SOURCE, MULTIPLE STREAMS

Slide 17

Slide 17 text

constructor( private db: DbService, private route: ActivatedRoute) { this.roomId$ = this.route.params.pipe( map(params => params.id) ); this.roomConfig$ = this.roomId$.pipe( switchMap((id) => this.db.getRoom(id)) ); this.seatsSelectForm$ = this.roomConfig$.pipe( map((room: Room) => this.createSeatsForm(room)) ); this.roomName$ = this.room$.pipe( map((value: Room) => value.roomName) ); } @ShmuelaJ Route Params Room Config Room ID ONE SOURCE, MULTIPLE STREAMS

Slide 18

Slide 18 text

constructor( private db: DbService, private route: ActivatedRoute) { this.roomId$ = this.route.params.pipe( map(params => params.id) ); this.roomConfig$ = this.roomId$.pipe( switchMap((id) => this.db.getRoom(id)) ); this.seatsSelectForm$ = this.roomConfig$.pipe( map((room: Room) => this.createSeatsForm(room)) ); this.roomName$ = this.room$.pipe( map((value: Room) => value.roomName) ); } @ShmuelaJ Route Params Room Config Room ID Seats Select Form ONE SOURCE, MULTIPLE STREAMS

Slide 19

Slide 19 text

constructor( private db: DbService, private route: ActivatedRoute) { this.roomId$ = this.route.params.pipe( map(params => params.id) ); this.roomConfig$ = this.roomId$.pipe( switchMap((id) => this.db.getRoom(id)) ); this.seatsSelectForm$ = this.roomConfig$.pipe( map((room: Room) => this.createSeatsForm(room)), share() ); this.roomName$ = this.room$.pipe( map((value: Room) => value.roomName) ); } @ShmuelaJ Route Params Room Config Room ID Seats Select Form ONE SOURCE, MULTIPLE STREAMS

Slide 20

Slide 20 text

constructor( private db: DbService, private route: ActivatedRoute) { this.roomId$ = this.route.params.pipe( map(params => params.id) ); this.roomConfig$ = this.roomId$.pipe( switchMap((id) => this.db.getRoom(id)) ); this.seatsSelectForm$ = this.roomConfig$.pipe( map((room: Room) => this.createSeatsForm(room)), share() ); this.roomName$ = this.room$.pipe( map((value: Room) => value.roomName) ); } @ShmuelaJ Route Params Room Config Room ID Room Name Seats Select Form ONE SOURCE, MULTIPLE STREAMS

Slide 21

Slide 21 text

CREATING THE FORM @ShmuelaJ createForm(room: Room) { const roomForm = new FormGroup({ rows: new FormArray( room.rows.map( (row, rowId) => { return new FormArray( const seatControl = row.seats.map( (seatType, seatId) => { return new FormControl({ rowId, seatId, seatType, selected: false, occupied: false }); }) return seatControl; ); ... return roomForm; _______Seat_______ row id seat id seat type selected occupied _______Room______ rows: Array _______Row_______ seats: Array

Slide 22

Slide 22 text

CREATING THE FORM @ShmuelaJ createForm(room: Room) { const roomForm = new FormGroup({ rows: new FormArray( room.rows.map( (row, rowId) => { return new FormArray( const seatControl = row.seats.map( (seatType, seatId) => { return new FormControl({ rowId, seatId, seatType, selected: false, occupied: false }); }) return seatControl; ); ... return roomForm; _______Seat_______ row id seat id seat type selected occupied _______Room______ rows: Array _______Row_______ seats: Array

Slide 23

Slide 23 text

CREATING THE FORM @ShmuelaJ createForm(room: Room) { const roomForm = new FormGroup({ rows: new FormArray( room.rows.map( (row, rowId) => { return new FormArray( const seatControl = row.seats.map( (seatType, seatId) => { return new FormControl({ rowId, seatId, seatType, selected: false, occupied: false }); }) return seatControl; ); ... return roomForm; _______Seat_______ row id seat id seat type selected occupied _______Room______ rows: Array _______Row_______ seats: Array

Slide 24

Slide 24 text

CREATING THE FORM @ShmuelaJ createForm(room: Room) { const roomForm = new FormGroup({ rows: new FormArray( room.rows.map( (row, rowId) => { return new FormArray( const seatControl = row.seats.map( (seatType, seatId) => { return new FormControl({ rowId, seatId, seatType, selected: false, occupied: false }); }) return seatControl; ); ... return roomForm; _______Seat_______ row id seat id seat type selected occupied --> disabled _______Room______ rows: Array _______Row_______ seats: Array

Slide 25

Slide 25 text

UPDATE

Slide 26

Slide 26 text

DISABLED IF OCCUPIED ➤ If occupied seats come with seat data - ➤ pass "disabled" on c'tor ➤ and on reset @ShmuelaJ createForm(room: Room) { const roomForm = new FormGroup({ rows: new FormArray( room.rows.map( (row, rowId) => { return new FormArray( const seatControl = row.seats.map( (seatType, seatId) => { return new FormControl({ value: { rowId, seatId, seatType, selected: false }, disabled: seat.occupied }); }) return seatControl; ); ...

Slide 27

Slide 27 text

DISABLED IF OCCUPIED ➤ Real time updates on occupied and released seats @ShmuelaJ this.roomId$.pipe( switchMap((id) => this.db.getSeatsUpdate(id)), withLatestFrom(this.seatsSelectForm$) ).subscribe(([updates, form]) => { ... // iterate over the updates, // get the rowId, seatId and value const control = form.get( ['rows', rowId, 'seats', seatId]); updateValue.occupied ? control.disable({ emitEvent: false }) : control.enable({ emitEvent: false }); }

Slide 28

Slide 28 text

DISABLED IF OCCUPIED ➤ Real time updates on occupied and released seats @ShmuelaJ this.roomId$.pipe( switchMap((id) => this.db.getSeatsUpdate(id)), withLatestFrom(this.seatsSelectForm$) ).subscribe(([updates, form]) => { ... // iterate over the updates, // get the rowId, seatId and value const control = form.get( ['rows', rowId, 'seats', seatId]); updateValue.occupied ? control.disable({ emitEvent: false }) : control.enable({ emitEvent: false }); }

Slide 29

Slide 29 text

DISABLED IF OCCUPIED ➤ Real time updates on occupied and released seats @ShmuelaJ this.roomId$.pipe( switchMap((id) => this.db.getSeatsUpdate(id)), withLatestFrom(this.seatsSelectForm$) ).subscribe(([updates, form]) => { ... // iterate over the updates, // get the rowId, seatId and value const control = form.get( ['rows', rowId, 'seats', seatId]); updateValue.occupied ? control.disable({ emitEvent: false }) : control.enable({ emitEvent: false }); }

Slide 30

Slide 30 text

DISABLED IF OCCUPIED ➤ Real time updates on occupied and released seats @ShmuelaJ this.roomId$.pipe( switchMap((id) => this.db.getSeatsUpdate(id)), withLatestFrom(this.seatsSelectForm$) ).subscribe(([updates, form]) => { ... // iterate over the updates, // get the rowId, seatId and value const control = form.get( ['rows', rowId, 'seats', seatId]); updateValue.occupied ? control.disable({ emitEvent: false }) : control.enable({ emitEvent: false }); }

Slide 31

Slide 31 text

DISABLED IF OCCUPIED ➤ Real time updates on occupied and released seats @ShmuelaJ this.roomId$.pipe( switchMap((id) => this.db.getSeatsUpdate(id)), withLatestFrom(this.seatsSelectForm$) ).subscribe(([updates, form]) => { ... // iterate over the updates, // get the rowId, seatId and value const control = form.get( ['rows', rowId, 'seats', seatId]); updateValue.occupied ? control.disable({ emitEvent: false }) : control.enable({ emitEvent: false }); }

Slide 32

Slide 32 text

CREATING THE FORM @ShmuelaJ createForm(room: Room) { const roomForm = new FormGroup({ rows: new FormArray( room.rows.map( (row, rowId) => { return new FormArray( const seatControl = row.seats.map( (seatType, seatId) => { return new FormControl({ rowId, seatId, seatType, selected: false, occupied: false }); }) return seatControl; ); ... return roomForm; _______Seat_______ row id seat id seat type selected occupied --> disabled _______Room______ rows: Array _______Row_______ seats: Array

Slide 33

Slide 33 text

COUNT

Slide 34

Slide 34 text

SELECTED SEATS COUNT ➤ Count each change ➤ scan 
 (=reduce for observables) @ShmuelaJ selectedSeatsChange$ = new BehaviorSubject(0); numOfSeats$ = this.selectedSeatsChange$.pipe( scan((acc, current) => acc + current, 0) ); createForm(room: Room) { ... const seatControl = ... seatControl.valueChanges.pipe( distinctUntilChanged(), map(value => value.selected ? 1 : -1) ).subscribe(val => { this.selectedSeatsChange$.next(val) }

Slide 35

Slide 35 text

INVALID

Slide 36

Slide 36 text

STATUS CHANGES ➤ Async Validation result ➤ If invalid (seat is already occupied) - ➤ emit the error ➤ unselect ➤ disable @ShmuelaJ seatControl.statusChanges.pipe( distinctUntilChanged(), filter(status => status === 'INVALID'), ).subscribe(() => { this.seatControlErrors$.next( seatControl.errors); seatControl.setValue( { ...seatControl.value, selected: false }); seatControl.disable(); // status --> VALID });

Slide 37

Slide 37 text

ERROR MESSAGE

Slide 38

Slide 38 text

SHOW ERROR MESSAGE + UNSELECT ➤ !form.valid && !form.errors (ngWat?!) ➤ Get errors - only through the control: control.errors.... form.get(errorKey, path).... ➤ Where should the logic be? ➤ async validator ➤ service/component ➤ seat form control ➤ How do we get the 
 error message? @ShmuelaJ

Slide 39

Slide 39 text

SHOW ERROR MESSAGE + UNSELECT ➤ Errors Subject ➤ Subscribe to statusChanges ➤ Emit error messages (or 'null') ➤ Unselect and disable @ShmuelaJ seatControlErrors$ = new BehaviorSubject(null); closeErrors$ = new Subject(); formErrors$ = merge( this.seatControlErrors$.pipe( map(errors => getErrorMessage(errors)), ), this.seatControlErrors$.pipe( switchMapTo( race( timer(3000), this.closeErrors$, ) ) ) );

Slide 40

Slide 40 text

VALIDATE

Slide 41

Slide 41 text

VALIDATION ➤ Sync validation: no gaps ➤ custom validation ➤ on seat / row ➤ use control.parent @ShmuelaJ

Slide 42

Slide 42 text

VALIDATION ➤ Async validation: is seat occupied? ➤ disable ➤ unselect ➤ show error message @ShmuelaJ

Slide 43

Slide 43 text

ASYNC VALIDATION ➤ Factory function isSeatOccupied(roomId, rowId, seatId) { return (control: FormControl) => { if (!control.value.selected) { return of(null); } return this.db.isSeatOccupied(roomId, rowId, seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); }; } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatOccupied(this.roomId, rowId, seatId) } ); @ShmuelaJ

Slide 44

Slide 44 text

ASYNC VALIDATION ➤ Factory function isSeatOccupied(roomId, rowId, seatId) { return (control: FormControl) => { if (!control.value.selected) { return of(null); } return this.db.isSeatOccupied(roomId, rowId, seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); }; } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatOccupied(this.roomId, rowId, seatId) } ); @ShmuelaJ

Slide 45

Slide 45 text

ASYNC VALIDATION ➤ Factory function isSeatOccupied(roomId, rowId, seatId) { return (control: FormControl) => { if (!control.value.selected) { return of(null); } return this.db.isSeatOccupied(roomId, rowId, seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); }; } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatOccupied(this.roomId, rowId, seatId) } ); @ShmuelaJ

Slide 46

Slide 46 text

ASYNC VALIDATION ➤ Factory function isSeatOccupied(roomId, rowId, seatId) { return (control: FormControl) => { if (!control.value.selected) { return of(null); } return this.db.isSeatOccupied(roomId, rowId, seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); }; } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatOccupied(this.roomId, rowId, seatId) } ); @ShmuelaJ

Slide 47

Slide 47 text

ASYNC VALIDATION ➤ Factory function isSeatOccupied(roomId, rowId, seatId) { return (control: FormControl) => { if (!control.value.selected) { return of(null); } return this.db.isSeatOccupied(roomId, rowId, seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); }; } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatOccupied(this.roomId, rowId, seatId) } ); @ShmuelaJ

Slide 48

Slide 48 text

➤ Factory function ➤ ...or a regular function ASYNC VALIDATION @ShmuelaJ isSeatOccupied(roomId, rowId, seatId) { return (control: FormControl) => { if (!control.value.selected) { return of(null); } return this.db.isSeatOccupied(roomId, rowId, seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); }; } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatOccupied(this.roomId, rowId, seatId) } );

Slide 49

Slide 49 text

ASYNC VALIDATION ➤ Factory function ➤ ...or a regular function @ShmuelaJ isSeatOccupied(control: FormControl) { if (!control.value.selected) { return of(null); } return this.db.isSeatOccupied( this.roomId, control.value.rowId, control.value.seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatTaken } );

Slide 50

Slide 50 text

ASYNC VALIDATION ➤ Factory function ➤ ...or a regular function @ShmuelaJ isSeatOccupied(control: FormControl) { if (!control.value.selected) { return of(null); } return this.db.isSeatOccupied( this.roomId, control.value.rowId, control.value.seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatTaken } );

Slide 51

Slide 51 text

ASYNC VALIDATION ➤ Factory function ➤ ...or a regular function @ShmuelaJ isSeatOccupied(control: FormControl) { if (!control.value.selected) { return of(null); } return this.db.isSeatOccupied( this.roomId, control.value.rowId, control.value.seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatTaken } );

Slide 52

Slide 52 text

ASYNC VALIDATION ➤ Factory function ➤ ...or a regular function @ShmuelaJ isSeatOccupied(control: FormControl) { if (!control.value.selected) { return of(null); } return this.roomId$.pipe( switchMap(roomId => this.db.isSeatOccupied( roomId, control.value.rowId, control.value.seatId) .pipe( map(isOccupied => { return isOccupied ? { seatOccupied: true } : null; }) ); } const seatControl = new FormControl( { ... }, { asyncValidators: isSeatTaken }

Slide 53

Slide 53 text

NOTES

Slide 54

Slide 54 text

A NOTE ABOUT... ➤ FormControl.registerOnChange() ➤ register a callback for every value change 
 that occurs only with setValue()
 (not user input!) ➤ FormControl.registerOnDisabledChange() ➤ register a callback for every emitting of disabled/enabled, 
 even when it hasn't really changed @ShmuelaJ

Slide 55

Slide 55 text

SUMMARY ➤ Observables in forms: ➤ valueChanges ➤ statusChanges ➤ Other observables we used: ➤ route params ➤ data from DB ➤ async validation @ShmuelaJ

Slide 56

Slide 56 text

SUMMARY ➤ Subjects we created: ➤ counting the seats ➤ error messages ➤ close the popup ➤ Easy to unit test: ➤ FakeAsync, setTimeout, tick @ShmuelaJ

Slide 57

Slide 57 text

Thank You! Michael Hladky Jan-Niklas Wortmann

Slide 58

Slide 58 text

Thank You! Shmuela Jacobs shmuela@ng-girls.org shmool @ShmuelaJ Support ngGirls