Change Detection with property reference - javascript

I cannot seem to figure out how to get my template to update. When I change a boolean property which is referenced in another array property, I would expect that my changes would change within the template. However, I am not seeing the changes.
When the app loads everything is loaded in its initial state (false: Login is visible and Logout is hidden), but when the isLogged boolean changes the navigation doesn't update to hide/show the correct item.
I think the issue is how Angular handles change detection on objects/arrays, but I am not sure.
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
public isLogged: boolean = false;
public navigation: INav = {
links: [
{
text: 'Login'
hidden: !this.isLogged
},
{
text: 'Logout'
hidden: this.isLogged
}
]
}
public ngOnInit(): void {
// Triggered whenever the login state changes
this.authService.loginState().subscribe(state => {
this.isLogged = state;
});
}
}
<third-party-nav [model]="navigation"></third-party-nav>

This problem is not related to angular's change detection.
The problem in your code is that you set the navigation only once when the component is created (an therefore the variable isLogged is false). Instead you should update it when the loginState changes. You can do this by using observables/rxjs:
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
public navigation: Observable<INav>;
public ngOnInit(): void {
// When the login state changes the navigation object also changes
this.navigation = this.authService.loginState().map(s => ({
links: [
{
text: 'Login',
hidden: !s
},
{
text: 'Logout',
hidden: s
}
]
}));
}
}
Then in your html you can use the async pipe to handle the observable
<third-party-nav [model]="navigation | async"></third-party-nav>

Related

Expression Changed After it has Been Checked When Using #HostBinding

I created a breadcrumb component, I have a service BreadcrumbService that has a function that reads the url path and converts them to an array of segments (this.breadService.getUrlPathSegments()). When breadcrumbs is loaded or updated, I get the following error:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'click-enabled': 'true'. Current value: 'false'.
What is the correct way to handle this? The code works the way I intended, but I need to handle the error message.
What I am trying to do is disable click events on the last item in the breadcrumb list, so when you click on it, none of the events fire. This all works even though I receive the error message.
What I am doing is when the view is checked, update the value of each breadcurmb's clickable state. This is done just like this:
#Component({
selector: 'breadcrumbs',
styleUrls: ['./breadcrumbs.component.scss'],
template: `
<ng-content select="breadcrumb"></ng-content>
`,
encapsulation: ViewEncapsulation.None
})
export class Breadcrumbs implements AfterViewChecked {
#Input() disableLast = true;
#ContentChildren(Breadcrumb, { descendants: false })
breadcrumbs!: QueryList<Breadcrumb>;
ngAfterViewChecked() {
this.enableDisableLast();
}
enableDisableLast() {
if (this.breadcrumbs && this.breadcrumbs.length > 0) {
this.breadcrumbs.forEach(item => { item.clickable = true; });
this.breadcrumbs.last.clickable = !this.disableLast;
}
}
}
Next in the breadcrumb I have a #HostBinding(), that updates the class of the element. Which is done like this:
#Component({
selector: 'breadcrumb',
styleUrls: ['./breadcrumb.component.scss'],
template: `
<button>{{label}}</button>
`
})
export class Breadcrumb {
#HostBinding('class.click-enabled')
get clickEnabled() { return this.clickable; }
}
I then combine the two in the component that I am using them with a forEach to create the child breadcrumbs. I also listen for navigation changes to re-generate the array of breadcrumb segments to keep the breadcrumb display up-to-date with the current path.
#Component({
selector: 'app-root',
templateUrl: `
<breadcrumbs>
<breadcrumb *ngFor="let crumb of breadcrumbs" [label]="crumb.label|titlecase" [routerLink]="crumb.uri"></breadcrumb>
</breadcrumbs>
`,
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
breadcrumbs: BreadcrumbSegment[] = [];
constructor(
private router: Router,
private breadService: BreadcrumbService
) { }
ngOnInit() {
this.router.events.subscribe(val => {
if (val instanceof NavigationEnd) {
// Returns an array formatted as: {label:string; uri:string;}[]
this.breadcrumbs = this.breadService.getUrlPathSegments();
}
});
}
}
I am not sure if this is the optimal solution, but it is working for my needs. When the Breadcrumbs component view is initialized, I set the QueryList to dirty, then pipe a delay before I subscribe to the changes. This stops the error from showing up and runs the change detection.
#Component({...})
export class Breadcrumbs implements AfterViewChecked {
ngAfterViewInit() {
// Set to dirty so the changes emit at least one time.
this.breadcrumbs.setDirty();
this.breadcrumbs.changes.pipe(delay(1)).subscribe(() => {
this.enableDisableLast();
});
}
}

custom if directive does not show expected results

