Does Ionic2 RestApi Promise work only with arrays? - api

I'm newbie in Ionic2 typescript angular2. I've developed an example app to get data from Rest API.
I've used a Page component which implements a Service Provider which provides all the data in a promise to an array like:
UserService.ts
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/map';
#Injectable()
export class UserService {
data: any;
constructor(private http: Http) {
this.data = null;
}
load() {
if (this.data) {
// already loaded data
return Promise.resolve(this.data);
}
// don't have the data yet
return new Promise(resolve => {
this.http.get('https://randomuser.me/api/?results=25')
.map(res => res.json())
.subscribe(data => {
this.data = data.results;
resolve(this.data);
});
});
}
}
HomePage.ts
import {Component} from '#angular/core';
import {NavController} from 'ionic-angular';
import {UserService} from '../../providers/user-service';
#Component({
selector: 'page-home',
templateUrl: 'home.html',
})
export class HomePage {
users: any[] = [];
constructor(
public navCtrl: NavController,
public userService: UserService
) {
this.userService.load()
.then(data => {
this.users = data;
}) ;
}
}
home.html
<ion-header>
<ion-navbar color="primary">
<ion-title>
Demo 103
</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngFor="let user of users">
<ion-avatar item-left>
<img [src]="user.picture.medium">
</ion-avatar>
<h2>{{ user.name.first | uppercase }}</h2>
<p>{{ user.email }}</p>
</ion-item>
</ion-list>
</ion-content>
Now, in the template with ngFor, everything works perfectly. But the problem comes, when I try to get the details of one of them, instead of a list of users.
When I replace users: any[] = []; to user:any; I get an error saying the model is undefined.
It is like if I don't get an array. My template doesn't wait the promise in order to render the page. Can't I use promise with an object? Is it only for arrays?

You can you Promises with objects as well. But notice that while the template is rendering your object "user" is uninitialized, when you declare it like that:
user:any
That's why you get an error. You can prevent the error by adding "?" mark after variable in your template.
<ion-list>
<ion-item>
<ion-avatar item-left>
<img [src]="user?.picture.medium">
</ion-avatar>
<h2>{{ user?.name.first | uppercase }}</h2>
<p>{{ user?.email }}</p>
</ion-item>
</ion-list>

NOTE: Your template is already rendered even before the promise is resolved for API data. Which is correct behavior. The template gets refreshed again when the data is loaded.
At first when the data is not loaded and the template is already rendered without any data. Your *ngFor="let user of users" tries to check for any data and it does not find any. So, it works and renders nothing. As soon as API promise is resolved and you have data, it refreshes to render your template correctly.
Now, when you try to access user (after doing user:any) directly like this without ngFor or ngIf:
<ion-item>
<ion-avatar item-left>
<img [src]="user.picture.medium">
</ion-avatar>
<h2>{{ user.name.first | uppercase }}</h2>
<p>{{ user.email }}</p>
</ion-item>
user:any does not have any property named as picture, name, etc. at the moment (before promise is resolved). So, it cannot access any data. And so the error.
This is not related to if it is an object or an array issue. It is how you are handling it.
If you want to correct this behavior, use any if condition indicating "if the data is present".
E.g.
<ion-item *ngIf="user">
<ion-avatar item-left>
<img [src]="user.picture.medium">
</ion-avatar>
<h2>{{ user.name.first | uppercase }}</h2>
<p>{{ user.email }}</p>
</ion-item>
Also, I do not understand how you are accessing user.picture or any property for that matter, if your API results in an array. Isn't user[<index>].picture will be correct? (Again, you need to make sure that user has data with <index>).

Now I get it... I didn't realize that and didn't know even about the "?".
Thank u both!

Related

Limit #click event on a dynamically created element using v-for to the element it's called on

