9

Is it possible to import a module based on condition? Specificly import external module only if angular 2 universal app being rendered in browser but not in server.

This question is relevant to some PrimeNG modules that depend on browser features and can be rendered only in browser. It would be great to omit them at server rendering cause calendars and other components are not really important for SEO.

Currently I can render Calendar component if turn off server rendering. But server produces an error 'ReferenceError: Event is not defined' in button.js when I include this code below in my app.module.ts and turn on server rendering.

import { CalendarModule } from 'primeng/components/calendar/calendar';
@NgModule({
    ...
    imports: [
        ...,
        CalendarModule
    ]
})

There is a isBrowser condition provided by angular.

import { isBrowser } from 'angular2-universal';

But I don't know how to use it for conditional imports. Is there really a way to do it for modules?

Rodion
  • 181
  • 2
  • 7

3 Answers3

9

So there is a way to render PrimeNG components in browser and omit them while server rendering. Those questions helped me start digging the right direction:

angular-cli: Conditional Imports using an environment variable

How can I conditionally import an ES6 module?

While server rendering I used mock component that renders a simple input field and uses the same selector 'p-calendar'. The final code I ended up with in my app.module.

...//other imports
import { isBrowser } from 'angular2-universal';

let imports = [
    ... //your modules here
];
let declarations = [
    ... //your declarations here
];

if (isBrowser) {
    let CalendarModule = require('primeng/components/calendar/calendar').CalendarModule;
    imports.push(CalendarModule);
}
else {
    let CalendarMockComponent = require('./components/primeng/calendarmock.component').CalendarMockComponent;
    declarations.push(CalendarMockComponent);
}

@NgModule({
    bootstrap: [AppComponent],
    declarations: declarations,
    providers: [
        ... //your providers here
    ],
    imports: imports
})

To make your mock component support [(ngModel)] binding use this tutorial. http://almerosteyn.com/2016/04/linkup-custom-control-to-ngcontrol-ngmodel

import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CalendarMockComponent),
    multi: true
};

@Component({
    selector: 'p-calendar',
    template: '<input type="text" class="form-control"/>',
    providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class CalendarMockComponent implements ControlValueAccessor {

    private innerValue: any = '';

    private onTouchedCallback: () => void = () => {};
    private onChangeCallback: (_: any) => void = () => {};

    //From ControlValueAccessor interface
    writeValue(value: any) {
        if (value !== this.innerValue) {
            this.innerValue = value;
        }
    }

    registerOnChange(fn: any) {
        this.onChangeCallback = fn;
    }

    registerOnTouched(fn: any) {
        this.onTouchedCallback = fn;
    }
}
Community
  • 1
  • 1
Rodion
  • 181
  • 2
  • 7
  • 1
    Do you know how to do this with isPlatformBrowser instead of isBrowser? – Raymond the Developer Jun 02 '17 at 13:21
  • @RaymondtheDeveloper I think isPlatformBrowser should work as described [here](https://www.npmjs.com/package/angular4-aspnetcore-universal) at Universal "Gotchas" section. Everything else should be same as isBrowser. Give it a try. – Rodion Jun 02 '17 at 15:34
  • 7
    The problem is that I want to use it inside app.module.ts. I have no idea how to inject PLATFORM_ID there. And the isPlatformBrowser needs the PLATFORM_ID to work. – Raymond the Developer Jun 02 '17 at 15:48
  • @RaymondtheDeveloper I did not get to try that yet. As I remember from 2 Angular version you could even pick AppModule depending from boot type. boot-client for browser and boot-server for server and in each you could specify different AppModule. Not super great solution but should work. Another possible way is lazy loading. Can try to dig into that dirrection also https://angular.io/docs/ts/latest/guide/ngmodule.html#!#lazy-load – Rodion Jun 02 '17 at 16:13
  • I'll try and look into lazy loading then. Thanks Rodion! – Raymond the Developer Jun 02 '17 at 16:28
  • 1
    @RaymondtheDeveloper did you ever find a solution? I have the same problem. – Kevin LeStarge Dec 01 '17 at 20:44
4

An alternative solution for module imports that does not require dynamic script loading: you can use the compilerOptions.paths option in your server app's tsconfig.json file to redirect the imported module to a server-only version:

{
    ...
    "compilerOptions": {
        ...
        "paths": {
            "path/to/browser/module": "path/to/server/module"
        }
    }
}

When the server app builds, the compiler will import the server module instead of the browser module.

asgallant
  • 26,060
  • 6
  • 72
  • 87
  • Seems like this is the best solution now. I was able to create wrapper modules that handled SSR and Dev builds – Partho Oct 16 '22 at 15:43
-2

I want to answer too. This is my solution. You need create three environments as here:

src/environments/server/environments.ts
src/environments/browser/environments.ts
src/environments/environments.ts

then in angular.json in build section spoofing files for browser:

"configurations": {
        "dev": {
          "fileReplacements": [
            {
              "replace": "src/environments/environment.ts",
              "with": "src/environments/browser/environment.ts"
            }
          ]
        },
        "production": {
          ...
          "fileReplacements": [
            {
              "replace": "src/environments/environment.ts",
              "with": "src/environments/browser/environment.ts"
            }
          ]
        }
      }

and for server:

"configurations": {
        "dev": {
          "fileReplacements": [
            {
              "replace": "src/environments/environment.ts",
              "with": "src/environments/server/environment.ts"
            }
          ]
        },
        "production": {
          ...
          "fileReplacements": [
            {
              "replace": "src/environments/environment.ts",
              "with": "src/environments/server/environment.ts"
            }
          ]
        }
      }

so anywere we can use a variable environment.isServer

import { environment } from '../../environments/environment';
...

environment.isServer ? MapMockModule : MapModule,

if something is unclear, you can see an example here sourse

  • This is not applicable for that question because you still need the module in the ssr build, but it should be only included on rendering in the browser. – neox5 Dec 19 '19 at 23:04