Angular2 functions in template and change detection - function

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>

Related

Angular - Dynamically load html that includes angular markups

In Angular 9+ I can successfully convert a string to a html and then load that that html using innerHtml and bypassSecurityTrustHtml().
My question is it possible to also dynamically load/render the converted html to include and recognise angular/javascript markup language eg *ngIf, handle bars and click events.
Below is the code and stackblitz at the attempt so far but as you can see it doesn't recognise the markup.
https://stackblitz.com/edit/dynamic-angular?file=app/app.component.ts
export class AppComponent implements OnInit {
text: string = "Hello world";
content: any;
constructor(private domSantizer: DomSanitizer) {}
ngOnInit() {
let body: any =
'<div>{{text}}<div><br><button (click)="test()">Test</button>';
this.content = this.domSantizer.bypassSecurityTrustHtml(body);
}
test() {
alert("It works");
}
}
Html
<div [innerHTML]="content"></div>
I have researched and tried many solutions.
My research and trial results are below.
html
<div #container></div>
typescript side as below
export class AppComponent implements OnInit {
#ViewChild("container", { read: ViewContainerRef })
container: ViewContainerRef;
constructor(private compiler: Compiler) {}
text: string = "asdasd";
ngOnInit() {
this.addComponent(
`<div>{{text}}<div><br><button (click)="test()">Test</button>
`,
{
text: "Hello word",
test: function() {
alert("It's work");
}
}
);
}
private addComponent(template: string, properties?: any = {}) {
#Component({ template })
class TemplateComponent {}
#NgModule({ declarations: [TemplateComponent] })
class TemplateModule {}
const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
const factory = mod.componentFactories.find(
comp => comp.componentType === TemplateComponent
);
const component = this.container.createComponent(factory);
Object.assign(component.instance, properties);
// If properties are changed at a later stage, the change detection
// may need to be triggered manually:
// component.changeDetectorRef.detectChanges();
}
demo
some posts I have reviewed
compile dynamic Component
angular-html-binding
I think it makes the most sense :)

Cannot Bind Dynamic Data in Component Angular 8

Error when component loading dynamic
DynamicBuilderComponent.ngfactory.js:198 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ng-pristine: true'. Current value: 'ng-pristine: false'.
Problem
after binding json in select2data to select2 component Angular throw exception.
component code
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'select2',
Imported changeDetection in component.
template: `
<div [formGroup]="form">
<ng-container>
<ng-select2
[data]="select2data"
[options]="options"
[width]="500"
[formControlName]="field.code"
(keyup)="changed($event.target.value)">
</ng-select2>
</ng-container>
</div>`
})
select2 component class
export class Select2Component implements OnInit {
#Input() field: any = {};
#Input() form: FormGroup;
public exampleData: Array<Select2OptionData>;
public options: Options;
public value: string[];
select2data: any;
public selected: string;
constructor(public cl: Services,private cd: ChangeDetectorRef) {
this.options = {
width: '258',
multiple: true,
tags: false
};
}
Problem Area After Binding subscribe data in ng select2 component
changed(search: any) {
//call service pass search text to service
return this.cl.searchFunc(search).subscribe(
res1 =>
this.select2data = res1.data;
this.cd.markForCheck(); // marks path
}
}
},
error => {
console.log('error = ', error);
});
}
}
i tried to print this.select2data in console.log its return me json.
Vendor.js
function expressionChangedAfterItHasBeenCheckedError(context, oldValue, currValue, isFirstCheck) {
var msg = "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '" + oldValue + "'. Current value: '" + currValue + "'.";
if (isFirstCheck) {
msg +=
" It seems like the view has been created after its parent and its children have been dirty checked." +
" Has it been created in a change detection hook ?";
}
return viewDebugError(msg, context);
}
Great Article
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
Reference
Expression ___ has changed after it was checked
any suggestion is most welcome.
I believe that you put your component select2 inside another component which contains a form which you then pass to select2 for create another <form> tag, is that correct? I mean do you have something like that?
<form [formGroup]="form">
<!-- Some code -->
<select2 [field]="something" [form]="form"></select2>
</form>
If so, then your select2 component SHOULD NOT contain re-declaration of form, it should not contain anything related to forms at all. It should be a form control. Please read a post by Netanel Basal on how to create custom form controls. You will need to create ControlValueAccessor for your select2 and wire it up to Angular forms through a custom provider.
The issue you're facing is that since you include form object twice in the DOM data changes are propagated twice as well and you run into issues. There should be only one reference to a specific instance of FormGroup in your templates.
Solution that worked
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'select2',
export class Select2Component implements OnInit {
constructor(public cl: Services,private cd: ChangeDetectorRef) {
this.options = {
width: '258',
multiple: true,
tags: false
};
}
Binding function
changed(search: any) {
//call service pass search text to service
return this.cl.searchFunc(search).subscribe(
res1 =>
this.select2data = res1.data;
this.cd.markForCheck(); // marks path
this.cd.detectChanges();
}
}
},
error => {
console.log('error = ', error);
});
}

