Svelte: Reactive computed variable bind to input - input

I have a reactive computed variable that is dependant to svelte store and gets initialized by making an API call when the page refreshes.
I want to bind this value to an input. With this code my input doesn't work (nothing can be typed in it)
Please see this REPL and here is the code:
This is App.svelte
<script>
import {loggedInUserProfile} from './store.js'
import { onMount } from 'svelte'
import {update} from './util.js'
let loggedInUserInfo
loggedInUserProfile.subscribe((value) => (loggedInUserInfo = value))
onMount(() => {
console.log('App onMount called')
update()
})
const capitalizeFirstLetter = (string) => {
return string?.charAt(0).toUpperCase() + string?.slice(1);
}
$: name = loggedInUserInfo?.name
$: lastName = loggedInUserInfo?.lastName
</script>
<div style="display:flex; flex-direction: column;">
<div>
<span>Name: </span><input label="name" bind:value={name}>
</div>
<div>
<span>Last Name: </span><input bind:value={lastName}>
</div>
</div>
And this is update in util mimicking an API call:
export const update = () => {
setTimeout(() => {
loggedInUserProfile.set({name: 'updated name', lastName: 'updated last name'})
}, 1000)
}
If I change the $ to let, the input will work, but I cannot have the updated value in the input. What is the solution here?

You should not use subscribe like that. For every manual subscribe you should call the returned function to unsubscribe. If you just want to get the value once outside a Svelte component, use get which can be imported from 'svelte/store'.
Just bind directly to the store value via $ syntax. You do not need any of the other script stuff. Using it like that the binding works both ways.
<input bind:value={$loggedInUserProfile.name} />
<input bind:value={$loggedInUserProfile.lastName} />

Related

Input checbox v-model not checking with async data

I have a component that at the start of it, fetchs data from database. And it supose to check the input if the value is true or false. But the problem is that is not checking when its true. I tried :checked and it works, but i need the v-model because of the two way binding
code:
<input type="checkbox" v-model="data">
const data = ref(false)
onBeforeMount(async () => {
await data = fetchingData()
})
I didnt write the hole code, because the code its on another computer, this was from head. But i am having poblems with v-model not checking. If i use :checked it works like a charm
Maybe this is a rookie mistake, but without using :checked with :change i am not seing any solution.
You should use data.value instead of data for the reactivity. For the demo purpose I am directly assign the value as true. You can replace that with the API call code.
Live Demo :
<script type="module" src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0-rc.5/vue.esm-browser.js"></script>
<div id="app">
<input type="checkbox" v-model="data"/>
</div>
<script type="module">
import {ref, createApp, onBeforeMount } from 'https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0-rc.5/vue.esm-browser.js';
const app = createApp({
setup() {
let data = ref(false)
onBeforeMount(() => {
// data = true ❌
data.value = true // ✅
})
return { data };
}
});
app.mount('#app')
</script>
I do not think it is the checkbox, but that you should use the .value when you write to a ref. Otherwise you loose the reactivity and v-model will not work.
onBeforeMount(async () => {
data.value = await fetchingData()
})
Hope this helps.

Vue3 Composition API - How to load default values from Ajax?

I have read everything I can find, but there is a confusing amount of variability between approaches. I want to use the "setup" form of the Vue3 composition API, which I believe is the recommended approach for future compatibility.
I have a form with elements like this:
<form #submit.prevent="update">
<div class="grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2">
<div>
<label class="text-gray-700" for="accountID">ID</label>
<input disabled id="accountID" v-model="accountID"
class="bg-slate-100 cursor-not-allowed w-full mt-2 border-gray-200 rounded-md focus:border-indigo-600 focus:ring focus:ring-opacity-40 focus:ring-indigo-500"
type="text"
/>
</div>
I want to load the current values with Ajax. If the user submits the form then I want to save the changed fields with a PATCH request.
I cannot work out how to change the form value with the result of the Ajax request and still maintain the binding.
Vue3 blocks changing the props directly (which makes sense), so the code below does not work:
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import axios from "axios";
import { useUserStore } from "#/stores/userStore";
const userStore = useUserStore();
const props = defineProps({
accountID: String,
});
const emit = defineEmits(['update:accountID'])
const accountID = computed({
get() {
return props.accountID;
},
set (value) {
return emit('update:accountID')
},
})
onMounted(async () => {
let response = await axios.get("http://localhost:8010/accounts", { headers: { "Authorization": "Bearer " + userStore.jws } });
// This is a readonly variable and cannot be reassigned
props.accountID = response.data.ID;
});
function update() {
console.log("Form submitted")
}
</script>
How can I set the form value with the result of the Ajax request?
Instead of trying to assign props.accountID, update the accountID computed prop, which updates the corresponding v-model:accountID via the computed setter. That v-model update is then reflected back to the component through the binding:
onMounted(async () => {
let response = await axios.get(…)
// props.accountID = response.data.ID ❌ cannot update readonly prop
accountID.value = response.data.ID ✅
})
Also note that your computed setter needs to emit the new value:
const accountID = computed({
get() {
return props.accountID
},
set(value) {
// return emit('update:accountID') ❌ missing value
return emit('update:accountID', value) ✅
},
})
demo

Pass Vue js search filter functionality through single file components with EventBus

