How we deliver more meaningful content to visitors using Angular Elements and WordPress

Empowering our Marketing Team to deliver more meaningful content by the means of Angular Elements and WordPress

Dusan Skiljevic
limehome-engineering
8 min readJun 8, 2022

--

As a part of our venture into Domain Driven Design, we’ve identified one of the improvements that could be made across different two domains that usually work closely together — Marketing and Customer Journey. The improvement was somewhat a major change but sought to empower the Marketing Team more and reduce its reliance on the Customer Journey Team for updates related to the homepage.

Although several options were considered as possible solutions, the final decision was made to move the homepage (which was at that point an Angular page) to WordPress. WordPress is a great solution for updating the content on the homepage as often as it’s needed and its overall interface is straightforward and easy to use.

The process of utilizing Angular Elements for extracting the desired behavior/widgets, as well as their utilization in WordPress is described below. Additionally, we’ve identified several optimizations regarding Angular Elements that we thing would benefit all Angular developers.

Changes in Angular

Identifying the desired code for Angular Element

The most important part of the homepage was actually the Booking Widget (floating widget with city, date and guest pickers) and it was automatically eligible to be turned into an Angular Element. With the goal of preserving the exact look and feel, we set of on the journey to achieve just that.

Image 1: Booking widget on the homepage

Defining the custom component

The first step, implementing the code that was required to make it a custom component. This process is described in the Angular Documentation (additional resource links mentioned below) but here are the steps that we took to make an Angular element from the Booking Widget Component.

The process of turning an Angular component into a custom component is pretty straightforward.

Firstly, install the elements package dependency from Angular:
npm install @angular/elements --save

After that, we need to setup the Booking Widget Component so it’s registered as a custom element. This is done in the components parent module (the component can be a part of any module) in the following way:

@NgModule(...)
export class AppModule {
constructor(private _injector: Injector) {}

ngDoBootstrap() {
const searchWidgetElement = createCustomElement(
BookingWidgetComponent,
{ injector: this._injector }
);
customElements.define(
'search-booking-element',
searchWidgetElement
);
}
}

In this way, we have defined an element called <search-booking-element> which we can use on any web page. How? We will see this later.

Separating the component into its own module

After further testing and verification that everything works as expected and that this is the way to go, we proceeded with the separation of the element code and the improvement in the build process. This step was necessary because we didn’t want to ship unnecessary code that wasn’t going to be used and would just “bloat” the final build files.

We introduced separate component and module files from the main application (search-widget.element.component.ts,app.elements.module.ts) that contain only the code related to the elements implementation and cleaned up the previous code from the other components/modules. This change also enabled separating the build process entirely from the main build for the whole application.

Search Widget Element Component

This component extends the original Booking Widget Component. There are 2 main reasons for the separation:

  • the element uses ShadowDom view encapsulation — this way there is no conflicts between the styles of the page and the styles of the component
  • the element specific code — this might include vanilla code that’s checking or setting conditions on the parent page (for example, setting overflow: hidden on the body of the main page when a dropdown is open); this approach was taken so the code doesn’t bloat the original Booking Widget Component

App Elements Module

The need for a separate module arises because the build process for Angular Elements differs from the usual build configuration for the main application. The module file contains the following:

  • separate dependencies (services, modules, other components…) — the element build isn’t using all of the components/modules from the whole application so there is no need to include all of them
  • translations — if you’re using @ngx-translate, you need to register your translations here (even if you’re already doing this in the main application) in order for them to work for the custom element; additionally, there is no need to include all of the translations, so the translations were reduced to only what the custom element required
  • custom element registration

One of the major differences from the usual build process is that the elements build should not produce hashed output. This was stated by several sources when we were looking into how to setup the build configuration.

Here is the main gist of the module file:

@NgModule({
declarations: [SearchWidgetElementsComponent],
imports: [...],
providers: [...]
})
export class AppElementsModule {
constructor(
private _injector: Injector,
private _translateService: TranslateService
) {
const enTranslation = require('./../assets/elements/en.json');
this._translateService.setTranslation(
languages.en,
enTranslation
);
this._translateService.setTranslation(
languages.de,
require('./../assets/elements/de.json'),
enTranslation
);
this._translateService.setTranslation(
languages.es,
require('./../assets/elements/es.json'),
enTranslation
);
this._translateService.setDefaultLang('en');
}
public ngDoBootstrap() {
const ngOptions = { injector: this._injector };
if (customElements.get('search-booking-element')) {
return;
}
const searchWidgetElement = createCustomElement<SearchWidgetElementsComponent>(
SearchWidgetElementsComponent,
ngOptions
);
customElements.define(
'search-booking-element',
searchWidgetElement
);
}
}

