7

We are converting our app from AngularJS to Angular5. I am trying to figure out how to replicate some behavior using Angular5 - namely using server-side rendering to create injectable values.

In our current Angular1.6 app, we have this index.hbs file:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Collaborative Tool</title>
  <link href="favicon.ico" rel="shortcut icon" type="image/x-icon">
</head>

<body class="aui content" ng-app="app">

  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.5/angular.js"></script>

  <script>

    /* globals angular */
    angular.module('app')
      .value('USER', JSON.parse('{{{user}}}'))
      .value('WORKSTREAM_ENUM', JSON.parse('{{{workStreamEnum}}}'))
      .value('CATEGORY_ENUM', JSON.parse('{{{categoryEnum}}}'))
      .value('ROLES_ENUM', JSON.parse('{{{roles}}}'))
      .value('FUNCTIONAL_TEAM_ENUM', JSON.parse('{{{functionalTeams}}}'))
      .value('CDT_ENV', '{{CDT_ENV}}')
      .value('CDT_HOST', '{{CDT_HOST}}')
      .value('CDT_LOGOUT_URL', '{{CDT_LOGOUT_URL}}');


  </script>

</body>
</html>

so what we do is load angular in the first script tag and then we create some values/enums/constants using the second script tag. Essentially using server-side rendering (handlebars) to send data to the front end.

My question: Is there some way to do something very similar with Angular5? How can we use-server side rendering to create injectable modules/values in Angular5?

4
  • I don't really understand where do you find the server side rendering here. You are simply embedding the values in the index.html file; server side rendering assumes you generate the whole view to the end user on the server. In other words, if you look for the real server side rendering you should take a look at angular universal. Commented May 1, 2018 at 5:30
  • @smnbbrv every time index.html / index.hbs is served, the angular values are rendered independently in the response, given the .hbs template Commented May 1, 2018 at 5:32
  • exactly, but this is not rendering. Do you see those values as an end user? Rendering is first of all showing. Commented May 1, 2018 at 5:33
  • 1
    sure, we can use a different word than "rendering" - perhaps "fill-in" the template Commented May 1, 2018 at 5:34

3 Answers 3

4

Dependency Injection still can be used inside your components when rendering it on the server side.

If you're planning to use server-side rendering with Angular 5 you should consider looking into Angular Universal it provides the building blocks for having Angular single page apps rendered in the server-side (for SEO-friendly indexable content).

There are many good angular universal starter projects out there. A good example is [universal-starter][2] . It uses ngExpressEngine to render your application on the fly at the requested url. It uses a webpack project configuration which contains a prerender task that compiles your application and prerenders your applications files. This task looks like this:

// Load zone.js for the server.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import {readFileSync, writeFileSync, existsSync, mkdirSync} from 'fs';
import {join} from 'path';

import {enableProdMode} from '@angular/core';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Import module map for lazy loading
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';
import {renderModuleFactory} from '@angular/platform-server';
import {ROUTES} from './static.paths';

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');

const BROWSER_FOLDER = join(process.cwd(), 'browser');

// Load the index.html file containing referances to your application bundle.
const index = readFileSync(join('browser', 'index.html'), 'utf8');

let previousRender = Promise.resolve();

// Iterate each route path
ROUTES.forEach(route => {
  var fullPath = join(BROWSER_FOLDER, route);

  // Make sure the directory structure is there
  if(!existsSync(fullPath)){
    mkdirSync(fullPath);
  }

  // Writes rendered HTML to index.html, replacing the file if it already exists.
  previousRender = previousRender.then(_ => renderModuleFactory(AppServerModuleNgFactory, {
    document: index,
    url: route,
    extraProviders: [
      provideModuleMap(LAZY_MODULE_MAP)
    ]
  })).then(html => writeFileSync(join(fullPath, 'index.html'), html));
});

Later on you can run an express server which renders your apps generated HTMLs:

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser'), {
  maxAge: '1y'
}));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render('index', { req });
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node Express server listening on http://localhost:${PORT}`);
});

You can run server-side specific code such as:

