Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular v5 throws an ExpressionChangedAfterItHasBeenCheckedError with shared data

I'm experiencing a very annoying error I cannot figure out. After clicking the details btn, the ‘ExpressionChangedAfterItHasBeenCheckedError’ appears.

property listings

property listing

property details

property listing details

==============================================

routing.module

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { PropertyComponent } from './property/property.component';
import { ListingComponent } from './property/listing/listing.component';
import { DetailsComponent } from './property/listing/details/details.component';

import { DetailsResolve } from './property/listing/details/details.resolve';

const routes: Routes = [
    {
        path: '',
        redirectTo: '/',
        pathMatch: 'full'
    },
    {
        path: '',
        component: PropertyComponent,
        children: [
            {
                path: '',
                component: ListingComponent
            },
            {
                path: 'listing/:address',
                component: DetailsComponent,
                resolve: {
                    DetailsResolve
                }
            }
        ]
    }
    //{ path: '**', component: PageNotFoundComponent }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule],
    providers: [
        DetailsResolve
    ]
})
export class AppRoutingModule { }

property html

<div fxFlex="70" fxLayout fxFill>
    <agm-map class="property__map" [latitude]="map.latitude" [longitude]="map.longitude" [zoom]="map.zoom">
        <agm-marker [latitude]="map.latitude" [longitude]="map.longitude"></agm-marker>
    </agm-map>
</div>
<div class="property__sidenav mat-elevation-z10" fxFlex="30" fxLayout fxFill>
    <router-outlet></router-outlet>
</div>

property component

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription }   from 'rxjs/Subscription';
import { UtilityService } from '../shared/utility.service';

@Component({
    selector: 'app-property',
    templateUrl: './property.component.html',
    styleUrls: ['./property.component.scss'],
    host: { 'class': 'property' }
})
export class PropertyComponent implements OnInit, OnDestroy {

    map: object;
    mapSubscription: Subscription;
    latitude: number;
    longitude: number;
    zoom: number;

    constructor(
        private utilityService: UtilityService
    ) {}

    ngOnInit(): void  {
        this.mapSubscription = this.utilityService.defaultMapMarker.subscribe(map => this.map = map);
        console.log('parent - property', this)
    }

    ngOnDestroy() {
        this.mapSubscription.unsubscribe();
    }
}

listing html

<mat-card class="listing" *ngFor="let property of properties.properties">
    <div class="listing__image">
        <img class="listing__image-background" mat-card-image src="../assets/images/properties/1327_s_colorado_st_philadelphia_pa_19146_picture_01.jpg" alt="Photo of a Shiba Inu">
    </div>
    <!--<div class="listing__image">
        <div class="listing__image-background" [style.backgroundImage]="'url('+ property.image[0].url +')'"></div>
    </div>-->
    <mat-card-title>
        {{property.type}} &mdash;
        {{property.price}} / mo.
    </mat-card-title>
    <mat-card-subtitle>
        {{property.address.street}}
        {{property.address.city}}
        {{property.address.state}}
    </mat-card-subtitle>
    <mat-card-content>
        <mat-list fxLayout>
            <mat-list-item fxFlex="20">
                <mat-icon matListIcon>hotel</mat-icon>
                <h4 class="listing__icon-title" matLine>{{property.bed}} beds</h4>
            </mat-list-item>
            <mat-list-item fxFlex="20">
                <mat-icon matListIcon>hot_tub</mat-icon>
                <h4 class="listing__icon-title" matLine>{{property.bath}} bath</h4>
            </mat-list-item>
            <mat-list-item fxFlex="20">
                <mat-icon matListIcon>view_compact</mat-icon>
                <h4 class="listing__icon-title" matLine>{{property.sqft}} sqft.</h4>
            </mat-list-item>
            <mat-list-item fxFlex>
                <mat-icon matListIcon>directions_walk</mat-icon>
                <h4 class="listing__icon-title" matLine>{{property.walkscore}} (Walker's Paradise)</h4>
            </mat-list-item>
        </mat-list>
        <p>{{property.description.short}}</p>
    </mat-card-content>
    <mat-card-actions>
        <button mat-button [routerLink]="['/listing', property.url]">DETAILS</button>
        <button mat-button>SHARE</button>
    </mat-card-actions>
</mat-card>

listing component

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription }   from 'rxjs/Subscription';
import { UtilityService } from '../../shared/utility.service';
import { PropertyService } from '../property.service';

