ViewChild, undefined


#1

My code supposed to load data from events when dashboard is loaded:

dashboard.ts

import {Page, NavController} from 'ionic-angular';
import {EventsPage} from '../events/events';
import {ViewChild} from 'angular2/core';

@Page({
	templateUrl: 'build/pages/dashboard/dashboard.html',
        directives:[EventsPage]
})
export class DashboardPage {

	@ViewChild(EventsPage) eventsPage: EventsPage

	constructor() {
	}

	ngAfterViewInit(){
            console.log(this.eventsPage);
            this.eventsPage.load();
        }
}

This fails in this.eventsPage.load(), cause of this.eventsPage is null.

events.ts

import {Page, NavController} from 'ionic-angular';
import {Connector} from '../../directives/connector';

@Page({
	templateUrl: 'build/pages/events/events.html',
	providers: [Connector]
})
export class EventsPage {

	events: Array<{ id: number }>;
	event: { id: number, caption: string, description: string; startTime: Date, endTime: Date };
	connector: Connector;

	constructor(public nav: NavController, con: Connector) {
		this.connector = con;
		this.events = [];
		this.load();
	}

	load() {
		this.connector.request({ api: "api/events/withMyAttendance", loading: "Načítám události" }, (data) => {
			console.log(data);
			for (let i in data.data) {
				let obj = data.data[i];
				this.event = { id: obj.id, caption: obj.caption, description: obj.description, startTime: new Date(obj.startTime),endTime: new Date(obj.endTime) };
				this.events.push(this.event);
			}
		});
	}
}

What is wrong with the code written according to http://learnangular2.com/viewChild/?

Cordova CLI: 6.1.1
Gulp version: CLI version 3.9.0
Gulp local: Local version 3.9.1
Ionic Framework Version: 2.0.0-beta.6
Ionic CLI Version: 2.0.0-beta.25
Ionic App Lib Version: 2.0.0-beta.15
ios-deploy version: Not installed
ios-sim version: 5.0.4
OS: Mac OS X El Capitan
Node Version: v4.0.0
Xcode version: Xcode 7.3 Build version 7D175

Thanks


#2

Found solution, has to be solved with @Injectable() and abstract data load.


#3

Have you solved this? I would be very curious, thanks for any info. I’m about to enter an issue at the Ionic2 github issues page.


#4

Are you really doing exactly what OP was, in terms of trying to make one page a @ViewChild of another? That doesn’t make any sense to me.


#5

I’m sorry but I do not understand what the acronym ‘OP’ stands for…


#6

“Original Poster”. The person who started the thread.


#7

Thanks. Yes. I’m trying to do what the OP did.

According to the angular2 docs it’s a reasonable thing, it seems:

http://learnangular2.com/viewChild/?

I wonder why it makes no sense.

But regardless, what I’m trying to accomplish is important - get at the tabs and be able to select them from the code at the app level (app class). Something similar to app.getComponent we used to have. Many people have reported this issue, e.g.

and

which claims you only get the navcontroller with the @ViewChild decorator - why only that one?

There are many more reports of this problem… I will post an issue on github if I don’t find an answer here soon. Thanks!


#8

Because I think of pages as independent entities - they don’t have a parent/child relationship in my conception of the framework.

Personally I think this is a design mistake. I would create some sort of event or observable, and have the tab page controller subscribe to it and be responsible for the select call. The app class should not be involved.


#9

Ok, I finally figured it out. This problem is solved for me now.

Getting tabs and side-menu working together (also using @ViewChild) – or… “Why is my @ViewChild(Tabs) property undefined?”

Short answer: If Tabs is only in the code and not in the template, you get undefined in the code. If it’s in the template as well, it works.

Long answer: If you already have a TabsPage component class in its own folder with scss and html and js/ts files, follow this to get it to work with your sidemenu:

  1. don’t create a TabsPage that is set to the root of your app’s main nav as is typically done, you’ll include its code in the app class itself
  2. get rid of the app’s menu nav element from the HTML template - the one that used to be an <ion-nav> - get rid of that <ion-nav> altogether
  3. replace that <ion-nav> with your <ion-tabs> template from what used to be tabs.html if you had a separate folder for that page, cut and paste the template HTML into app.html
  4. copy code from old TabsPage - take what you need (javascript, scss)
  5. delete the TabsPage folder altogether
    The way you can make @ViewChild work with a sub-component (not leave your property undefined but instead bind the property to the sub-component’s instance) is to include that sub-component in your parent component’s HTML template as well as in the code (javascript/typescript).

