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
Related
Select element bound to a state object loses update binding.
I am doing some exploration with lit-html and various routing/store combinations and have come across this strange behaviour. I have used the create-lit-app stackblitz.
I have a menu which is populated from an array on the store.state object. These links update a currentPage attribute on the store.
The navigation is self listens to the store and sets an [active] attribute if it matchs the store's currentPage.
Click events update the store. This is all working fine.
In another element (hello-world) I have a select box. The select box is populated by the same array on the store.state object. The [selected] attribute is set if the option matches the currentPage attribute. This also works fine.
I also put a couple of p tags which display the currentPage for sanity... or insanity... not yet sure.
Now, when I go back to using the menu the state changes and the menu [active] is working, as are the p tag updates in the other (hello-world) element. The select element however does not update. I can use the select to update the state but the binding down to the select seems to have stopped working all together.
I have tried super.update() and this.update() after googling similar issues but still no cheese. I am wondering if it is a function of an ugly anti-pattern on my part or if this might be an RC bug.
class HelloWorld extends LitElement {
constructor() {
super();
store.subscribe(() => super.update());
}
_updatePage(e) {
console.log(e.target.value);
store.update(s => s.currentPage = e.target.value);
}
render() {
return html`
<p class="app-intro">
Current page: ${store.state.currentPage}
</p>
<select #change=${(e) => this._updatePage(e)}>
${store.state.navItems.map(navItem => {
return html`<option ?selected=${store.state.currentPage===navItem.page}>${navItem.page}</option>`
})
}
</select>
<p class="app-intro">
Current page: ${store.state.currentPage}
</p>
`;
}
}
I want to be able to update the state from anywhere in the app and have all subscribers updated.
My suggestion is to set properties on your element when the store updates, that way your element is not tightly coupled to the store's structure.
Otherwise, you can use this.requestUpdate() to trigger an update. update() is just a lifecycle callback which is called by LitElement internally.
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
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
I am trying to build a small, 3 step form. It would be something similar to this:
The way I did this in react was by using redux to track form completion and rendering the form body markup based on the step number (0, 1, 2).
In angular 2, what would be a good way to do this? Here's what I am attempting at the moment, and I'm still working on it. Is my approach fine? Is there a better way to do it?
I have a parent component <app-form> and I will be nesting inside it <app-form-header> and <app-form-body>.
<app-form>
<app-header [step]="step"></app-header>
<app-body [formData]="formData"></app-body>
</app-form>
In <app-form> component I have a step: number and formData: Array<FormData>. The step is just a index for each object in formData. This will be passed down to the header. formData will be responsible the form data from user. Each time the form input is valid, user can click Next to execute nextStep() to increment the index. Each step has an associated template markup.
Is there a better way to do something like this?
don't overdo it, if it is a simple form you don't need to use the router or a service to pass data between the steps.
something like this will do:
<div class="nav">
</div>
<div id="step1" *ngIf="step === 1">
<form></form>
</div>
<div id="step2" *ngIf="step === 2">
<form></form>
</div>
<div id="step3" *ngIf="step === 3">
<form></form>
</div>
It's still a small template, and you kan keep all of the form and all the data in one component, and if you want to you can replace the ngIf with something that switches css-classes on the step1,2,3 -divs and animate them as the user moves to the next step
If you want to keep things extensible, you could try something like this:
<sign-up>
<create-account
[model]="model"
[hidden]="model.createAccount.finished">
</create-account>
<social-profiles
[model]="model"
[hidden]="model.socialProfiles.finished">
</social-profiles>
<personal-details
[model]="model"
[hidden]="model.personalDetails.finished">
</personal-details>
</sign-up>
export class SignUpVm {
createAccount: CreateAccountVm; //Contains your fields & finished bool
socialProfiles: SocialProfilesVm; //Contains your fields & finished bool
personalDetails: PersonalDetailsVm; //Contains your fields & finished bool
//Store index here if you want, although I don't think you need it
}
#Component({})
export class SignUp {
model = new SignUpVm(); //from sign_up_vm.ts (e.g)
}
//Copy this for personalDetails & createAccount
#Component({})
export class SocialProfiles {
#Input() model: SignUpVm;
}
I'm building a small sails.js + angular.js app.
Here is a fiddle that roughly shows what my code looks like: http://jsfiddle.net/WEk3F/
index: function(req, res, next) {
Food.find({}, function foundFoods(err, foods) {
if (err) return next(err);
var data = {
name1: "test1",
name2: "test2"
}
res.view(
'food/index', {
foods: data
});
});
},
<div ng-app="myApp">
<div ng-controller="FoodController">
<ul>
<li ng-repeat="food in foods">
{{food.name}}
</li>
</ul>
<form>
<label for="name">Name:</label>
<input name="name" ng-model="editableFood.name" />
</form>
</div>
</div>
My problem is, that whenever i try to retrieve the data from my controller, i don't get those 2 items but instead it renders more and more items and just doesn't stop. even the page gets slow and unresponsive and almost freezes.
When i say
$scope.foods = [{"name": "test1"},{"name": "test2"}];
instead of
$scope.foods = Food.query();
it works. but i want the data to be coming from the backend via the controller.
the other methods (add, update etc) of the angular.js $resource module work fine for me.
/food maps to the index action of my FoodController and just returns some fixed test data
i found out what the problem was.
the angular.js $resource should only be used in a restful way, so the GET request to my food/index should return an array of objects.
in my case this wasn't the case. instead i used the index action of my controller to render a view and the result was therefor html.
so my options are to define a new route, that the $resource takes for the Query() command or define a new action that i use for the view/html stuff and use the index action again for pure restful response.
thx #sam hunter for pointing me to the right direction
the infinite loop that i received is explained here: One dimensional array of strings being parsed to 2d by angular resource
i love stack overflow