How to pass parameters rendered from backend to angular2 bootstrap method
Asked Answered
H

4

77

Is there a way to pass arguments rendered on the backend to angular2 bootstrap method? I want to set http header for all requests using BaseRequestOptions with value provided from the backend. My main.ts file looks like this:

import { bootstrap } from '@angular/platform-browser-dynamic';
import { AppComponent } from "./app.component.ts";

bootstrap(AppComponent);

I found how to pass this arguments to root component (https://mcmap.net/q/49606/-angular-2-input-parameters-on-root-directive-duplicate), but i need it when I'm fireing bootstrap method... Any ideas?

edit:

webpack.config.js content:

module.exports = {
  entry: {
    app: "./Scripts/app/main.ts"
  },

  output: {
    filename: "./Scripts/build/[name].js"
  },

  resolve: {
    extensions: ["", ".ts", ".js"]
  },

  module: {
    loaders: [
      {
        test: /\.ts$/,
        loader: 'ts-loader'
      }
    ]
  }
};
Hulahula answered 3/6, 2016 at 10:10 Comment(0)
C
98

update2

Plunker example

update AoT

To work with AoT the factory closure needs to be moved out

function loadContext(context: ContextService) {
  return () => context.load();
}

@NgModule({
  ...
  providers: [ ..., ContextService, { provide: APP_INITIALIZER, useFactory: loadContext, deps: [ContextService], multi: true } ],

See also https://github.com/angular/angular/issues/11262

update an RC.6 and 2.0.0 final example

function configServiceFactory (config: ConfigService) {
  return () => config.load();
}

@NgModule({
    declarations: [AppComponent],
    imports: [BrowserModule,
        routes,
        FormsModule,
        HttpModule],
    providers: [AuthService,
        Title,
        appRoutingProviders,
        ConfigService,
        { provide: APP_INITIALIZER,
          useFactory: configServiceFactory
          deps: [ConfigService], 
          multi: true }
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

If there is no need to wait for the initialization to complete, the constructor of `class AppModule {} can also be used:

class AppModule {
  constructor(/*inject required dependencies */) {...} 
}

hint (cyclic dependency)

For example injecting the router can cause cyclic dependencies. To work around, inject the Injector and get the dependency by

this.myDep = injector.get(MyDependency);

instead of injecting MyDependency directly like:

@Injectable()
export class ConfigService {
  private router:Router;
  constructor(/*private router:Router*/ injector:Injector) {
    setTimeout(() => this.router = injector.get(Router));
  }
}

update

This should work the same in RC.5 but instead add the provider to providers: [...] of the root module instead of bootstrap(...)

(not tested myself yet).

update

An interesting approach to do it entirely inside Angular is explained here https://github.com/angular/angular/issues/9047#issuecomment-224075188

You can use APP_INITIALIZER which will execute a function when the app is initialized and delay what it provides if the function returns a promise. This means the app can be initializing without quite so much latency and you can also use the existing services and framework features.

As an example, suppose you have a multi-tenanted solution where the site info relies on the domain name it's being served from. This can be [name].letterpress.com or a custom domain which is matched on the full hostname. We can hide the fact that this is behind a promise by using APP_INITIALIZER.

In bootstrap:

{provide: APP_INITIALIZER, useFactory: (sites:SitesService) => () => sites.load(), deps:[SitesService, HTTP_PROVIDERS], multi: true}),

sites.service.ts:

@Injectable()
export class SitesService {
  public current:Site;

  constructor(private http:Http, private config:Config) { }

  load():Promise<Site> {
    var url:string;
    var pos = location.hostname.lastIndexOf(this.config.rootDomain);
    var url = (pos === -1)
      ? this.config.apiEndpoint + '/sites?host=' + location.hostname
      : this.config.apiEndpoint + '/sites/' + location.hostname.substr(0, pos);
    var promise = this.http.get(url).map(res => res.json()).toPromise();
    promise.then(site => this.current = site);
    return promise;
  }

NOTE: config is just a custom config class. rootDomain would be '.letterpress.com' for this example and would allow things like aptaincodeman.letterpress.com.

Any components and other services can now have Site injected into them and use the .current property which will be a concrete populated object with no need to wait on any promise within the app.

This approach seemed to cut the startup latency which was otherwise quite noticeable if you were waiting for the large Angular bundle to load and then another http request before the bootstrap even began.

original

You can pass it using Angulars dependency injection:

var headers = ... // get the headers from the server

bootstrap(AppComponent, [{provide: 'headers', useValue: headers})]);
class SomeComponentOrService {
   constructor(@Inject('headers') private headers) {}
}

or provide prepared BaseRequestOptions directly like

class MyRequestOptions extends BaseRequestOptions {
  constructor (private headers) {
    super();
  }
} 

var values = ... // get the headers from the server
var headers = new MyRequestOptions(values);

bootstrap(AppComponent, [{provide: BaseRequestOptions, useValue: headers})]);
Chaparral answered 3/6, 2016 at 10:12 Comment(14)
Yeah, but how to render heders from backend into typescript file?Hulahula
Not sure what you mean. The question seems to be how to get them into Angular. How to you get the headers from the server. Why do you need to render them into typescript file. If you send them as JSON from the server then just pass them as is. TypeScript is JS and JSON is JS, so there should be no problem.Hemi
I wanted to avoid additional AJAX call, because I can have these values rendered somewhere on the main page. So basically what I'm trying to do is to pass some backend-rendered values into bootstrap method.Hulahula
So you want to read the from the HTML. You could add a script tag on the server that assigns them to some global variable <script> function() { window.headers = someJson; }()</script>. Not sure about the syntax, I'm not using JS much myself. This way you wouldn't have to parse at all.Hemi
But is there a way to avoid polluting the global scope?Hulahula
So you want to put it in HTML on the server and than read it from there on the client?Hemi
Yes. For now I'm thinking about storing them in global variable as Thierry Templier but I'm wondering if there is better solution?Hulahula
Excellent solution, just a couple notes for future googlers like myself: 1) Note that load() must return a Promise, not an Observable. Use the .toPromise() function here if you're using Observables in your service like I am. 2) You might be wondering how you get the value sites.load() retrieved into your services, components, etc. Note that SitesService assigns it to this.current. So you just need to inject SitesService into your component and retrieve its current propertyGaytan
Does anyone knows why it isn't possible to use a resolver in the App component ?Inductive
@GünterZöchbauer: how do I get the Injector in? (from where Import and how to init it?)Taiwan
Good solution however when I build my project fresh I get the error: "ERROR in Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function (position 24:46 in the original .ts file), resolving symbol AppModule in .../src/app/app.module.ts". I believe its pointing to the lambda expression in useFactory. How would you translate the above lambda to an exported function? Is the function simply acting as a wrapper?Vastha
With AoT you need to move () => sites.load() to a function (outside of the class and decorator) and then in the provider replace it by that function nameHemi
@GünterZöchbauer thanks, I tried as you suggested but get the same error. But perhaps I'm not following also. Can you take a look at my question: #42999392Vastha
I think the post is very confusing to read. I want to read the original post first and then see the updates below. By pointing out the first part is the original post and there are updates below I can skip to the updates if I want.Ballocks
S
31

In Angular2 final release, the APP_INITIALIZER provider can be used to achieve what you want.

I wrote a Gist with a complete example: https://gist.github.com/fernandohu/122e88c3bcd210bbe41c608c36306db9

The gist example is reading from JSON files but can be easily changed to read from a REST endpoint.

What you need, is basically:

a) Set up APP_INITIALIZER in your existent module file:

import { APP_INITIALIZER } from '@angular/core';
import { BackendRequestClass } from './backend.request';
import { HttpModule } from '@angular/http';

...

@NgModule({
    imports: [
        ...
        HttpModule
    ],
    ...
    providers: [
        ...
        ...
        BackendRequestClass,
        { provide: APP_INITIALIZER, useFactory: (config: BackendRequestClass) => () => config.load(), deps: [BackendRequestClass], multi: true }
    ],
    ...
});

These lines will call the load() method from BackendRequestClass class before your application is started.

Make sure you set "HttpModule" in "imports" section if you want to make http calls to the backend using angular2 built in library.

b) Create a class and name the file "backend.request.ts":

import { Inject, Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class BackendRequestClass {

    private result: Object = null;

    constructor(private http: Http) {

    }

    public getResult() {
        return this.result;
    }

    public load() {
        return new Promise((resolve, reject) => {
            this.http.get('http://address/of/your/backend/endpoint').map( res => res.json() ).catch((error: any):any => {
                reject(false);
                return Observable.throw(error.json().error || 'Server error');
            }).subscribe( (callResult) => {
                this.result = callResult;
                resolve(true);
            });

        });
    }
}

c) To read the contents of the backend call, you just need to inject the BackendRequestClass into any class of you choice and call getResult(). Example:

import { BackendRequestClass } from './backend.request';

export class AnyClass {
    constructor(private backendRequest: BackendRequestClass) {
        // note that BackendRequestClass is injected into a private property of AnyClass
    }

    anyMethod() {
        this.backendRequest.getResult(); // This should return the data you want
    }
}

Let me know if this solves your problem.

Seigel answered 2/11, 2016 at 12:19 Comment(4)
In Angular 2.3.0, I'm getting an error: "Unhandled Promise rejection: appInits[i] is not a function ; Zone: <root> ; Task: Promise.then ; Value: TypeError: appInits[i] is not a function(…) TypeError: appInits[i] is not a function at new ApplicationInitStatus (eval at <anonymous> (localhost:8080/js/vendor.js:89:2), <anonymous>:3751:49) at MyModuleInjector.createInternal (/MyModule/module.ngfactory.js:454:36) -- It would seem the promise returned by load cannot also be returned by the useFactory function.Footling
@Footling note that the factory function doesn't return a function, it returns a function that returns a promise. This was the cause of that appInits[i] error for me.Gaytan
I got that exact same error. Resolved by adding the extra () => for useFactoryVenn
Trying to follow the same steps in angular4 but it does not work, no error with no data getting displayed on viewSewer
T
9

Instead of having your entry point calling bootstrap itself, you could create and export a function that does the work:

export function doBootstrap(data: any) {
    platformBrowserDynamic([{provide: Params, useValue: new Params(data)}])
        .bootstrapModule(AppModule)
        .catch(err => console.error(err));
}

You could also place this function on the global object, depending on your setup (webpack/SystemJS). It also is AOT-compatible.

This has the added benefit to delay the bootstrap, whenit makes sense. For instance, when you retrieve this user data as an AJAX call after the user fills out a form. Just call the exported bootstrap function with this data.

Takishatakken answered 21/12, 2016 at 22:46 Comment(3)
so how do access this passed "data" in the AppModule?Klutz
@Klutz inject Params on any injectable elementIllsuited
In my case, this was the better option. I wanted to manually kick off the loading of the app by another event on the page, and this worked greatEyeless
A
1

The only way to do that is to provide these values when defining your providers:

bootstrap(AppComponent, [
  provide(RequestOptions, { useFactory: () => {
    return new CustomRequestOptions(/* parameters here */);
  });
]);

Then you can use these parameters in your CustomRequestOptions class:

export class AppRequestOptions extends BaseRequestOptions {
  constructor(parameters) {
    this.parameters = parameters;
  }
}

If you get these parameters from an AJAX request, you need to bootstrap asynchronously this way:

var appProviders = [ HTTP_PROVIDERS ]

var app = platform(BROWSER_PROVIDERS)
  .application([BROWSER_APP_PROVIDERS, appProviders]);

var http = app.injector.get(Http);
http.get('http://.../some path').flatMap((parameters) => {
  return app.bootstrap(appComponentType, [
    provide(RequestOptions, { useFactory: () => {
      return new CustomRequestOptions(/* parameters here */);
    }})
  ]);
}).toPromise();

See this question:

Edit

Since you have your data in the HTML you could use the following.

You can import a function and call it with parameters.

Here is a sample of the main module that bootstraps your application:

import {bootstrap} from '...';
import {provide} from '...';
import {AppComponent} from '...';

export function main(params) {
  bootstrap(AppComponent, [
    provide(RequestOptions, { useFactory: () => {
      return new CustomRequestOptions(params);
    });
  ]);
}

Then you can import it from your HTML main page like this:

<script>
  var params = {"token": "@User.Token", "xxx": "@User.Yyy"};
  System.import('app/main').then((module) => {
    module.main(params);
  });
</script>

See this question: Pass Constant Values to Angular from _layout.cshtml.

Ashti answered 3/6, 2016 at 10:14 Comment(5)
But how to render these parameters into typescript file? Or should I run this bootstrap method into inline script on page? But how to do it, when using es6 imports?Hulahula
What do you exactly mean by render? Do you generate your main HTML file / JS files from server? Do you execute an AJAX request to get these parameters?Ashti
I generate my views from server. I thought I will render all necessary parameters on the backend side like this: {"token": "@User.Token", "xxx": "@User.Yyy"} so in rendered HTML i will have {"token": "123abc456def", "xxx": "yyy"}. And I want to somehow pass this rendered JSON into bootstrap method, which i have in .js file.Hulahula
Is there a way to run this without using SystemJS (I'm using webpack, and entry point is defined in webpack.config file)Hulahula
I'm not a webpack expert but I could try... Could you add the content of your webpack.config file? Thanks!Ashti

© 2022 - 2024 — McMap. All rights reserved.