Anyhow, you can see an example of @ViewChild working with Tabs by looking at app.html and app.ts here:

https://github.com/tracktunes/ionic-recorder/blob/master/app/


How do I link through to a child of a tab from the side menu
getComponent alternative
Sidemenu & Tabs Together?
#10

The link you posted doesn’t work. Is there any chance of getting an example of this? I am running a tabs page and getting the undefined error with view child. Thank you so much.


#11

Don’t use ViewChild until ngAfterViewInit().


#12

Sorry, no clue what link you’re talking about.


#13

The link doron posted to the github on how he made viewchild work with tabs. I’m having the same problem where my page is in a tabbed display and @ViewChild is always undefined.


#14

Can you please describe in more detail what you are fundamentally trying to achieve? I think that in general this thread is seeking for a way to do something that is best avoided entirely.


#15

Can you please describe in more detail what you are fundamentally trying to achieve? I think that in general this thread is seeking for a way to do something that is best avoided entirely.

Absolutely. So I have my main app check for authentication via the ionic services auth plugin, then if they are authorized it loads the “TabPage” which is nothing more than just a tab placeholder to have my main app in a tabbed view. Here is the “TabsPage” html…


<ion-tabs>
   <ion-tab tabTitle="Home" tabIcon="home" [root]="homeTab"></ion-tab>
   <ion-tab tabTitle="Search" tabIcon="search" [root]="searchTab"></ion-tab>
   <ion-tab tabTitle="Post" tabIcon="add" [root]="addPostTab"></ion-tab>
   <ion-tab tabTitle="Settings" tabIcon="cog" [root]="settingsTab"></ion-tab>
   <ion-tab tabTitle="Profile" tabIcon="person" [root]="profileTab"></ion-tab>
 </ion-tabs>

Very simple layout, just tabs. Then in my AddPost page I am trying to grab a hold of the input in it with “@viewChild”. Here is the html of my “AddPostPage”…

 <ion-header>
  <ion-navbar>
    <ion-title text-center>Add Post</ion-title>
  </ion-navbar>

</ion-header>


<ion-content>

  <img src="{{pathForImage(lastImage)}}" style="width: 100%;" [hidden]="lastImage === null" id="imageSrc">
  <h3 [hidden]="lastImage !== null">Please select an image</h3>
  <ion-buttons>
    <button ion-button icon-left (click)="showOptions()">
      <ion-icon name="camera"></ion-icon>
      Select Image
    </button>

  </ion-buttons>

  <ion-list>

    <ion-item>
      <ion-label floating>Title</ion-label>
      <ion-input [(ngModel)]="post.title" type="text"></ion-input>
    </ion-item>

    <ion-item>
      <ion-label floating>Content</ion-label>
      <ion-input [(ngModel)]="post.content" type="text"></ion-input>
    </ion-item>

  </ion-list>

</ion-content>

<ion-footer>

  <button (click)="save()" color="light" ion-button full>Save</button>

</ion-footer>

Then for my AddPost ts file I am using the “@ViewChild” to try to grab that input. And here is the AddPostPage ts file…

import {Component, ElementRef, ViewChild} from '@angular/core';
import {
  IonicPage, NavController, NavParams, Platform,
  Loading, ToastController, LoadingController, ActionSheetController
} from 'ionic-angular';
import { User } from '@ionic/cloud-angular';
import { Post } from '../../providers/post';
import { File } from '@ionic-native/file';
import { FileChooser } from '@ionic-native/file-chooser';
import {Transfer, TransferObject} from '@ionic-native/transfer';
import { FilePath } from '@ionic-native/file-path';
import { Camera } from '@ionic-native/camera';

import { PostModel } from '../../models/PostModel';
import { Data } from '../../providers/data';
import {Tabs} from "../tabs/tabs";
import Cropper from 'cropperjs';

