11. April 2017
timer-icon 6 min

Angular 2 in a multi-page application

A few weeks ago we dealt with the question if it's possible to mix client-side Angular 2 components with static content rendered on the server-side.

UPDATE (2019): The content of this blog post is unfortunately outdated. The internal structure of Angular changed a lot since version 2. Therefore it is no longer possible to follow the approach described in this post when working with newer versions.

The idea was to have different interactive Angular components like for example a chat or a search widget mixed together with static content rendered by the server. Therefore we came up with two different approaches. In this blog post we want to summarize our insights which we got from our rough proof-of-concept, so that people that are already a bit familiar with Angular can benefit from it. The problem is that Angular is template-based and you cannot mix it with server-side rendered html for example coming from a CMS like Wordpress. So we needed to find a solution to break out of the restrictions of the template to mix it with the static html content.

widgets

Different approaches

So our requirements for the proof-of-concept are:

  1. We have static html content which is delivered by the server.
  2. We have Angular components that should be able to communicate with each other, but be placed in different locations floating around the static content on the page.

First approach: Multiple Angular apps

Our first idea was to use multiple Angular apps on one page, which of course leads to the question, if it’s even possible to bootstrap them. Therefore we built a prototype based on the Angular Quickstart setup with SystemJS, a module loader tool. The code and a detailed guide (step-by-step) can be found on github. For the communication between those apps we built a shared service that can be injected in both apps. Theoretically this sounds like a good approach: With multiple apps you could nicely separate their development. In this case even different teams can provide their own apps. But in practice there are huge pitfalls – and that is the reason why we dismissed the idea and came up with a second different approach. The downsides were:

  • The two apps don’t share the same Angular context, this means for example you cannot use (inject) services/pipes from each other. So to establish a communication you have to create a shared service in the global namespace between these two apps.
  • Another downside is that Angular apps use many unique resources in the browser like cookies, title and location. This is also stated in the Angular code. Consequently this possibly results in conflicts if both apps manipulate the same resource.
  • The Angular version and versions of the dependencies used by the apps have to be identical. Because the dependencies are loaded in the same browser window context, different versions of the same dependency could cause conflicts. This limits an independent development.

Second approach: One Angular app

Because we wanted to address these major downsides, we moved over to just bootstrapping multiple components managed by one Angular app. Because these components stay together in the same app context it’s possible to use the standard capabilities of Angular like dependency injection or modularization. So the components neither require a shared communication service, nor do they have to share the unique resources of the Angular framework with other apps. Moreover the modules of this one app could be developed almost independently.

The html delivered by a server then only needs to contain specific tags that should be replaced by Angular components, as in the standard bootstrapping process of Angular – just with multiple components.

In order to bootstrap the Angular components we can use the regular NgModule bootstrap property, but we want to bootstrap only the components that really exist on the page. Considering that, we use the lifecycle-hook ngDoBootstrap to overwrite the default bootstrap behavior. With the selector of the component we query the DOM to check if the component should be displayed on the page (see line 4). If the selector matches we bootstrap the corresponding component (see line 5).


A drawback is that it’s currently not possible to pass input values to the top-level components that should be boostraped, so external data needs either be provided by the window object or by using ElementRef. Like shown in the following snippet:


This approach should be sufficient for the most use cases, but we wanted to try if it’s possible to lazy load some of the components to decrease the bundle size.

Lazy loading

With the code above we deliver all components in one bundle. For smaller or fewer components the load times should be fine. But in case we want to deliver complex nested components, lazy loading of these parts becomes more and more relevant. Therefore we can improve this by delivering just the components that are really used on the page, so the initial load times are shorter.

We are using Angular CLI which uses Webpack under the hood. Webpack supports lazy loading of different modules. But for now Angular CLI does not provide an add-on functionality to customize the Webpack configuration. Therefore we use the Routes definition as a workaround to let Webpack generate the lazily loaded chunks (see line 1). This is just a pragmatic way for our proof-of-concept and could be easily done in an adequately configured Webpack build. As the bundling shifts common modules to a separate file (vendor.ts) which can be cached by the browser, only the first page load uses a little more bandwidth. The lazy components can then be fetched with a relatively small overhead.


In Angular the Router allows us to implement a lazy loading mechanism. But instead of only loading one module, we want to be able to load multiple modules at a time. Therefore we utilized the SystemJsNgModuleLoader which is used by the Router under the hood. We use the load method of this module in the ngDoBootstrap function of AppModule to load the modules on demand (see line 12). The SystemJsNgModuleLoader uses System.import (which can be handled by Webpack) to load and compile modules.


