ChunkLoadError / Unhandled Promise rejection Ionic 5 /angular - javascript

I am trying to add offline functionality to my ionic 5 app. For that, I implemented a logic of storing information in sql lite while conected, and when offline bring data from sql lite instead of http request.
obtenerVisitaDetalle(idVisita: number): Observable<any> {
if (this.networkService.getCurrentNetworkStatus() == ConnectionStatus.Offline) {
return from(this.getLocalData(`Visita/${idVisita}`));
} else {
return this.http.get<any[]>(environment.UrlBaseApi + `Visita/${idVisita}`, this.httpOptions).pipe(
tap(res => {
this.setLocalData(`Visita/${idVisita}`, res);
})
)
}
}
So far so good, but I came across the following problem:
https://github.com/ionic-team/ionic-framework/issues/20859
Basically when I am offline, I am unable to open components like modal, since those have not been preloaded. Comment on link above mention using service worker to prefetch the chunks/assets you need ahead of time.
Any guidance, example on how to acomplish this, will be appreciated. Thanks
Edit: Based on first answer I am providing part of the routing file.
{
path: 'pago',
canActivate: [UserAuthenticatedGuard],
loadChildren: () => import('./Pages/pago/pago.module').then( m => m.PagoPageModule)
}
];
#NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})

Preloading is supported from Angular Cli V6 so you might want to look into https://angular.io/api/router/PreloadAllModules. Basically you can preload all your modules by adding the following to your RouterModule.
RouterModule.forRoot(
routes, { preloadingStrategy: PreloadAllModules ,}
),
Or you can preload specific routes by defining a custom PreloadingStrategy.
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: Function): Observable<any> {
return route.path == 'YOUR_PATH' ? load() : of(null);
}
}

Related

How can I separate the firebase credentials from the production build that I get in Angular 2+? [duplicate]

