Skip to content
Advertisement

Generate appropriate Angular Element dynamically without bloating the build size?

Summary:

When createCustomElement() is called multiple times inside a switch case, all of the components will be included in the build, instead of just the one that will actually be used. This increases the build size with duplicate code.

Details

I have an Angular 11 app with a multi-site architecture. There is a project in the angular.json for each site, so they can be built independently and generate their own dist bundles based on the appropriate environment.ts file that contains a “siteCode”.

For one of the big components in my site — let’s call it myWidget — I also export it as a generic web component (a.k.a “Angular Element”, a.k.a. “custom element”) for other sites to consume. So I have myWidget in a sub-project of the main app, and it also has its own projects listed in angular.json. Meaning I can run a build that should contain just myWidget for a given site (along with the core Angular framework, obviously).

app.module.ts of myWidget sub-project (simplified):

import { MyWidgetSite1Component } from './my-widget/site-widgets/my-widget-site1.component';
import { MyWidgetSite2Component } from './my-widget/site-widgets/my-widget-site2.component';
import { MyWidgetSite3Component } from './my-widget/site-widgets/my-widget-site3.component';

@NgModule({
  declarations: [AppComponent],
  imports: [MyWidgetModule]
})
export class AppModule {

  constructor(private injector: Injector) {

    //Create generic web component version of myWidget.  Use correct child version per site.
    
    switch (environment.siteCode) {
      case "site1": {
        const myWidgetCustomElement = createCustomElement(MyWidgetSite1Component , { injector: this.injector });
        customElements.define('site1-myWidget', myWidgetCustomElement);
        break;
      }
      case "site2": {
        const myWidgetCustomElement = createCustomElement(MyWidgetSite2Component , { injector: this.injector });
        customElements.define('site2-myWidget', myWidgetCustomElement);
        break;
      }
      case "site3": {
        const myWidgetCustomElement = createCustomElement(MyWidgetSite3Component , { injector: this.injector });
        customElements.define('site3-myWidget', myWidgetCustomElement);
        break;
      }
    }
  }
}

Problem: it includes all three of those components in the build, instead of just the one that will be used for that site (verified with webpack bundle analyzer).

Futher background

The three site-specific myWidget components here all inherit from a common base component where all the real logic is, so they are nearly-identical. I’m doing this so I can load the appropriate CSS files for that site and bundle them inside the exported MyWidget web component as component-specific CSS. It uses shadowDom encapsulation and this way the web component is completely sealed off from the parent page that it’s inserted into. So the components in question look like this:

my-widget-site1.component.ts

@Component({
  selector: 'my-widget-site1',
  templateUrl: '../my-widget/my-widget.component.html', //Shares base myWidget template

  //First file does nothing but import appropriate site's stylesheets from main project.  It would
  //have been better to directly include them here, but that resulted in odd build errors.
  styleUrls: [
    './my-widget-site1.component.scss',
    '../my-widget/my-widget.component.scss'],

  encapsulation: ViewEncapsulation.ShadowDom
})
export class MyWidgetSite1Component extends MyWidgetComponent implements OnInit {
  //Do not add any code here
}

my-widget-site1.component.scss

@import '../../../../../../src/app/sites/site1/theme.scss';
@import '../../../../../../src/styles.scss';
@import '../../../../../../src/app/sites/site1/styles.scss';

Conclusion

I can think of a few general ideas to solve this:

1) Some trick to load the desired component dynamically instead of in a switch case?

I haven’t been able to find anything. It seems as long as I have to import the component, it will be included in the build.

2) Avoid having multiple versions of the component per site entirely?

I would love to do this, but I don’t know how to get around the CSS problem. The appropriate CSS files for a given site need to be bundled into this component at build time so they are encapsulated in the shadow-root of the web component and not built as a separate CSS file that gets imported into the global scope of the consuming page. That means I can’t just list them in the “styles” section of the project build in angular.json

I tried to do a dynamic @import statement on the scss but that doesn’t seem possible.

Can you script something into the build process somehow to choose the right scss files at build time? I would have no idea where to start with something like that.

Advertisement

Answer

I figured out an interesting solution to this.

I can get rid of the need for multiple component files and effectively pull off a dynamic @import by using a ‘shortcut’ to point at the necessary scss files instead of the full path:

@import "theme";
@import "styles";
@import "site-styles";

You can configure the folders it will find the specified files in via angular.json:

"stylePreprocessorOptions": {
  "includePaths": [
    "src/app/sites/site1/",  //theme.scss and site-styles.scss are here
    "src/"  //styles.scss is here
  ]
}

So now I can use one component that always has the same imports, but at build time it will actually use the right file for the site being built.

Info about the shortcut syntax: https://www.digitalocean.com/community/tutorials/angular-shortcut-to-importing-styles-files-in-components

2 People found this is helpful
Advertisement