Vue Component does not render changes of object (despite $set being used) - vue.js

I am trying to build a Vue component that displays the hour and minute of a Date and emits a changed version when a + or - button is pressed.
Screenshot: https://i.imgur.com/hPUdrca.png
(not enough reputation to post image)
Using the Vue.js Devtools (in Google Chrome) I observed that:
The change event fires and contains a correct date
The date prop was updated correctly
It just does not rerender the date.
https://jsfiddle.net/JoshuaKo/6c73b2gt/2
<body>
<div id="app">
<time-input :date="meeting.startDate"
#change="$set(meeting, 'startDate', $event)"
></time-input>
<p>
{{meeting.startDate.toLocaleString()}}
</p>
</div>
</body>
Vue.component('time-input', {
props: {
date: Date,
minuteSteps: {
type: Number,
default: 1
}
},
methods: {
increaseTime: function() {
if (!this.date) return
const newDate = this.date
newDate.setMinutes(this.date.getMinutes() + this.minuteSteps)
this.$emit('change', newDate)
},
decreaseTime: function() {
if (!this.date) return
const newDate = this.date
newDate.setMinutes(this.date.getMinutes() - this.minuteSteps)
this.$emit('change', newDate)
},
time: function() {
if (!this.date) return '??:??'
const h = this.date.getHours().toString()
const m = this.date.getMinutes().toString()
return _.padStart(h, 2, '0') + ':' + _.padStart(m, 2, '0')
}
},
computed: {
},
template: `
<div class="time">
<button :disabled="!date" class="control" #click="decreaseTime">-</button>
<span>{{time()}}</span>
<button :disabled="!date" class="control" #click="increaseTime">+</button>
</div>
`.trim()
})
const app = new Vue({
el: '#app',
data: {
meeting: {
name: 'test meeting',
startDate: new Date(),
endDate: null
}
}
})

The object in meeting.startDate is always the same, so it doesn't trigger anything.
You should create a new Date object each time, so change the lines:
const newDate = this.date
to:
const newDate = new Date(this.date.getTime())
Also, the $set (alias of Vue.set) is intended to add properties that must be reactive. As no propperty is added (just modified), you don't need it here.

Related

getting promise instead of value in laravel vue js [duplicate]