The ngDoBootstrap method in the code above first queries tags on the page (see lines 5-9) which hold a data-module-path attribute (like: <app-lazy-widget data-module-path="./lazy/lazy.module#LazyModule"></app-lazy-widget>). Each specified module will then be lazily loaded and bootstrapped in parallel (see line 12). Important is that all components that should be bootstrapped are listed in the entryComponents of each lazy loaded module. Only then will Angular create a ComponentFactory and store it in the ComponentFactoryResolver, as shown in the code above (see lines 16-19). Finally the selector of the component is used to bootstrap it in the right place on the page (see lines 21-22).

One drawback of using lazy modules is that the providers of a lazy module are module-scoped and so are only visible in that module. That means for communication between lazy loaded modules we need services defined in the root module. This behaviour is also described in more detail in the FAQ: Lazy loaded module provider visibility

Conclusion

This proof-of-concept shows that it is possible to enrich server-side content with Angular components and if necessary even with lazy loaded modules. In this example we decided to focus on an Angular based way to achieve a solution to break out of the restrictions of its template to mix it with the static html content rendered on the server. It should be noted, however, that there might be other frameworks which can also do this.

Github repository

https://github.com/frontend-development-at-novatec/ng2-in-multi-page-apps

Comment article

Comments

  1. Pranesh

    Hi, I went through the POC, and when I execute it, its generating two files, 0.chunk.js and 1.chunk.js. Can you please help me in understanding how these files are generated?

  2. Johannes Schlaudraff

    Hi Robin,
    I’m not sure if I understand the problem correctly. Could you not simply inject the router service to read the url and then do your initialization based on that?
    Alternatively you could use the window.location property to get the url…

  3. Robin

    Hi guys, first of all, thanks a lot for this POC. In the project i am currently working in, we have the exact same use case and we implemented the solution you found one-to-one.

    Now i have a question: we are struggling to implement a routing for inside of the loaded components. For example, one module we load is called FaqComponent. Now i want to have a routing for this component if the URL is /faq/category/technical that the FaqComponent is internally fetching the parameter “technical” and uses this parameter to initialize the view.

    I would be really glad if you can help me with this.

    Thanks a lot!

    Best regards. Robin

  4. Shinya

    The poc on the github work nicely but how come I don’t see the webpack.config.js but still works?

  5. Esther

    I don’t have a plunkr but I did create a repo: https://github.com/techmind-git/angular-compilation. In the issue.md file I added a description between starting the forked situation and my version. In case you see something that’s not set correctly I’d love the hear from you. Thanks.

  6. Janis Köhr

    Hi Esther,
    thanks for your comment. We got our AOT setup working. Can you provide a plunkr (https://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5) that reproduces this problem?
    If you have further questions please don’t hesitate to contact us.

  7. Esther

    Hi, I decided on angular and trying to get your example working with aot compile. I’ve got the aot working but the combination with de module.load keeps giving me errors (it can’t find the path to module.ngfactory). Probably something wrong in my setup so I was wondering if you got your lazy laoding setup working with aot?

  8. Cosette M.

    It’s great to see more people writing about Angular 2.
    Since there aren’t many tools to manage the localization of Angular 2 apps yet, if you’re interested in this, I recommend the localization management platform https://poeditor.com supports which supports xmb and xtb files.

  9. Johannes Schlaudraff

    Hi Esther,
    thanks for your comment, we can’t say if angular is a good framework for your requirements but at least it should be possible to do it with angular.
    First alternative would be to change the selector in the component and also in the DOM during runtime to make it unique.
    E.g.

    <mycomponent-1></mycomponent-1><mycomponent-2></mycomponent-2><mycomponent-n>...
    compFactory.selector = "mycomponent-n";

    But we don’t like this approach.
    Perhaps a better approach would be to dynamically create the components and attach it directly to the dom.

    zone.run(() => {
    let componentFactory = componentFactoryResolver.resolveComponentFactory(myComponent);
    componentRef = componentFactory.create(this.injector, [], document.querySelector(componentFactory.selector));
    appRef.attachView(componentRef.hostView);
    });

    This approach is described here: https://www.lucidchart.com/techblog/2017/04/10/using-angular-2-components-in-a-non-angular-app/

    If you have further questions please don’t hesitate to contact us.

  10. Esther

    Hello Janis/Johannes, this is a very interesting approach. A good starting point to expand upon when dealing with a CMS. I was wondering what your thoughts are on the following: we’re using a CMS where the contenteditors decide which components are on the page. A component could be a form, but also as basic as some text with a backend call to get/send some data (ie clientside httprequest). It could also mean they have multiple of the same component on the page (only variations in content). The components will be angular components. As far as I can tell angular needs unique selectors so this could be a problem. Did you encounter this situation during your proof of concept? If not, do you have any ideas on how to deal with this dynamic situation? I’m considering not chosing angular due to this limitation and going for more webcomponent compatible framework.