import { PLATFORM_ID } from '@angular/core';
 import { isPlatformBrowser, isPlatformServer } from '@angular/common';

 constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... }

 ngOnInit() {
   if (isPlatformBrowser(this.platformId)) {
      // Client only code.
      ...
   }
   if (isPlatformServer(this.platformId)) {
     // Server only code.
     ...
   }
 }

but beware that window, document, navigator, and other browser types - do not exist on the server. So any library that might use these might not work.

Sign up to request clarification or add additional context in comments.

Comments

4
+25

My team had the same problem when transitioning from AngularJS to Angular (early release candidates of v2). We came up with a solution that we still use and I'm not aware of any updates to make it easier (at least when not using Angular Universal - if you are using that then there is stuff built in to bootstrap initial data). We pass data to our Angular app by serializing the JSON object and setting it up as an attribute on the app root Angular component in our HTML:

<app-root [configuration]="JSON_SERIALIZED_OBJECT"></app-root>

where JSON_SERIALIZED_OBJECT is the actual serialized object. We use .NET (non-Core, so Angular Universal isn't really an option) to render our page (doing [configuration]="@JsonConvert.SerializeObject(Model.Context)") so don't know what you need to do, but it looks like you should be able to do the same thing that you've done previously to serialize it.

Once that is setup, we have to manually JSON.parse(...) that object in our main app component, but we treat it just like an Angular input. This is what our component looks like to grab that:

import { Component, ElementRef } from '@angular/core';
import { ConfigurationService } from 'app/core';

@Component(...)
export class AppComponent {
    constructor(private element: ElementRef, private configurationService: ConfigurationService) {
        this.setupConfiguration();
    }

    private setupConfiguration() {
        const value = this.getAttributeValue('[configuration]');
        const configuration = value ? JSON.parse(value) : {};

        this.configurationService.setConfiguration(configuration);
    }

    private getAttributeValue(attribute: string) {
        const element = this.element.nativeElement;

        return element.hasAttribute(attribute) ? element.getAttribute(attribute) : null;
    }
}

As shown, we use a service to share the data around the system. It can be something as simple as this:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import { Configuration } from './configuration.model';

@Injectable()
export class ConfigurationService {
    private readonly configurationSubject$ = new BehaviorSubject<Configuration>(null);
    readonly configuration$ = this.configurationSubject$.asObservable();

    setConfiguration(configuration: Configuration) {
        this.configurationSubject$.next(configuration);
    }
}

Then in our components that need data from the configuration, we inject this service and watch for changes.

import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/takeUntil';

import { ConfigurationService } from 'app/core';

@Component(...)
export class ExampleThemedComponent implements OnDestroy {
    private readonly destroy$ = new Subject<boolean>();

    readonly theme$: Observable<string> = this.configurationService.configuration$
        .takeUntil(this.destroy$.asObservable())
        .map(c => c.theme);

    constructor(private configurationService: ConfigurationService) {
    }

    ngOnDestroy() {
        this.destroy$.next(true);
    }
}

Note: we make changes to our configuration sometimes while running so that is why we use a subject and observables. If your configuration won't change, then you can skip all of that portion of these examples.

Comments

4

Create the file: data.ts. In this file declare the variables and their types (I will show only one) and create InjectionToken for each of them:

import { InjectionToken } from '@angular/core';

// describes the value of the variable
export interface EmbeddedUserData {
  userId: string;
  // etc
}

// tells the app that there will be a global variable named EMBEDDED_USER_DATA (from index.html)
export declare const EMBEDDED_USER_DATA: EmbeddedUserData;

// creates injection token for DI that you can use it as a provided value (like value or constant in angular 1)
export UserData = new InjectionToken<EmbeddedUserData>('EmbeddedUserData');

Then come to your app.module.ts and provide this token:

// ...
providers: [
  { provide: UserData, useValue: EMBEDDED_USER_DATA }
],
// ...

Finally use it as any normal service / injected value:

// ...
constructor(@Inject(UserData) userData: EmbeddedUserData) {}
// ...

or use it as simple imported variable (there is no even need to provide / inject anything in this case):

import { EMBEDDED_USER_DATA } from './data.ts';

As a result you have nearly the same you had in angularjs. The only thing left is to add your variables to index.html before angular scripts (maybe it even makes sense to put it in the head):

<script>var EMBEDDED_USER_DATA = JSON.parse({ ... })</script>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.