I have a component which is part of a lazy load module.
Is there a way to matDialog.open() and lazy load the module and show the component?
export class testComponent implements OnInit {
constructor(
public matDialog: MatDialog,
private moduleLoader: NgModuleFactoryLoader
) {}
ngOnInit() {}
openModal() {
this.moduleLoader
.load("./modules/test-modal/test-modal.module#TestModalModule")
.then((module: NgModuleFactory<any>) => {
this.matDialog.open(/*insert component and load the module*/);
});
}
}
I found an example to lazy load module with component in mat-dialog.
Please see refer to:
https://medium.com/ngconf/routing-to-angular-material-dialogs-c3fb7231c177
Just in case the link is no longer available, i'd included a brief step and example to do it
1. Create a lazy load module
2. Create entry component(empty component) to launch your modal component
#Component({
template: ''
})
export class DialogEntryComponent {
constructor(public dialog: MatDialog, private router: Router,
private route: ActivatedRoute) {
this.openDialog();
}
openDialog(): void {
const dialogRef = this.dialog.open(DialogOverviewExampleDialog, {
width: '250px'
});
dialogRef.afterClosed().subscribe(result => {
this.router.navigate(['../'], { relativeTo: this.route });
});
}
}
3. Create a route for the lazy load module
const routes: any = [
{
path: "",
component: modalComponent(actual component with content)
}
];
#NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [DataResolver]
})
export class DialogEntryRoutingModule {}
4. At parent router module, include path to lazy load DialogEntryModule
RouterModule.forRoot([
{
path: 'home',
component: ParentComponent,
children: [
{
path: 'dialog',
loadChildren:
"../example/entry-dialog.module#DialogEntryModule"
}
]
},
{ path: '**', redirectTo: 'home' }
])
5. in ParentComponent open the modal by directing to the DialogEntryModule
<button mat-raised-button routerLink="dialog">Pick one</button>
Another alternative is to stick the mat dialog component in another module that has a route, assuming it isn't used by any other module.
For example, if you have app.module and a projects.module, and you have a mat dialog that displays project details, you could include the project details dialog component inside of projects.module instead of creating a separate module for it. The dialog code will load when the user navigates to the projects view.
#nicker's answer runs into issues when you close the dialog. This reloads the parent component and in some cases, you don't want the parent component view to be refreshed.
Related
How to prevent MatDialog from closing when clicked outside but in all the dialogs in my Angular application ? I also realized that the escape key is not closing the dialog after setting disableClose to true so i added a hostlistener to force the close but it's not really the best solution...
For a specific component I can do this.
export class MyAppComponent {
constructor(private dialog: MatDialog){}
open() {
this.dialog.open(ConfirmComponent, { disableClose: true });
}
#HostListener('window:keyup.esc') onKeyUp() {
this.dialogRef.close();
}
}
but how to do it globally for all the dialogs in my application instead of doing it in each dialog component and also apply the hostlistener?
You can define global setting in provider
import { MAT_DIALOG_DEFAULT_OPTIONS } from '#angular/material/dialog';
#NgModule({
providers: [
{ provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { disableClose: true }}
]
})
My application requires a brand code to determine the style and dom.
currently the on load my URL would be www.SiteName.com/HBL (HBL = brandName)
It is a simple site where it has the only header, footer, search component.
but I need to get the Brand info from service api.
So in Appcomponent.ts, I injected ActivatedRoute and in the ngOnInit method, I subscribed paramMap.
When I load the app I am getting null parameter value.
This what I have done
my app.compnent.html:
<div class="container">
<header [brand]="brand"></header>
<!-- <esw-search></esw-search> -->
<router-outlet></router-outlet> - Search will be populated thru route
<esw-footer></esw-footer>
</div>
I could have avoided router but sometimes the search page will be directly accessible.
like www.SiteName.com/HBL/search?trackingnumber=123456;language=en
my routing component:
import { NgModule } from '#angular/core';
import { RouterModule, Routes } from '#angular/router';
import { NotFoundComponent } from './notfound/notfound.component';
import { SearchComponent } from './tracking-search/search/search.component';
const routes: Routes = [
{ path: '', component: SearchComponent },
{ path: ':brandName/search', component: SearchComponent },
{ path: ':brandName/', component: SearchComponent },
{ path: '404', component: NotFoundComponent },
{ path: '**', redirectTo: '404' }
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
my appcomponent.ts code:
#Component({
selector: 'esw-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'logistics-tracking-ui';
apiUrl: string;
brand: Brand;
constructor(
private tracking: TrackingService,
private route: ActivatedRoute) {
}
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
const brandName = params.get('brandName');
this.tracking.getBrandData(brandName).subscribe((response) => this.brand = response);
});
}
}
}
SearchComponent.htm:
<div class="card-body">
<div class="card mx-auto">
<div class="card-body">
<h3 style=" text-align: center"> Track your International Package</h3>
<div>
<span class="ui-float-label">
<input [(ngModel)]="trackingNumber" id="float-input" type="text" size="30" pInputText>
<label for="float-input">Tracking Number</label>
</span>
<button pButton type="button" label="Click" (click)="searchTracking()"></button>
</div>
<esw-search-details [trackingDetails]='trackingDetails$'></esw-search-details>
</div>
</div>
</div>
searchComponent.ts:
#Component({
selector: 'esw-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit {
trackingNumber = '';
trackingDetails$: Observable<any>;
constructor(private trackingservice: TrackingService) { }
ngOnInit() {
}
searchTracking(): void {
alert('Search Clicked' + this.trackingNumber);
if (!this.trackingNumber.trim()) {
// if not search term, return empty hero array.
// Publish error message
console.log('Invalid Input');
return;
}
this.trackingDetails$ = this.trackingservice.getTrackingDetails(this.trackingNumber, 'en-GB');
}
Note: I have not added much logic to search & serachDetails component.
The issue's I have:
Access brand params value in App component.
Is this right approach to defining layout in app.coponent.html?
Is there any better approach I can use for this?
Sorry this is my first angular project, any help will be appriciated.
May be you need to add a route for the param and that has to be added as the first in the list of routes, like
const routes: Routes = [
{ path: ':brandName/:brand', component: SearchComponent },
{ path: ':brandName/search', component: SearchComponent },
{ path: ':brandName/', component: SearchComponent },
{ path: '404', component: NotFoundComponent },
{ path: '', component: SearchComponent },
{ path: '**', redirectTo: '404' }
];
and now in the app component we can access it like:-
this.route.paramMap.subscribe(params => {
const brandName = params['brand']
this.tracking.getBrandData(brandName).subscribe((response) => this.brand = response);
});
If you want to go the route you are with passing the the exports/imports, then you have to be careful of the asynchronous loading of JS. Assuming your api call, exports, and imports are set up correctly, the Api call is completed and the brand is filled after the header component is loaded, (verify by adding console log in the app component after the api call is completed. You'll see it logs after the header loads, making it inaccessible to the header component's ngOnInit method). So you can either prevent loading until you have the required element:
<header *ngIf="ReturnedObject.brand" [brand]="brand"></header>
Or you can load the element after the page is loaded with Ng life cycle hooks, such as
ngAfterContentInit(){}
(this is not a great option as your page will load with whatever default branding, then it will reload once the brand is updated)
my preferred method
You can use the "{{}}" notation to dynamically name your class of an element as needed, and instead of passing an export to load another component, set the class in the parent component, then load the child component:
(in your child css)
.texas {
background-image: texasFlag.png;
}
.newYork {
background-image: newYorkFlag.png;
}
(in your parent html)
<header class="{{ReturnedObject.brand}}"></header>
<your-child-component></your-child-component>
<footer class="{{ReturnedObject.brand}}"></footer>
That way, the class is already set by the parent before the child starts to load, taking away the "racing" your parent and child component are doing to load.
Is it possible to generate a html file from a component by bypassing all the data it needs without actually rendering it in the browser viewport?
I would like to just generate some html code to send it to the backend that generates a PDF from this html.
I don't think you can, since rendering of angular's components relies heavily on it's lifecycle hooks.
I can think of one way to fake it, though, by:
instantiating an invisible component from code
add it to the DOM, so it behaves like any regular component
retrieve it's HTML
and finally destroy it
Here's a working code example.
app.module.ts
Notice that i've added PdfContentComponent to the entryComponents of the module.
This is required for any component that you want to instantiate from code.
#NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, PdfContentComponent ],
bootstrap: [ AppComponent ],
entryComponents : [ PdfContentComponent ]
})
export class AppModule { }
pdf-content.component.html
<span>Hello, {{ name }}</span>
pdf-content.component.ts
Notice the host: {style: 'display: none'}, this renders the component effectivly invisible
#Component({
selector: 'my-pdf-content',
templateUrl: './pdf-content.component.html',
host: {
style: 'display: none'
}
})
export class PdfContentComponent implements OnInit {
#Input() name: string;
#Output() loaded: EventEmitter<void> = new EventEmitter<void>();
constructor(public element:ElementRef) {
}
ngOnInit() {
this.loaded.emit();
}
}
app.component.html
<button (click)='printPdf()'>Hit me!</button>
<ng-container #pdfContainer>
</ng-container>
app.component.ts
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
// the pdf content will be briefly added to this container
#ViewChild("pdfContainer", { read: ViewContainerRef }) container;
constructor(
private resolver: ComponentFactoryResolver
) {}
printPdf() {
// get the PdfContentComponent factory
const factory: ComponentFactory<any> = this.resolver.resolveComponentFactory(PdfContentComponent);
// instantiate a PdfContentComponent
const pdfContentRef = this.container.createComponent(factory);
// get the actual instance from the reference
const pdfContent = pdfContentRef.instance;
// change some input properties of the component
pdfContent.name = 'John Doe';
// wait for the component to finish initialization
// and delay the event listener briefly,
// so we don't run the clean up in the middle of angulars lifecycle pass
const sub = pdfContent.loaded
.pipe(delay(1))
.subscribe(() => {
// stop listening to the loaded event
sub.unsubscribe();
// send the html to the backend here
window.alert(pdfContent.element.nativeElement.innerHTML);
// remove the component from the DOM
this.container.remove(this.container.indexOf(pdfContentRef));
})
}
}
You can use Renderer2 class provided by angular. Try this sample code;
import { Component, Renderer2, OnInit } from '#angular/core';
....
constructor(private renderer: Renderer2){
}
ngOnInit(){
const div: HTMLDivElement = this.renderer.createElement('div');
const text = this.renderer.createText('Hello world!');
this.renderer.appendChild(div, text);
console.log(div.outerHTML);
}
Im trying to build a method inside a service that checks whether a navigation button should be showed to the current user based on his permissions or not (this is just cosmetic "security" I know). Therefore this is the button placed inside the template
<button [routerLink]="['/some/where']"
*ngIf="AuthService.isAuthorized(['some', 'where'])">
Personen
</button>
The method AuthService.isAuthorized uses the provided array to run through all available routes and get the required permissions from the particular route's data object:
{
path: 'some',
component: SomeComponent,
data: {
permissions: [
"read:some",
"edit:some"
]
},
children: [
{
path: 'where',
component: SomeComponent,
data: {
permissions: [
"read:where"
]
}
},
]
}
so in this case the permissions ["read:some","edit:some","read:where"] are needed by the current signed in user so that the button would be displayed to him. Working so far!
But since the function is called inside the template it is called multiple times because of angular change detection. How could I change my code so that the function is called only once? Even better if it would only be called once after the authentication finished writing all permissions assigned to the authenticated user into AuthService.permissions
You can make AuthService.isAuthorized() method returns a promise:
#injectable()
export class AuthService {
...
isAuthorized(arr: string[]): Promise<boolean> {
return new Promise(resolve =>{
// your logic here
resolve(yourResult);
});
}
...
}
You can call this method on your ngOnInit of a component (Therefore it will be called once). You pass the return value to a new variable (e.g. isAuthorized) in the component and use this variable in the template instead.
#Component({
selector: "your-component",
templateUrl: "yourTemplate.html"
})
export class YourComponent implements OnInit {
isAuthorized: boolean;
constructor(private authService: AuthService) {}
ngOnInit() {
this.authService.isAuthorized(['some', 'where']).then(result => {
this.isAuthorized = result;
});
}
}
In the template you can just use isAuthorized variable.
<button [routerLink]="['/some/where']"
*ngIf="isAuthorized">
Personen
</button>
Edit:
If AuthService.isAuthorized() needed to be called only once but for more than one element, code like these may suits your need:
#Component({
selector: "your-component",
templateUrl: "yourTemplate.html"
})
export class YourComponent {
isObjectAuthorized = {} as {
isFirstAuthorized: boolean;
isSecondAuthorized: boolean;
};
constructor(private authService: AuthService) {}
checkForAuthorization(isElementAuthorized, arr: string[]) {
if (isElementAuthorized !== undefined) {
return;
}
this.authService.isAuthorized(arr).then(result => {
isElementAuthorized = result;
});
}
}
And in your template:
<button [routerLink]="['/some/where']"
*ngIf="checkForAuthorization(isObjectAuthorized.isFirstAuthorized, ['some', 'where'])">
First
</button>
<button [routerLink]="['/some/where']"
*ngIf="checkForAuthorization(isObjectAuthorized.isSecondAuthorized, ['some', 'where', 'else'])">
Second
</button>
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://stackoverflow.com/a/35553650/3455681), 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'
}
]
}
};
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})]);
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.
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.
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:
angular2 bootstrap with data from ajax call(s)
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.