What is the correct way to call a method property inside an object in a store? - promise

I am working on my first Sveltekit app project, converting from a React based project. I got stuck with a bit of a tricky method inside an object inside a store. Before I refactor the entire data structure to something perhaps more intuitive ( this is still a data structure derived from the React way of doing things ) , I wanted to understand how to do this properly in case I need it again.
An object stores some meta data about a dataset.
const dataSetsIndex = [
{id: ':sample:alphabet',
permanent: true,
metadata: {
title: 'Alphabetic Letter Frequency',
source: 'https://observablehq.com/#d3/bar-chart-transitions'
},
attachments: {
data: import ('/src/dataSets/alphabet.json'),
}
}
];
export default dataSetsIndex;
There would be more objects with the { id: permanent: metadata: { title: source: } attachments: { data: ()=> } } structure in this dataSetsIndex component prop.
But when my program eventually tries to access the data from an external JSON ti display on a route in Sveltekit , I can't seem to find a way to make that Promise from the import('/src/dataSets/alphabet.json') method return.
Following the docs, I tried an interface that destructures the data and stores it in a writable - the data in the JSON file is fields:[] , rows:[]
import DataSets from "../dataSets/dataSetsIndex.js";
import {writable} from "svelte/store";
export const dataSetsStore = writable([]);
let destructedDataSets = () => {
const dataSets = DataSets.map( ( dataset, index ) =>
{
return {
id: index,
title: dataset.metadata.title,
source: dataset.metadata.source,
fields: dataset.attachments.data().then(
(success) => { return success.fields},
(fail) => {return fail})
}
}
)
dataSetsStore.set(dataSets);
};
destructedDataSets();
then bringing that in to a route which is reactive
<script>
import {dataSetsStore} from "../stores/dataSetsStore.js"
</script>
{#each $dataSetsStore as metadataObject}
<div>
{metadataObject.title.toUpperCase()}
{metadataObject.fields}
</div>
{/each}
only displays ALPHABETIC LETTER FREQUENCY [object Promise]
What am I missing here?

OK, so I figured it out and this is working. I looked at this related post to help me understand the role of {#await} in the context of this particular structure... my code excerpt below uses Bulma to draw up a table for the results
<script>
import {dataSetsStore} from "../stores/dataSetsStore.js"
</script>
{#each $dataSetsStore as metadataObject}
{#await metadataObject.importDataFrom()}
<p>loading...</p>
{:then theFields}
<table class="table">
<thead>{metadataObject.title.toUpperCase()}</thead>
{#each theFields.fields as f}
<th>
<abbr class="has-background-success is-size-5 p-1" title={f.name}>
{f.name.trim().slice(0,10)}
</abbr>
<span class="has-text-info is-size-8">
{f.type}
</span>
</th>
{/each}
</table>
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
{/each}

Related

select value binding and svelte-i18n integration problem

I'm dealing with something that seems to be a bit beyond my ken here ...
It's about select value binding. Here below, it's a simple peace of code that is working perfectly in a classic svelte SPA.
<script>
let countrySelected = {
code: 'BE',
name: 'Belgium',
};
const countries = [
{
code: 'FR',
name: 'France',
},
{
code: 'BE',
name: 'Belgium',
},
{
code: 'GA',
name: 'Gabon',
},
];
</script>
<select bind:value={countrySelected}>
{#each countries as country}
<option value={country} selected={country.code === countrySelected.code}>
{country.name}
</option>
{/each}
</select>
But for whatever reason, it doesn't work anymore when it's working in a sveltekit app which uses the svelte-i18n npm package.
/src/routes/__layout.svelte
<script>
import { setupI18n, isLocaleLoaded } from '$lib/services/i18n.js';
$: if (!$isLocaleLoaded) {
setupI18n({ withLocale: 'fr-FR' });
}
</script>
{#if !$isLocaleLoaded}
Please wait...
{:else}
<main>
<slot />
</main>
{/if}
/src/lib/services/i18n.js
The code of /src/lib/services/i18n.js comes from:
https://phrase.com/blog/posts/how-to-localize-a-svelte-app-with-svelte-i18n/
&
https://lokalise.com/blog/svelte-i18n/
import { derived } from 'svelte/store';
import { dictionary, locale, _, date, time, number } from 'svelte-i18n';
const MESSAGE_FILE_URL_TEMPLATE = 'http://localhost:3000/lang/{locale}.json';
let cachedLocale;
async function setupI18n({ withLocale: _locale } = { withLocale: 'en-GB' }) {
const messsagesFileUrl = MESSAGE_FILE_URL_TEMPLATE.replace(
'{locale}',
_locale
);
const res = await fetch(messsagesFileUrl);
const messages = await res.json();
dictionary.set({ [_locale]: messages });
cachedLocale = _locale;
locale.set(_locale);
}
// Before any locale is set, svelte-i18n will give locale an object type.
// Once it is correctly set, the libray will set locale
// to the code of the active locale, e.g. "en", a string type.
// We check for this in our devired store, and make sure that isLocaleLoaded‘s value
// is true only after i18n initialization is successful.
const isLocaleLoaded = derived(locale, $locale => typeof $locale === 'string');
export { _, locale, setupI18n, isLocaleLoaded, date, time, number };
Problem:
The <select> element in the index.svelte file should show "Belgium" because:
countrySelected = { code: 'BE', name: 'Belgium', }
The problem is that in __layout.svelte, svelte-i18n makes a kind of refresh just after the item has been selected so it looks nothing is selected.
It's probably because of the way I've integrated svelte-i18n in my project as I only understood the broad strokes but once again ... the devil is in the detail :D
Thank you so much for your help. You can clone this repos, it gonna be easier to understand:
git clone https://github.com/BigBoulard/sveltekit-sveltei18n
npm i
npm run dev
With your example, if you inspect the select element in a browser, the selected attribute is not set on any option.
Svelte has a very easy to use and straight forward way to set the initial selected value.
It automatically handles the required attribute on the option element when binding a value to the select element.
The issue in your example is, that countrySelected looks the same as the object/dictionary inside your countries array, but is actually a new object and therefore Svelte can not select it.
This example should work:
<script>
const countries = [
{
code: 'FR',
name: 'France',
},
{
code: 'BE',
name: 'Belgium',
},
{
code: 'GA',
name: 'Gabon',
},
];
let countrySelected = countries.find(x => x.code === 'BE');
</script>
<select bind:value={countrySelected}>
{#each countries as country}
<option value={country}>
{country.name}
</option>
{/each}
</select>

VueJS: How do I initialise data so it shows in my template on component load

This is driving me nuts!
//ProfilePage.vue
<template>
<div>
<p>{{ this.$data.profile.desc }}</p>
<profileImage v-bind:profile="profile"></profileImage>
<profileText v-bind:profile="profile" v-on:updateData="updateDesc"></profileText>
</div>
</template>
<script>
import profileText from './ProfileText.vue';
import profileImage from './ProfileImage.vue';
export default {
name: 'profilePage',
component: {
profileText,
profileImage
},
data() {
return {
profile: {
image: '',
desc: ''
}
}
},
created() {
this.fetchProfile();
},
methods: {
async fetchProfile() {
const uri = 'http://localhost:8000/api/......get';
const response = await axios.get(uri);
.then(response => this.updateProfileData(response.data))
},
updateProfileData(data) {
this.$data.profile.image = data['image'];
this.$data.profile.desc = data['description'];
},
updateDesc(data) {
this.$data.profile.desc = data.desc;
},
}
}
</script>
<style scoped>
</style>
In the above .vue file. I execute a fetch to the back end which successfully returns the correct data from the DB. I successfully save the data returned to the data() part of the file. Next I import a component (the code for which is below) from the correct page, add it as a component and add it to the template and use v-bind to pass in profile from the data() part of this page. Now the imported/child component looks like this:
//ProfileText.vue
<template>
<div>
<form #submit="update">
<textarea v-model="description"></textarea>
<button type="submit">Submit</button>
</form>
<div>
<template>
<script>
export default{
name: "profileText",
props: ["profile"],
data() {
return {
description: this.$props.profile.desc
}
},
methods: {
update(e) {
e.preventDefault();
const newData = {
desc: this.$data.description
}
this.$emit('updateData', newData);
}
}
}
</script>
<style scoped>
</style>
I use v-model to bind the contents of "description" in data() to the contents of the textarea. I have it so when i edit the text area and click submit the function emits the data to the parent component which triggers a function that updates the parent data() with the new data from the text area of this component. This parts works perfectly.
However, the part I can't figure out is when the parent component executes the fetch and binds the response with the child component, why isn't the response showing up in the textarea when it loads.
I have done the exact same thing with another lot of components and it works fine on that lot. The only difference there is that with that lot the execute function brings back a response with an array of data and I use v-for(x in xs) and then bind the attributes of data() with the component x. That's the only difference. What am I missing in the code above to load the data sent in "profile" from the parent component with v-bind to the textarea in the child component with v-model. In data() i have it to return description: this.$props.profile.desc, but it is not initialising description with profile.desc - Going nuts here $#! I've been staring at the code for two days straight trying different things.
mounted Function
Called after the instance has been mounted, where el is replaced by
the newly created vm.$el. If the root instance is mounted to an
in-document element, vm.$el will also be in-document when mounted is
called.
Note that mounted does not guarantee that all child components have
also been mounted. If you want to wait until the entire view has been
rendered, you can use vm.$nextTick inside of mounted:
mounted: function () { console.log('component mounted'); }
This hook is not called during server-side rendering.
Source
Component Lifecycle
Few things:
Your syntax has errors in the ProfileText.vue file. Missing closing template and div tags
<template>
<div>
<form #submit="update">
<textarea v-model="description"></textarea>
<button type="submit">Submit</button>
</form>
</div>
</template>
You are mixing async/await and .then(). It should be:
async fetchProfile() {
const uri = 'http://localhost:8000/api/......get';
const response = await axios.get(uri);
this.updateProfileData(response.data)
},

Refresh preloaded data with Sapper

I am currently learning Sapper and integrating it with a GraphQL service.
To start with an easy spot, I made a FAQ page with a simple list of question/answer plus a creation form.
<script context="module">
import graphql from '../graphql';
import gql from 'graphql-tag';
export function preload({ params, query }) {
const graphQuery = gql`
{
faqEntries {
question,
answer
}
}
`;
return graphql.request(graphQuery).then((data) => data);
}
</script>
<script>
import title from './title';
import Input from '../components/form/Input';
import Button from '../components/Button';
export let faqEntries;
const newEntry = {
question: '',
answer: '',
};
const addEntry = () => {
console.log(newEntry);
graphql.request(gql`
mutation {
createFaqEntry(
question: "${newEntry.question}"
answer: "${newEntry.answer}"
) {
id
question
answer
}
}
`).then((data) => {
console.log(data);
newEntry.question = '';
newEntry.answer = '';
})
}
</script>
<svelte:head>
<title>{title('Foire aux questions')}</title>
</svelte:head>
<section class="container">
<h1>Foire aux questions</h1>
<form on:submit|preventDefault>
<Input id="question" label="Question" bind:value={newEntry.question} />
<Input id="answer" label="Réponse" bind:value={newEntry.answer} />
<Button on:click={addEntry} >Ajouter une entrée</Button>
</form>
{#each faqEntries as faqEntry}
<div class="py-4">
<h4>{faqEntry.question}</h4>
<p>
{faqEntry.answer}
</p>
</div>
{/each}
</section>
The current script works great, allowing me to add FAQ entries directly from the coded form.
Now, I would like to make the FAQ entries list to be refreshed when I submit a new one.
What is the best practice to do this? Also, is my GraphQL implementation done the right way?
You can reuse the preload function to refresh the data:
.then((data) => {
console.log(data);
newEntry.question = '';
newEntry.answer = '';
preload().then(props => {
faqEntries = props.faqEntries
})
The approach is useful when you want to show entries that other users have added at the cost of making extra api calls.
But if you only want to add the recently added entry:
.then((data) => {
faqEntries = [...faqEntries, {...newEntry}]

How to render data with AJAX in Vuejs 2?

I have problems while trying to set data in a Vue instance
(example in: https://jsfiddle.net/a5naw6f8/4/ )
I assign the values to data property by AJAX. I can render exam object but when I want to access to an inner array exam.questions.length I get an error:
"TypeError: exam.questions is undefined"
This is my code :
// .js
new Vue({
el: '#app',
data:{
id: "1",
exam: {}
},
created: function () {
this.fetchData();
},
methods: {
fetchData: function () {
$.get("https://dl.dropboxusercontent.com/s/15lkz66wy3fut03/data.json",
(data) => {
this.exam = data;
}
);
}
}
});
<!-- .html -->
<div id="app">
<h1>ID: {{ id }}</h1>
<h1>Exam: {{ exam }}</h1>
<!-- exam.questions array is undefined -->
<!-- <h1>Questions length: {{ exam.questions.length }}</h1> -->
</div>
I found a similar problem but it was relative to this reserved word scope:
Vuejs does not render data from ajax
Does someone know how to fix this problem?
You have a couple problems:
Your JSON file isn't valid. The trailing commas after the the false cause the parser to choke.
You aren't parsing your JSON (which is why you probably didn't see the JSON error). Once you fix the JSON file, you need to parse it with something like this.exam = JSON.parse(data) rather than assigning the string to this.exam
Once you do that, you can test for the object in the template with v-if before trying to access its properties:
<h1 v-if="exam.questions">Questions length: {{ exam.questions.length }}</h1>
v-if will prevent the attempt to access exam.questions.length until the ajax request has returned data

Vue.js raise an error when view data in wrong case

I have the following template:
<template>
<div>
<h1>Users from SQL Server!</h1>
<p>This component demonstrates fetching data from the server.</p>
<table v-if="userlist.length" class="table">
<thead>
<tr>
<th>Id</th>
<th>First</th>
<th>Last</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<!-- nb: These items must match the case they are passed in with (the Json passed back from .net Core serialises to lowerCamelCase) otherwise they will
show nothing and no error is raised! -->
<tr v-for="item in userlist">
<td>{{ item.userid }}</td>
<td>{{ item.firstname }}</td>
<td>{{ item.lastname }}</td>
<td>{{ item.createdonDate }}</td>
</tr>
</tbody>
</table>
<p v-else><em>Loading...</em></p>
</div>
This displays a table with no data. This is because the data passed in from the server has a different case to that used in the template items. If I fix them for example:
item.userId
item.firstName
item.lastName
item.createdOnDate
Then it works and data is displayed. The issue for me is that no error is returned. I am learning Vue using the .Net Core SPA template as a starter. But it took me a while to realise what I was doing wrong. If this was a model in the razor view it would have blown with a helpful error.
Is there a way to raise an error for this kind of thing?
I do have the Chrome Vue extension installed and realised the problem when I looked at the data there. But I was stumped for a while.
UPDATE 1: Thanks #ndpu for your solution but I am having trouble fitting it into my project. I have a boot.ts file like this:
import 'bootstrap';
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use((VueRouter) as any);
Vue.config.devtools = true;
const routes = [
{ path: '/', component: require('./components/home/home.vue.html') },
{ path: '/counter', component:
require('./components/counter/counter.vue.html') },
{ path: '/fetchdata', component:
require('./components/fetchdata/fetchdata.vue.html') },
{ path: '/users', component: require('./components/users/users.vue.html') },
{ path: '/user', component: require('./components/user/user.vue.html') }
];
new Vue({
el: '#app-root',
router: new VueRouter({ mode: 'history', routes: routes }),
render: h => h(require('./components/app/app.vue.html'))
});
Where do I put the Object.prototype.safeGet? In there or in my component templates? Nowhere seems to work.
UPDATE 2:
I got it to work by putting the code from #ndpu into app.ts like this:
import Vue from "vue";
import { Component } from 'vue-property-decorator';
#Component({
components: {
MenuComponent: require('../navmenu/navmenu.vue.html')
}
})
export default class AppComponent extends Vue {
}
Object.defineProperty(Object.prototype, 'safeGet', {
get: function () {
return this;
}
});
Simplest way: define additional property to Object with getter that will only return himself.
/* eslint no-extend-native: ["error", { "exceptions": ["Object"] }] */
Object.defineProperty(Object.prototype, 'safeGet', {
get: function () {
return this;
}
});
So, by adding safeGet to any data attribute, you can be sure that you will get TypeError if attribute is undefined:
this:
<td>{{ item.userid.safeGet }}</td>
will produce exception (if actual property name is userId):
TypeError: Cannot read property 'safeGet' of undefined
FIDDLE: http://jsfiddle.net/yMv7y/2785/
Also, you can define simple method in object prototype to check property existence:
UPDATE: i couldn't make it work in 'complex' application with modules, webpack etc - vue trying to make added to Object.prototope method reactive. Didnt know why it is work in simple case like in applied fiddle.
Object.prototype.safeGet = function() {
var val, args = Array.prototype.slice.call(arguments);
var currLvlObj = this;
for (var i=0; i < args.length; i++) {
val = currLvlObj = currLvlObj? currLvlObj[args[i]] : undefined;
if (val === undefined) {
throw Error('property with name ' + args[i] + ' is undefined');
}
}
return val;
}
and use it like this:
<td>{{ item.safeGet('userid') }}</td>
this call should throw error (if actual property name is userId): Error: property with name userid is undefined
PS: nested objects properties can be accessed by passing all property names as arguments. For example, to access 'userId' in {'data': {'userId': 0}}:
<td>{{ item.safeGet('data', 'userid') }}</td>
FIDDLE: http://jsfiddle.net/yMv7y/2778/

Resources