Optimizing the module build

Over the course of the development and the implementation of this feature, there were some optimizations that have been made to facilitate a reduction of 70% for the build size from the original initial element build to the final size of around 1,1MB (uncompressed). These size optimizations were necessary because the custom element needed to be loaded, displayed and become interactive for the user as quickly as possible.

Some or all of these have been mentioned above, but we feel the need to put them all together in one place.

  • Only the necessary dependencies were included in the element bundle — the element component doesn’t need all of the components/modules defined in the rest of the application, so these were excluded from the build.
  • Splitting the build files into multiple smaller files — because the goal was to load the component files as quickly as possible, splitting the final build into several files was a good option (main, runtime, polyfills and vendor); this way some of them can be cached for longer and the rest are going to be smaller to fetch.
  • Splitting the translations and only including the ones used in the custom element — there are a lot of translations for the overall application so this was a necessary step to reduce the size of the build because the translation are bundled in the final build
  • Removing polyfills —this was a strategic move because this feature targets modern browsers which use ES6 and later and, therefore, would support custom elements out of the box; removing them caused a big reduction in size, up to 50%

Bundling the elements build code

In order for the Angular Elements approach to work, we need to provide all of the dependent files for Angular itself. After all, that’s exactly what we are getting, the full power of Angular packaged inside one script file. The most important file is the main.js file. Why? All of the code for the custom element(s) is bundled in this file and there would be no custom element without it.

There are two approaches here as well: single and multiple script import.

If we’re choosing a single script import approach, then we need to merge together the following files into one: main.*.js, runtime.*.js, polyfills.*.js and vendor.*.js (if present depending on the build configuration).

In our case, we decided to go for multiple script import to gain some benefits come with it, main benefits being the speed and caching. In this case, it was not necessary to merge the files.

Changes in WordPress

On the WordPress side there were a couple of requirements that needed to be fulfilled in order to get the elements working. Once they were included/done, there were no issues. The requirements were:

  • adding a <base> tag
  • importing the files in a specific order

The only major requirement was that the Angular Element required a <base> tag in order to work. The way this was done was to include it on the page level because it needed to match the URL of the page (for different languages as well). The decision to do it in this way was related to the way WordPress handles multiple languages (a language code is included in the URL). Here is what the base tag looks like for the English homepage:

<base href="/en/">

Other than this, the script files needed to be added in the specific order. Incorrect order would cause loading issue and the custom element would not be displayed at all. From testing, we found that the best order in which to load the scripts is the following: main, polyfills, runtime, vendor.

This is how the scripts are provided in the page:

<script defer src="https://www.limehome.com/elements/main.js"></script>
<script defer src="https://www.limehome.com/elements/polyfills.js"></script>
<script defer src="https://www.limehome.com/elements/runtimejs"></script>
<script defer src="https://www.limehome.com/elements/vendor.js"></script>

Adding the Search Widget to the page

Now, the magic of the Angular Elements. Once the script files are included, all that is left is to add the custom element itself to the page:

<search-booking-element></search-booking-element>

With this, the element was displayed on the page and it was working exactly the same way as the component in the main Angular application.

Optimizing the loading of the widget

In order to improve the loading speed, we decided to put the script tags in the body of the page after the content. The reason behind this was to only load the element on the page it is being used on.

Here is what the script tags contain once again:

<script defer src="https://www.limehome.com/elements/main.js"></script>
<script defer src="https://www.limehome.com/elements/polyfills.js"></script>
<script defer src="https://www.limehome.com/elements/runtimejs"></script>
<script defer src="https://www.limehome.com/elements/vendor.js"></script>

The imports are deferred, optimized and minified in the build process and served compressed. This way they are loaded as fast as possible and take minimal time, if any, blocking the rest of the page.

Image 2: WordPress Homepage with the Search Widget — Desktop
Image 3: WordPress Homepage with the Search Widget — Mobile

Conclusion

We wanted a CMS that would preserve the speed of the site and reduce the need for engineering time, but still be able to control some crucial elements of the page, and this hybrid approach for the page using #angular #elements was the right solution.

By the way, check out the open roles in our Tech and Engineering team.

--

--