Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Reactive Powered: RxJS with Angular Forms

Reactive Powered: RxJS with Angular Forms

Angular’s Reactive Forms module gives us a lot of control and flexibility to easily build and define our forms. While looking at async data and form lifecycle events as streams, we can leverage the power of RxJS to react to these events. Presenting a non-trivial custom form control, Shmuela will show how she uses Observables when creating the form, reacting to value and state changes, and validating the input.

4f9c232c7f8804cb73a13aa1b005a18a?s=128

Shmuela Jacobs

April 03, 2019
Tweet

More Decks by Shmuela Jacobs

Other Decks in Programming

Transcript

  1. REACTIVE POWERED: RXJS WITH ANGULAR FORMS Shmuela Jacobs

  2. Shmuela Jacobs Cloud Advocate @ Microsoft shmool @ShmuelaJ

  3. Shmuela Jacobs Cloud Advocate @ Microsoft shmool @ShmuelaJ

  4. None
  5. DEMO

  6. SERVER-CLIENT ARCHITECTURE @ShmuelaJ Server Client Room Occupied Seats Is Occupied?

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

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

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

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

    Save selection Room Id
  11. 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
  12. 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
  13. REACTIVE FORMS ➤ Create form - get room data ➤

    Get realtime updates ➤ Selection count ➤ Async validation ➤ Errors - ➤ display message ➤ update seat @ShmuelaJ
  14. INITIALIZE

  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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> _______Row_______ seats: Array<SeatType>
  22. 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> _______Row_______ seats: Array<SeatType>
  23. 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> _______Row_______ seats: Array<SeatType>
  24. 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> _______Row_______ seats: Array<SeatType>
  25. UPDATE

  26. 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; ); ...
  27. 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 }); }
  28. 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 }); }
  29. 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 }); }
  30. 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 }); }
  31. 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 }); }
  32. 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> _______Row_______ seats: Array<SeatType>
  33. COUNT

  34. 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) }
  35. INVALID

  36. 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 });
  37. ERROR MESSAGE

  38. 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
  39. 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$, ) ) ) );
  40. VALIDATE

  41. VALIDATION ➤ Sync validation: no gaps ➤ custom validation ➤

    on seat / row ➤ use control.parent @ShmuelaJ
  42. VALIDATION ➤ Async validation: is seat occupied? ➤ disable ➤

    unselect ➤ show error message @ShmuelaJ
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. ➤ 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) } );
  49. 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 } );
  50. 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 } );
  51. 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 } );
  52. 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 }
  53. NOTES

  54. 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
  55. SUMMARY ➤ Observables in forms: ➤ valueChanges ➤ statusChanges ➤

    Other observables we used: ➤ route params ➤ data from DB ➤ async validation @ShmuelaJ
  56. SUMMARY ➤ Subjects we created: ➤ counting the seats ➤

    error messages ➤ close the popup ➤ Easy to unit test: ➤ FakeAsync, setTimeout, tick @ShmuelaJ
  57. Thank You! Michael Hladky Jan-Niklas Wortmann

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