ChangeDetectionStrategy.OnPush doesn't work with Observable's error callback - javascript

I'm learning Angular's change detection and can't find an answer why the following snippet doesn't work.
Suppose we have a Component with ChangeDetectionStrategy.OnPush set:
#Component({
selector: 'material-app',
templateUrl: 'app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
public isBusy: boolean;
public message: any;
public version = VERSION;
constructor(
private readonly httpClient: HttpClient,
private readonly dialog: MatDialog) {
}
public onSeeBugClick(): void {
this.sendApiRequest();
this.dialog.open(DialogComponent, {
width: '250px'
});
}
private sendApiRequest(): void {
this.message = 'Sending request';
this.isBusy = true;
this.httpClient.get<void>('https://jsonplaceholder.typicode.com').pipe(
finalize(() => {
this.isBusy = false;
}))
.subscribe(
(): void => {
console.log('Success from jsonplaceholder.typicode.com');
this.message = 'Success';
},
(error: string): void => {
console.log('Error from jsonplaceholder.typicode.com');
this.message = 'Error '+ error.toString() + ': ' + JSON.stringify(error);
});
}
}
Here is StackBlitz.
When API request ends up with an error, neither ENABLE ME BACK button disabled state gets updated, nor message text below it do. But if I comment ChangeDetectionStrategy.OnPush line - it works fine.
Could anyone explain why ChangeDetectionStrategy.OnPush strategy doesn't work in this case?

Related

Cannot assign to read only property 'winner' of object '[object Object]'

