collection-repeat with angular component, what is happening? - ionic-framework

I'm trying to use collection-repeat to display an angular component for each object in an array. I pass each object as parameter to an angular component but when I try to access the object in my component's controller I get undefined.
<ion-content>
<ion-list>
<ion-item
collection-repeat="user in users"
item-width="100%"
item-height="90px">
{{user}} //renders my user data correctly instantly
<usser user="user"></user>
</ion-item>
</ion-list>
</ion-content>
My component
angular
.module('app')
.component('user', {
templateUrl: 'components/user.html',
scope: true,
bindings: {
user: '<'
},
controller: function() {
console.log(self.user) //prints undefined
}
})
I've tried wrapping the console.log in a $timeout without success
Printing self displays {user: undefined} in my chrome console, but if I expand the object I can see that user contains the correct data (only for the some of the items)
Accessing self.user doesn't work
EDIT: I can't really understand what's going on..
controller: function() {
console.log(self.user) //prints undefined
setTimeout(function() {
console.log(self.user) // prints my data
}, 2000)
}
What am I doing wrong?
Thanks in advance!

Lost 3 hours to figure out this
This is a known issue at the moment with collection repeat. A fix
would require a refactor of collection repeat, which would be too big
of a change the moment.
Always check the issues on Github, [V] lesson learned

Related

Infinite Loop on ngModelChange

I have a list containing users, and i have an item that i want it to redirect me to a modal page.
My problem is as soon as my page is available, i get an infinite pop up of my modal, instead of going on my ion-select and select the item so i can get the pop up.
html
<ion-select interface="popover" (ngModelChange)="onChange($event)">
<ion-option>Bacon</ion-option>
<ion-option [value]="openConfig()"></ion-option>Black Olives</ion-option>
<ion-option>Extra Cheese</ion-option>
<ion-option>Mushrooms</ion-option>
<ion-option>Pepperoni</ion-option>
<ion-option>Sausage</ion-option>
</ion-select>
ts
onChange(value: any) {
if (value === 'openConfig') {
this.openConfig()
}
}
openConfig() {
this.modalCtrl.create('ConfigModal').present;
console.log('heeey')
}
Setting the [value] in the template is actually calling your openConfig() function, creating an infinite loop on the page load. To do what you're trying to do here you don't need to reference your openConfig function in the template at all.
ion-select uses the output event ionChange, which outputs the value of the ion-option selected. So the normal way to do this in Ionic 3 would be something like this:
html:
<ion-content padding>
<ion-select interface="popover" [ngModel]="option" (ionChange)="onChange($event)">
<ion-option>Bacon</ion-option>
<ion-option>Black Olives</ion-option>
<ion-option>Extra Cheese</ion-option>
<ion-option>Mushrooms</ion-option>
<ion-option>Pepperoni</ion-option>
<ion-option>Sausage</ion-option>
</ion-select>
</ion-content>
js:
onChange(value: any) {
if (value === 'Black Olives') {
this.openConfig()
}
}
openConfig() {
this.modalCtrl.create(ConfigModal).present();
}
Note that the value of an ion-option is simply the text of the label. So that's what you should check for in your "onChange" function.
You have a couple of other unrelated typos, but I believe this addresses the question of your infinite loop. Hope this helps!

Ionic / Firebase - error splice is not a function (reorderArray)

