3

There are actually two challenges I face. I am looping through an array of values and need to

  1. set a class name depending on an observable variable from a child component.
  2. reevaluate the class as soon as the child variable changes.

location.component.ts

import { Component, Input } from '@angular/core';

import { BusinessLocation, SpecialDays, RegularHours } from './location';
import { DbService } from './db-service.component';

@Component({
  selector: '[data-locations]',
  templateUrl: 'app/location.component.html',
  providers: [DbService]
})

export class LocationComponent  {

    locations:BusinessLocation[];
    selectedLocationId:Number;
    constructor(private api:DbService){}

    isOpenOnDay(day):Boolean {

      let _weekDay = day.getDay();
      let _retour = false;

      this.locations.forEach(loc => {
        if ( loc.id == this.selectedLocationId && loc.regularHours.weekDay == _weekDay ) {
          _retour = true;
        }
      });

      this.locations.forEach(loc => {
        if ( loc.id == this.selectedLocationId && loc.specialDays.singleDate.getDay() == _weekDay) {
          _retour = true;
        }
      });

      return _retour;
    }

    getLocation():Number {
      return this.selectedLocationId;
    }

    setLocation(id):void {
      this.selectedLocationId = id;
    }

    getLocations():void {
        this.api.getLocations().subscribe(
          locations => {
            this.locations = locations as BusinessLocation[];
            this.setLocation(this.locations[0].id);
          }
          );
    }

}

a snippet from db-services.component.ts

    getLocations():Observable<BusinessLocation[]> {
        return this.http.get(this.apiUrl + '/get_locations.php')
                        .map(response => response.json().data as BusinessLocation[]);
    }
}

it all works fine. However, here is where the challenge is. The parent component initiates the locations, but it also needs to know what location is selected right now. Here is the month.component.html

<span class="location-container" #location data-locations><span class="loading">Loading locations...</span></span>

        <div *ngFor="let day of week.days" class="day" data-can-drop="day" 
            [class.today]="isToday(day)" 
            [class.in-other-month]="day.getMonth() != jsMonth"
            [class.is-closed]="!isOpenAtLocation(day)">
            <div class="day-marker"></div>
            <span class="day-date">{{day | date:'d'}}</span>
            <span *ngIf="checkMonth(day)" class="day-month">{{months[day.getMonth()]}}</span>
        </div>

and a snippet from month.component.ts is

  @ViewChild('location') locationComponent:LocationComponent;

  isOpenAtLocation(day):Boolean {
    return this.locationComponent.isOpenOnDay(day);
  }

  ngOnInit(): void {
    this.locationComponent.getLocations();
 }

The error I get is pretty straightforward and totally understandable:

Subscriber.ts:238 TypeError: Cannot read property 'forEach' of undefined
    at LocationComponent.isOpenOnDay (location.component.ts:25)
    at MonthComponent.isOpenAtLocation (month.component.ts:176)

And this is just about Challenge 1. The Challenge 2 has not been even addressed yet.

I just can't wrap my head around it. >_< Thanks.

pop
  • 3,464
  • 3
  • 26
  • 43

1 Answers1

1

Well, this was a bad joke on my side. First, this is a reminder, that a change of a property of an object will reflect itself in DOM if there is a binding available. So going with [class.isOpenOnDay]="day.isOpenAtLocation" would be totally sufficient, where day is an object and isOpenAtLocation is its property. Even if it is not set initially (meaning it is null) and will be updated in the future – it is all good. This is basically how NG works (and has worked all the time). Silly me.

The other problem – changing the value depending on a child component variable – has been solved by emitting events (from child), listening to the events (in parent) and resetting the property isOpenAtLocation again.

So the updated child component location.component.ts has been updated like this:

@Output() locationChanged = new EventEmitter<Number>();

setLocation(id):void {
  this.selectedLocationId = id;
  this.locationChanged.emit(this.selectedLocationId);
}

The view for the location component now has this line:

<select (change)="setLocation($event.target.value)">
 <option *ngFor="let loc of locations" value="{{loc.id}}">{{loc.longName}}</option>
</select>

The parent component's view is bound to the event like this:

<span class="location-container" #location data-locations (locationChanged)="onLocationChange($event)"><span class="loading">Loading locations...</span></span>

And the parent month.component.ts itself has two more methods:

  onLocationChange(event) {
    if ( this.selectedLocationId != event ) {
      this.selectedLocationId = event;
      this.setLocation();
      this.dispatchResize();
    }
  }

  setLocation():void {

    if ( this.selectedLocationId >= 0) {

      for ( let i = 0; i < this.weeks.length; i++) {
        let _week = this.weeks[i];

        _week.forEach(_day => {
          let _isOpen = this.locationComponent.isOpenOnDay(_day.date);
          _day['isOpenOnDay'] = _isOpen.isOpenOnDay;
          _day['isSpecialDay'] = _isOpen.isSpecialDay;
          _day['dayHours'] = _isOpen.dayHours;
        });

      }

    }

  }

As one can see, I have added even more dynamically checked properties, not just isOpenOnDay, but also isSpecialDay and dayHours, which are not yet defined initially, but are set as soon as the data is available – and are reflected in view as soon as they change.

Basic stuff, actually. Still might be helpful to some NG2 noob like me.

pop
  • 3,464
  • 3
  • 26
  • 43