Nested component not updating view from parent parameter

I am working on a music player app in Ionic V3. I have a sliding player (component) that overlays the content on the screen similar to the Apple Music functionality. The player is a nested component and is rendering correctly. However when selecting a song and passing the name of the song through to the player overlay component, the song is not updating on the screen.

Oddly enough, it is updating in the component.ts file (checked logging and inspecting), it just doesn’t update on the view. I have tried ngZone.run(), but no luck. Any ideas? I’ll provide code below.

Parent Page (content on the screen)

<ion-content class="">
<div ion-fixed class="noScroll">
    <ion-scroll scrollY=true style="height: 100vh">
      <div class="imageContainer">
        <ion-grid>
          <ion-row>
            <ion-col class="songColumn" col-6 *ngFor="let track of recommendedTracksJson">
              <div class="songContainer" (click)="playSong($event, track)">
                <ion-thumbnail item-start>
                  <img class="albumCover" [ngClass]="{'activeTrack': track.active}" [src]="track.album.images[0].url">
                  <div [ngClass]="{'albumOverlay': track.active}">
                     <ion-icon name="pause" *ngIf="track.active" [ngClass]="{'ios-pause-outline': track.active}"></ion-icon>
                   </div>
                </ion-thumbnail>
                <h4 class="trackArtist">{{track.album.artists[0].name}}</h4>
                <p class="trackName">{{track.name}}</p>
              </div>
            </ion-col>
          </ion-row>
        </ion-grid>
      </div>
    </ion-scroll>

    <!-- Music player component -->
    <page-player></page-player>

  </div>
</ion-content>

Inside of playSong func (only providing relevant part of the func)

this.player.setSong(item.name)

Where this.player is the sliding player component that overlays the top of the content

Component setSong() func

setSong(song) {
    this.ngZone.run(() => {
        this.activeSong = song;
    });

    console.log("Active song: " + this.activeSong)
  }

Where the output logs the correct name of the passed in song, but it does not render on the view, the placeholder text remains there.

Would you mind posting the entirety of the .ts controller files for both the parent and child component? I don’t care about the HTML, but need to see how you are doing the communication in a reproducible way.

@rapropos sure! I still stripped out a decent amount from each file, this is a very rough/work in progress project so I have a lot of unneeded/extra code that I don’t want to confuse, but I’ll include both below with a description.

I realize I made the Player component as a page rather than a component, but I don’t think that should be causing problems. To give a brief description, DemoPage (or any page that I want the player to show on) imports and passes the PlayerPage into the constructor, and when that page hits ionViewDidLoad, it calls the player to set up the HammerJS associated with the player. When a user hits the playSong function, some logic is done to move the player into the view and then passes the song name from DemoPage into setSong() in Player.ts. I am seeing this.accessToken set correctly in the controller, but it doesn’t reflect the update in the view. Please let me know if anything looks incorrect to you. Thank you!

Demo.ts - Parent Page

import { Component, NgZone } from '@angular/core';
import { Platform, ModalController, NavController, NavParams } from 'ionic-angular';
import { HTTP } from '@ionic-native/http';
import { Storage } from '@ionic/storage';
import sampleTracks from "../../assets/sampleMusic.js";

import { ErrorModalPage } from '../errorModal/errorModal';
import { PlayerPage } from '../player/player'

@Component({
  selector: 'page-demo',
  templateUrl: 'demo.html'
})

export class DemoPage {

  result = {}

  pictureList: any[];

  previousPlayedTrack = { active: false };

  recommendedTracksJson = [{}]
  height: any

  // poor choice here, but to keep it simple
  // setting up a few vars to keep track of things.
  // at issue is these values need to be encapsulated
  // in some scope other than global.
  lastPosX = 0;
  lastPosY = 0;
  isDragging = false;
  activeSong: string
  playingTrack = false;
  timerId: any

  constructor(public modalController: ModalController,
              public navCtrl: NavController,
              public platform: Platform,
              private http: HTTP,
              public navParams: NavParams,
              public ngZone: NgZone,
              private player: PlayerPage,
              public storage: Storage) {

              this.recommendedTracksJson = sampleTracks

              this.platform.ready().then(() => {

                this.height = this.platform.height()

              })

  }

  ionViewDidLoad() {
    this.player.setupPlayer()
  }