in this tutorial
https://www.sitepoint.com/practical-guide-angular-directives/
i am learning how to create a customised directive. i followed the steps as shown in the code posted below, but despite added the exact code as explained in the aforemenrtioned website, when i run the command
ng serve --open
i get something as shown in the image posted below.
please let me know why myCustomIf is not working. i say that myCustomIf is not working because what i got on the localhost:4200 is something as shown in the image posted
please let me know how to make the myCustomIf works as explained in the tutorial in the above posted link
app.component.ts:
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'ngDirective1';
name = 'Angular';
condition = false;
}
app.myCustomeIfDirective.ts:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[myCustomIf]'
})
export class MyCustomeIfDirective{
constructor(private templateRef: TemplateRef<any>,private viewContainer: ViewContainerRef){ }
#Input()
setMyCustomIf(condition : boolean) {
if(condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
app.module:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[myCustomIf]'
})
export class MyCustomeIfDirective{
constructor(private templateRef: TemplateRef<any>,private viewContainer: ViewContainerRef){ }
#Input()
setMyCustomIf(condition : boolean) {
if(condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
app.component.html:
<h1 my-error>Hello {{name}}</h1>
<h2 *myCustomIf="condition">Hello {{name}}</h2>
<button (click)="condition = !condition">Click</button>
image:
If you open console it should show smth like:
NG0303: Can't bind to 'myCustomIf' since it isn't a known property of
'h2'
An Angular structural directive, that is written in a short syntax(with *) and that takes one input or more inputs, must have an #Input with the same name as directive's attribute selector(other inputs follow another rule described here What is the exact grammar for Angulars structural directives), e.g.:
#Directive({
selector: '[anyAttr]'
})
export class MyCustomeIfDirective{
#Input()
anyAttr: any;
or
#Directive({
selector: '[anotherAttr]'
})
export class MyCustomeIfDirective{
#Input()
set anotherAttr(val: any) {}
Why is it so?
That's because *ngIf is just a shortcut for expanded version:
<ng-template [ngIf]="...">...
or
*anyAttr => <ng-template [anyAttr]="...">...
Now, let's look at your code:
#Directive({
selector: '[myCustomIf]'
})
export class MyCustomeIfDirective{
#Input()
setMyCustomIf(condition : boolean) {
Several things to notice:
setMyCustomIf is just a method in your case
if you convert it to a setter set MyCustomIf then MyCustomIf doesnt match myCustomIf because js is case-sensitive.
The solution is:
#Input()
set myCustomIf(condition : boolean) {
Ng-run Example
in your directive (app.myCustomeIfDirective.ts), you need to match the name of your input to the name of the directive (because the condition is passed with that attribute):
#Input("myCustomIf")
set myCustomIf(condition : boolean) {
if(condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
(note you can also change the name of the function to match the directive name)
stackblitz demo

Angular #Input() Not Updating UI in Child

I've implemented a child component to render a table based on a list provided via #Input(). The data is loaded via http, however the UI (child component) is not updated unless I wave my mouse over the screen. I've seen people post about implementing ngOnChanges() in my child, but I thought Angular was supposed to do this by default? Am I missing something? Why would the UI not update with this?
Child code looks something like this:
child.component.ts
#Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss'],
})
export class ChildComponent implements {
#Input() data: any[] = [];
constructor() {}
}
child.component.html
<table>
<tr *ngFor="let item of data"><td>{{ item }}</td></tr>
</table>
Parent code that uses the component looks something like this:
parent.component.ts
#Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss'],
})
export class ParentComponent implements OnInit {
data: string[] = [];
constructor(private endpointService: EndpointService) {}
ngOnInit() {
// response is a string array like: ['hello', 'world']
this.endpointService.loadData().subscribe((response) => {
this.data = response;
});
}
}
parent.component.html
<child [data]="data"></child>
============================= EDIT ==================================
I verified that it only fails to load when updating inside of the subscribe callback (if I set a static array, it loads just fine).
So it looks like I'm able to resolve this by running changeDetectorRef.detectChanges() in the parent component, but this feels hackish like I shouldn't have to do this. Is this a good way to resolve this? Or does this indicate something wrong with my implementation?
parent.component.ts
#Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss'],
})
export class ParentComponent implements OnInit {
data: string[] = [];
constructor(private endpointService: EndpointService,
private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit() {
// response is a string array like: ['hello', 'world']
this.endpointService.loadData().subscribe((response) => {
this.data = response;
this.changeDetectorRef.detectChanges();
});
}
}
You can also try to force change detection by forcing the value reference update via, for example, the spread operator:
this.endpointService.loadData().subscribe((response) => {
this.data = [...response];
});
hummm well .. when the component is rendered as first time it will show with the empty array becouse the api call stills happening and needs the onchanges method in child component in order to listen the complete api call and the list will re render
Seems that you have some other errors in template expressions which force the whole template to fail. Here's a stackblitz I've created and everything works: https://stackblitz.com/edit/angular-ivy-w2ptbb?file=src%2Fapp%2Fhello.component.ts
Do you have maybe some errors in console?
I replaced the service with a static string array and it worked well. I think there is problem with the observable subscription.
child.component.ts
import { Component, OnInit,Input } from '#angular/core';
#Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
#Input() data: any[] = [];
constructor() { }
ngOnInit() {
}
}
parent.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.css'],
})
export class ParentComponent implements OnInit {
data: string[] = [];
constructor() {}
ngOnInit() {
this.data = ['hello', 'world','aaa','ddd'];
}
}