I have a component that generates a div for every element in an array using v-for. To get more info on the component you click an icon that uses fetches API data and displays it under the icon. It currently displays the fetched info under every element in the array instead of the element it's called on. How can I fix this? (new to vue.js)
Strain.vue
<template>
<div>
<div id="strain-container"
v-for="(strain, index) in strains"
:key="index"
>
<h3>{{ strain.name }}</h3>
<p>{{ strain.race }}</p>
<i class="fas fa-info-circle" #click="getDetails(strain.id)"></i>
<strain-description :strainData="strainData"></strain-description>
</div>
</div>
</template>
<script>
import axios from 'axios';
import strainDescription from './strainDescription'
export default {
props: ['currentRace'],
components: {
'strain-description': strainDescription,
},
data(){
return{
strains: [],
apiKey: 'removed-for-stack-overflow',
strainData: {},
}
},
methods: {
getDetails: function(id){
const descApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/desc/${id}`);
const effectApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/effects/${id}`);
const flavourApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/flavors/${id}`);
axios.all([descApi, effectApi, flavourApi])
.then((values)=> axios.all(values.map(value => value.json())))
.then((data) => {
this.strainData = data;
});
}
},
Then output the data in strain-description component:
strainDescription.vue
<template>
<div id="strain-description">
<p>{{ strainData[0].desc }}</p>
<p>{{ strainData[1] }}</p>
<p>{{ strainData[2] }}</p>
</div>
</template>
<script>
export default {
props: ['strainData'],
}
</script>
Understandably (though not to me) this outputs it into every instance of the "strain-container", instead of the instance it's called on.
Any help is appreciated!
Add the strainData to the strain in the strain array. So first you can pass the index through to your click function
<i class="fas fa-info-circle" #click="getDetails(strain.id, index)"></i>
then you can update the strains array by index with your data
getDetails: function(id, index){
const descApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/desc/${id}`);
const effectApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/effects/${id}`);
const flavourApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/flavors/${id}`);
axios.all([descApi, effectApi, flavourApi])
.then((values)=> axios.all(values.map(value => value.json())))
.then((data) => {
this.strains[index].strainData = data;
});
}
then back in the template you can display like so
<strain-description :strainData="strain.strainData"></strain-description>
Bonus to this is you can check whether the strainData already exists on the clicked strain by checking if strain[index].strainData is defined or not before you make an api call
EDIT
If it doesn't update the template you may need to use vue set to force the render
this.$set(this.strains[index], 'strainData', data);

How to manually call query (e.g. on click) using Vue Apollo Composible?

The default behavior is that useQuery is loaded immediately with component setup.
I want to trigger the query on some event like click.
How to do it?
i searched for it a lot too, but it is writtin in their documentation vue apollo v4e
import { useQuery, useResult } from '#vue/apollo-composable'
import gql from 'graphql-tag'
export default {
setup () {
const { result, loading, error, refetch } = useQuery(<query>)
const users = useResult(result)
return {
users,
loading,
error,
refetch,
}
},
}
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else-if="users">
<li v-for="user of users" :key="user.id">
{{ user.firstname }} {{ user.lastname }}
</li>
<button #click="refetch()">Refresh</button>
</ul>
</template>
just add the "refetch" to the useQuery line and call it from the button click event.
EDIT: You could disable the query, so it wont trigger on component setup and on button click you could enable the query and refetch it.
Best Regards

Vue/Vuex Accessing Objects elements

