{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