@softeq/angular-route-data
@softeq/angular-route-data
allows to keep URL up to date with the page content.
Motivation
Let's consider the following example to understand what problem this library tries to solve.
Imagine we have a page under the route /heroes
displaying list of heroes and toggle that allows to show only active heroes.
When user turns switch on we need to show only active heroes and update URL to /heroes?onlyActive=true
. In addition user can open this URL by putting it into the address bar of the browser and go to the same page having switch show only active heroes on.
In the real world, page can have more criteria, like name of hero, set of checkboxes, etc.
Such behavior allows user to keep bookmarkable URLs or send URLs via messenger. For developer it can simplify implementation of back behavior (when user press on browser back button).
Example
The following code demonstrates behavior described in Motivation section.
interface Hero {
name: string;
active: boolean;
}
@Component({
selector: 'hero-list',
template: `
<mat-slide-toggle [formControl]="onlyActiveControl">Show only active heroes</mat-slide-toggle>
<mat-list>
<mat-list-item *ngFor="let hero of filteredHeroes">{{ hero.name }}</mat-list-item>
</mat-list>
`,
})
export class HeroListComponent implements OnInit {
onlyActiveControl = new FormControl(false);
heroes: Hero[] = [
{ name: 'Black Widow', active: false },
{ name: 'Groot', active: true },
{ name: 'Hulk', active: true },
{ name: 'Iron Man', active: false },
{ name: 'Rocket Raccoon', active: true },
{ name: 'Thor', active: true },
];
filteredHeroes: Hero[];
constructor(private route: ActivatedRoute,
private routeData: RouteDataService) {
}
ngOnInit(): void {
const tracker = this.routeData.tracker(controlTracker('onlyActive', this.onlyActiveControl, booleanMapper()));
tracker.listen(this.route);
tracker.filterChanges.subscribe(([onlyActive]) => {
this.filteredHeroes = onlyActive ? this.heroes.filter((hero) => hero.active) : this.heroes;
});
}
}
Here we track value of onlyActiveControl
and URL of the current page.
If user switches value of control, URL and set of filteredHeroes
are updated correspondingly.
From the other side when user opens page by /heroes?onlyActive=true
URL, onlyActiveControl
is already set to true
and list of heroes shows only active heroes.
Let's consider this code in more details.
-
All tracking logic is provided by
RouteDataService
. Also we needActivatedRoute
from@angular/router
package to track route parametersconstructor(private route: ActivatedRoute, private routeData: RouteDataService)
-
Create instance of
RouteTracker
.RouteTracker
is a key entity for tracking behavior. It can be created usingRouteDataService.tracker
method.const tracker = this.routeData.tracker(controlTracker('onlyActive', this.onlyActiveControl, booleanMapper()));
tracker
method accepts set ofRouteParamTracker
.
RouteParamTracker
tracks subset of route query parameters. It is responsible for- tracking of route query parameters (merge query parameters into the underlying page object)
- tracking of filter changes (notify when underlying page object has changed, like toggling of switch or changing of text value);
- notifying about new query parameters (generate set of query parameters to be merged into the current URL)
In our example the following code create
RouteParamTracker
controlTracker('onlyActive', this.onlyActiveControl, booleanMapper())
which tracks value of
onlyActive
query parameter, mirrors its value in the instance ofFormControl
(onlyActiveControl
) and treats this value asboolean
(usingbooleanMapper()
. It is important to map query parameter value, because query parameter is always astring
).This way
RouteParamTracker
created bycontrolTracker
- merges value of
onlyActive
query parameter into theonlyActiveControl
- notifies about new value coming from
onlyActiveControl
when user toggles switch - and emits new value of
onlyActive
query parameter when user toggles switch
-
Bind route to the instance of
RouteTracker
tracker.listen(this.route);
This way
RouteTracker
can listen for changes in route query parameters. -
Update displayed data when filter is changed
tracker.filterChanges.subscribe(([onlyActive]) => { this.filteredHeroes = onlyActive ? this.heroes.filter((hero) => hero.active) : this.heroes; });
filterChanges
emits new value as soon as one of assignedRouteParamTracker
s notifies about changes.
Default value
What if value of onlyActiveControl
from the Example section should be set to true
by default.
This way when user opens the page by URL /heroes
, he should see switch Show only active heroes in toggled state. When user clicks on switch, URL become /heroes?onlyActive=false
. One more click makes URL /heroes?onlyActive=true
and shows only active heroes again.
There is no big sense to set onlyActive=true
when true
is a default value. From the perspective of page content URLs /heroes
and /heroes?onlyActive=true
are the same.
In order to hide query parameter when it accepts default value, you can tell RouteParamTracker
what value is a default one.
const tracker = this.routeData
.tracker(controlTracker('onlyActive', this.onlyActiveControl, booleanMapper()).withDefault(true));
Filters and effects
RouteParamTracker
can be registered as filter or effect. Example of filter RouteParamTracker
was shown in the Example section. Effect RouteParamTracker
is used when you need to avoid dramatic changes in UI.
Look at the following snippet.
Say, you need to track scroll position and save it in the URL like /heroes?onlyActive=true&scroll=150
. This allows to open page on the same scroll position it had before.
const tracker = this.routeData.tracker(
controlTracker('onlyActive', this.onlyActiveControl, booleanMapper()),
scrollTracker(...)); // this is just an example, library does not provide such tracker
tracker.listen(this.route);
tracker.filterChanges.subscribe(([onlyActive]) => {
// filter set of heroes
});
Here, scrollTracker
is registered as filter. It means each time scroll position is changed filterChanges
will trigger new filter and set of filteredHeroes
will be recalculated. This is barely expected behavior. We need the way to track scroll position, but should not emit new filter. To implement such behavior we can register scrollTracker
as effect.
const tracker = this.routeData
.tracker(controlTracker('onlyActive', this.onlyActiveControl, booleanMapper()))
.withEffects(scrollTracker(...));
tracker.listen(this.route);
tracker.filterChanges.subscribe(([onlyActive]) => {
// filter set of heroes
});
Now scroll position will be tracked, but this will not lead to emitting of new filter.
RouteParamTracker
s
Existing
RouteParamTracker factory |
Note |
---|---|
controlTracker |
Creates tracker that mirrors value of single query parameter in the provided AbstractControl
|
filterTracker |
Creates tracker that mirrors value of single query parameter in the instance of Filterable object |
manualTracker |
Creates the simplest tracker that allows to track value manually and mirror it in the single query parameter |
observableFilterTracker |
Creates tracker that mirrors values of provided stream in the single query parameter |
observablePlainFilterTracker |
Creates tracker that mirrors values of provided stream in the set of query parameters |