Angular: Getting the last 4 posts from Contentful - angular5

Hi I'm trying to get the last four blog posts from contentful api in angular 5. I've created a service and I'm capable of retrieving 1 with the following code.
getContent(contentId) {
const promise = this.client.getEntry(contentId);
return Observable.fromPromise(promise).map(entry => entry.fields);
};
I would like to return it as an observable, however if returned as a promise, I would like to know how to work with a promise in the component.ts and -.html.
getLastByCount(number) {
var promise = this.client.getEntries({
limit: number
})
...
}
If i do the same for getting multiple entries I get a 'PromiseObservable', which contains a 'promise: ZoneAwarePromise'. In which it has 'items: Array', where object are as when i log the single entry. How do I work which such objects?
Edited:
I've done as suggested by: Stephan
getLastByCount(number) {
var promise = this.client.getEntries({
limit: number
})
return Observable.fromPromise(promise).mergeMap((collection) => (
Observable.from(collection.items)
))
}
And in my component.ts in OnInit()
posts$: Observable<any>;
this.posts$ = this.contentfulService.getLastByCount(4).map(entry => entry.fields);
In my component.html, i do this when displaying the one entry
<div *ngIf="post$ | async as post">
<h1>{{ post.headline }}</h1>
<time>Published on {{ post.published | date: 'fullDate' }}</time>
<hr>-->
</div>
I try this when using the collection:
<div *ngIf="posts$ | async as posts">
<ul>
<li class="post" *ngFor="let post of posts$ | async">
<h1>{{ post.headline }}</h1>
<time>Published on {{ post.published | date: 'fullDate' }}</time>
<hr>
</li>
</ul>
</div>
I get this error: Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.

What you probably want is an Observable that emits every single item of the collection that the Contentful client returns.
To do so, you can use the mergeMap functionality:
Observable
.fromPromise(promise)
.mergeMap((collection) => (
Rx.Observable.from(collection.items)
))
Now you can work with every item in the observable sequence as with the single entry above, e.g. you can
.map(entry => entry.fields)
EDIT:
Turns out Angular expects you to have an Observable<Entry[]>, instead of Observable<Entry>, so the proper way is:
Observable
.fromPromise(promise)
.map((collection) => collection.items)

Related

How to keep track of an array change in Vue.js when the index is a dynamic value?

