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.

Shmuela Jacobs

April 03, 2019
Tweet

More Decks by Shmuela Jacobs

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. REACTIVE FORMS ➤ Create form - get room data ➤

    Get realtime updates ➤ Selection count ➤ Async validation ➤ Errors - ➤ display message ➤ update seat @ShmuelaJ
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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>
  11. 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>
  12. 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>
  13. 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>
  14. 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; ); ...
  15. 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 }); }
  16. 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 }); }
  17. 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 }); }
  18. 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 }); }
  19. 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 }); }
  20. 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>
  21. 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) }
  22. 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 });
  23. 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
  24. 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$, ) ) ) );
  25. VALIDATION ➤ Sync validation: no gaps ➤ custom validation ➤

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

    unselect ➤ show error message @ShmuelaJ
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. ➤ 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) } );
  33. 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 } );
  34. 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 } );
  35. 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 } );
  36. 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 }
  37. 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
  38. SUMMARY ➤ Observables in forms: ➤ valueChanges ➤ statusChanges ➤

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

    error messages ➤ close the popup ➤ Easy to unit test: ➤ FakeAsync, setTimeout, tick @ShmuelaJ