I want to add an App Settings section into my App where It will contain some consts and pre defined values.
I've already read this answer which uses OpaqueToken But it is deprecated in Angular. This article explains the differences but it didn't provide a full example , and my attempts were unsuccessful.
Here is what I've tried ( I don't know if it's the right way) :
//ServiceAppSettings.ts
import {InjectionToken, OpaqueToken} from "#angular/core";
const CONFIG = {
apiUrl: 'http://my.api.com',
theme: 'suicid-squad',
title: 'My awesome app'
};
const FEATURE_ENABLED = true;
const API_URL = new InjectionToken<string>('apiUrl');
And this is the component where I want to use those consts :
//MainPage.ts
import {...} from '#angular/core'
import {ServiceTest} from "./ServiceTest"
#Component({
selector: 'my-app',
template: `
<span>Hi</span>
` , providers: [
{
provide: ServiceTest,
useFactory: ( apiUrl) => {
// create data service
},
deps: [
new Inject(API_URL)
]
}
]
})
export class MainPage {
}
But it doesn't work and I get errors.
Question:
How can I consume "app.settings" values the Angular way?
plunker
NB Sure I can create Injectable service and put it in the provider of the NgModule , But as I said I want to do it with InjectionToken , the Angular way.
It's not advisable to use the environment.*.ts files for your API URL configuration. It seems like you should because this mentions the word "environment".
Using this is actually compile-time configuration. If you want to change the API URL, you will need to re-build. That's something you don't want to have to do ... just ask your friendly QA department :)
What you need is runtime configuration, i.e. the app loads its configuration when it starts up.
Some other answers touch on this, but the difference is that the configuration needs to be loaded as soon as the app starts, so that it can be used by a normal service whenever it needs it.
To implement runtime configuration:
Add a JSON config file to the /src/assets/ folder (so that is copied on build)
Create an AppConfigService to load and distribute the config
Load the configuration using an APP_INITIALIZER
1. Add Config file to /src/assets
You could add it to another folder, but you'd need to tell the Angular CLI that it is an asset, by updating the angular.json configuration file. Start off using the assets folder:
{
"apiBaseUrl": "https://development.local/apiUrl"
}
2. Create AppConfigService
This is the service which will be injected whenever you need the config value:
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class AppConfigService {
private appConfig: any;
constructor(private http: HttpClient) { }
loadAppConfig() {
return this.http.get('/assets/config.json')
.toPromise()
.then(data => {
this.appConfig = data;
});
}
// This is an example property ... you can make it however you want.
get apiBaseUrl() {
if (!this.appConfig) {
throw Error('Config file not loaded!');
}
return this.appConfig.apiBaseUrl;
}
}
3. Load the configuration using an APP_INITIALIZER
To allow the AppConfigService to be injected safely, with config fully loaded, we need to load the config at app startup time. Importantly, the initialisation factory function needs to return a Promise so that Angular knows to wait until it finishes resolving before finishing startup:
import { APP_INITIALIZER } from '#angular/core';
import { AppConfigService } from './services/app-config.service';
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [AppConfigService],
useFactory: (appConfigService: AppConfigService) => {
return () => {
//Make sure to return a promise!
return appConfigService.loadAppConfig();
};
}
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Now you can inject it wherever you need to and all the config will be ready to read:
#Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
apiBaseUrl: string;
constructor(private appConfigService: AppConfigService) {}
ngOnInit(): void {
this.apiBaseUrl = this.appConfigService.apiBaseUrl;
}
}
I can't say it strongly enough, configuring your API urls as compile-time configuration is an anti-pattern. Use runtime configuration.
If you are using angular-cli, there is yet another option:
Angular CLI provides environment files in src/environments (default ones are environment.ts (dev) and environment.prod.ts (production)).
Note that you need to provide the config parameters in all environment.* files, e.g.,
environment.ts:
export const environment = {
production: false,
apiEndpoint: 'http://localhost:8000/api/v1'
};
environment.prod.ts:
export const environment = {
production: true,
apiEndpoint: '__your_production_server__'
};
and use them in your service (the correct environment file is chosen automatically):
api.service.ts
// ... other imports
import { environment } from '../../environments/environment';
#Injectable()
export class ApiService {
public apiRequest(): Observable<MyObject[]> {
const path = environment.apiEndpoint + `/objects`;
// ...
}
// ...
}
Read more on application environments on Github (Angular CLI version 6) or in the official Angular guide (version 7).
I figured out how to do this with InjectionTokens (see example below), and if your project was built using the Angular CLI you can use the environment files found in /environments for static application wide settings like an API endpoint, but depending on your project's requirements you will most likely end up using both since environment files are just object literals, while an injectable configuration using InjectionToken's can use the environment variables and since it's a class can have logic applied to configure it based on other factors in the application, such as initial HTTP request data, subdomain, etc.
Injection Tokens Example
/app/app-config.module.ts
import { NgModule, InjectionToken } from '#angular/core';
import { environment } from '../environments/environment';
export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');
export class AppConfig {
apiEndpoint: string;
}
export const APP_DI_CONFIG: AppConfig = {
apiEndpoint: environment.apiEndpoint
};
#NgModule({
providers: [{
provide: APP_CONFIG,
useValue: APP_DI_CONFIG
}]
})
export class AppConfigModule { }
/app/app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { AppConfigModule } from './app-config.module';
#NgModule({
declarations: [
// ...
],
imports: [
// ...
AppConfigModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Now you can just DI it into any component, service, etc:
/app/core/auth.service.ts
import { Injectable, Inject } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Router } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { APP_CONFIG, AppConfig } from '../app-config.module';
import { AuthHttp } from 'angular2-jwt';
#Injectable()
export class AuthService {
constructor(
private http: Http,
private router: Router,
private authHttp: AuthHttp,
#Inject(APP_CONFIG) private config: AppConfig
) { }
/**
* Logs a user into the application.
* #param payload
*/
public login(payload: { username: string, password: string }) {
return this.http
.post(`${this.config.apiEndpoint}/login`, payload)
.map((response: Response) => {
const token = response.json().token;
sessionStorage.setItem('token', token); // TODO: can this be done else where? interceptor
return this.handleResponse(response); // TODO: unset token shouldn't return the token to login
})
.catch(this.handleError);
}
// ...
}
You can then also type check the config using the exported AppConfig.
I found that using an APP_INITIALIZER for this doesn't work in situations where other service providers require the configuration to be injected. They can be instantiated before APP_INITIALIZER is run.
I've seen other solutions that use fetch to read a config.json file and provide it using an injection token in a parameter to platformBrowserDynamic() prior to bootstrapping the root module. But fetch isn't supported in all browsers and in particular WebView browsers for the mobile devices I target.
The following is a solution that works for me for both PWA and mobile devices (WebView). Note: I've only tested in Android so far; working from home means I don't have access to a Mac to build.
In main.ts:
import { enableProdMode } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { APP_CONFIG } from './app/lib/angular/injection-tokens';
function configListener() {
try {
const configuration = JSON.parse(this.responseText);
// pass config to bootstrap process using an injection token
platformBrowserDynamic([
{ provide: APP_CONFIG, useValue: configuration }
])
.bootstrapModule(AppModule)
.catch(err => console.error(err));
} catch (error) {
console.error(error);
}
}
function configFailed(evt) {
console.error('Error: retrieving config.json');
}
if (environment.production) {
enableProdMode();
}
const request = new XMLHttpRequest();
request.addEventListener('load', configListener);
request.addEventListener('error', configFailed);
request.open('GET', './assets/config/config.json');
request.send();
This code:
kicks off an async request for the config.json file.
When the request completes, parses the JSON into a Javascript object
provides the value using the APP_CONFIG injection token, prior to bootstrapping.
And finally bootstraps the root module.
APP_CONFIG can then be injected into any additional providers in app-module.ts and it will be defined. For example, I can initialise the FIREBASE_OPTIONS injection token from #angular/fire with the following:
{
provide: FIREBASE_OPTIONS,
useFactory: (config: IConfig) => config.firebaseConfig,
deps: [APP_CONFIG]
}
I find this whole thing a surprisingly difficult (and hacky) thing to do for a very common requirement. Hopefully in the near future there will be a better way, such as, support for async provider factories.
The rest of the code for completeness...
In app/lib/angular/injection-tokens.ts:
import { InjectionToken } from '#angular/core';
import { IConfig } from '../config/config';
export const APP_CONFIG = new InjectionToken<IConfig>('app-config');
and in app/lib/config/config.ts I define the interface for my JSON config file:
export interface IConfig {
name: string;
version: string;
instance: string;
firebaseConfig: {
apiKey: string;
// etc
}
}
Config is stored in assets/config/config.json:
{
"name": "my-app",
"version": "#{Build.BuildNumber}#",
"instance": "localdev",
"firebaseConfig": {
"apiKey": "abcd"
...
}
}
Note: I use an Azure DevOps task to insert Build.BuildNumber and substitute other settings for different deployment environments as it is being deployed.
Poor man's configuration file:
Add to your index.html as first line in the body tag:
<script lang="javascript" src="assets/config.js"></script>
Add assets/config.js:
var config = {
apiBaseUrl: "http://localhost:8080"
}
Add config.ts:
export const config: AppConfig = window['config']
export interface AppConfig {
apiBaseUrl: string
}
Here's my solution, loads from .json to allow changes without rebuilding
import { Injectable, Inject } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Location } from '#angular/common';
#Injectable()
export class ConfigService {
private config: any;
constructor(private location: Location, private http: Http) {
}
async apiUrl(): Promise<string> {
let conf = await this.getConfig();
return Promise.resolve(conf.apiUrl);
}
private async getConfig(): Promise<any> {
if (!this.config) {
this.config = (await this.http.get(this.location.prepareExternalUrl('/assets/config.json')).toPromise()).json();
}
return Promise.resolve(this.config);
}
}
and config.json
{
"apiUrl": "http://localhost:3000/api"
}
Quite a few articles are recommending that you get Angular config settings using an AppConfigService such as this one.
But I found that sometimes this just didn't work.
It was simpler, and more reliable to have a "config.json" file, then create a class which just read it in, and returned a value, eg my config file would look like this:
{
"appName": "Mike's app",
"version": "1.23.4",
"logging_URL" : "https://someWebsite.azurewebsites.net/logs"
}
And I'd access the values using this:
import config from '../../assets/config.json';
#Injectable({
providedIn: 'root'
})
export class AppConfigService {
get appName() {
return config.appName;
}
get appVersion() {
return config.version;
}
get loggingUrl() {
return config.logging_URL;
}
}
(A few months later...)
After patting myself on the back for making a simpler Angular solution, I realised this has a big drawback. If you use an AppConfigService, and you're using CI/CD, then you can get your build process to update the config .json file, and the Angular app will use these settings.
With my version, yes, it's simpler, but there is no config setting file to overwrite. For automated build processes, this might not be desirable.
Here's my two solutions for this
1. Store in json files
Just make a json file and get in your component by $http.get() method. If I was need this very low then it's good and quick.
2. Store by using data services
If you want to store and use in all components or having large usage then it's better to use data service. Like this :
Just create static folder inside src/app folder.
Create a file named as fuels.ts into static folder. You can store other static files here also. Let define your data like this. Assuming you having fuels data.
__
export const Fuels {
Fuel: [
{ "id": 1, "type": "A" },
{ "id": 2, "type": "B" },
{ "id": 3, "type": "C" },
{ "id": 4, "type": "D" },
];
}
Create a file name static.services.ts
__
import { Injectable } from "#angular/core";
import { Fuels } from "./static/fuels";
#Injectable()
export class StaticService {
constructor() { }
getFuelData(): Fuels[] {
return Fuels;
}
}`
Now You can make this available for every module
just import in app.module.ts file like this and change in providers
import { StaticService } from './static.services';
providers: [StaticService]
Now use this as StaticService in any module.
That's All.
We had this problem years ago before I had joined and had in place a solution that used local storage for user and environment information. Angular 1.0 days to be exact. We were formerly dynamically creating a js file at runtime that would then place the generated api urls into a global variable. We're a little more OOP driven these days and don't use local storage for anything.
I created a better solution for both determining environment and api url creation.
How does this differ?
The app will not load unless the config.json file is loaded. It uses factory functions to create a higher degree of SOC. I could encapsulate this into a service, but I never saw any reason when the only similarity between the different sections of the file are that they exist together in the file. Having a factory function allows me to pass the function directly into a module if it's capable of accepting a function. Last, I have an easier time setting up InjectionTokens when factory functions are available to utilize.
Downsides?
You're out of luck using this setup (and most of the other answers) if the module you want to configure doesn't allow a factory function to be passed into either forRoot() or forChild(), and there's no other way to configure the package by using a factory function.
Instructions
Using fetch to retrieve a json file, I store the object in window and raise a custom event. - remember to install whatwg-fetch and add it to your polyfills.ts for IE compatibility
Have an event listener listening for the custom event.
The event listener receives the event, retrieves the object from window to pass to an observable, and clears out what was stored in window.
Bootstrap Angular
-- This is where my solution starts to really differ --
Create a file exporting an interface whose structure represents your config.json -- it really helps with type consistency and the next section of code requires a type, and don't specify {} or any when you know you can specify something more concrete
Create the BehaviorSubject that you will pass the parsed json file into in step 3.
Use factory functions to reference the different sections of your config to maintain SOC
Create InjectionTokens for the providers needing the result of your factory functions
-- and/or --
Pass the factory functions directly into the modules capable of accepting a function in either its forRoot() or forChild() methods.
-- main.ts
I check window["environment"] is not populated before creating an event listener to allow the possiblilty of a solution where window["environment"] is populated by some other means before the code in main.ts ever executes.
import { enableProdMode } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { configurationSubject } from './app/utils/environment-resolver';
var configurationLoadedEvent = document.createEvent('Event');
configurationLoadedEvent.initEvent('config-set', true, true);
fetch("../../assets/config.json")
.then(result => { return result.json(); })
.then(data => {
window["environment"] = data;
document.dispatchEvent(configurationLoadedEvent);
}, error => window.location.reload());
/*
angular-cli only loads the first thing it finds it needs a dependency under /app in main.ts when under local scope.
Make AppModule the first dependency it needs and the rest are done for ya. Event listeners are
ran at a higher level of scope bypassing the behavior of not loading AppModule when the
configurationSubject is referenced before calling platformBrowserDynamic().bootstrapModule(AppModule)
example: this will not work because configurationSubject is the first dependency the compiler realizes that lives under
app and will ONLY load that dependency, making AppModule an empty object.
if(window["environment"])
{
if (window["environment"].production) {
enableProdMode();
}
configurationSubject.next(window["environment"]);
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
}
*/
if(!window["environment"]) {
document.addEventListener('config-set', function(e){
if (window["environment"].production) {
enableProdMode();
}
configurationSubject.next(window["environment"]);
window["environment"] = undefined;
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
});
}
--- environment-resolvers.ts
I assign a value to the BehaviorSubject using window["environment"] for redundancy. You could devise a solution where your config is preloaded already and window["environment"] is already populated by the time any of your Angular's app code is ran, including the code in main.ts
import { BehaviorSubject } from "rxjs";
import { IConfig } from "../config.interface";
const config = <IConfig>Object.assign({}, window["environment"]);
export const configurationSubject = new BehaviorSubject<IConfig>(config);
export function resolveEnvironment() {
const env = configurationSubject.getValue().environment;
let resolvedEnvironment = "";
switch (env) {
// case statements for determining whether this is dev, test, stage, or prod
}
return resolvedEnvironment;
}
export function resolveNgxLoggerConfig() {
return configurationSubject.getValue().logging;
}
-- app.module.ts - Stripped down for easier understanding
Fun fact! Older versions of NGXLogger required you to pass in an object into LoggerModule.forRoot(). In fact, the LoggerModule still does! NGXLogger kindly exposes LoggerConfig which you can override allowing you to use a factory function for setup.
import { resolveEnvironment, resolveNgxLoggerConfig, resolveSomethingElse } from './environment-resolvers';
import { LoggerConfig } from 'ngx-logger';
#NgModule({
modules: [
SomeModule.forRoot(resolveSomethingElse)
],
providers:[
{
provide: ENVIRONMENT,
useFactory: resolveEnvironment
},
{
provide: LoggerConfig,
useFactory: resolveNgxLoggerConfig
}
]
})
export class AppModule
Addendum
How did I solve the creation of my API urls?
I wanted to be able to understand what each url did via a comment and wanted typechecking since that's TypeScript's greatest strength compared to javascript (IMO). I also wanted to create an experience for other devs to add new endpoints, and apis that was as seamless as possible.
I created a class that takes in the environment (dev, test, stage, prod, "", and etc) and passed this value to a series of classes[1-N] whose job is to create the base url for each API collection. Each ApiCollection is responsible for creating the base url for each collection of APIs. Could be our own APIs, a vendor's APIs, or even an external link. That class will pass the created base url into each subsequent api it contains. Read the code below to see a bare bones example. Once setup, it's very simple for another dev to add another endpoint to an Api class without having to touch anything else.
TLDR; basic OOP principles and lazy getters for memory optimization
#Injectable({
providedIn: 'root'
})
export class ApiConfig {
public apis: Apis;
constructor(#Inject(ENVIRONMENT) private environment: string) {
this.apis = new Apis(environment);
}
}
export class Apis {
readonly microservices: MicroserviceApiCollection;
constructor(environment: string) {
this.microservices = new MicroserviceApiCollection(environment);
}
}
export abstract class ApiCollection {
protected domain: any;
constructor(environment: string) {
const domain = this.resolveDomain(environment);
Object.defineProperty(ApiCollection.prototype, 'domain', {
get() {
Object.defineProperty(this, 'domain', { value: domain });
return this.domain;
},
configurable: true
});
}
}
export class MicroserviceApiCollection extends ApiCollection {
public member: MemberApi;
constructor(environment) {
super(environment);
this.member = new MemberApi(this.domain);
}
resolveDomain(environment: string): string {
return `https://subdomain${environment}.actualdomain.com/`;
}
}
export class Api {
readonly base: any;
constructor(baseUrl: string) {
Object.defineProperty(this, 'base', {
get() {
Object.defineProperty(this, 'base',
{ value: baseUrl, configurable: true});
return this.base;
},
enumerable: false,
configurable: true
});
}
attachProperty(name: string, value: any, enumerable?: boolean) {
Object.defineProperty(this, name,
{ value, writable: false, configurable: true, enumerable: enumerable || true });
}
}
export class MemberApi extends Api {
/**
* This comment will show up when referencing this.apiConfig.apis.microservices.member.memberInfo
*/
get MemberInfo() {
this.attachProperty("MemberInfo", `${this.base}basic-info`);
return this.MemberInfo;
}
constructor(baseUrl: string) {
super(baseUrl + "member/api/");
}
}
I find this Angular How-to: Editable Config Files from Microsoft Dev blogs being the best solution. You can configure dev build settings or prod build settings.
If running in a container, this article https://pumpingco.de/blog/environment-variables-angular-docker/ shows how to use envsubst on the entry point command to fix this static compile issue on the fly via environment variable.