I'm encountering the error
ContestDetailsModalComponent.html:21 ERROR TypeError: Cannot assign to read only property 'winner' of object '[object Object]'
at ContestDetailsModalComponent.onWinner (contest-details-modal.component.ts:88:24)
at Object.eval [as handleEvent] (ContestDetailsModalComponent.html:21:17)
at handleEvent (core.js:43993:77)
at callWithDebugContext (core.js:45632:1)
at Object.debugHandleEvent [as handleEvent] (core.js:45247:1)
at dispatchEvent (core.js:29804:1)
at core.js:42925:1
at HTMLButtonElement.<anonymous> (platform-browser.js:2668:1)
at ZoneDelegate.invokeTask (zone-evergreen.js:399:1)
at Object.onInvokeTask (core.js:39680:1)
when I try to make the assignment
this.contest.winner = winnerId;
you see in the following code snippet:
public onWinner(winnerId: number): void {
const post: Post = new Post();
post.title = `The winner of the contest has been elected: ${this.contest.title}`;
post.description = `Tizio has been elected winner of the contest and ${this.user.name} gives him the most welcome compliments`;
post.user = this.user;
post.contest = this.contest;
post.type = Type.WINNER;
post.level = Level.SUCCESS;
this.contest.winner = winnerId;
this.store.dispatch(PostActions.savePost({post}));
this.store.dispatch(ContestActions.saveContest({contest: this.contest}));
}
I've done this kind of assignment with other classes around the project and they never bothered me.
I enclose the contest class and post if it is useful:
export class Contest {
public id: number;
public title: string;
public description: string;
public rules: string;
public startDate: Date;
public endDate: Date;
public status: Status;
public winner: number;
public bannedUser: number[];
}
export class Post {
public id: number;
public title: string;
public description: string;
public level: Level;
public type: Type;
public user: User;
public publishDate: Date;
public contest: Contest;
}
I also tried some solutions always found on this forum such as:
Object.assign(target, source);
But he rightly tells me that this.contest.winner is null or undefined.
I hope for your help. Thank you
Edit:
This is the entire component.ts where onWinner() is present.
#Component({
selector: 'app-contest-details-modal',
templateUrl: './contest-details-modal.component.html',
styleUrls: ['./contest-details-modal.component.scss'],
})
export class ContestDetailsModalComponent implements OnInit, OnDestroy, AfterViewChecked {
private readonly subscriptions: Subscription = new Subscription();
public id: number;
public user: User;
public contest: Contest;
public images: Image[];
public hasVoted: boolean;
constructor(
private readonly bsModalRef: BsModalRef,
private readonly modalService: BsModalService,
private readonly store: Store<AppState>,
private cdRef: ChangeDetectorRef
) { }
public ngOnInit(): void {
this.subscriptions.add(this.store.pipe(select(AuthSelectors.getUser)).subscribe((user: User) => {
if (user) {
this.user = user;
}
}));
this.subscriptions.add(this.store.pipe(select(ContestSelectors.getById)).subscribe((contest: Contest) => {
if (contest) {
this.contest = contest;
}
}));
this.subscriptions.add(this.store.pipe(select(ImageSelectors.getImages)).subscribe((images: Image[]) => {
if (images.length) {
this.images = images;
}
}));
this.subscriptions.add(this.store.pipe(select(UserSelectors.check)).subscribe((ack: Ack) => {
if (ack) {
this.store.dispatch(AuthActions.updatedUser({userId: this.user.id}));
this.modalService.show(UploadImageModalComponent);
this.bsModalRef.hide();
}
}));
this.subscriptions.add(this.store.pipe(select(ImageSelectors.check)).subscribe((ack: Ack) => {
if (ack) {
this.bsModalRef.hide();
}
}));
}
public ngOnDestroy(): void {
this.store.dispatch(UserActions.clean());
this.store.dispatch(ContestActions.clean());
this.subscriptions.unsubscribe();
}
public onWinner(winnerId: number): void {
const post: Post = new Post();
post.title = `The winner of the contest has been elected: ${this.contest.title}`;
post.description = `Tizio has been elected winner of the contest and ${this.user.name} gives him the most welcome compliments`;
post.user = this.user;
post.contest = this.contest;
post.type = Type.WINNER;
post.level = Level.SUCCESS;
this.contest.winner = winnerId;
this.store.dispatch(PostActions.savePost({post}));
this.store.dispatch(ContestActions.saveContest({contest: this.contest}));
}
public onJoin(): void {
this.store.dispatch(UserActions.saveUser({idUser: this.user.id, id: this.contest.id}));
}
public onVote(image: Image): void {
let vote: number = image.vote;
vote = vote + 1;
this.store.dispatch(ImageActions.updateImage({photoId: image.id, votes: vote, userId: this.user.id}));
this.store.dispatch(AuthActions.updatedUser({userId: this.user.id}));
}
public isContain(contestId: number): boolean {
if (this.user.myContest) {
for (const i of this.user.myContest) {
if (contestId === i) {
return true;
}
}
}
return false;
}
public isImageVoted(id: number): boolean {
if (this.user.favouritePhoto) {
for (const imageVoted of this.user.favouritePhoto) {
if (id === imageVoted) {
this.hasVoted = true;
return true;
}
}
return false;
}
}
public onBan(image: Image): void {
this.modalService.show(BanModalComponent, {initialState : image});
this.bsModalRef.hide();
}
public isBan(contestId: number): boolean {
if (this.user.whereBanned) {
for (const contestBanned of this.user.whereBanned) {
if (contestId === contestBanned) {
return true;
}
}
return false;
}
}
public ngAfterViewChecked(): void {
this.cdRef.detectChanges();
}
}
contest contains a reference to an Observable emission, and all references emitted by Observables are readonly. Either clone contest:
this.contest = { ...contest };
Or, better, leave it as an Observable and consume it as such, generally via the async pipe. Also, if you're using NgRx, you want to be using store.select():
this.contest$ = this.store.select(ContestSelectors.getById);

Angular Component: Impossible to loop through an array of object with TypeScypt