lazy load module in matDialog

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.

Angular 2 edit cart total after remove a product

i'm stucked on a problem and i don't know how to get out of it.
I have two component sibiling:
One that show a list of products with a button for each product that delete from the cart the single product with a POST call at one REST API.
And another component that simple call a REST API for get the totals of the cart and show it.
The problem is that when i delete correctly the item from the cart, obviously the cart total doesn't update itself.
So, i've searched on the communities and i think that there are two different solutions:
Use a shared service;
Use #Input and #Output
I've tried using the first option, but without success, i tried also with #input and #Output but i don't think that i really understand how to use it between two components that aren't Parent > Child or opposite.
What i need is to call the function GetTotals inside the CARTTOTAL.COMPONENT from the CARTITEMS.COMPONENT for updating the prices.
I've tried to inject the same service in both components and call the function from the first one, but seems doesn't work.
Here the code:
cartitems.component.ts
import { Component, OnInit, Inject, Output } from '#angular/core';
import { ManageGuestCartService } from '../manageCartServices/addtoguestcart.service';
//import { CarttotalComponent } from '../carttotal/carttotal.component';
// Service for guest total cart
import { TotalguestcartService } from '../manageCartServices/totalguestcart.service';
#Component({
selector: 'app-cartitems',
templateUrl: './cartitems.component.html',
styleUrls: ['./cartitems.component.css'],
providers: [ManageGuestCartService, TotalguestcartService]
})
export class CartitemsComponent implements OnInit {
itemofcart:any[];
constructor(private _guestcartservice: ManageGuestCartService, private _totalguestcart: TotalguestcartService) { }
ngOnInit() {
this.listCartItems();
}
listCartItems() {
return this._guestcartservice.getCartDetails()
.subscribe(data => {
this.itemofcart = data;
//console.log(this.itemofcart);
},
(err) => {
//alert('vuoto');
});
}
removeProductFromCart(itemid) {
return this._guestcartservice.deleteProductFromCart(itemid)
.subscribe(data => {
// this.itemofcart = data;
// console.log(this.itemofcart);
this.listCartItems();
this._totalguestcart.getTotals();
},
(err) => {
alert('errore');
});
}
}
carttotals.component.ts
import { Component, OnInit, Input} from '#angular/core';
// Service for guest total cart
import { TotalguestcartService } from '../manageCartServices/totalguestcart.service';
#Component({
selector: 'app-carttotal',
templateUrl: './carttotal.component.html',
styleUrls: ['./carttotal.component.css'],
providers: [TotalguestcartService]
})
export class CarttotalComponent implements OnInit {
constructor(private _totalguestcart: TotalguestcartService) { }
totals:any[];
ngOnInit() {
this.retrieveTotals();
}
retrieveTotals() {
return this._totalguestcart.getTotals()
.subscribe(data => {
this.totals = data;
console.log(this.totals);
},
(err) => {
alert(err);
});
}
}
totalguestcart.service.ts
import { Injectable } from '#angular/core';
import { Http, Headers, RequestOptions} from '#angular/http';
#Injectable()
export class TotalguestcartService {
constructor(public http: Http) { }
public getTotals() {
let cartid;
cartid = localStorage.getItem("guestCartId");
let contentHeaders = new Headers();
contentHeaders.append('Accept', 'application/json');
contentHeaders.append('Content-Type', 'application/json');
return this.http.get(URL, { headers: contentHeaders})
//.map(res => res.json())
.map((res) => {
if(res.status == 404) {
return res.status;
} else {
return res.json();
}
});
}
}
Can someone give me the correct way to find a solution to this issue? all the feeds are accepted :)
Thanks in advance!
As mentioned, you need to mark your providers in the module, so that you share the same service between your components.
Shared Service would be best here. From my understanding you want to fire an event in the carttotals component, when deleting an item in the cartitems component.
Well we can set up Observable which will fire that event. So in your totalguestcart service add this:
private fireEvent = new Subject<boolean>();
event = this.fireEvent.asObservable();
emitEvent(bool: boolean) {
this.fireEvent.next(bool);
}
Here we are just using boolean values, as you do not need to pass any specific values, but only fire an event.
Then when you are performing the deletion, let's notify the other component, which subscribes to this, that the method should be fired.
removeProductFromCart(itemid) {
return this._guestcartservice.deleteProductFromCart(itemid)
.subscribe(data => {
this.itemofcart = data;
this.listCartItems();
this._totalguestcart.emitEvent(true); // add this!!
},
(err) => {
alert('error');
});
}
And in your cart totals, subscribe to this in your constructor, and execute the getTotals method:
constructor(private _totalguestcart: TotalguestcartService) {
_totalguestcart.event.subscribe(res => {
this.retrieveTotals();
})
}
this.retrieveTotals will then be fired each time you are deleting an item. Of course this can be used in other methods as well, like adding and updating (if you need it).
Hope this helps! :)
Throw out the service TotalguestcartService out of the providers of your components and put it into the providers of the app-module: What is happening: each component is getting a local copy of the service, so they cannot exchange information, as there are TWO services injected. Putting it global (app.module) provides it for every component as long as the component doesn't do an own provider.