I have beat my head for a couple days...trying to use the (ionItemReorder)="reorderItems($event)" to reorder a list. I have a list of songs I'm getting from FireBase. When I fire the reOrderItems click event I get an error: TypeError: array.splice is not a function at reorderArray
I assume it's probably something very simple in the way I'm defining "songs". I have tried several different ways...but at this point I'm just grasping at straws.
Any suggestions would be greatly appreciated. Thank you! ER
Typescript:
import { Component } from '#angular/core';
import { IonicPage, NavController, NavParams, reorderArray } from 'ionic-angular';
import { AngularFireModule} from 'angularfire2';
import { AngularFireDatabase } from 'angularfire2/database';
#IonicPage()
#Component({
selector: 'page-songs',
templateUrl: 'songs.html',
})
export class SongsPage {
//songs: any = {};
//songs = {};
//songs = [];
//songs: any = [];
songs: any;
btnName: any = 'Reorder';
flag: any = false;
constructor(
public navCtrl: NavController,
public navParams: NavParams,
public afd: AngularFireDatabase
)
{
this.songs = this.afd.list('/songs/').valueChanges();
}
//Button in navbar to toggle reordering the list of songs
actionBtn(){
if (this.btnName == 'Reorder') {
this.btnName = 'Done';
this.flag = true;
}
else{
this.btnName = 'Reorder';
this.flag = false;
}
};
reorderItems(indexes){
//let element = this.songs[indexes.from];
//this.songs.splice(indexes.from, 1);
//this.songs.splice(indexes.to, 0, element);
this.songs = reorderArray(this.songs, indexes);
};
showChords(song){
this.navCtrl.push('ChordsPage', song)
}
}
HTML:
<ion-header>
<ion-navbar>
<ion-title>Songlist</ion-title>
<ion-buttons end>
<button ion-button small clear (click)="actionBtn();">
{{btnName}}
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list reorder="{{flag}}" (ionItemReorder)="reorderItems($event)">
<ion-item-sliding *ngFor="let song of songs | async ; let i = index">
<ion-item>
<h2>{{i+1}}. {{ song.Title}}</h2>
<p>{{song.Artist}}</p>
</ion-item>
<ion-item-options side="right">
<button ion-button (click)="showChords(song)">Chords</button>
</ion-item-options>
<ion-item-options side="left">
<button ion-button color="danger" (click)="removeSong(song)">Delete
<ion-icon name="trash"></ion-icon>
</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
</ion-content>
Subscribe to the Observable and map the items to be pushed into the songs array. Also, use the lifecycle hook ionViewDidLoad instead of the constructor for doing things at initialization.
import { AngularFireDatabase } from 'angularfire2/database';
import { ISubscription } from 'rxjs/Subscription';
import { OnDestroy } from '#angular/core';
import { Observable } from 'rxjs/Observable';
...
export class ... implements OnDestroy {
songslist: Observable<any[]>;
subscription: ISubscription;
songs: any[];
constructor(...
ionViewDidLoad() {
this.songslist = this.afd.list<Item>('songs').valueChanges();
this.subscription = songslist.subscribe(items => {
return items.map(item => { this.songs.push(item); });
});
}
...
reorderItems(...
...
ngOnDestroy {
this.subscription.unsubscribe(); // Make sure to unsubscribe, don't leave them open.
}
}
Trying to accomplish something very similar - except with Firestore instead of the RealTime Database - had me wanting to bang my head against the wall, but finally got it working so hopefully it's helpful to you or others even though some things will be slightly different.
The array.splice is not a function error comes up when you are trying to perform the function on something that isn't an array, in this case the observable which for reasons beyond my understanding seems to be returned as a single object.
To get things working I declared each of these variables separately:
photoCollectionRef:AngularFirestoreCollection < PhotoSlide>;
photo$: Observable < PhotoSlide [] >;
photo = {} as PhotoSlide;
photoArray: any=[];
Then set each value as follows
this.photoCollectionRef = this.afs.collection('posts').doc(this.postId).collection('photos/', ref => ref.orderBy('index'));
this.photo$ = this.photoCollectionRef.valueChanges();
this.photo$.take(1).subscribe((pictures) => {
pictures.forEach(p=>{
return this.photoArray.push({index:p.index, photoURL: p.photoURL, id:p.id })
})
});
In the template file *ngFor loops over photo of photoArray (without an async pipe) and the reorderItem function called from the item group works perfectly with standard single line syntax from the ionic docs.
Then to update the indexes in the Firestore Collection, I call the below function from a button that's visible once the list order has been modified:
saveNewOrder(){
this.photoArray.forEach((element, i) => {
this.photoCollectionRef.doc(element.id).update({
index: i
});
});
}
I should note that I also always use the .createId() method and then .doc(newId).set({id:newId etc}) when adding new documents rather than just .add because I find it a lot less annoying to do that than be stuck using .snapshotChanges and map function rather than just .valueChanges() - which is why the above functions work with the syntax shown
Also if you don't use the rxjs take(1) operator before the subscription it all looks fine at first but after the saveNewOrder function's executed the list repeats several times
I am late getting back to this but here is my attempt at explaining how I got this working.
**Caveat - I am a hobbyist coder so I apologize in advance if I'm explaining using wrong terms or if I just understand it wrong..the code works:).
The goal is to get a list from Firebase using the NEW version of Firebase and pull it into an array so that I can present it to the screen and then use the reorderArray function to keep the array indexes in order after moving items around in a list. So if I have an array of songs...I'll call it songList (think no Firebase yet) and assume this is my HTML:
<ion-list reorder="true" (ionItemReorder)="reorderItems($event)">
<ion-item *ngFor="let song of songList; let i = index">
{{i+1}}. {{song.title}} - {{song.artist}}
</ion-item>
</ion-list>
Then I have the standard function to reorder the array:
reorderItems(indexes) {
this.songList = reorderArray(this.songList, indexes);
};
Add the 'reorderArray' to the import to enable this:
import { NavController, AlertController, reorderArray } from 'ionic-angular';
After looking into this a little I think the reorderArray function does a few .splice commands to get the indexes moved around in the array.
So fast forward to replacing my array with a Firebase list. See all the above code from the 1st post...all that works to get the list to show up on the HTML page. But as soon as the reorderArray is fired I get "splice" errors thrown. As it turns out the Firebase list at this point is an object and the reorderArray expects an array. I got the below code from a Youtube video:
//Set up the songList array
var x = this.afDatabase.list('/songs');
x.snapshotChanges().subscribe(item => {
this.songList = [];
item.forEach(element => {
var y = element.payload.toJSON();
y["fbKey"] = element.key;
this.songList.push(y);
})
})
I will try to explain as best I can what this is doing. I set x to be a ref to my Firebase list /songs. I invoke snapShotChanges() to get my list values and the key. I then subscribe to the list to walk through the items. I declare setList as an array. I iterate over the list of items and I guess 'payload' is a special property that gets all the object data?? I think I cast all that object data into an array. I add a new field to the list I think so I can get back at the .key value from a field? I then push all that data into an array.
Again I'm not sure how all this magic works but it does. Now I have an array in songList that holds all my data...and now I can use the reorderArray function to keep the indexes straight on the client side after a reorder.
But...new problem. There is no client side representation of that index value out in the Firebase list.
The rest of this code is a little hazy as when things started working I was all over the map and adding lots of stuff to see it work. Right now I'm having Ionic Serve issues and can't get this running right now without deploying it up to Firebase or Ionic View...so I have to go by memory.
So here is what my final HTML looks like:
<ion-list reorder="true" (ionItemReorder)="reorderItems($event)">
<ion-item *ngFor="let song of songList | orderBy: 'sortOrder'; let i = index">
{{i+1}}. {{song.title}} - {{song.artist}} ({{song.sortOrder}})
</ion-item>
</ion-list>
The only real difference here is that I have an orderBy on a new field called sortOrder.
Here's how all this works:
reorderItems(indexes) {
var luTitle = '';
var luArtist = '';
this.songList = reorderArray(this.songList, indexes);
this.songList.forEach( song => {
this.afDatabase.database.ref('songs/' + song.fbKey + '/sortOrder').set(this.songList.indexOf(song));
this.afDatabase.database.ref('songs/' + song.fbKey)
.on('value', function(snapshot) {
luTitle = snapshot.child('title').val();
luArtist = snapshot.child('artist').val();
})
console.log("Index: " + this.songList.indexOf(song));
// console.log("Title: " + song.title);
console.log("LU Title: " + luTitle);
console.log("LU Artist: " + luArtist);
console.log("FB Key: " + song.fbKey);
console.log("Sort Order: " + song.sortOrder);
})
};
A lot of this is just logging stuff to the console but the real work is this:
The first thing I do is run the reorderArray over the this.songList and it gets all the indexes in the right place on the client side. Then I iterate over all the items in that array and I create a reference to the FB list using the song.fbKey that we set when we converted the initial FB list to an array. Then I .set a sortOrder field for that song equal the the current index of that song as it exists on the client side at that moment in time. Everything else I think after that is me logging stuff to the console to look at values. The LU (Look Up) stuff was just me figuring out how to get a value back in from Firebase.
Now the orderBy in the ngFor immediately orders everything by the sortOrder field that basically comes in real time from FB. I can't remember but I think if the list is brand new from FB and there is no sortOrder field yet it defaults to sorting by the key...which is fine...the first time reorder is fired all the sortOrders get set.
I'm sure there are some bugs I will discover when I get back to this...but it's working code as of now.
Thanks for reading if you made it this far.
ER

Ionic: reloading current child state from side menu causes loss of header

I'm creating a simple Ionic menu app and I would like to reload the current state from the side menu.Here's a Plunkr to illustrate the problem.
In the 'Search' state, the current time is displayed. The desired behavior is the following: when I click 'Search' from the side menu, the time is refreshed, i.e. the controller is reloaded. This should happen even when I'm already on the Search page. In reality, the controller is of course much more complex, but this example is enough to illustrate the problem.
I started from the ionic 'menu' starter template. To be able to reload the state, I changed two things:
disabled view caching in app.js config function: $ionicConfigProvider.views.maxCache(0);
In menu.html, I'm passing ui-sref-opts to explicitly reload the state:
ui-sref="app.search" ui-sref-opts="{reload: true}"
The result is that the time is indeed updated, however, the header of the view is gone.
Any suggestions as to what I'm doing wrong?
try something like this in the routing:
.state('tab.home', {
url: '/home',
cache: false, //<-- FORCE TO CLEAR CACHE
views: {
'tab-home': {
templateUrl: 'templates/home/home.html',
controller: 'HomeController'
}
}
})
You can broadcast an event to achieve that. But if you want to keep it like that, you could use a parameter and it would be all right because basically the one in control of the date var is your menu and not your state.
So you could do this:
app.js
.state('app.search', {
url: "/search",
params:{
date: new Date()
},
views: {
'menuContent' :{
templateUrl: "search.html",
controller: 'SearchCtrl'
}
}
})
Menu item
<ion-item nav-clear menu-close ui-sref="app.search({date: updateDate()})">
Search
</ion-item>
controllers.js (App controller or specific menu controller)
.controller('AppCtrl', function($scope) {
$scope.updateDate = updateDate;
function updateDate(){
return new Date();
}
})
controllers.js
.controller('SearchCtrl', function($scope, $stateParams) {
$scope.now = $stateParams.date;
})

Rxjs workflow for MongoDB Document References

I am developing an application on Ionic2/rc0. I got a ReplaySubject on a singlenton service that keeps the current user consistent across the whole app. It all works fine, I can subscribe to it and get a User object as easy as
this._user.Current.subscribe(user=>{ console.log(user)});
The User object looks like this
User {
confirmed:true
devices:["57f65werwe343bn8843f7h","7yr3243h5429hf2hjd"]
friends:["t245y53h65346htyh","356ytrer75dfhg43we56df"]
email:"francescoaferraro#gmail.com"
id:"57f6525e926bbc7615fc5c5c"
notification:false
password="$2a$04$.Fk/8eMj18ZrkfurbbdP4uT3yOs7Lb9db74GkNfgtABVY.ez2Q0I."
picture:"https://api.cescoferraro.xyz/kitty"
role:"master"
username:"cesco"
}
As you can see my backend is using MongoDB with One-to-Many Relationships with Document References as described here.
I have created a devices tab where I want to display all data about those user devices, but I need to call this._devices.info for each one of current.devices and concat the result back to TrueDevices
#Component({
template: `
<ion-header>
<ion-navbar>
<ion-title>Tabs</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<h2>Device:list</h2>
<h2 *ngFor="let item of devices | async">{{item}}</h2>
<button ion-button (click)="readDevice()">Read Random Device</button>
</ion-content>
`
})
export class DeviceComponent {
devices: Observable<string[]>;
TrueDevices: Observable<Device[]>;
constructor(public _user: UserService, public _device: DeviceService) {
this._user.Current.subscribe(user=>{ this.devices = Observable.of(user.devices)});
// Get current User
// call this._devices.info for each one of current.devices
// concat the result back to TrueDevices
this._user.Current
.subscribe((result) => { console.log(result) });
}
readDevice(){
console.log(this.devices);
this._device.info(this.devices.value[0]).subscribe(data=>console.log(data))
}
}
I will need to repeat the same procedure to the friends tab and so on. I am pretty sure there are a couple operators that would do the magic, but I am fairly new to rxjs and not familiar with all of them. Whats the right approach?
this._user.Current
.switchMap(user => Observable.from(user.devices)) // after this line, you have an Observable<string>
.mergeMap(device => this._device.info(device)) // each device will be mapped to another observable(or stream), and all the streams will be merged together
.toArray() // wait for all the streams to complete and reduce all the results into an array.
.subscribe(array => console.log(array));
or go to the gitter room:
https://gitter.im/Reactive-Extensions/RxJS