can any one please tell me why I can not loop through this array?
In ngOnInit, everything works fine. I got an array that I successfully display in the template.
But in ngAfterViewInit, console.log show the array but when looping through with "for of" or "forEach", nothing works.
import { JobsService } from '../jobs.service';
import {Job} from '../models/Job';
#Component({
selector: 'app-job',
templateUrl: 'job.component.html'
})
export class JobComponent implements OnInit, AfterViewInit {
title = 'Job';
jobs: Job[] = [];
InProcess = '';
CurrentPartner = '';
ShowProcess = false;
sended = '';
constructor(private jobsService: JobsService) {
}
ngOnInit() {
this.jobs = this.jobsService.getJobs();
}
ngAfterViewInit() {
console.log(this.jobs); // Show the array
// Nothing happened when looping through the array
this.jobs.forEach((oneJob) => {
console.log(oneJob);
});
}
}
Screenshot of the console in Google Chrome
The content of the service:
import { HttpClient, HttpErrorResponse } from '#angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import {Job} from './models/Job';
interface IJob {
message: string;
jobs: any[];
}
#Injectable({
providedIn: 'root'
})
export class JobsService {
constructor(private httpClient: HttpClient) { }
private REST_API_SERVER = 'http://localhost:8080/myband/api/getjobs.php';
private REST_API_SERVER_SEND = 'http://localhost:8080/myband/api/sendjob.php';
jobList: Job[] = [];
errorMessage: any;
message: string;
static handleError(err: HttpErrorResponse) {
let errorMessage = '';
if (err.error instanceof ErrorEvent) {
errorMessage = `An error occurred: ${err.error.message}`;
} else {
errorMessage = `Server returned code: ${err.status}, error message is: ${err.message}`;
}
console.error(errorMessage);
return throwError(errorMessage);
}
public getJobs() {
this.requestJobs().subscribe(
iJob => {
this.message = iJob.message;
for (const job of iJob.jobs) {
const oneJob: Job = new Job(job);
this.jobList.push(oneJob);
}
},
error => this.errorMessage = error as any
);
return this.jobList;
}
public requestJobs(): Observable<IJob> {
return this.httpClient.get<IJob>(this.REST_API_SERVER).pipe(
catchError(JobsService.handleError)
);
}
}
The first thing I want to say to you is about isolation of responsibilities.
Your service must have just one job: provider one way to access your data; It means your logic inside getJobs() method could be done in your component.
export class JobsService {
constructor(
private httpClient: HttpClient,
) {}
private REST_API_SERVER = 'http://localhost:8080/myband/api/getjobs.php';
public requestJobs(): Observable<IJob> {
return this.httpClient.get<IJob>(this.REST_API_SERVER);
}
}
Now, you can handler your data in your component.
import { JobsService } from '../jobs.service';
#Component({
selector: 'app-job',
templateUrl: 'job.component.html'
})
export class JobComponent implements OnInit, AfterViewInit {
title = 'Job';
jobs$;
InProcess = '';
CurrentPartner = '';
ShowProcess = false;
sended = '';
constructor(private jobsService: JobsService) {
}
ngOnInit() {
this.jobs$ = this.jobsService.requestJobs();
}
ngAfterViewInit() {
this.jobs$
.pipe(
map(() => {}), // change your data here
catchError(() => {}) // handler your error here;
)
.subscribe(
() => {} // have access to your final data here.
);
}
}
Things to know:
You can remove the subscribe() execution and use the async pipe in your template;
The use of the operator map in pipe() is optional, you can handler your final data directly from your first callback subscribe().
You can convert your Observable to Promise using toPromise() method in one observable. Don't forgot async / await in your ngAfterViewInit.
Let me know if there is something I can help.
Try:
Object.keys(this.jobs).forEach(job => {
console.log(this.jobs[job]);
});
Try to assign an iterator function with below part replacement by this code:
// Nothing happened when looping through the array
this.jobs.forEach(oneJob, function(value, key) {
console.log(key + ': ' + value);
});
Usage of forEach in AngularJS:
For documentation try to check AngularJS forEach Docs
Syntax:
someIterable.forEach(object, iterator, [context])
Please check below example
class Job {
id: any;
status: any;
constructor(obj: any) {
this.id = obj.id;
this.status = obj.status;
}
}
let arr = [
{
id: 1,
status: "job"
}, {
id: 2,
status: "job2"
}
];
let newArr: any = [];
arr.forEach(a => {
let obj: Job = new Job(a);
newArr.push(obj);
})
console.log(newArr);
newArr.forEach((a: any) => {
console.log(a);
})

ERROR Error: Cannot insert a destroyed View in a ViewContainer