I am building an app using Node.js and Vue.
My DATA for the component is the following:
data() {
return {
campaign: {
buses: [],
weeks: [
{
oneWayBuses: [],
returnBuses: []
}
]
},
busesMap: {
// id is the bus ID. Value is the index in the campaign.buses array.
},
};
},
I fill the buses and weeks array in MOUNTED section in two separate methods after getting the data from the server:
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
So the idea is that my busesMap looks like busesId keys and index values:
busesMap = {
'k3jbjdlkk': 0,
'xjkxh834b': 1,
'hkf37sndd': 2
}
However, when I try to iterate over weeks, v-if does not update so no bus info is shown:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[busesMap.busId]">
<strong>{{ campaign.buses[busesMap.busId].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap.busId].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
On the other side, if I shorten the v-if condition to campaign.buses, then I get into the condition but campaign.buses[busesMap.busId] is still undefined, so I get an ERROR trying to display busLabel and oneWayDepartureDate
I've read vue in depth documentation, but couldn't come up with a resolution.
Any gotchas you can find out?
Try this:
async mounted(){
await responseForWeeks
await responseForBuses
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
// this is partial since it is what you provided
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
}
Basically you want to make sure that before your component loads your data is in place. You can also create computed properties which will force re rendering if dependencies are changed and they are in the dom.
Actually, the problem was indeed in the HTML.
When trying to access the object keys, better use [] intead of a dot .
Final HTML result would be as follows:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[[busesMap[busId]]]">
<strong>{{ campaign.buses[busesMap[busId]].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap[busId]].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
What was happening is that previously campaign.buses[busesMap.busId] did not exist, thus not rendering anything. Solved to campaign.buses[busesMap[busId]]. Also used claudators for the displayed moustache sintach.
Hope it helps someone else messing with Objects!

Returning $key in AngularFire 5

I have the following code that works with AngularFire 5:
export class NavigationComponent {
items: Observable<any[]>;
constructor(db: AngularFireDatabase) {
this.items = db.list('/pages', ref => {
let query = ref.limitToLast(100).orderByChild('sortOrder');
return query;
}).valueChanges();
}
}
But now need to return item.$key which apparently is no longer returned by default in AngularFire 5. I see mention in the migration guide of needing to "map" this, but can't seem to get the right syntax working on the above code.
Update: followed the advice below, and it seemed to have worked but there appears to be some difference still between the behavior between old and new.
<nav class="nav-standard">
<app-logo></app-logo>
<div class="nav-dropdown">
<ul *ngFor="let item of items | async | filter : 'parent' : '00000000-0000-0000-0000-000000000000'" class="nav-dropdown">
<li>
<a mat-button class="mat-button" href="{{item.path}}" data-id="{{item.key}}" target="{{item.target}}" title="{{item.tooltip}}" [attr.data-sort]="item.sortOrder" *ngIf="item.content; else elseBlock">{{item.menuText}}</a>
<ng-template #elseBlock>
<a mat-button class="mat-button" data-id="{{item.key}}" target="{{item.target}}" title="{{item.tooltip}}">{{item.menuText}}</a>
</ng-template>
<ul class="nav-dropdown-content">
<li *ngFor="let childItem of items | async | filter : 'parent' : item.key" class="">
<a class="mat-button" href="{{childItem.path}}" data-id="{{childItem.key}}" target="{{childItem.target}}" title="{{childItem.tooltip}}" [attr.data-sort]="childItem.sortOrder">{{childItem.menuText}}</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
The nested *ngFor never seems to fire, whereas before it did. It appears that items in the nested *ngFor is null ?? I found if I create another Observable in my component called childItems and assign duplicate logic to that, it works okay -- but to me that feels dirty and wrong. How can I get the data in the observable to persist long enough to use it in the nested *ngFor ?
item key information isn't 'free' anymore upon the new AngularFire API changes. Instead of use '.valueChanges()' to turn reference into Observable, you can use '.snapshotChanges()':
.snapshotChanges()
.map(changes => {
return changes.map(change => ({key: change.payload.key, ...change.payload.val()}));
})
from then on you can reference item key by using 'item.key' (notice that you cannot use '$key').

Angular 2 http - get data from API

I'm trying to get data in JSON format from this site https://api.chucknorris.io/ (random joke), but i get an error:ERROR Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays. And "DebugContext_ {view: Object, nodeIndex: 11, nodeDef: Object, elDef: Object, elView: Object}"
This is routing.ts
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/map';
#Injectable()
export class RandomService {
constructor(private http: Http) {
console.log('working');
}
getRandomJokes() {
return this.http.get('https://api.chucknorris.io/jokes/random')
.map(res => res.json());
}
}
This is the component i'm trying to get data into
<div *ngFor="let joke of jokes">
<h3>{{joke.value}}</h3>
</div>
`,
providers: [RandomService]
})
export class PocetnaComponent {
jokes: Joke[];
constructor(private jokesService: RandomService){
this.jokesService.getRandomJokes().subscribe(jokes => {this.jokes = jokes});
}
}
interface Joke{
id: number;
value: string;
}
Since what you are receiving is not an array, but an object, therefore the error you are getting is because an object cannot be iterated. You might want to rethink the naming, since we are dealing with just one object, joke would be a more suitable name. But here, let's use jokes.
So you should do is remove the *ngFor completely and simply display the value with:
{{jokes?.value}}
Nothing else is needed :) Notice the safe navigation operator here, which safeguards null and undefined values in property paths. Be prepared to use this a lot in the asynchronous world ;)
Here is more info about the safe navigation operator, from the official docs.
You are not binding an array to the *ngFor, you have to make sure that jokes is actually an array and not a random joke object.
To solve it if it is an object you can just do this.jokes = [jokes].
The url you mention only provides a single value in an object, so you cannot iterate over it.
Maybe make multiple calls like this:
getRandomJokes(n = 10){
return Promise.all(Array(n).fill(0)
.map(() => this.http.get('https://api.chucknorris.io/jokes/random').toPromise()
.then(res => res.json()));
}
and use with promise
this.jokesService.getRandomJokes().then(jokes => {this.jokes = jokes});
you can check if it is an array and then go for ngFor or convert the jokes to array like let jokes = [jokes];
<div *ngIf = "jokes.length > 0" ; else check>
<div *ngFor="let joke of jokes">
<h3>{{joke.value}}</h3>
</div>
</div>
<ng-template #check>
<p>print the value of the object </p>
</ng-template>
May be jokes isn't an array.
<div *ngIf="Array.isArray(jokes)>
<div *ngFor="let joke of jokes">
<h3>{{joke.value}}</h3>
</div>

vue.js method can't access variable from data object with multiple rows

I am currently learning vue.js and having trouble accessing data in the methods.
data is loaded and set as a global variable (for now, this will probably change but not part of the problem now i think)
through ajax call this data is received:
data":[{"itemId":"58646f066803fa62388b4573","color":"#ffb878","name":"test1","startDate":"04/24/2017","work":"9.25"},{"itemId":"58646f066803fa62388b4572","color":"#ffb878","name":"test2","startDate":"04/24/2017","work":"4.25"},{"itemId":"58646f066803fa62388b4571","color":"#a4bdfc","name":"test3","startDate":"05/01/2017","work":"24.00"}]
which is set as a global (variable data is set outside of the functions) with:
...success: function (jsonObj)
{
data['item'] = jsonObj.data
....
now for the vue part:
var app = new Vue({
el:'#canvas',
data: {
items: data['item']
},
methods: {
moveItem: function(){
console.log("new date: "+this.startDate);
}
}
})
the html:
<div v-for="row in items" class="entirerow" v-bind:id="'row'+row.itemId">
<div class="itemrow">{{ row.name }}</div>
<div class="itemrow"><input type="text" v-model="row.startDate" #change="moveItem"></div>
<div class="itemrowlast">{{ row.work }}</div>
</div>
this nicely shows 3 rows with the correct data in each row. So far so good. But now if I change something in the input value the method moveItem is triggered but states "new date: undefined" in the console.log
I've tried console.log("new date: "+this.items.startDate) as well but no cigar and then it would seem the method wouldn't know which row is handled.
How can I access the correct data in the method, so from a certain row in the loop?
Thanks!
You refer to data object in method (this.startDate) not to item
moveItem: function(){
console.log("new date: "+this.startDate);
}
You can change your code like this
template
#change="moveItem(row)"
script
moveItem: function(row){
console.log("new date: " + row.startDate);
}

Aurelia iterate over map where keys are strings

I'm having trouble getting Aurelia to iterate over a map where the keys are strings (UUIDs).
Here is an example of the data I'm getting from an API running somewhere else:
my_data = {
"5EI22GER7NE2XLDCPXPT5I2ABE": {
"my_property": "a value"
},
"XWBFODLN6FHGXN3TWF22RBDA7A": {
"my_property": "another value"
}
}
And I'm trying to use something like this:
<template>
<div class="my_class">
<ul class="list-group">
<li repeat.for="[key, value] of my_data" class="list-group-item">
<span>${key} - ${value.my_property}</span>
</li>
</ul>
</div>
</template>
But Aurelia is telling me that Value for 'my_data' is non-repeatable.
I've found various answer by googling, but they have not been clearly explained or incomplete. Either I'm googling wrong or a good SO question and answer is needed.
As another resource to the one supplied by ry8806, I also use a Value Converter:
export class KeysValueConverter {
toView(obj) {
if (obj !== null && typeof obj === 'object') {
return Reflect.ownKeys(obj).filter(x => x !== '__observers__');
} else {
return null;
}
}
}
It can easily be used to do what you're attempting, like this:
<template>
<div class="my_class">
<ul class="list-group">
<li repeat.for="key of my_data | keys" class="list-group-item">
<span>${key} - ${my_data[key]}</span>
</li>
</ul>
</div>
</template>
The easiest method would be to convert this into an array yourself (in the ViewModel code)
Or you could use a ValueConverter inside repeat.for as described in this article Iterating Objects
The code...
// A ValueConverter for iterating an Object's properties inside of a repeat.for in Aurelia
export class ObjectKeysValueConverter {
toView(obj) {
// Create a temporary array to populate with object keys
let temp = [];
// A basic for..in loop to get object properties
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/for...in
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
temp.push(obj[prop]);
}
}
return temp;
}
}
/**
* Usage
* Shows how to use the custom ValueConverter to iterate an objects properties
* aka its keys.
*
* <require from="ObjectKeys"></require>
* <li repeat.for="prop of myVmObject | objectKeys">${prop}</li>
*/
OR, you could use the Aurelia Repeat Strategies provided by an Aurelia Core Team member
You'd have to import the plugin into your app.
Then you'd use it using the pipe syntax in your repeat.for....like so....
<div repeat.for="[key, value] of data | iterable">
${key} ${value.my_property}
</div>