Property ‘indexOf’ does not exist on type 'FirebaseListObservable<any[]> - firebase

I am trying to iterate and find the index of items in a Firebase backend and swipe right and left through these:
export class articleSwiperComponent implements OnInit {
articles: FirebaseListObservable<any[]>;
public orientation: Orientation;
public selectedArticle: article;
private changeDetectorRef: ChangeDetectorRef;
constructor(private af: AngularFire, changeDetectorRef: ChangeDetectorRef) {
this.articles = af.database.list('/articles');
this.changeDetectorRef = changeDetectorRef;
this.orientation = "none";
this.selectedArticle = this.articles[ Math.floor( Math.random() * this.articles.length ) ];
}
I have methods to swipe through the the database articles which will randomly bring forward an article upon first navigating to this page.
public showNextArticle() : void {
this.orientation = "next";
this.changeDetectorRef.detectChanges();
var index = this.articles.indexOf( this.selectedArticle );
this.selectedArticle = this.articles[ index + 1 ]
? this.articles[ index + 1 ]
: this.articles[ 0 ]
;
}
public showPrevarticle() : void {
this.orientation = "prev";
this.changeDetectorRef.detectChanges();
// Find the currently selected index.
var index = this.articles.indexOf( this.selectedArticle );
this.selectedArticle = this.articles[ index - 1 ]
? this.articles[ index - 1 ]
: this.articles[ this.articles.length - 1 ]
;
}
}
However I am getting Property ‘indexOf’ does not exist on type 'FirebaseListObservable<any[]> Property 'length' does not exist on type 'FirebaseListObservable<any[]> errors. What is the equivalent of these properties in Firebase?