I have designed a page generator by configuration app and it works fine and generate components and render them on the page as good as it should.
But when i try to render a new page by new configuration, i get this error ERROR Error: Cannot insert a destroyed View in a ViewContainer! right when the generator service try to render the first component on the page after cleaning the page.
The pages configuration arrives from pageConfigService at ngOnInit in NemoContainerComponent and error appears when pageGenerator try to render the WidgetContainerComponent.
** UPDATE **
Page will be generate after changing the rout, all of the routes base component is the NemoContainerComponent and when route changes, the NemoContainerComponent destroyed and created again.
** UPDATE **
This is NemoContainerComponent:
#Component({
selector: 'nemo-container',
templateUrl: './nemo-container.component.html',
encapsulation: ViewEncapsulation.None
})
export class NemoContainerComponent {
private subscription: Subscription = new Subscription();
#ViewChild(ChildReferenceDirective) childReference: ChildReferenceDirective;
constructor(
private pageGenerator: PageGeneratorService,
private route: ActivatedRoute,
private routeService: RouteService,
private router: Router,
private storeEngine: StoreEngineService,
private pageConfigService: PageConfigService
) {
this.subscription.add(route.params.subscribe((params) => {
this.routeService.setRouteService = params;
}));
let activeRoute = router.url.split('/').join(' ');
document.body.className += ' ' + activeRoute;
}
ngOnInit() {
this.pageGenerator.containerRef = this.childReference.viewReference;
this.subscription.add(this.pageConfigService
.get(this.route.data['value'].page)
.subscribe(data => {
this.pageGenerator.renderNewPageByConfigurationFile(data.json)
}));
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
WidgetContainerComponent is here:
#Widget({
typeName: 'widget-container',
displayName: '',
type: 'parent',
settings: []
})
#Component({
selector: 'widget-container',
templateUrl: "widget-container.component.html",
encapsulation: ViewEncapsulation.None
})
export class WidgetContainerComponent {
#ViewChild(ChildReferenceDirective, { read: ViewContainerRef }) childRef;
get childReference(): ViewContainerRef {
return this.childRef;
}
private data: ObjectInterface = {};
set widgetData(value: ObjectInterface) {
for (let item in value) {
this.data[item] = value[item];
}
}
get widgetData(): ObjectInterface {
return this.data;
}
public id: string = '';
}
** UPDATE **
angular pack version: 4.4.6
** UPDATE **
thanks in advance for your helps :)

Angular 2 observable issue

