Svelte - UI does not get rendered which data I fetch from API - fetch

This is my script-tag:
<script lang="ts">
import Card from "../../components/Card.svelte";
import { onMount } from "svelte";
let show_all = false;
let data: Array<{id: number, image_url: string, description: string, link: string, title: string}> = [];
onMount(async () => {
try {
console.log(data)
let response = await fetch("http://localhost:4000/entries");
data = await response.json();
} catch(err) {
console.log(err);
}
});
const getData = async () => {
console.log(data)
if (!show_all) {
return data.slice(0, 12);
} else {
return data;
}
</script>
I render the data like this:
{#await getData()}
<p>...waiting</p>
{:then array}
{#each array as item (item.id)}
<Card image_url={item.image_url} description={item.description} link={item.link} title={item.title} />
{/each}
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
Fetching the data from the API works fine, but it seems to getData() function works with the empty array, not with data after it has been updated.
What am I doing wrong here? I thought using the onMount hook would guarantee that the UI gets rendered after the data was fetched.
Can anyone help me to fix this
The order is also logged here:
getdata
hook
hook should be called before getData

The problem is, as you mentioned, the order. getData() runs before the onMount callback. I assume waiting... should be shown while the data is fetched and the displayed data should also react to changes of show_all?
Here's one way in case data isn't otherwise needed inside the script tag
<script lang="ts">
import Card from "../../components/Card.svelte";
let show_all = false;
const getData = async () => {
try {
let response = await fetch("http://localhost:4000/entries");
return await response.json();
} catch(err) {
console.log(err);
}
}
</script>
{#await getData()}
<p>...waiting</p>
{:then data}
{#const dataToBeShown = show_all ? data : data.slice(0, 12)}
{#each dataToBeShown as item (item.id)}
<Card {...item} />
{/each}
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
in case it is
<script lang="ts">
import Card from "../../components/Card.svelte";
let show_all = false;
let data: Array<{id: number, image_url: string, description: string, link: string, title: string}> = [];
$: dataToBeShown = show_all ? data : data.slice(0, 12)
const getData = async () => {
try {
let response = await fetch("http://localhost:4000/entries");
data = await response.json();
} catch(err) {
console.log(err);
}
}
</script>
{#await getData()}
<p>...waiting</p>
{:then _}
{#each dataToBeShown as item (item.id)}
<Card {...item} />
{/each}
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
Since the property names seem to match >> <Card {...item} />

Related

How to Transfer input Data to another Data using next.js 12

this is my index code and I want to transfer / send this data to another page
I try this code but it's not working I don't why
import { useRouter } from "next/router";
function index() {
const router = useRouter();
const [inputValue, setInputValue] = useState("");
const handleImgInput = (e) => {
e.preventDefault();
router.push({
pathname: "/createproject/uploadnfts/nftdetails",
query: inputValue,
});
};
return (
<form action="" onSubmit={handleImgInput}>
<input
type="email"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</form>
);
}
this is the data page code. when this page open, I didn't receive the input value from my index page and also what i input in the index file, it appears to my localhost link like this:
http://localhost:3001/data?demo#gmail.com
import { useRouter } from "next/router";
import React from "react";
function Data() {
const router = useRouter();
const {
query: { inputValue },
} = router;
const props = {
inputValue,
};
console.log(props);
return (
<div>
<p>{inputValue}</p>
</div>
);
}
export default Data;
The query property in the router options requires passing an object to it. The query params are formed from the key-value pairs in the object.
See below example from the Next.js docs:
import { useRouter } from 'next/router'
export default function ReadMore({ post }) {
const router = useRouter()
return (
<button
type="button"
onClick={() => {
router.push({
pathname: '/post/[pid]',
query: { pid: post.id },
})
}}
>
Click here to read more
</button>
)
}
Taken from: https://nextjs.org/docs/api-reference/next/router#with-url-object
In your case, this would be something like:
const handleImgInput = (e) => {
e.preventDefault();
router.push({
pathname: "/createproject/uploadnfts/nftdetails",
query: { value: inputValue },
});
};
When you want to access the query params, make sure to reference them like an object's properties. For the example above, this would be:
const { value } = router.query;

How to populate text-fields from pinia store state without changing the rendered values from other components?

hope you're well!
I have a Vue 3 app using Pinia + Vuetify 3. I've defined a "client" store and a component that, upon render, will call a store action that calls my backend API and sets my client state (JSON) with the result.
clientStore.js:
export const useClientStore = defineStore('clients', {
state: () => ({
//Loading state and client(s)
loading: false,
clients: [],
client: {}
}),
getters: {
//Get all clients
getClients(state) {
return state.clients
},
//Get one client
getClient(state) {
return state.client
}
},
actions: {
//Get one client
async fetchClient(clientId) {
try {
this.loading = true
const data = await axiosConfig.get('/clients/' + clientId)
this.client = data.data
this.loading = false
} catch (error) {
this.loading = false
console.log("Error fetching client: " + clientId)
},
//snipped
I have a computed property that returns the client from the store and render them as follows:
Component.vue:
<template>
<div class="text-center py-5">
<div class="text-h4 font-weight-bold">{{ client.name }}</div>
</div>
<div class="d-flex justify-space-between">
<div class="text-h5">Description</div>
<v-btn #click="dialog = true" prepend-icon="mdi-cog" color="primary">Edit</v-btn>
</div>
<v-textarea class="py-5" :value="client.description" readonly auto-grow outlined>{{ client.description
}}</v-textarea>
<updateClient v-model="dialog" />
</template>
<script setup>
import updateClient from '#/components/clients/updateClient.vue'
import { useClientStore } from '#/store/clients'
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router'
const store = useClientStore()
const route = useRoute()
const dialog = ref(false)
const client = computed(() => {
return store.client
})
onMounted(() => {
store.fetchClient(route.params.clientId)
})
</script>
My aim is to make an "EDIT" component - a popup dialog - that takes the client state values and pre-populate them in my text fields and upon changing the values, submit and PATCH the client in the backend.
updateClient.vue
<template>
<v-dialog max-width="500">
<v-card class="pa-5">
<v-card-title>Edit client</v-card-title>
<v-text-field label="Name" v-model="client.name"></v-text-field>
<v-textarea label="Description" v-model="client.description"></v-textarea>
<v-btn block outlined color="primary" #click="updateClient">Update Client</v-btn>
</v-card>
</v-dialog>
</template>
<script setup>
import { useClientStore } from '#/store/clients'
import {computed} from 'vue'
const store = useClientStore()
const client = computed(() => {
return store.client
})
</script>
Problem is when I edit the pre-populated values in the fields, it changes the values outside the dialog as seen in the video and stay changed even after closing the pop-up. Ideally I'd like the values in my Component.vue to be static and have my state values unaltered. How can this be solved?
Thanks!
When you bind client.name to a text field in "Edit component", you directly change values stored in pinia. This, by design, changes values in your "View component".
A simple answer is... just create a copy of the object.
Now, I know, I know... there is a reason why you used computed properties in both places. Because you're waiting on the server to return the initial values.
The easiest way to solve this is to create a copy of the client object in pinia store. Then, just use copy of the object for text field binding in "Edit component".
state: () => ({
//Loading state and client(s)
loading: false,
clients: [],
client: {},
clientEdit: {} // Make changes to this object instead
})
In api response
actions: {
//Get one client
async fetchClient(clientId) {
try {
this.loading = true
const data = await axiosConfig.get('/clients/' + clientId)
this.client = data.data
this.clientEdit = { ...this.client } // Copy client object
this.loading = false
} catch (error) {
this.loading = false
console.log("Error fetching client: " + clientId)
},
}

How to update page when data is changed in nextjs?

When I delete one of the notes, it deletes from the DB. And to see the effect, I need to reload the page every time I delete a note.
How do I see the not deleted notes without reloading the page?
Here's the code for my page:
export default function Home(notes) {
const [notesData, setNotesData] = useState(notes);
const deleteNote = async (note) => {
const res = await fetch(`http://localhost:3000/api/${note}`, {
method: "DELETE",
});
};
return (
<div>
<h1>Notes:</h1>
{notesData.notes.map((note) => {
return (
<div className="flex">
<p>{note.title}</p>
<p onClick={() => deleteNote(note.title)}>Delete</p>
</div>
);
})}
</div>
);
}
export async function getServerSideProps() {
const res = await fetch(`http://localhost:3000/api`);
const { data } = await res.json();
return { props: { notes: data } };
}
If you're fetching the data with getServerSideProps you need to recall that in order to get the updated data like this :
import { useRouter } from 'next/router';
const router = useRouter()
const refreshData = () => router.replace(router.asPath);
But also you can store the data from getServerSideProps in a state and render that state and trigger a state update after a note is deleted like this :
export default function Home(notes) {
const [notesData, setNotesData] = useState(notes);
const deleteNote = async (note) => {
const res = await fetch(`http://localhost:3000/api/${note}`, {
method: "DELETE",
});
};
return (
<div>
<h1>Notes:</h1>
{notesData.notes.map((note) => {
return (
<div className="flex">
<p>{note.title}</p>
<p onClick={() => deleteNote(note.title).then(()=>{
const res = await fetch(`http://localhost:3000/api`);
const { data } = await res.json();
setNotesData(data)
})
}>Delete</p>
</div>
);
})}
</div>
);
}
export async function getServerSideProps() {
const res = await fetch(`http://localhost:3000/api`);
const { data } = await res.json();
return { props: { notes: data } };
}

How to use Firestore in Nuxt3 with SSR?

I am using Nuxt RC8 combined with Firestore.
My goal is to make the firestore request SSR and then combine it with Firestore's onSnapshot to get realtime updates after hydration is done.
I have created this composable useAssets:
import { computed, ref } from 'vue';
import { Asset, RandomAPI, RandomDatabase } from '#random/api';
/**
* Asset basic composable
* #param dbClient Database client
* #param options Extra options, like live data binding
*/
export function useAssets(dbClient: RandomDatabase) {
const assets = ref([]);
const unsubscribe = ref(null);
const searchQuery = ref('');
const randomAPI = RandomAPI.getInstance();
async function fetchAssets(options?: { live: boolean }): Promise<void> {
if (options?.live) {
try {
const query = randomAPI.fetchAssetsLive(dbClient, (_assets) => {
assets.value = _assets as Asset<any>[];
});
unsubscribe.value = query;
} catch (error) {
throw Error(`Error reading assets: ${error}`);
}
} else {
const query = await randomAPI.fetchAssetsStatic(dbClient);
assets.value = query;
}
}
const filteredAssets = computed(() => {
return searchQuery.value
? assets.value.filter((asset) =>
asset.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
: assets.value;
});
function reverseAssets(): void {
const newArray = [...assets.value];
assets.value = newArray.reverse();
}
return {
assets,
fetchAssets,
filteredAssets,
searchQuery,
reverseAssets,
unsubscribe,
};
}
The randomAPI.fetchAssetsLive comes from the firestore queries file:
export function fetchAssetsLive({
db,
callback,
options,
}: {
db: Firestore;
callback: (
assets: Asset<Timestamp>[] | QueryDocumentSnapshot<Asset<Timestamp>>[]
) => void;
options?: { fullDocs: boolean };
}): Unsubscribe {
const assetCollection = collection(db, 'assets') as CollectionReference<
Asset<Timestamp>
>;
if (options?.fullDocs) {
return onSnapshot(assetCollection, (querySnapshot) =>
callback(querySnapshot.docs)
);
}
// Return unsubscribe
return onSnapshot(assetCollection, (querySnapshot) =>
callback(querySnapshot.docs.map((doc) => doc.data()))
);
}
And then the component:
<template>
<div>
<h1>Welcome to Random!</h1>
<Button #click="reverseAssets">Reverse order</Button>
<ClientOnly>
<!-- <Input name="search" label="Search for an asset" v-model="searchQuery" /> -->
</ClientOnly>
<ul>
<li class="list-item" v-for="asset in assets" :key="asset.name">
Asset Name: {{ asset.name }} Type: {{ asset.type }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { Button, Input } from '#random/ui';
import { useNuxtApp, useAsyncData } from '#app';
const { $randomFirebase, $firestore, $getDocs, $collection } = useNuxtApp();
const { fetchAssets, filteredAssets, searchQuery, reverseAssets, assets } =
useAssets($randomFirebase);
// const a = process.client ? filteredAssets : assets;
onMounted(() => {
// console.log(searchQuery.value);
// fetchAssets({ live: true });
});
watch(
assets,
(val) => {
console.log('watcher: ', val);
},
{ deep: true, immediate: true }
);
// TODO: make SSR work
await useAsyncData(async () => {
await fetchAssets();
});
</script>
Why is it only loading via SSR and then assets.value goes []? Refreshing the page retrieves renders the items correctly but then once hydration comes in, it's gone.
Querying both, in onMounted and useAsyncData, makes it send correctly via SSR the values, makes it work client-side too but there is still a hydration missmatch, even being the values the same. And visually you only see the ones from the client-side request, not the SSR.
Is there a better approach? What am I not understanding?
I don't want to use firebase-admin as the SSR query maker because I want to use roles in the future (together with Firebase Auth via sessions).
I solved the hydration issue in two ways:
By displaying in the template only specific information, since JS objects are not ordered by default so there could be different order between the SSR query and the CS query.
By ordering by a field name in the query.
By making sure that the serverData is displayed until first load of the onsnapshot is there, so theres is not a mismatch this way: [data] -> [] -> [data]. For now I control it in the template in a very cheap way but it was for testing purposes:
<li class="list-item" v-for="asset in (isServer || (!isServer && !assets.length) ? serverData : assets)" :key="asset.name">
Asset Name: {{ asset.name }} Type: {{ asset.type }}
</li>
By using /server/api/assets.ts file with this:
import { getDocs, collection, query, orderBy, CollectionReference, Timestamp, Query } from 'firebase/firestore';
import { Asset } from '#random/api/dist';
import { firestore } from '../utils/firebase';
export default defineEventHandler(async (event) => {
const assetCollection = collection(firestore, 'assets');
let fullQuery: CollectionReference<Asset<Timestamp>> | Query<Asset<Timestamp>>;
try {
// #ts-ignore
fullQuery = query(assetCollection, orderBy('name'));
} catch (e) {
console.error(e)
// #ts-ignore
fullQuery = assetCollection;
}
const ref = await getDocs(fullQuery);
return ref.docs.map((doc) => doc.data());
});
And then in the component, executing:
const { data: assets } = useFetch('/api/assets');
onMounted(async () => {
fetchAssets({ live: true });
});
Still, if I try via useAsyncData it does not work correctly.

How to populate FormKit input fields with dynamic data fetched from a database

I'm making a fullstack app with vue3, axios using FormKit. For editing existing records I want to populate the input fields with the current data fetched from a mysql database. I stripped down the code to everything needed to display my problem, which in this code example is populating the FormKit input field with the lotnumber I fetched via the asynchronous function "getLotById". The lotnumber appears in the paragraph section but not in the input field. How can I properly delay the rendering of the FormKit element until the lotnumber has been fetched? Here's my code:
<script>
// import axios
import axios from "axios";
export default {
name: "LotEdit",
data() {
return {
lotnumber: this.lotnumber
}
},
props: {
lotid: Number
},
created: async function () {
await this.getLotById();
},
methods: {
// Get Lot By Id
async getLotById() {
try {
const response = await axios.get(`http://localhost:5000/lot/${this.$route.params.id}`);
this.lotnumber = response.data.lotnumber;
console.log(response.data);
}
catch (err) {
console.log(err);
}
},
}
};
</script>
<template>
<div>
<FormKit
type="text"
name="lotnumber"
label="lotnumber"
placeholder=""
validation="required"
:value="lotnumber"
/>
</div>
<div>
<p> Here the lotnumber appears: {{ lotnumber }}</p>
</div>
</template>
I suggest using a v-model on the FormKit input. Because it is two-way bound it means as soon as the async/await completes the data is populated on the template too. Something like...
<FormKit
v-model="lotnumber"
type="text"
name="lotnumber"
label="lotnumber"
placeholder=""
validation="required"
:value="lotnumber"
/>
Getting a little smarter I managed to solve the problem in the following way:
<script>
// import axios
import axios from "axios";
export default {
name: "LotEdit",
data() {
return {
lotnumber: this.lotnumber
}
},
props: {
lotid: Number
},
mounted: async function () {
const response = await this.getLotById();
const node = this.$formkit.get('lotnumber')
node.input(response.data.lotnumber, false)
},
methods: {
// Get Lot By Id
async getLotById() {
try {
const response = await axios.get(`http://localhost:5000/lot/${this.$route.params.id}`);
console.log(response.data);
return response;
}
catch (err) {
console.log(err);
}
},
}
};
</script>
<template>
<div>
<FormKit
type="text"
id="lotnumber"
name="lotnumber"
label="lotnumber"
placeholder=""
validation="required"
:value="lotnumber"
/>{{ lotnumber }}
</div>
</template>
Feel free to post any recommendations as I'm not a pro yet...
I'm also still figuring out how to handle controlled forms but I guess an alternative way to do it is with Form Generation
<script>
export default {
// ...
async setup() {
try {
const response = await axios.get(`http://localhost:5000/lot/${this.$route.params.id}`);
const schema = [
{
$formkit: "text",
label: "Lot Number",
value: response.data.lotnumber,
validation: "required",
},
];
} catch (err) {
console.log(err);
}
return { schema }
}
// ...
}
</script>
<template>
<FormKit type="form">
<FormKitSchema :schema="schema" />
</FormKit>
</template>

Resources