DOM elements not accessible after onsen pageinit in ons.ready

I am using the Onsen framework with jQuery and jQuery mobile, it appears that there is no way to catch the event that fires once the new page is loaded.
My current code, which executes in the index.html file (the master page)
<script src="scripts/jQuery.js"></script>
<script src="scripts/jquery.mobile.custom.min.js"></script>
<script src="scripts/app.js"></script>
<script>
ons.bootstrap();
ons.ready(function() {
$(document.body).on('pageinit', '#recentPage', function() {
initRecentPage();
});
});
in app.js is the following code
function initRecentPage() {
$("#yourReports").on("tap", ".showReport", recentShowReport);
var content = document.getElementById("yourReports");
ons.compile(content);
}
and the HTML:
<ons-page id="recentPage">
<ons-toolbar id="myToolbar">
<div id="toolBarTitle" class="center">Recent Checks</div>
<div class="right">
<ons-toolbar-button ng-click="mySlidingMenu.toggleMenu()">
<ons-icon icon="bars"></ons-icon>
</ons-toolbar-button>
</div>
</ons-toolbar>
<ons-scroller>
<h3 class="headingTitle"> Checks</h3>
<div id="Free" class="tabArea">
<ons-list id="yourReports">
</ons-list>
<ons-button id="clearFreeRecentButton">
<span id="clearRecentText" class="bold">Clear Recent Checks</span>
</ons-button>
</div>
</ons-scroller>
</ons-page>
in this instance the variable 'content' is always null. I've debuged significantly, and the element I am trying to get is not present when this event fires. It is loaded later.
So, the question is, how do I ensure that all of the content is present before using a selector. It feels like this is an onsen specific issue.
In the end I could only find one reliable way of making this work.
Essentially I had to wait, using setTimeout 300 milliseconds for the DOM elements to be ready. It feels like a hack, but honestly there is no other reliable way of making this behave. The app is in the app store and works well, so even though it seems like a hack, it works:
$(document).on('pageinit', '#homePage', function() {
initHomePage();
});
function initHomePage() {
setTimeout(function() {
setUpHomePage();
}, 300);
}
Move your $(document.body).on('pageinit', '#recentPage', function() { outside of ons.ready block.
JS
ons.bootstrap();
ons.ready(function() {
console.log("ready");
});
$(document.body).on('pageinit', '#recentPage', function() {
initRecentPage();
});
function initRecentPage() {
//$("#yourReports").on("tap", ".showReport", recentShowReport);
var content = document.getElementById("yourReports");
alert(content)
ons.compile(content);
}
I commented out a line because I do not have access to that "recentShowReport"
You can see how it works here: 'http://codepen.io/vnguyen972/pen/xCqDe'
The alert will show that 'content' is not NULL.
Hope this helps.