Set value to APP_BASE_HREF on click event

Currently I am using APP_BASE_HREF to set current language as base href of my application.
const appLanguageProvider: any[] = [
{
provide: APP_BASE_HREF,
useFactory: appInitializerFactory,
deps: [ TranslateService ]
}
];
export const appInitializerFactory = (
translateService: TranslateService,
) => {
return translateService.currentLang;
};
Base href is setting up properly at app initialization stage but I want to change basehref of application when user has selected new language. How can I achieve that?
Why don't you just injecting the TranslateService token directly in your component and setting the new language on demand?
Something like this:
changeLang(lang: string) {
this.translateService.use(lang);
}
If you want to edit your Token providers in runtime you can use the following trick - use the DI to inject the injector, then:
this.injector.create({providers: [...]})

Injecting Angular 1 service into Angular 4

Using the process outlined here, I'm trying to inject Angular 1 services into an Angular 4 app. The app is bootstrapped in hybrid mode (and works as I have some Angular 4 components and services running).
Whenever I try to inject the Angular 1 service, I get Cannot read property 'get' of undefined.
upgraded-providers.ts:
import {LinksService} from "./+services/links/links";
export function linksServiceFactory(i: any) {
return i.get('bfLinksService'); // <--- Errors here!
}
export const linksServiceProvider = {
provide: LinksService,
useFactory: linksServiceFactory,
deps: ['$injector']
};
My Angular 4 service which is trying to use LinksService looks like:
#Injectable()
export class EntityService {
constructor (
private http: Http,
private links: LinksService
) {
}
get(id: string): Observable<OrgDetails> {
// Do something
}
}
And finally LinksService (the Angular 1 service, written in Typescript) looks like:
export class LinksService {
static $inject = ["$log", "$location", PROPERTIES_SERVICE];
private contentHost : string;
private thisAppHost : string;
constructor (private $log : ng.ILogService, private $location : ng.ILocationService, private props : IPropertiesService) {
this.init();
}
// Service functions elided
}
The bootstrap and module stuff:
#NgModule({
imports: [
BrowserModule,
HttpModule,
UpgradeModule,
],
declarations: [
AppComponent,
OrgSummaryComponent,
],
providers: [
EntityService,
linksServiceProvider
],
bootstrap: [
AppComponent,
],
})
export class AppModule {
ngDoBootstrap() {
// Does nothing by design.
// This is to facilitate "hybrid bootstrapping"
}
}
platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef => {
const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(document.body, [AppModuleName], {strictDi: false});
});
The Angular 1 (legacy) stuff all works fine.
It seems like Angular cant find the $injector, but shouldn't that be there regardless?
Many thanks for any suggestions,
Jeff
Two days of my life I won't get back but...
Just found this:
https://github.com/angular/angular.io/issues/3317
Basically the documentation is wrong. By adding a constrcutor to the app module with the call to upgrade.bootstrap in it, everything works.
export class AppModule {
constructor(upgrade: UpgradeModule) {
upgrade.bootstrap(document.body, [AppModuleName], {strictDi: true});
}
// Does nothing by design.
// This is to facilitate "hybrid bootstrapping"
ngDoBootstrap() {}
}
platformBrowserDynamic().bootstrapModule(AppModule);
Thank you to those who responded.
Actually the better way to instantiate AngularJS is after:
platformBrowserDynamic().bootstrapModule(AppModule)
.then(platformRef => {
const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(document.body, ['app'], { strictDi: false });
})
.catch(err => console.log(err));