@Component({
    selector: 'app-listing',
    templateUrl: './listing.component.html',
    styleUrls: ['./listing.component.scss']
})
export class ListingComponent implements OnInit, OnDestroy {

    properties: any;
    propertiesSubscription: Subscription;
    map: object;

    constructor(
        private utilityService: UtilityService,
        private propertyService: PropertyService
    ) {}

    ngOnInit() {
        this.getProperties();
        console.log('child - listing', this)
    }

    ngOnDestroy() {
        this.propertiesSubscription.unsubscribe();
    }

    getProperties() {
        this.properties = [];
        this.propertiesSubscription = this.propertyService.getProperties().subscribe(properties => this.properties = properties);
    }
}

details html

<p>
  details works!
</p>

details component

import { Component, OnInit, AfterViewInit } from '@angular/core';
import { ActivatedRoute } from "@angular/router";
import { Meta, Title } from '@angular/platform-browser';
import { UtilityService } from '../../../shared/utility.service';

@Component({
    selector: 'app-details',
    templateUrl: './details.component.html',
    styleUrls: ['./details.component.sass']
})
export class DetailsComponent implements OnInit, AfterViewInit  {

    propertyDetails: any;

    constructor(
        private meta: Meta,
        private title: Title,
        private route: ActivatedRoute,
        private utilityService: UtilityService
    ) {
        title.setTitle('Davis');
        meta.addTags([
            { name: 'author', content: '' },
            { name: 'keywords', content: '' },
            { name: 'description', content: '' }
        ]);
    }

    ngOnInit() {
        this.propertyDetails = this.route.snapshot.data
        this.mapCoordinates();
        //console.log('details - child', this)
    }

    mapCoordinates() {
        let map = this.propertyDetails.DetailsResolve.map;

        //property listing location
        let coordinates = {
            latitude: map.latitude,
            longitude: map.longitude,
            zoom: map.zoom
        };

        //update map in parent
        return this.utilityService.onUpdateMapMarker(coordinates)
    }
}

details resolve

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Resolve } from '@angular/router';
import { ActivatedRouteSnapshot } from '@angular/router';
import { DetailsService } from './details.service';

@Injectable()
export class DetailsResolve implements Resolve<any> {

    propertyDetails: any;

    constructor(
        private detailsService: DetailsService
    ) { }

    resolve(route: ActivatedRouteSnapshot) {
        let propertyUrl = route.params.address;

        return this.detailsService.getPropertyDetails().then(details => {
            let propertyDetails = details['details'];

            for (let index = 0, len = propertyDetails.length; index < len; index++) {
                let property = propertyDetails[index];

                //check which property listing 
                if (property.url === propertyUrl) {
                    this.propertyDetails = property;
                }
            }

            return this.propertyDetails;
        });
    }
}

Property service

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class PropertyService {

    constructor(
        private http: HttpClient
    ) { }

    getProperties() {
       return this.http.get('api/mock-property.json');
    }

}

Utility service

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Injectable()
export class UtilityService {
    private mapMarker = new BehaviorSubject<object>({
        latitude: 39.9525839,
        longitude: -75.16522150000003,
        zoom: 10
    });
    defaultMapMarker = this.mapMarker.asObservable();

    onUpdateMapMarker(coordinates: object) {
        console.log('UtilityService - coordinates', coordinates)
        this.mapMarker.next(coordinates);
    }
}
like image 449
Davis Avatar asked Dec 17 '17 22:12

Davis


2 Answers

Figure it out! Two things that resolved my issue.

  1. ChangeDetectorRef
  2. Placing another emit method in the listing component for default map coordinates oppose to in the UtilityService

Here are my changes:

Utility service

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Injectable()
export class UtilityService {
    private mapMarker = new BehaviorSubject<object>({});
    defaultMapMarker = this.mapMarker.asObservable();

    onUpdateMapMarker(coordinates: object) {
        this.mapMarker.next(coordinates);
    }
}

Property component

import { Component, OnInit, OnDestroy, OnChanges, ChangeDetectorRef } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { UtilityService } from '../shared/utility.service';

@Component({
    selector: 'app-property',
    templateUrl: './property.component.html',
    styleUrls: ['./property.component.scss'],
    host: { 'class': 'property' }
})
export class PropertyComponent implements OnInit, OnDestroy {

    map: object;
    mapSubscription: Subscription;

    constructor(
        private utilityService: UtilityService,
        private changeDetectorRef: ChangeDetectorRef
    ) { }

    ngOnInit() {
        this.mapCoordinates();
    }

    mapCoordinates() {
        return this.mapSubscription = this.utilityService.defaultMapMarker.subscribe(map => {
            this.map = map;
            this.changeDetectorRef.detectChanges();
        });
    }

    ngOnDestroy() {
        this.mapSubscription.unsubscribe();
    }
}

Listing component

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { UtilityService } from '../../shared/utility.service';
import { PropertyService } from '../property.service';

@Component({
    selector: 'app-listing',
    templateUrl: './listing.component.html',
    styleUrls: ['./listing.component.scss']
})
export class ListingComponent implements OnInit, OnDestroy {

    properties: any;
    propertiesSubscription: Subscription;
    map: object;
    mapSubscription: Subscription

    constructor(
        private utilityService: UtilityService,
        private propertyService: PropertyService
    ) { }

    ngOnInit() {
        //listing all properties 
        this.getProperties();

        //Set default map coordinates
        this.defaultMapCoordinates();

        //Subscribe to latest map coordinates 
        this.mapSubscription = this.utilityService.defaultMapMarker.subscribe((map: object) => this.map = map);
    }

    ngOnDestroy() {
        this.propertiesSubscription.unsubscribe();
        this.mapSubscription.unsubscribe();
    }

    defaultMapCoordinates() {
        let defaultMapCoordinates = {
            latitude: 39.9525839,
            longitude: -75.16522150000003,
            zoom: 16
        };

        //emit new map coordinates
        return this.utilityService.onUpdateMapMarker(defaultMapCoordinates);
    }

    getProperties() {
        this.properties = [];
        this.propertiesSubscription = this.propertyService.getProperties().subscribe(properties => this.properties = properties);
    }
}

Details component

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from "@angular/router";
import { Meta, Title } from '@angular/platform-browser';
import { UtilityService } from '../../../shared/utility.service';

@Component({
    selector: 'app-details',
    templateUrl: './details.component.html',
    styleUrls: ['./details.component.sass']
})
export class DetailsComponent implements OnInit {

    propertyDetails: any;
    map: object;

    constructor(
        private meta: Meta,
        private title: Title,
        private route: ActivatedRoute,
        private utilityService: UtilityService
    ) {
        title.setTitle('Davis');
        meta.addTags([
            { name: 'author', content: '' },
            { name: 'keywords', content: '' },
            { name: 'description', content: '' }
        ]);
    }

    ngOnInit() {
        this.propertyDetails = this.route.snapshot.data
        this.emitNewMapCoordinates();
    }

    emitNewMapCoordinates() {
        let map = this.propertyDetails.DetailsResolve.map;

        //property listing location
        let coordinates = {
            latitude: map.latitude,
            longitude: map.longitude,
            zoom: map.zoom
        };

        //emit new map coordinates
        return this.utilityService.onUpdateMapMarker(coordinates);        
    }
}
like image 183
Davis Avatar answered Sep 27 '22 22:09

Davis


You can use OnPush change detection

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-listing',
  templateUrl: './listing.component.html',
  styles: ['./property.component.scss']
})

so you can avoid this pitfall, please try with this. Thanks

like image 37
Adeeb basheer Avatar answered Sep 27 '22 21:09

Adeeb basheer