I am trying to get data on the basis of id which is set in url
working on add mode but getting error for edit
export class GradeComponent implements OnInit {
public isNew:boolean=true;
public frmGrade: FormGroup;
public subscription:any;
public oldGrade:Grade;
constructor(
private formBuilder:FormBuilder ,
private gradeService:GradeService,
private router:Router,
private activatedRoute:ActivatedRoute
) { }
ngOnInit() {
if(typeof this.activatedRoute.snapshot.params['id'] ==='undefined'){
this.frmGrade = this.formBuilder.group({
grade: ['', Validators.required],
description: ''
});
}else{
this.setForUpdate();
}
}
private setForUpdate(){
this.isNew=false;
this.gradeService
.getOneGrade(this.activatedRoute.snapshot.params['id'])
.subscribe(
data => {
this.oldGrade = data,
this.frmGrade = this.formBuilder.group({
grade: [this.oldGrade.grade, Validators.required],
description: this.oldGrade.description
});
},
err => console.error(err),
() => console.log('done')
);
}
but i am getting error
this.formBuilder is undefined how to handle this .
FormBuilder is a service Injectable, just get an instance with Dependancy Injection.
Add this in your class:
constructor(private formBuilder: FormBuilder) {}
And you are ready to go.
export class GradeComponent implements OnInit {
public isNew:boolean=true;
public frmGrade: FormGroup;
public subscription:any;
public oldGrade:Grade;
constructor(
private formBuilder:FormBuilder ,
private gradeService:GradeService,
private router:Router,
private activatedRoute:ActivatedRoute
) { }
ngOnInit() {
this.frmGrade = this.formBuilder.group({
grade: ['', Validators.required],
description: ''
});
if(typeof this.activatedRoute.snapshot.params['id'] !=='undefined') {
this.setForUpdate();
}
}
private setForUpdate(){
this.isNew=false;
this.gradeService
.getOneGrade(this.activatedRoute.snapshot.params['id'])
.subscribe(
data => {
this.oldGrade = data,
this.frmGrade = this.formBuilder.group({
grade: [this.oldGrade.grade, Validators.required],
description: this.oldGrade.description
});
},
err => console.error(err),
() => console.log('done')
);
}

Angular2 with ngUpgrade: Angular does not recognize component in view

After few days of playing with Angular 2 Hero tutorial, I decided to play with ngUpgrade.
So I bootstrap Angular with upgradeAdapter and downgrade Angular 2 component to match Angular 1 version:
///<reference path="../node_modules/angular2/typings/browser.d.ts"/>
import {UpgradeAdapter} from "angular2/upgrade";
export const upgradeAdapter: any = new UpgradeAdapter();
import {TagFormController} from "../tags/form/TagFormController";
(function(): void {
"use strict";
upgradeAdapter.bootstrap(
document.body,
["application"],
{
strictDi: true
}
);
angular
.module("application")
.directive("tag-form", upgradeAdapter.downgradeNg2Component(TagFormController));
})();
Typescript TagFormController:
/// <reference path="../../typings/angularjs/angular.d.ts" />
///<reference path="../../custom-ts-types/custom-ts-types.ts"/>
import {Component, Input, Output, OnInit} from "angular2/core";
#Component({
selector: "tag-form",
templateUrl: "src/tags/form/tagForm.html",
})
export class TagFormController
implements IAngularComponent, OnInit {
#Input()
public articles: any[];
#Input()
public mode: string;
#Output()
public saveTag: any;
#Output()
public goToEditMode: any;
public tag: any;
#Input()
public tagId: number;
#Input()
public tagValue: number;
#Input()
public tagArticles: any[];
#Output()
public cancel: any;
constructor() {
console.log("Running");
}
public $onInit(): void {
this.tag = {
articles: this.tagArticles,
id: this.tagId,
value: this.tagValue,
};
}
public ngOnInit(): void {
this.tag = {
articles: this.tagArticles,
id: this.tagId,
value: this.tagValue,
};
}
public save(tag: any): void {
if (typeof tag.id !== "number") {
throw new TypeError("Id should be provided for tag, but is " +
typeof tag.id + " with value: " + String(tag.id));
}
console.log(tag.value);
this.saveTag({
$tag: tag
});
}
public edit(tag: any): void {
if (typeof this.cancel !== "function") {
throw new TypeError("cancel function should be provided and will be checked later!");
}
if (typeof tag.id !== "number") {
throw new TypeError("Id should be provided for tag, but is " +
typeof tag.id + " with value: " + String(tag.id));
}
this.goToEditMode({
$tag: tag
});
}
public cancelEdit(): void {
this.cancel();
}
}
console.log("I am here!");
If I look into Developer Tools in Chrome, everything should be OK, request for TagFormController is sent and I am here is displayed in console.
But usage of tagForm directive is empty inside, for me it looks like Angular does not recognize it properly. I use tagForm diretcive in this way from other tag directive:
<tag-form
*ngIf="$ctrl.tagLoaded"
[articles]="$ctrl.articles"
[mode]="$ctrl.mode"
(saveTag)="$ctrl.saveTag($tag)"
(goToEditMode)="$ctrl.goToEditMode($tag)"
[tag-id]="$ctrl.tag.id"
[tag-value]="$ctrl.tag.value"
[tagArticles]="$ctrl.tag.articles"
(cancel)="$ctrl.cancel()">
</tag-form>
I have to slightest idea what I am doing from. Maybe is important that I don't use SystemJS for Angular 1 part of project, but as I wrote request for TagFormController is sent. Can you see where I make mistake? I will be grateful if anybody help me - thank you in advance!
Perhaps you could try the following:
angular
.module("application")
.directive("tagForm", upgradeAdapter.downgradeNg2Component(TagFormController));
instead of:
angular
.module("application")
.directive("tag-form", upgradeAdapter.downgradeNg2Component(TagFormController));
Here is a working plunkr: http://plnkr.co/edit/2i46p48bTUN6jP9rTarR?p=preview.

Categories

Resources