I believe your problem is that you're not really working with an array, you're working with an AngularFire Observable (which is effectively an RXJS Observable). This means you would need to subscribe to the observable in order to get the real value of the array, as the observable is used to emit new values of the array as the data changes. See below...
export class articleSwiperComponent implements OnInit, OnDestroy {
public articles: any[] = [];
public orientation: Orientation = 'none';
public selectedArticle: article;
private _articlesSubscription: Subscription;
constructor(private af: AngularFire, private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit() {
// Listen to database
this._articlesSubscription = this.af.database.list('/articles').snapshotChanges().subscribe((snapshot) => {
// Assign articles to local variable as they come in
this.articles = snapshot.map((d) => {
return { $key: d.key, ... d.payload };
});
// Only assign selected article if none assigned
if (!this.selectedArticle) {
this.selectedArticle = this.articles[ Math.floor( Math.random() * this.articles.length ) ];
}
});
}
ngOnDestroy() {
// Destroy listener to database
this._articlesSubscription.unsubscribe();
}
}
If you're not interested in listening to changes to the firebase database you could do this instead:
export class articleSwiperComponent implements OnInit, OnDestroy {
public articles: any[] = [];
public orientation: Orientation = 'none';
public selectedArticle: article;
constructor(private af: AngularFire, private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit() {
// Listen to first emission from database
this.af.database.list('/articles').snapshotChanges().pipe(first()).subscribe((articles) => {
// Assign articles to local variable
this.articles = snapshot.map((d) => {
return { $key: d.key, ... d.payload };
});
this.selectedArticle = this.articles[ Math.floor( Math.random() * this.articles.length ) ];
});
}
}
No matter which option you choose, your showNextArticle() should work as you now have an array object to work from.

Related

ngrx/data add new property to existing collection

I'm currently using NgRx/data and I have 2 collections: Courses and Lessons.
I found something in their documentation to overwrite EntityCollectionReducer:
#Injectable()
export class AdditionalEntityCollectionReducerMethodsFactory {
constructor(private entityDefinitionService: EntityDefinitionService) {}
create<T>(entityName: string): EntityCollectionReducerMethodMap<T> {
const definition = this.entityDefinitionService.getDefinition<T>(
entityName
);
const methodsClass = new AdditionalEntityCollectionReducerMethods(
entityName,
definition
);
return methodsClass.methods;
}
}
and in AdditionalEntityCollectionReducerMethods, I overwrote some methods to add the new property:
export class AdditionalEntityCollectionReducerMethods<T> extends EntityCollectionReducerMethods<T> {
constructor(
public entityName: string,
public definition: EntityDefinition<T>
) {
super(entityName, definition);
}
protected saveAddOne(collection: EntityCollection<T>, action: EntityAction<T>): EntityCollection<T> {
const ec = super.saveAddOne(collection, action);
(ec as any).saving = true;
return ec;
}
protected saveAddOneSuccess(collection: EntityCollection<T>, action: EntityAction<T>): EntityCollection<T> {
const ec = super.saveAddOneSuccess(collection, action);
(ec as any).saving = false;
return ec;
}
protected saveAddOneError(collection: EntityCollection<T>, action: EntityAction<EntityActionDataServiceError>): EntityCollection<T> {
const ec = super.saveAddOneError(collection, action);
(ec as any).saving = false;
return ec;
}
}
Also in course.module.ts I specified this property as additionalCollectionState in entityMedatata:
const entityMetadata: EntityMetadataMap = {
Course: {
...
additionalCollectionState: {
saving: false,
},
}
...
};
The AdditionalEntityCollectionReducerMethods was registered in app.module.ts as provider:
{
provide: EntityCollectionReducerMethodsFactory,
useClass: AdditionalEntityCollectionReducerMethodsFactory,
},
So, in this way, I'm adding a new property called saving in the Courses collection.
But my problem is that if I'm using the saveAddOne method in other modules, the saving property will be added also here and I don't want this.
I think this is happening because I've registered the AdditionalEntityCollectionReducerMethodsFactory in app.module, but I tried to register this in course.module and to debug, but the breakpoint is not hit there (only in app.module). I also have to mention that the course.module is lazyloaded.
Is there a possibility to add a new property in only one specific collection?
Here is a simple way to do this
For in-depth: https://ngrx.io/guide/data/entity-metadata
export const appEntityMetadata: EntityMetadataMap = {
Hero: {
/* optional settings */
filterFn: nameFilter,
sortComparer: sortByName
},
Villain: {
villainSelectId, // necessary if key is not `id`
/* optional settings */
entityName: 'Villain', // optional because same as map key
filterFn: nameAndSayingFilter,
entityDispatcherOptions: { optimisticAdd: true, optimisticUpdate: true }
}
};
Register metadata:
EntityDataModule.forRoot({
...
entityMetadata: appEntityMetadata,
...
})

Unable to map autocomplete valueChanges with Cloud Firestore database

I am using material autocomplete in a project but unlike their example, I'm pulling from cloud firestore database (beta).
I previously set up a service to handle retrieving the data from json server and this worked fine.
ngOnInit() {
this.branches = this.branchService.get_branches();
...
}
Since moving to the firebase I'm successfully able to display my data but typeahead functionality is not working as expected.
I've tried using both valueChanges() and snapshotChanges() but neither appears to make a difference and I don't know why.
component.ts file:
branches: any = [];
branchCtrl: FormControl = new FormControl();
filteredBranches: Observable<any[]>;
branchCol: AngularFirestoreCollection<Branch>;
branch$: Observable<Branch[]>;
constructor( private afs: AngularFirestore ) {
}
ngOnInit() {
this.branchCol = this.afs.collection('branches');
//this.branch$ = this.branchCol.valueChanges();
this.branch$ = this.branchCol.snapshotChanges().map(actions => { return actions.map(action => { const data = action.payload.doc.data() as Branch; const id = action.payload.doc.id; return { id, ...data }; }); });
this.branches = this.branch$;
this.filteredBranches = this.branchCtrl.valueChanges
.startWith(null)
//.map(b => b && typeof b === 'object' ? b.name : b)
.switchMap(val => {
return this.filterBranches(val || '')
});
}
displayFn(b): string {
return b ? b.name : b;
}
filterBranches(val: string) {
return this.branches
.map(response => response.filter(option => {
return option.name.toLowerCase().indexOf(val.toLowerCase()) === 0
}));
}
getBranch(value){
console.log('branch selected');
}
My assumption is filteredBranches is not able to map the value changes correctly due to collection/document data structure of firebase. Any help would greatly be appreciated.

How to change immutablejs Record with methods from derived class?

I have 3 classes derived from Record. Definitions of first two classes are below.
// Base.js
import {Record} from 'immutable';
import * as uuid from 'uuid';
export const Base = defaultValues => {
return class extends Record({
key: null,
...defaultValues,
}) {
constructor(props) {
super(Object.assign({}, props, {key: (props && props.key) || uuid.v4()}));
}
};
};
// LOBase.js
import {Base} from './BaseModel';
export const LOBase = defaultValues => {
return class extends Base({
created_at: new Date(null),
updated_at: new Date(null),
deleted_at: new Date(null),
isActive: new Boolean(),
isDeleted: new Boolean(),
publishState: new String(),
...defaultValues,
}) {};
};
And this is my last class derived from LOBase and where my problem is.
// Question.js
import {List, Record, fromJS} from 'immutable';
import _ from 'lodash';
import {LOBase} from './base/LOBaseModel';
export class Question extends LOBase({
id: '',
name: 'test',
description: '',
questionType: 1,
title: 'title',
version: new String(),
customData: {},
//...
}) {
insertOption() {
let index = this.customData.options.length;
this.updateIn(['customData', 'options'], options => {
return options.splice(index, 0, {
someGenericStuff: [],
// ...
});
});
return this;
}
static MultipleChoice() {
let defaultCustomData = {
options: [],
//...
};
let question = new Question()
.set('questionType', QUESTION_TYPE_MULTIPLE_CHOICE)
.set('customData', new Record(defaultCustomData)())
//...
.insertOption()
.insertOption()
.insertOption();
return question;
}
// ...
}
I use let question = Question.MultipleChoice() to create a new Question instance. And when i use question.insertOption() it works fine. But when I do this in the reducer on the state I get an error saying "A state mutation was detected inside a dispatch".
How can I achieve to change question object in the state? Should I clone original Record before doing that? What is the Immutablejs way to do that?
Thanks in advance.
insertOption uses this.updateIn but does not return or store the result.
When you return this at the end of the function you actually return the same immutable Record without the changes.
So, unless I'm missing something here, you should probably go with:
insertOption() {
let index = this.customData.options.length;
return this.updateIn(['customData', 'options'], options => {
return options.splice(index, 0, {
someGenericStuff: [],
// ...
});
});
}
The updateIn will return a new instance of the Record with the updated values.
You did not add your state structure and reducer (if you can please do), but you should be sure to return a new state object every time and not just changing the question field.
BTW, you are doing a sequence of mutation methods one after the other (set, set, updateIn). This is not suggestable from a performance perspective. I'd suggest replacing it with withMutations in the following manner:
static insertOption(record) {
let index = record.customData.options.length;
return record.updateIn(['customData', 'options'], options => {
return options.splice(index, 0, {
someGenericStuff: [],
// ...
});
});
}
static MultipleChoice() {
// ...
let question = new Question();
question.withMutations(record => {
record.merge({
questionType: QUESTION_TYPE_MULTIPLE_CHOICE,
customData: new Record(defaultCustomData)()
})
Question.insertOption(record);
})
return question;
}

Problems with onQueryEvent observable failing upon routing in Nativescript with Angular

I am using Nativescript with Angular and have code written that succesfully calls an onQueryEvent from the nativescript-firebase-plugin for data set upon first building the application. However after following a route to a second component containing the exact same onQueryEvent the data succeeds to retreive a complete list but skips the onQueryEvent.
In all honesty I don't know best practices for queries in any situation let alone this one, so I hope it is just a matter of manipulating how I call the onQueryEvent.
I believe the problem to be in the firebase.query inside the getMyTransactionList() function of the firebase.service.ts file.
the overview.component.html page has a transaction | async RadListView that successfully filters upon running tns run android. Then clicking any link directing to the deal-summary.component.html page where the function is re-iterated refuses to query by the static storage variable set in the firebase.service
Here is my code:
firebase.service.ts
export class FirebaseService {
private _allItems: Array<any> = [];
items: BehaviorSubject<Array<any>> = new BehaviorSubject([]);
public storage: any = '-KomUSGcX-j6qQmY4Wrh'; // set statically to test different routes
constructor(
private ngZone: NgZone,
){}
// fetch data
getMyDealList(): Observable<any> {
return new Observable((observer: any) => {
let path = `deals/${BackendService.token}`;
let onValueEvent = (snapshot: any) => {
this.ngZone.run(() => {
let results = this.handleSnapshot(snapshot.value);
observer.next(results);
});
};
firebase.addValueEventListener(onValueEvent, `/${path}`);
}).share();
}
getMyTransactionList(): Observable<any> {
return new Observable((observer: any) => {
let path = `transactions/${BackendService.token}`;
// this is a merge of jen loopers giftler code combined with nativescrip-firebase-plugins standard onQueryEvent. It works on the first load but routing to a second instance of the same function retrieves all the data without queryEvent
let onQueryEvent = (snapshot: any) => {
this.ngZone.run(() => {
let results = this.handleSnapshot(snapshot.value);
observer.next(results);
});
};
firebase.query(
onQueryEvent,
`/transactions/${BackendService.token}`,
{
singleEvent: true,
orderBy: {
type: firebase.QueryOrderByType.CHILD,
value: 'dealId' // mandatory when type is 'child'
},
range:
{
type: firebase.QueryRangeType.EQUAL_TO,
value: `${this.storage}` // this calls a static variable for testing consistency
}
,
}
);
firebase.addValueEventListener(onQueryEvent, `/${path}`);
console.log("transaction Listener added");
}).share();
}
handleSnapshot(data: any) {
//empty array, then refill and filter
this._allItems = [];
if (data) {
for (let id in data) {
let result = (<any>Object).assign({id: id}, data[id]);
this._allItems.push(result);
}
this.publishUpdates();
}
return this._allItems;
}
publishUpdates() {
// here, we sort must emit a *new* value (immutability!)
this._allItems.sort(function(a, b){
if(a.date < b.date) return -1;
if(a.date > b.date) return 1;
return 0;
})
this.items.next([...this._allItems]);
}
}
app.component.ts
<page-router-outlet></page-router-outlet>
overview.component.ts
export class OverviewComponent implements OnInit {
public deals: Observable<any>;
public transactions: Observable<any>;
constructor(private router: Router,
private firebaseS: FirebaseService,
){ }
ngOnInit() {
this.deals = <any>this.firebaseS.getMyDealList();
this.transactions = <any>this.firebaseS.getMyTransactionList();
}
viewDealSumm(id){
this.router.navigate(['dashboard/deal-summary', id]);
}
}
overview.component.html
<RadListView [items]="deals | async ">
<ng-template tkListItemTemplate let-item="item">
<StackLayout (tap)="viewDealSumm(item.id)">
<Label [text]="item.dealName"></Label>
</StackLayout>
</ng-template>
</ListViewGridLayout>
</RadListView>
<RadListView [items]="transactions | async " >
<ng-template tkListItemTemplate let-item="item">
<GridLayout>
<Label [text]="item.transName"></Label>
</GridLayout>
</ng-template>
</RadListView>
deal-summary.component.ts
export class DealSummaryComponent implements OnInit {
public transactions: Observable<any>;
constructor(
private firebaseS: FirebaseService,
){ }
ngOnInit() {
this.transactions = <any>this.firebaseS.getMyTransactionList();
}
deal-summary.component.html
<RadListView [items]="transactions | async " >
<ng-template tkListItemTemplate let-item="item">
<GridLayout >
<Label col="1" [text]="item.transName"></Label>
</GridLayout>
</ng-template>
</RadListView>

Data in one component wont bind to array in injectable

I have following issue:
I have one component, in which I am calling:
this.users = UsersInj.getUsersCollection()
In UsersInj, I have:
#Injectable()
export class UsersInj{
public users:any = [];
constructor(private _http:Http){
this.getUsers().subscribe(
success=>{
this.users = success.json();
},
error =>{
console.log('error')
}
)
}
getUsers(){
return this._http.get('/api/user');
}
getUsersCollection(){
console.log('GET USERS COLLECTION :',this.users);
return this.users;
}
}
However, this.users.length in my component is always 0. Any ideas?
UPDATE
It works when I pack this.users in UsersInj in an object.
PLNKR
In the plunker you copy the values (references) once when TheContent is created.
export class TheContent {
name: any;
constructor(public nameService: NameService) {
console.log("content started");
this.info = nameService.info
this.names = nameService.names;
}
changeMyName() {
this.nameService.change();
}
}
In NameService you assign a new array to this.names.
this.names = success.json();
this.names in TheContent and this.names in NameService are now not connected anymore.
What you probably want to do is
change(){
this.info.name = "Jane";
this.http.get('https://api.github.com/repos/vmg/redcarpet/issues?state=closed').subscribe(success=>{
while(this.names.length > 0) {
this.names.pop();
}
this.names.push.apply(this.names, success.json());
console.log('names: ' + this.names);
});
}
or alternatively copy the new array to TheContent again.
In Angular using an Observable that allows interested parties to subscribe to changes and pass the new value with the notification is the preferred way. See also https://stackoverflow.com/a/35568924/217408

Resources