declare var cordova: any;

@IonicPage()
@Component({
  selector: 'page-add-post-page',
  templateUrl: 'add-post-page.html',
})
export class AddPostPage {

  @ViewChild('imageSrc') imageElement: ElementRef;

  loading: Loading;
  post: PostModel = new PostModel();
  activeTab: number = 2;
  lastImage: any = null;
  load: any;
  cropperInstance: any;


  constructor(
      public navCtrl: NavController,
      public navParams: NavParams,
      public postService: Post,
      public user: User,
      public platform: Platform,
      public imageUploader: ImageUploader,
      public toastCtrl: ToastController,
      public dataService: Data,
      public loader: LoadingController,
      public pushService: PushService,
      public actionSheetCtrl: ActionSheetController,
      private camera: Camera,
      private transfer: Transfer,
      private filePath: FilePath,
      private file: File,
      private fileChooser: FileChooser
  ) {
  }

  pathForImage(img) {
    return this.imageUploader.pathForImage(img);
  }

  ionViewDidLoad() {
    this.imageUploader.setType('post');
  }

  showOptions() {
    this.presentActionSheet();
    // this.imageUploader.getLastImage().subscribe(data => this.lastImage = data);
  }

  getPicture(sourceType) {
    this.imageUploader.takePicture(sourceType).then(data => {
      this.cropImage();
      // this.lastImage = data;
    });
  }

  cropImage() {
    console.log("Cropping image");
    console.log(this.imageElement.nativeElement);
    this.cropperInstance = new Cropper(this.imageElement.nativeElement, {
      aspectRatio: 1/1,
      dragMode: 'move',
      modal: true,
      guides: false,
      highlight: false,
      background: false,
      autoCrop: true,
      autoCropArea: 0.9,
      responsive: false,
      zoomable: true,
      movable: false
    });
  }

  presentActionSheet() {
    let actionSheet = this.actionSheetCtrl.create({
      title: 'Select Image Source',
      buttons: [
        {
          text: 'Load from Library',
          handler: () => {
            if(this.platform.is('android') || this.platform.is('ios')) {
              this.camera.getPicture({
                quality: 100,
                destinationType: this.camera.DestinationType.FILE_URI,
                sourceType: this.camera.PictureSourceType.PHOTOLIBRARY,
                encodingType: this.camera.EncodingType.JPEG,
                mediaType: this.camera.MediaType.PICTURE,
                allowEdit: false,
                correctOrientation: true
              }).then(imageData => {
                console.log(this.imageElement);
                console.log(this);
                console.log(imageData);
                this.imageElement.nativeElement.src = imageData;
                this.cropImage();
              })
              // this.takePicture(this.camera.PictureSourceType.PHOTOLIBRARY);
            } else {
              this.fileChooser.open().then(uri => {
                this.imageElement.nativeElement.src = uri;
                this.cropImage();
                // let currentName = uri.substr(uri.lastIndexOf('/') + 1),
                //     currentPath = uri.substr(0, uri.lastIndexOf('/') + 1),
                //     name = this.user.details.username + '-' + new Date().getTime() + '.jpg';
                // this.copyFileToLocalDir(currentPath, currentName, name);
              });
            }
          }
        },
        {
          text: 'Use Camera',
          handler: () => {
            this.camera.getPicture({
              quality: 100,
              destinationType: this.camera.DestinationType.FILE_URI,
              sourceType: this.camera.PictureSourceType.CAMERA,
              encodingType: this.camera.EncodingType.PNG,
              mediaType: this.camera.MediaType.PICTURE,
              allowEdit: false,
              correctOrientation: true
            }).then((imageData) => {
              this.imageElement.nativeElement.src = imageData;
              this.cropImage();
            })
          }
        },
        {
          text: 'Cancel',
          role: 'cancel'
        }
      ]
    });
    actionSheet.present();
  }

  copyFileToLocalDir(namePath, currentName, newName) {
    return new Promise(resolve => {
      this.file.copyFile(namePath, currentName, cordova.file.dataDirectory, newName).then(success => {
        this.lastImage = newName;
        resolve(newName);
      }, error => {
        this.presentToast('Error while storing file.');
        console.log(error);
        resolve(false);
      });
    });
  }