I am calling an async function which loads the profile pic, the await call returns the value to the variable 'pf' as expected, but I couldn't return that from loadProfilePic. At least for the start I tried to return a static string to be displayed as [object Promise] in vue template.
But when I remove await/asnyc it returns the string though.
<div v-for="i in obj">
{{ loadProfilePic(i.id) }}
</div>
loadProfilePic: async function(id) {
var pf = await this.blockstack.lookupProfile(id)
return 'test data';
//return pf.image[0]['contentUrl']
},
That is because async function returns a native promise, so the loadProfilePic method actually returns a promise instead of a value. What you can do instead, is actually set an empty profile pic in obj, and then populate it in your loadProfilePic method. VueJS will automatically re-render when the obj.profilePic is updated.
<div v-for="i in obj">
{{ i.profilePic }}
</div>
loadProfilePic: async function(id) {
var pf = await this.blockstack.lookupProfile(id);
this.obj.filter(o => o.id).forEach(o => o.profilePic = pf);
}
See proof-of-concept below:
new Vue({
el: '#app',
data: {
obj: [{
id: 1,
profilePic: null
},
{
id: 2,
profilePic: null
},
{
id: 3,
profilePic: null
}]
},
methods: {
loadProfilePic: async function(id) {
var pf = await this.dummyFetch(id);
this.obj.filter(o => o.id === id).forEach(o => o.profilePic = pf.name);
},
dummyFetch: async function(id) {
return await fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(r => r.json());
}
},
mounted: function() {
this.obj.forEach(o => this.loadProfilePic(o.id));
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="i in obj">
{{ i.profilePic }}
</div>
</div>

VueJS change date format

Now VueJS display me date in format 2021-02-24 00:12:42, but how i can change display only 00:12? Without year, month, date and seconds?
In vue i use:
<div class="jhistory" v-for="game in histories" :key="game.game_id">
{{ game.date }}
</div>
<script>
export default {
data() {
return {
histories: {},
}
},
mounted() {
this.$root.isLoading = true
this.getHistory();
},
methods: {
getHistory() {
this.$root.axios.post('/jackpot/history').then(res => {
this.histories = res.data;
this.$root.isLoading = false;
});
}
}
}
</script>
One way to do this is to create a component method that converts the string into a Date object, uses Date.prototype.toTimeString(), and takes only the first 5 characters, which contains only the hh:mm portion. Another simpler way is to just extract the substring with String.prototype.substr(), assuming the format never changes.
export default {
methods: {
toTime(date) {
// Option 1
return new Date(date).toTimeString().substr(0,5)
// Option 2
return date.substr(11,5)
}
}
}
Then use the method in your template:
{{ toTime(game.date) }}
new Vue({
el: '#app',
data: () => ({
game: {
date: '2021-02-24 00:12:42'
}
}),
methods: {
toTime(date) {
return new Date(date).toTimeString().substr(0,5)
// OR
// return date.substr(11,5)
}
}
})
<script src="https://unpkg.com/vue#2.6.12"></script>
<div id="app">
<p>{{ toTime(game.date) }}</p>
</div>

Vue: pass instantiated component to slot

I'm writing a component that renders a text. When a word starts with '#' it's a user's reference (like in twitter), and I must create a tooltip with the user's info.
This is how I instantiate the user's info component (this works fine, I'm using it in other places of the app):
const AvatarCtor = Vue.extend(AvatarTooltip);
let avatarComponent = new AvatarCtor({
propsData: {
user: user
}
});
This is the TooltipWrapper component:
<template>
<el-tooltip>
<slot name="content" slot="content"></slot>
<span v-html="text"></span>
</el-tooltip>
</template>
<script>
import {Tooltip} from 'element-ui';
export default {
name: "TooltipWrapper",
components: {
'el-tooltip': Tooltip
},
props: {
text: String
}
}
</script>
And this is how I wire it up all together:
const TooltipCtor = Vue.extend(TooltipWrapper);
const tooltip = new TooltipCtor({
propsData: {
text: "whatever"
}
});
tooltip.$slots.content = [avatarComponent];
tooltip.$mount(link);
This doesn't work. But if I set some random text in the content slot, it works fine:
tooltip.$slots.content = ['some text'];
So my problem is that I don't know how to pass a component to the slot. What am I doing wrong?
this.$slots is VNodes, but you assign with one component instance.
Below is one approach (mount the component to one element then reference its vnode) to reach the goal.
Vue.config.productionTip = false
const parentComponent = Vue.component('parent', {
template: `<div>
<div>
<slot name="content"></slot>
<span v-html="text"></span>
</div>
</div>`,
props: {
text: {
type: String,
default: ''
},
}
})
const childComponent = Vue.component('child', {
template: `<div>
<button #click="printSomething()">#<span>{{user}}</span></button>
<h4>You Already # {{this.clickCount}} times!!!</h4>
</div>`,
props: {
user: {
type: String,
default: ''
},
},
data(){
return {
clickCount: 1
}
},
methods: {
printSomething: function () {
console.log(`already #${this.user} ${this.clickCount} times` )
this.clickCount ++
}
}
})
const TooltipCtor = Vue.extend(parentComponent)
const tooltip = new TooltipCtor({
propsData: {
text: "whatever"
}
})
const SlotContainer = Vue.extend(childComponent)
const slotInstance = new SlotContainer({
propsData: {
user: "one user"
}
})
slotInstance.$mount('#slot')
tooltip.$slots.content = slotInstance._vnode
tooltip.$mount('#link')
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="link">
</div>
<div style="display:none"><div id="slot"></div></div>

Getting a Vue.js computed to run without being part of the DOM

I can't figure out how to get Vue.js to always evaluate a computed regardless of if I'm actually using it in the page. A simplified version of what I'm trying to accomplish is to have a couple input fields which I want to influence the value of another field when either has been updated. I also want this field to be manually editable too. Example jsfiddle.
html:
<div id="app">
<p v-if="updateUsername">Just here to get the darn thing to run</p>
<div>
yourName:<input v-model="yourName">
</div>
<div>
dogsName:<input v-model="dogName">
</div>
<div>
username:<input v-model="userName">
</div>
</div>
js:
var main = new Vue({
el: '#app',
data: {
yourName: 'Adam',
dogName: 'Barkster',
userName: ''
},
methods: {
},
computed: {
updateUsername: function(){
this.userName = this.yourName + this.dogName;
}
}
});
This works exactly as I want it to but requires I BS the use of "updateUsername" in the html. I'm sure there's a better way.
You could add a watch:
watch: { updateUsername() {} }
Fiddle: https://jsfiddle.net/acdcjunior/k6rknwqg/2/
But it seems what you want are two watchers instead:
var main = new Vue({
el: '#app',
data: {
yourName: 'Adam',
dogName: 'Barkster',
userName: ''
},
watch: {
yourName: {
handler() {
this.userName = this.yourName + this.dogName;
},
immediate: true
},
dogName() {
this.userName = this.yourName + this.dogName;
}
}
});
Fiddle: https://jsfiddle.net/acdcjunior/k6rknwqg/6/
Another option (watching two or more properties simultaneously):
var main = new Vue({
el: '#app',
data: {
yourName: 'Adam',
dogName: 'Barkster',
userName: ''
},
mounted() {
this.$watch(vm => [vm.yourName, vm.dogName].join(), val => {
this.userName = this.yourName + this.dogName;
}, {immediate: true})
}
});
Fiddle: https://jsfiddle.net/acdcjunior/k6rknwqg/11/
For Vue2, computed properties are cached based on their dependencies. A computed property will only re-evaluate when some of its dependencies have changed.
In your example, computed property=updateUserName will be re-evaluate when either dogName or yourName is changed.
And I think it is not a good idea to update other data in computed property, you will remeber you update userName in computed property=updateUserName now, but after a long time, you may meet some problems why my username is updated accidentally, then you don't remember you update it in one of the computed properties.
Finally, based on your example, I think watch should be better.
You can define three watcher for userName, dogName, yourName, then execute your own logic at there.
var main = new Vue({
el: '#app',
data: {
yourName: 'Adam',
dogName: 'Barkster',
userName: 'Adam Barkster'
},
methods: {
},
computed: {
updateUsername: function(){
return this.yourName + this.dogName;
}
},
watch: {
dogName: function(newValue){
this.userName = this.yourName + ' ' + newValue
},
yourName: function(newValue){
this.userName = newValue + ' '+this.dogName
},
userName: function(newValue) {
// below is one sample, it will update dogName and yourName
// when end user type in something in the <input>.
let temp = newValue.split(' ')
this.yourName = temp[0]
this.dogName = temp[1]
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div id="app">
<p v-if="updateUsername">Just here to get the darn thing to run</p>
<div>
yourName:<input v-model="yourName">
</div>
<div>
dogsName:<input v-model="dogName">
</div>
<div>
username:<input v-model="userName">
</div>
</div>
</div>

Why value doesn't updated correctly in Vue when using v-for?

I've created basic jsfiddle here.
var child = Vue.component('my-child', {
template:
'<div> '+
'<div>message: <input v-model="mytodoText"></div> <div>todo text: {{todoText}}</div>'+
'<button #click="remove">remove</button>' +
'</div>',
props:['todoText'],
data: function(){
return {
mytodoText: this.todoText
}
},
methods: {
remove: function(){
this.$emit('completed', this.mytodoText);
}
}
});
var root = Vue.component('my-component', {
template: '<div><my-child v-for="(todo, index) in mytodos" v-bind:index="index" v-bind:todoText="todo" v-on:completed="completed"></my-child></div>',
props:['todos'],
data: function(){
return {
mytodos: this.todos
}
},
methods: {
completed: function(todo){
console.log(todo);
var index = this.mytodos.indexOf(todo, 0);
if (index > -1) {
this.mytodos.splice(index, 1);
}
}
}
});
new Vue({
el: "#app",
render: function (h) { return h(root, {
props: {todos: ['text 1', 'text 2', 'text 3']}
});
}
});
Code is simple: root component receives array of todos (strings), iterates them using child component and pass some values through the props
Click on the top "remove" button and you will see the result - I'm expecting to have
message: text 2 todo text: text 2
but instead having:
message: text 1 todo text: text 2
From my understanding vue should remove the first element and leave the rest. But actually I have some kind of "mix".
Can you please explain why does it happen and how it works?
This is due to the fact that Vue try to "reuse" DOM elements in order to minimize DOM mutations. See: https://v2.vuejs.org/v2/guide/list.html#key
You need to assign a unique key to each child component:
v-bind:key="Math.random()"
where in real-world the key would be for example an ID extracted from a database.