Access text (not instance of another component) with ContentChild

How can I access a string of text given within the tags of a component
<my-custom-component>THIS TEXT</my-custom-component>
Within a template, I can use ng-content, or if it is an instance of some other class I can access it within the component definition like demonstrated in these examples. However I am interested in detecting if there is a string of text there or not, which I believe would make providedText undefined. However, I am always getting undefined.
#ContentChild(Element, { static: true }) providedText: Text | undefined;
I have tried Text as the first element passed to #ContentChild. Passing any will not work (I don't know why).
StackBlitz
I am interested mostly in finding if there is a string or undefined, but am also curious why ContentChild(Text... isn't working.
Edit:
I have added a potential solution, but it seems pretty imperfect, so I hope something better comes along.
Edit 2:
I now understand that #ContentChild is not a mechanism for selecting whatever native HTML I want without wiring it up to Angular’s dependency graph with a ref, directive, etc.
I am still curious if my proposed solution below is a bad idea for any reason.
My solution for now (since I wish to capture all transcluded content) is to wrap ng-content in a containing element, then get its innerText.
#Component({
selector: "app-parent",
template: `
<span #transcludedContainerRef>
<ng-content></ng-content>
</span>
`
})
export class ParentComponent implements AfterViewInit {
#ViewChild("transcludedContainerRef", { static: false })
transcludedContainerRef: ElementRef | undefined;
buttonText: string;
ngAfterViewInit() {
const isButtonTextPresent = this.transcludedContainerRef.nativeElement
.innerText;
if (isButtonTextPresent) {
console.log(isButtonTextPresent); // successfully logs content
}else {
console.log('No text set');
}
}
}
It does feel hacky, but it works. I am holding out for something better.
it's difficult if I don't know about your <my-custom-component>
In general if your custom component it's only
<ng-content></ng-content>
You can inject in constructor the elementRef
constructor(public el:ElementRef){}
From a parent
<hello >
Start editing to see some magic happen :)
</hello>
You can use
#ViewChild(HelloComponent,{static:false}) helloComponent:HelloComponent
click()
{
console.log(this.helloComponent.el.nativeElement.innerHTML)
}
If your component has any variable -or ViewContent-, you can access this variables in a similar way
So the other way to read the inner text from the component is that child component emit the value whatever it get's as input from other component. See below:
hello.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '#angular/core';
#Component({
selector: 'hello',
template: `<h1>Hello {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent implements OnInit {
#Input() name: string;
#Output() innerText: EventEmitter<string> = new EventEmitter();
ngOnInit() {
this.innerText.emit(this.name);
}
}
app.component.ts
import { Component, ContentChild, AfterContentInit, OnInit } from "#angular/core";
#Component({
selector: "app-parent",
template: "content from <code>app-parent</code>"
})
export class ParentComponent implements AfterContentInit {
#ContentChild(Element, { static: true }) providedText: Text | undefined;
ngAfterContentInit() {
console.log("ngAfterContentInit Content text: ", this.providedText);
}
}
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
name = "Angular";
_innerText: string;
ngAfterContentInit() {}
get childContent(): string {
return this._innerText;
}
set childContent(text) {
this._innerText = text;
}
innerTextFn(innertext: string) {
this.childContent = innertext;
console.log('Event: ', innertext);
}
}
app.component.html
<hello name="{{ name }}" (innerText)="innerTextFn($event)"></hello>
<app-parent>This is the content text</app-parent>
Here is stackblitz url to check: https://stackblitz.com/edit/angular-bacizp
I hope this may helpful for you and if yes then accept this as correct answer.

Angular's `cdRef.detectChanges` doesn't prevent checked exception?

I have a situation where I have an inner component :
#Component({
selector: 'hello',
template: `<h1>Name = {{name}}!</h1> `
})
export class HelloComponent {
#Input() name: string;
#Output() ev: EventEmitter<string> = new EventEmitter<string>();
constructor(private cdRef: ChangeDetectorRef) { }
ngOnInit() {
this.ev.emit("new name");
this.cdRef.detectChanges();
}
}
Where in the parent component :
<hello name="{{ name }}" (ev)="changeName($event)"></hello>
#Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent {
name = 'Angular 5';
changeName(e) {
this.name = e;
console.log(e)
}
}
So basically , when the inner component loads , it emits an event which in turn received by parent and re-set the value of its inner component.
But this code ( and I do know why) - yields an exception :
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has
changed after it was checked. Previous value: 'Angular 5'. Current
value: 'new name'.
But according to other answers in SO:
This line : this.cdRef.detectChanges(); should have caused a new detection and prevent the exception.
I already know that setTimeout()... does solve it in a hacky way.
Question:
Why doesn't detectChanges prevent the exception ?
Stackblitz

Categories