  playSong(event, item) {

    var myBlock = document.getElementById('myElement');

    myBlock.classList.add('animate');

    if(!item.active || item.active == false) {
      this.previousPlayedTrack.active = false;
      item.active = true;
      this.previousPlayedTrack = item
      this.player.activeSong = item.name
      this.player.setSong(item.name)
      myBlock.style.top = this.height * .9 + "px"
    } else {
      this.player.setSong("")
      item.active = false
      myBlock.style.top = this.height + "px"
    }

  }

}

Player.ts - Component

import { Component, NgZone } from '@angular/core';
import { IonicPage, Platform } from 'ionic-angular';
import Hammer from 'hammerjs';
/**
 * Generated class for the PlayerPage page.
 *
 * See https://ionicframework.com/docs/components/#navigation for more info on
 * Ionic pages and navigation.
 */

@IonicPage()
@Component({
  selector: 'page-player',
  templateUrl: 'player.html',
})

export class PlayerPage {

  height: any
  lastPosX = 0;
  lastPosY = 0;
  isDragging = false;
  activeSong: string = "Placeholder"
  playingTrack = false;
  timerId: any

  constructor(public ngZone: NgZone, public platform: Platform) {

    this.platform.ready().then(() => {

      this.height = this.platform.height()

    })

  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad PlayerPage');
  }

  setupPlayer() {

    var myBlock = document.getElementById('myElement');

    if (myBlock != null) {

      var screenBottom = this.height
      var screenMiddle = this.height * 0.9
      var screenTop = this.height * 0.2

      // create a simple instance on our object
      var mc = new Hammer(myBlock);

      // add a "PAN" recognizer to it (all directions)
      mc.add( new Hammer.Pan({ direction: Hammer.DIRECTION_ALL, threshold: 0 }) );

      // tie in the handler that will be called
      mc.on("pan", this.handleDrag.bind(this), this.handleDrag);
      // mc.on("pandown", handleDragDown);
      var Tap = new Hammer.Tap({
        taps: 1
      });

      mc.add(Tap)

      mc.on('tap', this.handleTap.bind(this), this.handleTap)

    } else {
      console.log("Null block 2")
    }

  }

  setSong(song) {
    this.ngZone.run(() => {
        this.activeSong = song;
    });

    console.log("Active song: " + this.activeSong)
  }

  handleTap(ev) {
    var elem = ev.target;
    document.getElementById('myElement').classList.add('animate');
    elem.style.top = this.height * 0.2 + "px"
  }

  handleDrag(ev) {

    var screenBottom = this.height
    var screenMiddle = this.height * 0.9
    var screenTop = this.height * 0.2

    // for convience, let's get a reference to our object
    var elem = ev.target;
    document.getElementById('myElement').classList.remove('animate');
    // DRAG STARTED
    // here, let's snag the current position
    // and keep track of the fact that we're dragging
    if ( ! this.isDragging ) {
      this.isDragging = true;
      this.lastPosX = elem.offsetLeft;
      this.lastPosY = elem.offsetTop;
    }

    // we simply need to determine where the x,y of this
    // object is relative to where it's "last" known position is
    // NOTE:
    //    deltaX and deltaY are cumulative
    // Thus we need to always calculate 'real x and y' relative
    // to the "lastPosX/Y"

    var posY = ev.deltaY + this.lastPosY;
    elem.style.top = posY + "px";

    // DRAG ENDED
    // this is where we simply forget we are dragging
    if (ev.isFinal) {

      var verticalDirection = this.lastPosY - posY

      var positionAbove
      var positionBelow
      var closestPosition

      if (posY <= screenMiddle) {
        positionAbove = screenTop
        positionBelow = screenMiddle
      } else {
        positionAbove = screenMiddle
        positionBelow = screenBottom
      }

      if ((posY - positionAbove) < (positionBelow - posY)) {
          closestPosition = positionAbove
      } else {
          closestPosition = positionBelow
      }

      document.getElementById('myElement').classList.add('animate');

      if (verticalDirection < 0) {
          elem.style.top = positionBelow + "px"
      } else if (verticalDirection > 0 ){
          elem.style.top = positionAbove + "px"
      } else {
          elem.style.top = closestPosition + "px"
      }

      this.isDragging = false;

    }
  }

}

OK, this is the problem. You can’t inject pages (or components) like this. Emulate the stuff documented here using an @Input binding (or use a mutually injected service).

Thank you so much! That did the trick. Appreciate the help.