angular

angular - Javascript

Have you ever wonder how to make an Angular Application that you could lazy load the hell out of, but that also allows you to use the lazy loaded modules right after loading for, let’s say… searching?

We at CODDDE had this problem and I was tasked with solving it. As I was doing my research, I found that there weren’t many articles or examples of this, so I will share my experience, and the code behind it with a simple example.

The first thing we need to do is create the 3 modules: MainApplication, App1 and App2. The latter 2 will be lazy loaded.

Step 0

First we need to generate a main application module that will load the other modules. We can do this by creating an application with ng new my-app.

After that, navigate to /src/app and create a layout with ng generate component outer-layout that will hold our router-outlet, then open outer-layout.component.html and change the content to this:

https://gist.githubusercontent.com/sikolio/9611dfd74b679c3825dc8e77da1c2727/raw/036c4a7ecc3fa335d891daf120ffaac8acf6dfe8/outer-layout.component.html

As you see in that snippet, I will be using observables to hold our interactive data. So, let’s create the component logic in outer-layout.component.ts:

searchResults: Observable<any>;
queryString: BehaviorSubject<string>;

constructor(private service: MyService) { }

ngOnInit() {
this.searchResults = this.service.searchResults;
this.queryString = new BehaviorSubject<string>(”);
this.queryString
.debounceTime(400)
.subscribe((query) => {
this.service.search(query);
});
}

changeQuery(query) {
this.queryString.next(query);
}

Here I am injecting MyService to the component. This service will be responsible for the communication between all the modules, so we need to create it, but we want it in a shared module, since we want to be able to use it everywhere. So, let’s create our shared module with ng g m shared and then create the service inside it ng g s shared/service/myService (this is because we want the service to be inside the shared folder and inside its own folder, and the angular cli generates services on the current folder).

Open /shared/service/myService and add the following code:

searchQuery: BehaviorSubject<string>;
searchResults: BehaviorSubject<any>;

constructor() {
console.log(‘Shared Service created’);
this.searchResults = new BehaviorSubject<any>({});
this.searchQuery = new BehaviorSubject<string>(”);
}

updateSearch(from, data) {
const newData = this.searchResults.getValue();
newData[from] = data;
this.searchResults.next(newData);
}

search(query) {
console.log(‘Searching for ‘, query);
this.searchQuery.next(query);
}

Here we create two BehaviorSubject that are both an observer and an observable. Also when you subscribe to them, they return the last value emitted by them. This is perfect for our use case, as we want anyone who subscribe to both of them to immediately know the current search query.

Now it is time to provide our service, we will do this in the SharedModule as I explained early. To do this open shared/shared.module.ts and add the following code:

@NgModule({
imports: [
CommonModule
],
declarations: []
})
export class SharedModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: SharedModule,
providers: [
MyService
]
};
}
}

We need the forRoot() method here to prevent the creation of multiple instances of our service that Angular defaults to.

Lastly, we need to import this module in app.module.ts of the main application, so open it and add the SharedModule.forRoot() to the imports array. ## Step 1

Next we need to create the lazy loaded modules, ng g m app-one ng g m app-two (I created 2 for this article, you can have as many as you want). Create an app.routes.ts inside each of those and add the following:

const routes: Routes = [
{ path: ”, redirectTo: ‘home’, pathMatch: ‘full’},
{ path: ‘home’, component: HomeComponent },
{ path: ‘view1’, component: ViewOneComponent },
{ path: ‘view2’, component: ViewTwoComponent },
];

export const routingModule: ModuleWithProviders = RouterModule.forChild(routes);

I won’t go over the creation of those Presentational Components, as they can be whatever you want (you can copy them from the repo if you want).

Then we need to add the SharedModule to the imports of each of these modules. To do this, add SharedModule to the imports array (notice that we aren’t adding the .forRoot())

We alse need to create a service in these apps that will be in charge of updating the shared service. To create them do ng g s app-one/services/AppOne and insert the following code in them:

subscription: Subscription;
searchableItems: BehaviorSubject<any>;

constructor(private shared: MyService, private http: Http) {
console.log(‘App One Service created’);
this.subscription = Observable.combineLatest(
this.getItems(),
this.shared.searchQuery$
)
.subscribe((data) => {
this.search(data[0].filter((item) => {
return item.toLowerCase().includes(data[1]);
}));
});
}

getItems() {
return this.http.get(‘/assets/items.json’)
.map(res => res.json());
}

search(data) {
console.log(‘appone search’, data);
this.shared.updateSearch(‘appone’, data);
}

Here I am logging to the console when the instance is created and when we search.

Finally, we need to add the AppOneService to the Providers array of each of the AppModules.

Step 2 (or “where the magic happens”)

Let’s create a file called app.routes.ts where you will define the routes of the main application (it will also determine which modules will be lazy loaded). There you should add the following routes:

const routes: Routes = [
{ path: ”, component: OuterLayoutComponent,
children: [
{ path: ”, redirectTo: ‘home’, pathMatch: ‘full’},
{ path: ‘home’, component: HomeComponent},
{ path: ‘appone’, loadChildren: ‘app/app-one/app-one.module#AppOneModule’ },
{ path: ‘apptwo’, loadChildren: ‘app/app-two/app-two.module#AppTwoModule’ },
]
},
];

Here we are redirecting the empty route to the home route, and we are adding a route for each of the applications using the loadChildren parameter of the routes. This lets us lazy load the application and also tells angular how to bundle our app.

After this you should import the RouterModule.forRoot(routes) and export it in a new Module AppRoutingModule.

Ok, so let’s see what happens if we try to run this. Try running ng serve. You should see something like the following

images before adding the lazy loader service

images before adding the lazy loader service

As you can see there, we are still missing the functionality we were seeking. In order to make this work, we have to do two things.

  1. Add the PreloadAllModules strategy to the main router module.
  2. Eager load the services of the lazy loaded modules. This is to avoid needing navigation to a page from that modules for the service to be created.

So, the first thing we need to do is add the preloadingStrategy to the options of the RouterModule.forRoot() method:

@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
exports: [RouterModule],
providers: []
})
export class AppRoutingModule {}

The second thing you need to do is npm install angular-eager-provider-loader. This will let you load the services without the need to have the user navigate to any route. In order for this to work you need to add the following to each one of the lazy loaded modules:

import { EAGER_PROVIDER, EagerProviderLoaderModule } from ‘angular-eager-provider-loader’;

imports: [
…,
EagerProviderLoaderModule
],

providers: [
{ provide: EAGER_PROVIDER, useValue: AppTwoService, multi: true }
]

And there you have it, the application with lazy loaded modules that updates the searchable upon upload.

Read More