http with Observable in Angular 2 cant use data

i am new to angular 2 and to observables but i wanted to give it a shot. So i have installed the angular-cli and made a simple test project.
All i wanted it to do is read a json file and work with the data inside of a component (the first intention was to make a service but i wanted to start on a low basis).
So i have created a json file in the assets/json folder (testjson.json):
{
"teststring": "test works"
}
then i have imported the http from angular and the rxjs map stuff inside of my content.component.ts file:
import { Component, OnInit } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/map';
#Component({
selector: 'app-content',
templateUrl: './content.component.html',
styleUrls: ['./content.component.css']
})
export class ContentComponent implements OnInit {
title: string = "Default";
data;
constructor(private http:Http) {
http.get('assets/json/testjson.json').map(res => res.json()).subscribe(data => {this.data = data; this.title = data.teststring; console.log(this.data);});
}
ngOnInit() {
}
}
So far so good, the app prints out the following:
app works!
test works [object Object]
But i want to use this data in the whole component, not only in the constructor. but if i try to console.log "this.data" outside of the constructor (inside the ngOnInit function), it prints undefined in the console.
I know, that it must have something to do with asynch loading but unfortunately i have no clue how to tell the app to wait until this.data is filled.
I hope you can help me with that. Of course in the future i want a service which does that kind of stuff and more than one component should grab data from it.
Thanks in advance!
You should move the initialization code to the initialization method.
Your data becomes available once the callback completes. In your template you can use *ngIf to execute code inside a block once there is data. As long as the *ngIf does not eval to true the inner code will not run.
The only way you can run console.log(data) is from inside the callback or called from the callback because you have to wait until the data is loaded.
content.component.html
<div *ngIf="data">
<span>{{data.teststring}}</span>
</div>
content.component.ts
export class ContentComponent implements OnInit {
title: string = "Default";
data: any = null;
constructor(private http:Http) {
}
ngOnInit() {
this.http.get('assets/json/testjson.json')
.map(res => res.json())
.subscribe(data => {
this.data = data;
this.title = data.teststring;
console.log(this.data);
});
}
}
Edit
In response to the comment below If you abstract out the http call to a service you can see the exact same logic still applies. You are still using the concept of a promise of data and that you can subscribe to that promise once it has completed. The only difference here is the http call is abstracted to a different class.
content.component.ts
export class ContentComponent implements OnInit {
title: string = "Default";
data: any = null;
// inject service
constructor(private contentService:ContentService) {
}
ngOnInit() {
this.contentService.getData()
.subscribe(data => {
this.data = data;
this.title = data.teststring;
console.log(this.data);
});
}
Service
export class ContentService {
constructor(private http:Http) {
}
getData(): IObservable<{teststring:string}> { // where string can be some defined type
return http.get('assets/json/testjson.json')
.map(res => res.json() as {teststring:string});
}