{show and tell}

Angular: a declarative dynamic button-cascade

# Imperative vs Declarative

Imperative I

in each function one can acces the classMembers and change them, so the dev has to follow all the steps carefully to understand the flow

# Imperative vs Declarative

Imperative II

when one uses myData, in template will trigger change detection, so accessing this later in functions will not show all functionality in a glance

# Imperative vs Declarative

Declarative I

define all behavior in streams and to not reassign

 

example:

the result of someThingOne can later be used in someThingFour

# Imperative vs Declarative

Declarative II

no need to define timing 

 

myData -> get data

 

filteredData -> transform data

Show and Tell

# The Format: Show and Tell

a standard web dev AC from our customer and we want to implement this declarative so we use Angular reactive Forms Module

i wanted to share my experience of developing this feature to showcase the pitfalls and to show you this, i mvp'd the example into a stackblitz

 

https://stackblitz.com/~/github.com/janpauldahlke/ccwt-example

Acceptance Criteria

# real world AC

Prerequisites:

  • there is a endpoint for fetching the involved pokemons. these can change name, id, and how many!
  • there is another endpoint for fetching the FilterSettings fo a user for pokemons

State of the form:

  • the app loads initially, set checkboxes for the pokemons found in the filter those from the other endpoint
  • when the user toggles the allSelect. we select all or none pokemon and update the checkboxes in the dynamic form
  • when there is at least one, BUT not all pokemon selected by the user, update the allSelect by indeterminate state
  • when none pokemon is selected, remove the check on allSelected
  • when all pokemon is selected, check the allSelected
// in src/app/pokemon/filter/services/pokemon.service.ts
// simulate the behavior of the endpoint here

  //active pokemones by admin
  randomPokemonNumbers = [240, 146, 401] 
  // selected pokemon by user
  couldBeHttpFilter: PokemonFilter = { 
    allSelected: false,
    pokemon: [
      {
        name: 'foo',
        id: 240
      },
    ]
  }

How to simulate enpdpoint in the example?

pokemon/filter.component.ts
pokemon/filter.component.ts

reactive Form setup // initializing

form$ = this.formBuilder.nonNullable.group({
    allSelected: this.formBuilder.nonNullable.control(false),
    pokemons: this.formBuilder.nonNullable.group({}),
  })

And observe the form changes in ngOnInit

ngOnInit() {
  this.form.valueChanges
  	.pipe(
  		//some methods here
  	)
  	.subscribbe()
}
pokemon/filter.component.ts

how to data?

filterSettings$ : Observable<PokemonFilter> = this.pokemonService.getPokemonFilterSettings();
pokemonsHttp$: Observable<Pokemon[]> = this.pokemonService.getPokemons();

pokemons$: Observable<Pokemon[]> = combineLatest({
  pokemonInfo: this.pokemonsHttp$,
  filters: this.filterSettings$
})
  .pipe(
  //more methods
)
pokemon/filter.component.ts

adding dynamic controls to form

tap(({pokemonInfo, filters}) => {
      this.pokemonInfo = pokemonInfo;
      const pokemonsControls =
        // create object, where PokeId = FormControl
        pokemonInfo.reduce((acc: { [key: string]: FormControl }, pokemon: Pokemon) => {
        const isSelected =
          filters.pokemon.some(filterPokemon => filterPokemon.id === pokemon.id);

        acc[pokemon.id.toString()] = this.formBuilder.control(isSelected);
        return acc;
      }, {});
	  // set this in the empty pokemons fb.group
      this.form$.setControl('pokemons', this.formBuilder.group(pokemonsControls));
      this.form$.get('allSelected')?.setValue(filters.allSelected, {emitEvent: false});
      this.indeterminate = !filters.allSelected && filters.pokemon.length > 0;
    }),
listenToPokemonSelections() {
  const pokemonsGroup = this.form$.controls.pokemons as FormGroup;
  Object.values(pokemonsGroup.controls).forEach((control) => {
    const formControl = control as FormControl;

    // we create a subscription to each pokemonControl here
    // care to remove those correct
    // in TAS are other techniques for this
    const subscription= formControl.valueChanges.subscribe((poke) => {
      const allSelected = Object.values(pokemonsGroup.controls).every(c => c.value);
      const someSelected = Object.values(pokemonsGroup.controls).some(c => c.value);
      this.form$.controls.allSelected.setValue(allSelected, {emitEvent: false});
      this.indeterminate = someSelected && !allSelected;
    });
    this.nestedFormSubscriptions.add(subscription);
  });
}

updating the state of 'allSelected' from the singles

* will be called in ngOnInit

pokemon/filter.component.ts
pokemon/filter.component.ts

updating the state pokemnes from 'allSelected'

toggleAll() {
    const pokemonsControls = this.form$.controls.pokemons;
    const toggle = this.form$.controls.allSelected.value!;
    const pokemonUpdates =  Object.keys(pokemonsControls.controls).reduce((acc, key) => ({
      ...acc,
      [key]: toggle
    }), {});

    this.form$.controls.pokemons.patchValue(pokemonUpdates);
  }
//inside the formValueChanges

if (changes.allSelected !== this.savedAllSelected) {
  this.toggleAll();
  this.savedAllSelected = changes.allSelected ? changes.allSelected : false
}

Problems for the developer

* component sometimes feels flaky and slow

* dynamic controls can not be accessed in ngOnit! (show old commit)

* a are observing the very form we apply changes to

* this could lead infinite recursion

* we need techniques to stop it

hands on // tinker around

in the blitz:

https://stackblitz.com/~/github.com/janpauldahlke/ccwt-example

 

* distinctUntilChanged (comment in and out)

* log valueChanges

* remove the memoize if in changes (comment in and out)

ways to gain control

* debounceTime() // controlling how often the check occurs

* distinctUntilChanged. works partially, it is shallow compare,

* deepEqual is aways expensive, when the form is large and is considered to a be not "best practice"

* when setting the formControls. we might need to manually trigger changeDetection, since structrual changes do not get recon by angular

 

We opted to mix imperative and declarative to achieve our AC.

* like  '[indeterminate]="indeterminate"' in template using the reassign

Questions while developing

1. are there better way's to implement, can we get rid of 

* distinctUntilChanged

... and the other thing we used

 

2. is reative Forms module the ideal candidate for such usecase ?

* could ngModel help out?

 

what are your thoughts, let's talk  ... :-)

 

 

Code

By JanPaulDahlke

Code

  • 34