Router’s data with the decorator in Angular
Insert resolved data inside components right now!
As an Angular developer, I often want to resolve data before a component appears on the route. It makes components more stable, without blinks or skeleton loading. Users want to see a page with data at the same time when that page appears. That's why the Angular team invented router resolvers.
☹️ Problem.
Currently, I have to inject ActivatedRoute or ActivatedRouteSnapshot and get my data from them.
Moreover, Angular doesn’t rebuild the route’s component when you have dynamic IDs in the URL, Angular thinks that this is the same route that you had before (library/1 = library/2). Because under the hood Angular compares one route with another by simple object’s reference comparing. You can tell Angular not to do this by using a flag or rewriting RouteReuseStrategy class with a compilation approach.
So if you have a dynamic route and want to get new resolved data every time the dynamic part has changed you have to use ActivatedRoute and subscribe to its data field.
I often ask speakers on frontend meetups whether they found a solution to the problem of getting data inside a component without weird injections or subscriptions. Seems like no one even thought about it. The Angular team doesn’t provide us with some decorator like Input for resolved data.
I don’t want to inject ActivatedRoute, subscribe to it, and pipe observables whenever I need resolved data.
I want something like this instead:
😎 Solution.
The idea is to find the current route from the router context, get its router outlet, and subscribe it in the component, with the help of the getter, we will return the last data from the subscription. However, the decorated component’s class won’t be destroyed so we can’t keep the subscription on the router outlet. That is because every time Angular compiles that component, our decorator will return data from the first router outlet instead of the proper one.
Let’s make some functions!
First of all, we need an Injectable with a static field (as I described in my previous article) to give access to the Angular DI container since decorators don’t exist in the Angular environment as is. However, we have to use its services and injectables. We need at least a Router to distinguish which route is currently opened and ChangeDetectorRef to run change detection on the component when the data is being resolved by the router.
As you can see below, we provide our StaticInjectorService inside the module (8th line). It has been done only to make an instance of the service and execute its constructor.
Now we have everything to implement our decorator!
Let's declare several variables and figure out why we need them line by line.
I described how typescript decorators work in my previous article. So let's skip it and imagine that everyone knows that.
So we need:
— A router to find our route (7th line).
— Trigger subject (8th line) will help us to rerun the subscription on the router’s data, we’ll see it in action later.
— Destroy subject (9th line) will do the opposite thing — unsubscribe us when the component destroys.
— The router keeps every route context in a private field so we have to cast ‘any’ and do that dirty trick to get context for our needs (10th line).
— Keep the router’s data in a closure for reusing (15th line).
— Prepare variable for ChangeDetectorRef that will be built later (16th line).
— Boolean flag to indicate the component’s initialization (17th line). We will see the usage later in this file.
— Component’s reference to run detect changes and bind function’s context.
As you remember we have created a trigger subject earlier. Its purpose is to notify us about the component’s creation through the emission of an EMPTY whenever we get a value from the field that was decorated.
Now we understand when a component has been created and we can get the router instance to do our magic ✨and get a value.
We look for the currently activated outlet from contextMap (4th line) with a simple function.
Then, we take a component reference from the outlet (13th line) and sequentially search for a router with the data we need.
We should get a change detector reference depended on our component (2nd line). Then, we register onDestroy callback (4th line) where we set flag inited to false and emit empty value from destroyer subject to unsubscribe from router’s data observable (11th line). In the end, just return name || key from data.
In subscribing callback, we receive resolved data that we were looking for and assign it to the target class to the proper key. Afterward, we should mark the component as dirty with the markForCheck method.
The decorator returns a descriptor that contains getter and setter for the decorated field.
Inside the getter, we set inited to true if it was false and emit an empty value to notify our trigger subject. Afterward, we run the original getter or return the router’s data.
Setter assigns data to a variable in a closure to keep it for getter and run original setter with the latest router’s or user’s data (if a user decides to set the value to the decorated field it will work too).
And that's it. The whole decorator is here:
Simply use it the following way,
where ‘data’ is a field from the router’s configuration.
I don’t know why the Angular team does not provide a similar solution. It is more declarative and easier to use a single decorator rather than repeatedly manage subscriptions, at least from my perspective. Although I believe that there are cases where this decorator approach might cause issues.
The whole code you can find in my repository.
You can have a look at usage here.