Hi I am getting confused as to how I can access some data within my Object. I am using Vuex and I have a standard page. Here, I use a getter to obtain the Payment
Object and pass it to a component.
<template>
<div>
<div class="container">
<div class="row">
<div class="col-12 flex">
<payment-details :payment="payment"></payment-details>
</div>
</div>
</div>
</div>
</template>
<script>
import {
PaymentDetails
} from "#/components/Payment";
export default {
components: {
'payment-details': PaymentDetails
},
created () {
if (!this.payment.paymentID) {
this.$store.dispatch('paymentById', this.$route.params['id'])
}
},
computed: {
payment () {
return this.$store.getters.paymentById(this.$route.params['id'])
}
}
}
</script>
Now within the component, within the template, I can do something like this
<template>
<div v-if="payment">
<div class="row">
<div class="col-12">
<h3 class="h-100">{{ payment.details }}</h3>
</div>
</div>
</div>
</template>
This all works fine. However, within this component, I need to access some elements of the payment object. This is where I get confused, if I create a mounted or created hook and do this
created() {
console.log(this.payment)
}
I always get an Observer object. If I try accessing an element from this Object e.g.
created() {
console.log(this.payment.details)
}
I get undefined. I basically have a slideshow I am creating in a mounted hook. I need to push some items contained within this Object onto the slideshow array.
So how can I actually get access to the elements of this Object?
Thanks
You should use watcher on your vuex object.
Here is link to documentation https://v2.vuejs.org/v2/guide/computed.html#Computed-vs-Watched-Property
Most probably your this.payment.details is instantiated after your created method was called.
Move your code from created method to:
export default {
watch: {
payment: function (val) {
console.log('-------- this is this.payment.details:');
console.log(val.details);
},
...
Yes it will gave you of undefined because in your props you declare only a payment object alone not like this one below.
payment : {
details: '',
etc: ''
}
But it will still works when you use this payment data in your component, it's like it only gives you an error something like 'calling details on null' like that. I prefer to put condition first if payment has already data before you call in your component. Like below
<div v-if="payment.details">{{payment.details}}</div>

Vue access errors inside computed

I'm just starting to learn Vue and am on to validation.
I've found some older examples which use Vee-Validate but it seems to have changed recently. How can I convert this code to use the new version of Vee-Validate?
As far as I can tell, the code below is attempting to send a bespoke error message rather than the default to the screen if there is an error.
Chrome browser is telling me that it cannot read property 'first' of undefined, so I don't think I can access the error using this.errors.
Is it still possible to access the errors inside 'computed'?
<template>
<div>
<ValidationProvider rules="required" v-slot="{ errors }">
<input type="text" v-model="input" name="myfield">
<span>{{ myError }}</span>
</ValidationProvider>
</div>
</template>
<script>
import { ValidationProvider } from 'vee-validate';
export default {
components: {
ValidationProvider
},
computed: {
myError () {
if (this.errors.first('myfield') === 'The myfield field is required.') {
return 'My bespoke message'
}
return this.errors.first('myfield')
}
}
};
</script>
As answered by MartinT, ValidationProvider uses Scoped Slots and therefore you cannot access it directly in the parent component. However, you still have some options to achieve what you want:
Using Methods
You can pass the error array as an argument of a method and return the bespoke error message you want!
<template lang="html">
<validation-provider rules="required" v-slot="{ errors }">
<input type="text" v-model="input" name="myfield">
<span>{{ myError(errors) }}</span>
</validation-provider>
</template>
<script lang="js">
import { ValidationProvider } from 'vee-validate'
export default {
components: { ValidationProvider },
data () {
return {
input: null
}
},
methods: {
myError (errors) {
if (errors[0] === 'The myfield field is required.') {
return 'My bespoke message'
}
return errors[0]
}
}
}
</script>
But this isn't probably the best option.
Customizing Rules' Messages
In Vee-Validate 3.x.x you can easily customize the error message from an existing rule or you can even create a new rule.
import { extend } from 'vee-validate'
// overwriting the 'required' rule error message.
extend('required', {
...required
message: 'My bespoke message'
})
And then you can display the bespoke message.
<template lang="html">
<validation-provider rules="required" v-slot="{ errors }">
<input type="text" v-model="input" name="myfield">
<span>{{ errors[0] }}</span>
</validation-provider>
</template>
Have a look at Scoped slots. In a nutshell, ValidationProvider component is using a slot, which provides you with errors object. You can think of it as an internal object of ValidationProvider. However, the problem is, it can only be used inside of the slot scope (within ValidationProvider). Your usage assumes, that errors obj is part of your component instance (either data, computed, method...), which is not true. More reading can be found here.

Aurelia Custom Elements Inside of Custom Elements & Sharing Variables

How do I access & share variables between custom elements? I have the following files...
tip.html
<template>
<div class="tip-container">
<content select="tip-trigger"></content>
<content select="tip-content"></content>
</div>
</template>
tip.js
export class Tip {}
tip-trigger.html
<template>
<span class="tip-trigger" click.trigger="showTip()">
<content></content>
</span>
</template>
tip-trigger.js
export class TipTrigger {
showTip() {
console.debug(this);
}
}
tip-content.html
<template>
<span class="tip">
<content></content>
<span class="tip__close tip__close--default">×</span>
<span class="tip__color"></span>
</span>
</template>
tip-content.js
export class TipContent {}
In my Tip class I would like to have a variable name visible. When showTip is triggered visible would be set to true, which I would then use to add a class in tip-content.html. How can I share variables between these custom elements to do this?
The idea is to create an element to show tip pop-ups where any type of content can be the trigger and any type of content can be displayed when triggered. Basic example:
<tip>
<tip-trigger><button>?</button></tip-trigger>
<tip-content><div>Here is some helpful info...</div></tip-content>
</tip>
Here is a solution to your problem in Plunker.
Note that the tip-trigger and tip-content elements are just replaceable parts of the template. They don't needed to be components themselves (that confused me a lot in the "original" custom elements article).
app.html:
<template>
<require from="tip"></require>
<tip>
<tip-trigger><button>?</button></tip-trigger>
<tip-content><div>Here is some helpful info...</div></tip-content>
</tip>
</template>
tip.html:
<template>
<div class="tip-container">
<div>
<div click.trigger="showContent()">
<content select="tip-trigger"></content>
</div>
</div>
<div show.bind="contentVisible">
tip content:
<content select="tip-content"></content>
</div>
</div>
</template>
tip.js:
export class Tip {
showContent(){
this.contentVisible = !this.contentVisible;
}
}
Do you just need to turn Tip into a service-like class and import it?
export class Tip {
constructor() {
this.visible = false;
}
show() {
this.visible = true; // Or whatever to show the content
}
hide() {
this.visible = false;
}
}
Then:
import {inject} from 'aurelia-framework';
import {Tip} from './tip';
#inject(Tip)
export class TipTrigger {
constructor(tip) {
this.tip = tip;
}
showTip() {
this.tip.show();
// Or I suppose you could access 'visible' directly
// but I like the implementation details a method call offers.
}
}
*Disclaimer: This is untested.