App.settings - the Angular way?

I want to add an App Settings section into my App where It will contain some consts and pre defined values.
I've already read this answer which uses OpaqueToken But it is deprecated in Angular. This article explains the differences but it didn't provide a full example , and my attempts were unsuccessful.
Here is what I've tried ( I don't know if it's the right way) :
//ServiceAppSettings.ts
import {InjectionToken, OpaqueToken} from "#angular/core";
const CONFIG = {
apiUrl: 'http://my.api.com',
theme: 'suicid-squad',
title: 'My awesome app'
};
const FEATURE_ENABLED = true;
const API_URL = new InjectionToken<string>('apiUrl');
And this is the component where I want to use those consts :
//MainPage.ts
import {...} from '#angular/core'
import {ServiceTest} from "./ServiceTest"
#Component({
selector: 'my-app',
template: `
<span>Hi</span>
` , providers: [
{
provide: ServiceTest,
useFactory: ( apiUrl) => {
// create data service
},
deps: [
new Inject(API_URL)
]
}
]
})
export class MainPage {
}
But it doesn't work and I get errors.
Question:
How can I consume "app.settings" values the Angular way?
plunker
NB Sure I can create Injectable service and put it in the provider of the NgModule , But as I said I want to do it with InjectionToken , the Angular way.
It's not advisable to use the environment.*.ts files for your API URL configuration. It seems like you should because this mentions the word "environment".
Using this is actually compile-time configuration. If you want to change the API URL, you will need to re-build. That's something you don't want to have to do ... just ask your friendly QA department :)
What you need is runtime configuration, i.e. the app loads its configuration when it starts up.
Some other answers touch on this, but the difference is that the configuration needs to be loaded as soon as the app starts, so that it can be used by a normal service whenever it needs it.
To implement runtime configuration:
Add a JSON config file to the /src/assets/ folder (so that is copied on build)
Create an AppConfigService to load and distribute the config
Load the configuration using an APP_INITIALIZER
1. Add Config file to /src/assets
You could add it to another folder, but you'd need to tell the Angular CLI that it is an asset, by updating the angular.json configuration file. Start off using the assets folder:
{
"apiBaseUrl": "https://development.local/apiUrl"
}
2. Create AppConfigService
This is the service which will be injected whenever you need the config value:
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class AppConfigService {
private appConfig: any;
constructor(private http: HttpClient) { }
loadAppConfig() {
return this.http.get('/assets/config.json')
.toPromise()
.then(data => {
this.appConfig = data;
});
}
// This is an example property ... you can make it however you want.
get apiBaseUrl() {
if (!this.appConfig) {
throw Error('Config file not loaded!');
}
return this.appConfig.apiBaseUrl;
}
}
3. Load the configuration using an APP_INITIALIZER
To allow the AppConfigService to be injected safely, with config fully loaded, we need to load the config at app startup time. Importantly, the initialisation factory function needs to return a Promise so that Angular knows to wait until it finishes resolving before finishing startup:
import { APP_INITIALIZER } from '#angular/core';
import { AppConfigService } from './services/app-config.service';
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [AppConfigService],
useFactory: (appConfigService: AppConfigService) => {
return () => {
//Make sure to return a promise!
return appConfigService.loadAppConfig();
};
}
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Now you can inject it wherever you need to and all the config will be ready to read:
#Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
apiBaseUrl: string;
constructor(private appConfigService: AppConfigService) {}
ngOnInit(): void {
this.apiBaseUrl = this.appConfigService.apiBaseUrl;
}
}
I can't say it strongly enough, configuring your API urls as compile-time configuration is an anti-pattern. Use runtime configuration.
If you are using angular-cli, there is yet another option:
Angular CLI provides environment files in src/environments (default ones are environment.ts (dev) and environment.prod.ts (production)).
Note that you need to provide the config parameters in all environment.* files, e.g.,
environment.ts:
export const environment = {
production: false,
apiEndpoint: 'http://localhost:8000/api/v1'
};
environment.prod.ts:
export const environment = {
production: true,
apiEndpoint: '__your_production_server__'
};
and use them in your service (the correct environment file is chosen automatically):
api.service.ts
// ... other imports
import { environment } from '../../environments/environment';
#Injectable()
export class ApiService {
public apiRequest(): Observable<MyObject[]> {
const path = environment.apiEndpoint + `/objects`;
// ...
}
// ...
}
Read more on application environments on Github (Angular CLI version 6) or in the official Angular guide (version 7).
I figured out how to do this with InjectionTokens (see example below), and if your project was built using the Angular CLI you can use the environment files found in /environments for static application wide settings like an API endpoint, but depending on your project's requirements you will most likely end up using both since environment files are just object literals, while an injectable configuration using InjectionToken's can use the environment variables and since it's a class can have logic applied to configure it based on other factors in the application, such as initial HTTP request data, subdomain, etc.
Injection Tokens Example
/app/app-config.module.ts
import { NgModule, InjectionToken } from '#angular/core';
import { environment } from '../environments/environment';
export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');
export class AppConfig {
apiEndpoint: string;
}
export const APP_DI_CONFIG: AppConfig = {
apiEndpoint: environment.apiEndpoint
};
#NgModule({
providers: [{
provide: APP_CONFIG,
useValue: APP_DI_CONFIG
}]
})
export class AppConfigModule { }
/app/app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { AppConfigModule } from './app-config.module';
#NgModule({
declarations: [
// ...
],
imports: [
// ...
AppConfigModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Now you can just DI it into any component, service, etc:
/app/core/auth.service.ts
import { Injectable, Inject } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Router } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { APP_CONFIG, AppConfig } from '../app-config.module';
import { AuthHttp } from 'angular2-jwt';
#Injectable()
export class AuthService {
constructor(
private http: Http,
private router: Router,
private authHttp: AuthHttp,
#Inject(APP_CONFIG) private config: AppConfig
) { }
/**
* Logs a user into the application.
* #param payload
*/
public login(payload: { username: string, password: string }) {
return this.http
.post(`${this.config.apiEndpoint}/login`, payload)
.map((response: Response) => {
const token = response.json().token;
sessionStorage.setItem('token', token); // TODO: can this be done else where? interceptor
return this.handleResponse(response); // TODO: unset token shouldn't return the token to login
})
.catch(this.handleError);
}
// ...
}
You can then also type check the config using the exported AppConfig.
I found that using an APP_INITIALIZER for this doesn't work in situations where other service providers require the configuration to be injected. They can be instantiated before APP_INITIALIZER is run.
I've seen other solutions that use fetch to read a config.json file and provide it using an injection token in a parameter to platformBrowserDynamic() prior to bootstrapping the root module. But fetch isn't supported in all browsers and in particular WebView browsers for the mobile devices I target.
The following is a solution that works for me for both PWA and mobile devices (WebView). Note: I've only tested in Android so far; working from home means I don't have access to a Mac to build.
In main.ts:
import { enableProdMode } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { APP_CONFIG } from './app/lib/angular/injection-tokens';
function configListener() {
try {
const configuration = JSON.parse(this.responseText);
// pass config to bootstrap process using an injection token
platformBrowserDynamic([
{ provide: APP_CONFIG, useValue: configuration }
])
.bootstrapModule(AppModule)
.catch(err => console.error(err));
} catch (error) {
console.error(error);
}
}
function configFailed(evt) {
console.error('Error: retrieving config.json');
}
if (environment.production) {
enableProdMode();
}
const request = new XMLHttpRequest();
request.addEventListener('load', configListener);
request.addEventListener('error', configFailed);
request.open('GET', './assets/config/config.json');
request.send();
This code:
kicks off an async request for the config.json file.
When the request completes, parses the JSON into a Javascript object
provides the value using the APP_CONFIG injection token, prior to bootstrapping.
And finally bootstraps the root module.
APP_CONFIG can then be injected into any additional providers in app-module.ts and it will be defined. For example, I can initialise the FIREBASE_OPTIONS injection token from #angular/fire with the following:
{
provide: FIREBASE_OPTIONS,
useFactory: (config: IConfig) => config.firebaseConfig,
deps: [APP_CONFIG]
}
I find this whole thing a surprisingly difficult (and hacky) thing to do for a very common requirement. Hopefully in the near future there will be a better way, such as, support for async provider factories.
The rest of the code for completeness...
In app/lib/angular/injection-tokens.ts:
import { InjectionToken } from '#angular/core';
import { IConfig } from '../config/config';
export const APP_CONFIG = new InjectionToken<IConfig>('app-config');
and in app/lib/config/config.ts I define the interface for my JSON config file:
export interface IConfig {
name: string;
version: string;
instance: string;
firebaseConfig: {
apiKey: string;
// etc
}
}
Config is stored in assets/config/config.json:
{
"name": "my-app",
"version": "#{Build.BuildNumber}#",
"instance": "localdev",
"firebaseConfig": {
"apiKey": "abcd"
...
}
}
Note: I use an Azure DevOps task to insert Build.BuildNumber and substitute other settings for different deployment environments as it is being deployed.
Poor man's configuration file:
Add to your index.html as first line in the body tag:
<script lang="javascript" src="assets/config.js"></script>
Add assets/config.js:
var config = {
apiBaseUrl: "http://localhost:8080"
}
Add config.ts:
export const config: AppConfig = window['config']
export interface AppConfig {
apiBaseUrl: string
}
Here's my solution, loads from .json to allow changes without rebuilding
import { Injectable, Inject } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Location } from '#angular/common';
#Injectable()
export class ConfigService {
private config: any;
constructor(private location: Location, private http: Http) {
}
async apiUrl(): Promise<string> {
let conf = await this.getConfig();
return Promise.resolve(conf.apiUrl);
}
private async getConfig(): Promise<any> {
if (!this.config) {
this.config = (await this.http.get(this.location.prepareExternalUrl('/assets/config.json')).toPromise()).json();
}
return Promise.resolve(this.config);
}
}
and config.json
{
"apiUrl": "http://localhost:3000/api"
}
Quite a few articles are recommending that you get Angular config settings using an AppConfigService such as this one.
But I found that sometimes this just didn't work.
It was simpler, and more reliable to have a "config.json" file, then create a class which just read it in, and returned a value, eg my config file would look like this:
{
"appName": "Mike's app",
"version": "1.23.4",
"logging_URL" : "https://someWebsite.azurewebsites.net/logs"
}
And I'd access the values using this:
import config from '../../assets/config.json';
#Injectable({
providedIn: 'root'
})
export class AppConfigService {
get appName() {
return config.appName;
}
get appVersion() {
return config.version;
}
get loggingUrl() {
return config.logging_URL;
}
}
(A few months later...)
After patting myself on the back for making a simpler Angular solution, I realised this has a big drawback. If you use an AppConfigService, and you're using CI/CD, then you can get your build process to update the config .json file, and the Angular app will use these settings.
With my version, yes, it's simpler, but there is no config setting file to overwrite. For automated build processes, this might not be desirable.
Here's my two solutions for this
1. Store in json files
Just make a json file and get in your component by $http.get() method. If I was need this very low then it's good and quick.
2. Store by using data services
If you want to store and use in all components or having large usage then it's better to use data service. Like this :
Just create static folder inside src/app folder.
Create a file named as fuels.ts into static folder. You can store other static files here also. Let define your data like this. Assuming you having fuels data.
__
export const Fuels {
Fuel: [
{ "id": 1, "type": "A" },
{ "id": 2, "type": "B" },
{ "id": 3, "type": "C" },
{ "id": 4, "type": "D" },
];
}
Create a file name static.services.ts
__
import { Injectable } from "#angular/core";
import { Fuels } from "./static/fuels";
#Injectable()
export class StaticService {
constructor() { }
getFuelData(): Fuels[] {
return Fuels;
}
}`
Now You can make this available for every module
just import in app.module.ts file like this and change in providers
import { StaticService } from './static.services';
providers: [StaticService]
Now use this as StaticService in any module.
That's All.
We had this problem years ago before I had joined and had in place a solution that used local storage for user and environment information. Angular 1.0 days to be exact. We were formerly dynamically creating a js file at runtime that would then place the generated api urls into a global variable. We're a little more OOP driven these days and don't use local storage for anything.
I created a better solution for both determining environment and api url creation.
How does this differ?
The app will not load unless the config.json file is loaded. It uses factory functions to create a higher degree of SOC. I could encapsulate this into a service, but I never saw any reason when the only similarity between the different sections of the file are that they exist together in the file. Having a factory function allows me to pass the function directly into a module if it's capable of accepting a function. Last, I have an easier time setting up InjectionTokens when factory functions are available to utilize.
Downsides?
You're out of luck using this setup (and most of the other answers) if the module you want to configure doesn't allow a factory function to be passed into either forRoot() or forChild(), and there's no other way to configure the package by using a factory function.
Instructions
Using fetch to retrieve a json file, I store the object in window and raise a custom event. - remember to install whatwg-fetch and add it to your polyfills.ts for IE compatibility
Have an event listener listening for the custom event.
The event listener receives the event, retrieves the object from window to pass to an observable, and clears out what was stored in window.
Bootstrap Angular
-- This is where my solution starts to really differ --
Create a file exporting an interface whose structure represents your config.json -- it really helps with type consistency and the next section of code requires a type, and don't specify {} or any when you know you can specify something more concrete
Create the BehaviorSubject that you will pass the parsed json file into in step 3.
Use factory functions to reference the different sections of your config to maintain SOC
Create InjectionTokens for the providers needing the result of your factory functions
-- and/or --
Pass the factory functions directly into the modules capable of accepting a function in either its forRoot() or forChild() methods.
-- main.ts
I check window["environment"] is not populated before creating an event listener to allow the possiblilty of a solution where window["environment"] is populated by some other means before the code in main.ts ever executes.
import { enableProdMode } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { configurationSubject } from './app/utils/environment-resolver';
var configurationLoadedEvent = document.createEvent('Event');
configurationLoadedEvent.initEvent('config-set', true, true);
fetch("../../assets/config.json")
.then(result => { return result.json(); })
.then(data => {
window["environment"] = data;
document.dispatchEvent(configurationLoadedEvent);
}, error => window.location.reload());
/*
angular-cli only loads the first thing it finds it needs a dependency under /app in main.ts when under local scope.
Make AppModule the first dependency it needs and the rest are done for ya. Event listeners are
ran at a higher level of scope bypassing the behavior of not loading AppModule when the
configurationSubject is referenced before calling platformBrowserDynamic().bootstrapModule(AppModule)
example: this will not work because configurationSubject is the first dependency the compiler realizes that lives under
app and will ONLY load that dependency, making AppModule an empty object.
if(window["environment"])
{
if (window["environment"].production) {
enableProdMode();
}
configurationSubject.next(window["environment"]);
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
}
*/
if(!window["environment"]) {
document.addEventListener('config-set', function(e){
if (window["environment"].production) {
enableProdMode();
}
configurationSubject.next(window["environment"]);
window["environment"] = undefined;
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
});
}
--- environment-resolvers.ts
I assign a value to the BehaviorSubject using window["environment"] for redundancy. You could devise a solution where your config is preloaded already and window["environment"] is already populated by the time any of your Angular's app code is ran, including the code in main.ts
import { BehaviorSubject } from "rxjs";
import { IConfig } from "../config.interface";
const config = <IConfig>Object.assign({}, window["environment"]);
export const configurationSubject = new BehaviorSubject<IConfig>(config);
export function resolveEnvironment() {
const env = configurationSubject.getValue().environment;
let resolvedEnvironment = "";
switch (env) {
// case statements for determining whether this is dev, test, stage, or prod
}
return resolvedEnvironment;
}
export function resolveNgxLoggerConfig() {
return configurationSubject.getValue().logging;
}
-- app.module.ts - Stripped down for easier understanding
Fun fact! Older versions of NGXLogger required you to pass in an object into LoggerModule.forRoot(). In fact, the LoggerModule still does! NGXLogger kindly exposes LoggerConfig which you can override allowing you to use a factory function for setup.
import { resolveEnvironment, resolveNgxLoggerConfig, resolveSomethingElse } from './environment-resolvers';
import { LoggerConfig } from 'ngx-logger';
#NgModule({
modules: [
SomeModule.forRoot(resolveSomethingElse)
],
providers:[
{
provide: ENVIRONMENT,
useFactory: resolveEnvironment
},
{
provide: LoggerConfig,
useFactory: resolveNgxLoggerConfig
}
]
})
export class AppModule
Addendum
How did I solve the creation of my API urls?
I wanted to be able to understand what each url did via a comment and wanted typechecking since that's TypeScript's greatest strength compared to javascript (IMO). I also wanted to create an experience for other devs to add new endpoints, and apis that was as seamless as possible.
I created a class that takes in the environment (dev, test, stage, prod, "", and etc) and passed this value to a series of classes[1-N] whose job is to create the base url for each API collection. Each ApiCollection is responsible for creating the base url for each collection of APIs. Could be our own APIs, a vendor's APIs, or even an external link. That class will pass the created base url into each subsequent api it contains. Read the code below to see a bare bones example. Once setup, it's very simple for another dev to add another endpoint to an Api class without having to touch anything else.
TLDR; basic OOP principles and lazy getters for memory optimization
#Injectable({
providedIn: 'root'
})
export class ApiConfig {
public apis: Apis;
constructor(#Inject(ENVIRONMENT) private environment: string) {
this.apis = new Apis(environment);
}
}
export class Apis {
readonly microservices: MicroserviceApiCollection;
constructor(environment: string) {
this.microservices = new MicroserviceApiCollection(environment);
}
}
export abstract class ApiCollection {
protected domain: any;
constructor(environment: string) {
const domain = this.resolveDomain(environment);
Object.defineProperty(ApiCollection.prototype, 'domain', {
get() {
Object.defineProperty(this, 'domain', { value: domain });
return this.domain;
},
configurable: true
});
}
}
export class MicroserviceApiCollection extends ApiCollection {
public member: MemberApi;
constructor(environment) {
super(environment);
this.member = new MemberApi(this.domain);
}
resolveDomain(environment: string): string {
return `https://subdomain${environment}.actualdomain.com/`;
}
}
export class Api {
readonly base: any;
constructor(baseUrl: string) {
Object.defineProperty(this, 'base', {
get() {
Object.defineProperty(this, 'base',
{ value: baseUrl, configurable: true});
return this.base;
},
enumerable: false,
configurable: true
});
}
attachProperty(name: string, value: any, enumerable?: boolean) {
Object.defineProperty(this, name,
{ value, writable: false, configurable: true, enumerable: enumerable || true });
}
}
export class MemberApi extends Api {
/**
* This comment will show up when referencing this.apiConfig.apis.microservices.member.memberInfo
*/
get MemberInfo() {
this.attachProperty("MemberInfo", `${this.base}basic-info`);
return this.MemberInfo;
}
constructor(baseUrl: string) {
super(baseUrl + "member/api/");
}
}
I find this Angular How-to: Editable Config Files from Microsoft Dev blogs being the best solution. You can configure dev build settings or prod build settings.
If running in a container, this article https://pumpingco.de/blog/environment-variables-angular-docker/ shows how to use envsubst on the entry point command to fix this static compile issue on the fly via environment variable.