I have the following components:
/components/SearchBlogs.vue Search component to filter on blog.title and blog.description.
/components/BlogList.vue Here I list all the Blog items.
SearchBlogs.vue
<template>
<div>
<input type="text" v-model="search" #change="emitSearchValue" placeholder="search blog">
</div>
</template>
<script>
import { EventBus } from '../event-bus.js'
export default {
name: 'SearchBlogs',
data: () => {
return {
search: ''
}
},
methods: {
emitSearchValue() {
EventBus.$emit('search-value', 'this.search')
}
}
}
</script>
BlogList.vue
<template>
<div>
<div v-for="blog in filteredBlogs" :key="blog">
<BlogListItem :blog="blog" />
</div>
</div>
</template>
<script>
import BlogListItem from './BlogListItem'
import { EventBus } from '../event-bus.js'
export default {
name: 'BlogList',
components: {
BlogListItem,
},
data: () => {
return {
blogs: [],
searchvalue: ''
}
},
computed: {
filteredBlogs() {
return this.blogs.filter(blog =>
blog.name.toLowerCase().includes(
this.searchvalue.toLowerCase()
)
)
}
},
created() {
fetch('http://localhost:3000/blogs')
.then(response => {
return response.json();
})
.then(data => {
this.blogs = data;
}),
EventBus.$on('search-value', (search) => {
this.searchvalue = value;
})
}
}
</script>
In another page component Blogs I register both components:
<template>
<div>
<h1>Blog</h1>
<TheSidebar>
<SearchBlogs />
</TheSidebar>
<BlogList/>
</div>
</template>
Can anybody see what's missing here? I want, as soon as the user types something in the search input (from the SearchBlogs.vue component), it start filtering and updating the list.
Look at my solution condesandbox
Here is an explanation:
You don't need to use EventBus. You can communicate with Search Component by v-model, using prop value and emiting updated value from the Input.
Then your Main (List) Component is responsible for all the logic.
It keeps the state of a Search
It keeps the items and filtered Items
Thanks to that your Search Component is very clear and has no data, that means it has very little responsibility.
Please ask questions if I can add something to help you understand 😉
UPDATE:
EventBus is a great addition in some cases. Your case is simple enough, there is no need to add it. Right now your architecture is "over engineered".
When you have added listener on EventBus, on created:hookyou should always remove it while Component is being destroyed. Otherwise you can encounter a trouble with double calling function etc. This is very hard to debug, tryst me I'he been there 😉
Going with my suggestion gives you comfort of "no-need-to-remember-about-this" because Vue is doing it for you.
Hope that help.
Couple of issues but essentially the computed prop filteredData will look like:
computed: {
filteredData() {
return this.experiences.filter(
el => el.category.indexOf(this.search) > -1
);
}
}
Also, used quotes around 'this.search' when passing its value back which made it a string.
Fixed sandbox
https://codesandbox.io/s/reverent-lamarr-is8jz

Use moment().fromNow() with a vue component in array

How can I use Momentjs to format a date that is inside my vue component? The moment().fromNow() function works fine when I manually put in a date, but I want to use the date that is fetched from my API.
This is currently what it looks like, I put the part where I need help with in *.
Vue.js
<div class="post d-flex flex-row" v-for="(post, i) in Post" :key="i">
<h6 class="card-subtitle mb-2 text-muted">Posted **{{moment(datePosted).fromNow()}}** by {{post.user}}</h6>
</div>
data() {
return{
Post: []
}
},
async created() {
try{
const res = await axios.get(url)
this.Post = res.data;
} catch(err){
console.log(err)
}
}
Variable datePosted seems undefined. The format function should be something like {{ moment(post.datePosted).fromNow() }}. Also don't use variables which differ just by the case: post and Post.
In my opinion, you could map res.data inside created and append attribute with formatted data to the object.
E.g:
this.Post = res.data.map(post => ({
...post,
datePost: moment(post.date).fromNow()
})
Obviously, I cannot know if post.date is proper attribute. You have to type there the proper one. Also make sure you imported moment js module.
Then in the template, you could use:
{{ post.datePost }}

The object sent as prop is undefined while testing a Vue SFC with Jest

I want to test a Vue single file component which receives a prop as input. When I mock the prop, which is an object, I get an error that the object is undefined, The error comes from the HTML where the values of the object are used. If I make the prop to be a string for example (and I remove answer.value and :class="{'active': answer.selected}" from HTML), everything works fine.
Component:
<template>
<div class="answer-container" #click="setActiveAnswer()" :class="{'active': answer.selected}">
<div class="answer">
<p>{{answer.value}}</p>
</div>
</div>
</template>
<script>
export default {
name: 'Answer',
props: {
answer: Object,
},
methods: {
setActiveAnswer() {
this.$emit('selectedAnswer', this.answer);
}
}
}
</script>
Test file:
import { mount } from '#vue/test-utils'
import Answer from './../../src/components/Answer'
describe('Answer', () => {
it('should receive "answer" as prop', () => {
const answer = {
value: 'testAnswer',
selected: true
};
const wrapper = mount(Answer, {
propsData: {
answer: answer
}
});
expect(wrapper.props().answer.value).toBe('testAnswer');
})
})
The error I get is:
TypeError: Cannot read property 'selected' of undefined
Please advise what am I doing wrong. Thanks!
I managed to fix this by adding a v-if="answer" on <div class="answer-container" ..., which is quite strange (as this is not async data) since the code works fine when checking the application in the browser - the problem only appeared while unit testing the component. I suppose there is also a fix in a Jest/Unit testing way, something like declaring the prop after the component finished rendering/mounting...