  presentToast(text) {
    let toast = this.toastCtrl.create({
      message: text,
      duration: 3500,
      position: 'top'
    });
    toast.present();
  }

  cropDone() {
    this.lastImage = this.cropperInstance.getCroppedCanvas({width: 500, height: 500}).toDataURL('image/jpeg');
    this.imageUploader.setImage(this.lastImage);
  }

  save() {
    this.showLoader('Uploading...');
    let toast, targetUsers = this.post.getTargetUsers();
    this.post._id = this.post.title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
    this.post.datePublished = new Date().toISOString();
    this.post.dateUpdated = new Date().toISOString();
    this.post.author = this.user.details.username;
    this.post.author_id = this.user.id;
    this.post.avatar = this.user.details.image;
    this.post.image = this.imageUploader.lastImage;
    this.post.tags = this.post.getPostTags();
;    this.postService.addPost(this.post.toObject()).then(data => {
      if(data) {
        this.imageUploader.uploadImage(this.post._id).then(([success, response]) => {
          if(success) {
            if(targetUsers.length > 0) {
              this.pushService.sendPostNotification(targetUsers, this.user.details).then(data => {
                this.load.dismissAll();
                this.navCtrl.setRoot(Tabs);
              });
            }
            this.load.dismissAll();
            this.navCtrl.setRoot(Tabs);
          } else {
            this.load.dismissAll();
            toast = this.toastCtrl.create({
              message: "Unable to upload the post image.",
              duration: 3500,
              position: 'top'
            });
            toast.present();
          }
        });
      } else {
        this.load.dismissAll();
        toast = this.toastCtrl.create({
          message: "Unable to upload the new post.",
          duration: 3500,
          position: 'top'
        });
        toast.present();
      }
    });
  }

  showLoader(message) {
    this.load = this.loader.create({
      content: message
    });
    this.load.present();
  }


}

So, as you can see I am trying to get the img tag in my view page and fill it in after the user crops the image. However, after I select an image, in the dev console in chrome I am getting an error saying that imageElement is undefined. I just want to get that img element so I can dynamically change the image, and because the plugin, the only one I could find for ionic 2, all others were written in ionic 1, needs that to build the display for the cropping functions, but it is always undefined.
The gentleman above was saying that it had something to do with the tabs, and had a link to a version he said was working, but the link doesn’t work, so I was hoping he had an updated link, or at least an example of how he made it work. I can’t figure out how to get my “@ViewChild” to not be undefined.

Thank you so much, I seriously appreciate it. I am still kind of new to ionic and this is my first app I have built, so I am still learning best practices and such.

On a side note, I know I should be using a provider or something else to handle uploading images, and I plan to create one after I get this part figured out. Since I am going to be uploading images on more than just this page.


#16

Your use of a function call in the template to define a source is suspicious. That could be called before the template is fully rendered. Better to set the source to a variable, in the constructor initialize the variable to a dummy value, then in ngAfterViewInit() (or any later hook) set the source variable to the value you want. Maybe there’s a different issue (also), but that’s the first thing I’d change, and then see what happened.


#17

Which function call are you talking about? The cropImage function was a copy/paste from another tutorial/post in these forums. The main problem is, even before that and at every point in the class, including the ionViewDidLoad method, the this.imageElement is always undefined.


#18

That was the first danger point I saw. You don’t control the order in which pathForImage is asynchronously called. But you might have issues in ImageUploader also. It looks as though you cut out code when you pasted here. So it’s hard for me to know for sure where the issues are. But, just as a side point, tutorials are often outdated, so it’s better to rely on deep diving into your own code, instead of trusting something you saw online.


#19

Ah, ok, I will change that to be just src="{{lastImage}}" and just have the main function run the pathForImage prior to setting lastImage. Can you see any reason why my this.imageElement is undefined though? That is what is killing me. I can’t run the image crop until that darn @viewChild works and doesn’t return undefined.


#20

Somehow, somewhere, you’re calling ViewChild before ngAfterViewInit(). I can’t be 100% sure about that, because of incomplete code, but it’s very likely.

Edit: Also use #imageSrc inside the template element you want ViewChild to refer to.