Using generated Swagger TypeScript Rest Client in Angular2

I am working on a Meteor web application using Angular 2 and TypeScript. For using a REST API, I have generated client code with Swagger Codegen. Unfortunately there is no sample on GitHub, how to use the rest client.
I have an angular 2 view component which is injecting an Angular 2 service (ProductTreeService) and the generated API (both marked as "#Injectable"):
#Component({
selector: 'panel-product-selection',
template,
providers: [ProductTreeService, UserApi],
directives: [ProductTreeComponent]
})
export class PanelProductSelectionComponent
{
private categoriesProductsTree: Collections.LinkedList<CategoryTreeElement>;
private productTreeService: ProductTreeService;
private userApi: UserApi;
constructor(productTreeService: ProductTreeService, userApi: UserApi)
{
this.productTreeService = productTreeService;
this.userApi = userApi;
}
ngOnInit(): void
{
//...
}
}
While the angular service only is accessable and all is working fine, the application is crashing when I inject UserApi. The only difference between UserApi and ProductTreeService is the constructor: The service has no constructor parameters, the generated Api class has:
constructor(protected http: Http, #Optional()#Inject(BASE_PATH) basePath: string) {
if (basePath) {
this.basePath = basePath;
}
}
So how can I inject the generated API?
After further researches I got the solution. The constructor of the rest client UserApi is expecting an HTTP provider as following:
constructor(protected http: Http, #Optional()#Inject(BASE_PATH) basePath: string) {
if (basePath) {
this.basePath = basePath;
}
}
Coming from Java, I thought maybe this provider has to be initialized somehow in the constructor which is injecting this API. In actual fact, this initialization has to be done in the NgModule containing the injecting component, as described in this thread.
This solution worked for me.
I would use the environment.ts file with a property for the url:
export const environment = {
production: false,
basePath: 'https://virtserver.swaggerhub.com/user/project/1.0.0'
};
And then in the app.module you provide the config to the service:
providers: [
{ provide: BASE_PATH, useValue: environment.basePath },
],
This works with Swagger generated code.
Hope that helps! (updated to Angular6+)
https://blog.angulartraining.com/how-to-manage-different-environments-with-angular-cli-883c26e99d15
Old question but ... that's how you should do it:
providers: [
{
provide: API_BASE_URL, // or BASE_PATH
useFactory: () => {
return environment.apiBaseUrl // or return just "your base url as text"
}
}]

Categories