Vue.js get v-for array value in computed - javascript

I'm passing user data to a component via Laravel, this data contains a distance in miles, however the user has the option to set the view distance in km, so I have to pass profile.distance_mi to be computed, how do I accomplish this?
HTML:
<saved profiles="{{json_encode($profiles)}}" unit="{{Auth::user()->settings->unit}}"></saved>
<div v-for="profile in profiles" class="col-xs-12 col-sm-4 col-md-3">
...
<h4>#{{distance}}</h4>
....
</div>
JS
new Vue(
{
el: 'body',
components:
{
saved:
{
template: '#saved',
props: ['profiles', 'unit'],
created: function ()
{
this.profiles = JSON.parse(this.profiles);
},
computed:
{
distance: function ()
{
var unit = this.unit;
return unit == 0 ? Math.round(distance * 1.60934) + ' km' : distance + ' miles';
}
}
}
}
});

A computed property like this won't work in a for loop on a component.
I think the easiest way to achieve what you want to achieve is to make another component. I'm a bit unclear on which properties you're trying to observe and what not, but I think this will get you in the right direction:
Profile Distance Component
var ProfileDistance = Vue.extend({
props: ['profile'],
template: '<span>{{distance}}</span>',
computed: {
distance: function() {
if (this.profile.hasOwnProperty('distance_mi')) {
return this.profile.distance_mi + ' miles away';
} else if(this.profile.hasOwnProperty('distance_km')){
return this.profile.distance_km + ' kilometers away';
} else {
return 'Distance N/A';
}
}
}
})
Of course make sure you use this component inside of your existing saved component
components: {
//....
'profile-distance': ProfileDistance
}
Then just pass your profile as a property to your profile-distance component:
<profile-distance :profile="profile"></profile-distance>
here's a working jsfiddle

Related

How to use/link external api to show the fetched data in the grapejs component

I am new to grape and backbonejs.I want to make a template builder which apart from having
default components provided by grapejs like the label, image etc will also have custom components
for ex - Top 5 products today having an image and product name and it will get the fresh data from the server. In this scenario I am not able to figure out where should I make the API call and how can I use the fetched results to show in components. Link to the code which I have tried is in the comments.
if you move your code to your prop change handler, instead of the onRender function of the view you will be able to use the values from the API call as you want.
Check this small change:
https://jsfiddle.net/jvas28/8sb3tn94/1/
const editor = grapesjs.init({
container: '#gjs',
fromElement: 1,
height: '100%',
storageManager: { type: 0 },
});
editor.DomComponents.addType('test-component', {
model: {
defaults: {
testprop: '12345',
url: 'https://jsonplaceholder.typicode.com/posts/1'
},
init() {
console.log('Local hook: model.init', this.attributes.testprop);
fetch(this.attributes.url)
.then(response => response.json())
.then(commits => {
this.set('testprop', 'Test');
console.log(this.attributes.testprop);
});
this.listenTo(this, 'change:testprop', this.handlePropChange);
// Here we can listen global hooks with editor.on('...')
},
updated(property, value, prevValue) {
console.log('Local hook: model.updated',
'property', property, 'value', value, 'prevValue', prevValue);
},
removed() {
console.log('Local hook: model.removed');
},
handlePropChange() {
let prop = this.get('testprop');
// Here inside view it is getting the old value. of "testprop" i.e '12345' but not
//the new value
//which is being fetched from server in the init() of model.
let comp1 = '<div>' +
'<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_created_with_a_mobile_phone.png/1200px-Image_created_with_a_mobile_phone.png" />' +
'<span title="foo">' + prop + '</span>' + '</div>';
const component = editor.addComponents(comp1);
return component
}
},
view: {
init() {
console.log('Local hook: view.init');
},
},
});
// A block for the custom component
editor.BlockManager.add('test-component', {
label: 'Test Component',
content: `<div data-gjs-type="test-component"></div>`,
});

Displaying random text in Vue.js template

I have a simple Loader component in my Vue.js app that just displays one of messages, randomly. I did it like that:
Vue.component('Loader', {
data() {
const textEntries = [
'Just a second',
'Please wait',
'Almost there',
'And there we go',
];
return {
text: textEntries[Math.trunc(Math.random() * textEntries.length)]
};
},
template: '<p class="loading">{{ text }}...</p>'
});
I'm not sure if keeping this in data like that is fine. Won't my text ever get re-rendered with another text? Also, having the array in the data() method seems awkward. Would it be more suitable to use lifecycle hooks for that instead? Or computed property?
You should use the lifecycle hooks which Vue provides. More precisely, I believe that for your example, you should use the created hook. So your code will be like this:
Vue.component('Loader', {
created: function () {
this.$options.textEntries = [
'Just a second',
'Please wait',
'Almost there',
'And there we go',
];
this.$options.randomIndex = Math.trunc(Math.random() * textEntries.length);
},
data() {
return {
text: this.$options.textEntries[this.$options.randomIndex]
};
},
template: '<p class="loading">{{ text }}...</p>'
});
If you leave textEntries within the data Vue property, then you will not have access to it as it will be erased from the memory as soon as the data function is processed.
WARNING: The $options, which I am using, cannot be changed since it is read-only. For more information, I would redirect you to this.
As I know data() will called only once, so the text will be one of the textEntries
I think it's better to extract textEntries out of the data method(since it will not be changed, so it should not be generated each data() call.
The right way would be to define the array in data and get the random text as computed:
new Vue({
el:"#app",
data() {
return{
textEntries: [
'Just a second',
'Please wait',
'Almost there',
'And there we go',
]
}
},
computed:{
text: function(){
return this.textEntries[Math.trunc(Math.random() * this.textEntries.length)]
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<p class="loading">{{ text }}...</p>
</div>
If you want the text to be re-rendered every period of time, you can use setTimeout and keep track of the random index:
new Vue({
el:"#app",
data() {
return{
textEntries: [
'Just a second',
'Please wait',
'Almost there',
'And there we go',
],
index: 0
}
},
mounted(){
setInterval(function(){
this.index = Math.trunc(Math.random() * this.textEntries.length)
}.bind(this), 3000);
},
computed:{
text: function(){
return this.textEntries[this.index];
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<p class="loading">{{ text }}...</p>
</div>

How can I convert a long list of 'watch' into a functional way in VueJS?

I'm a novice for vueJS.
I have a long list of watch list. It's all same.
But I don't know how to convert them into a functional way.
They are all for adding comma in input tag and v-model.
It works very well. But the codes look very dumb because they are exactly the same, but not their name.
new Vue({
data: {
tmp_price1: '',
tmp_price2: '',
tmp_price3: '',
tmp_a_price: '',
tmp_b_price: '',
},
watch: {
tmp_price1: function(newValue) {
if (newValue != '') {
const result = newValue.replace(/\D/g, "").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
Vue.nextTick(() => this.tmp_price1 = result);
}
},
tmp_price2: function(newValue) {
if (newValue != '') {
const result = newValue.replace(/\D/g, "").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
Vue.nextTick(() => this.tmp_price2 = result);
}
},
....(repeat)
},
Please help me improve these dumb codes efficient way.
It may seem like over-engineering, but I would probably make a component to encapsulate the commafying behavior. The component would have a settable computed in it that emits the commafied value for the parent to use in updating.
new Vue({
el: '#app',
data: {
tmp_price1: '',
tmp_price2: '',
tmp_price3: '',
tmp_a_price: '',
tmp_b_price: '',
},
components: {
commafiedInput: {
props: ['value'],
template: '<input v-model="commaValue">',
computed: {
commaValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', this.addCommas(newValue));
}
}
},
methods: {
addCommas(v) {
return v.replace(/\D/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div> {{tmp_price1}}
<commafied-input v-model="tmp_price1"></commafied-input>
</div>
<commafied-input v-model="tmp_price2"></commafied-input>
<commafied-input v-model="tmp_price3"></commafied-input>
<commafied-input v-model="tmp_a_price"></commafied-input>
<commafied-input v-model="tmp_b_price"></commafied-input>
</div>
Just displaying the formatted version of the value can be done with a simple filter:
new Vue({
el: '#app',
data: {
tmp_price1: '123123',
tmp_price2: '',
tmp_price3: ''
},
filters: {
myFilter(v) {
return v.replace(/\D/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div><input v-model="tmp_price1">{{tmp_price1 | myFilter}}</div>
<div><input v-model="tmp_price2">{{tmp_price2 | myFilter}}</div>
<div><input v-model="tmp_price3">{{tmp_price3 | myFilter}}</div>
</div>
...but that's not enough for the input fields; you can't just throw a filter onto the v-model attribute. A sub-component as described in Roy J's answer is probably the best and most reusable way to handle that, but if you're ok with something a bit quick-and-dirty (but not too dirty) you could just throw a change handler at the problem:
new Vue({
el: '#app',
data: {
tmp_price1: '123123',
tmp_price2: '',
tmp_price3: ''
},
methods: {
myFormatter(fieldname) {
/* replace the user's input with the formatted value.
There's probably some clever way to read the v-model
name from the input field instead of passing it to the
method as a string, but I'm not going to mess around
with that for what is after all a quick-and-dirty technique */
this[fieldname] = this[fieldname].replace(/\D/g, "").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
},
mounted() {
// if the initial values aren't always empty, you'll need to run the
// formatter function on component load as well as on user input:
['tmp_price1', 'tmp_price2', 'tmp_price3'].forEach(f => {
this.myFormatter(f);
});
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div><input v-model="tmp_price1" #input="myFormatter('tmp_price1')">{{tmp_price1}}</div>
<div><input v-model="tmp_price2" #input="myFormatter('tmp_price2')">{{tmp_price2}}</div>
<div><input v-model="tmp_price3" #input="myFormatter('tmp_price3')">{{tmp_price3}}</div>
</div>
(Or as another variation, this answer to a similar question uses essentially the same technique, but wraps it in a Vue directive to bind the input events instead of attaching #input handlers.)
Note that in the "filter" version, the v-model values remain the original user input; the filter only affects the displayed value. In the second example the formatting is applied to the v-modeled values themselves, so if you pass those values on to somewhere else you'll get the formatted version. Both techniques can be useful, depending on the circumstances. (Or you could even use them in combination -- remove non-digits in the v-model, then add the commas via a filter just for display, for example.)

Vuejs 2: debounce not working on a watch option

When I debounce this function in VueJs it works fine if I provide the number of milliseconds as a primitive. However, if I provide it as a reference to a prop, it ignores it.
Here's the abbreviated version of the props:
props : {
debounce : {
type : Number,
default : 500
}
}
Here is the watch option that does NOT work:
watch : {
term : _.debounce(function () {
console.log('Debounced term: ' + this.term);
}, this.debounce)
}
Here is a watch option that DOES work:
watch : {
term : _.debounce(function () {
console.log('Debounced term: ' + this.term);
}, 500)
}
It suspect that it is a scope issue but I don't know how to fix it. If I replace the watch method as follows...:
watch : {
term : function () {
console.log(this.debounce);
}
}
... I get the correct debounce value (500) appearing in the console.
Another variation to #Bert's answer is to build the watcher's function in created(),
// SO: Vuejs 2: debounce not working on a watch option
console.clear()
Vue.component("debounce",{
props : {
debounce : {
type : Number,
default : 500
}
},
template:`
<div>
<input type="text" v-model="term">
</div>
`,
data(){
return {
term: "",
debounceFn: null
}
},
created() {
this.debounceFn = _.debounce( () => {
console.log('Debounced term: ' + this.term);
}, this.debounce)
},
watch : {
term : function () {
this.debounceFn();
}
},
})
new Vue({
el: "#app"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<div id="app">
<debounce :debounce="2000"></debounce>
</div>
Example on CodePen
The primary issue here is using this.debounce as the interval when defining your debounced function. At the time _.debounce(...) is run (when the component is being compiled) the function is not yet attached to the Vue, so this is not the Vue and this.debounce will be undefined. That being the case, you will need to define the watch after the component instance has been created. Vue gives you the ability to do that using $watch.
I would recommend you add it in the created lifecycle handler.
created(){
this.unwatch = this.$watch('term', _.debounce((newVal) => {
console.log('Debounced term: ' + this.term);
}, this.debounce))
},
beforeDestroy(){
this.unwatch()
}
Note above that the code also calls unwatch which before the component is destroyed. This is typically handled for you by Vue, but because the code is adding the watch manually, the code also needs to manage removing the watch. Of course, you will need to add unwatch as a data property.
Here is a working example.
console.clear()
Vue.component("debounce",{
props : {
debounce : {
type : Number,
default : 500
}
},
template:`
<input type="text" v-model="term">
`,
data(){
return {
unwatch: null,
term: ""
}
},
created(){
this.unwatch = this.$watch('term', _.debounce((newVal) => {
console.log('Debounced term: ' + this.term);
}, this.debounce))
},
beforeDestroy(){
this.unwatch()
}
})
new Vue({
el: "#app"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<debounce :debounce="250"></debounce>
</div>
The debounced method needs to be abstracted since we need to call the same function everytime the watch is triggered. If we place the debounced method inside a Vue computed or watch property, it will be renewed everytime.
const debouncedGetData = _.debounce(getData, 1000);
function getData(val){
this.newFoo = val;
}
new Vue({
el: "#app",
template: `
<div>
<input v-model="foo" placeholder="Type something..." />
<pre>{{ newFoo }}</pre>
</div>
`,
data(){
return {
foo: '',
newFoo: ''
}
},
watch:{
foo(val, prevVal){
debouncedGetData.call(this, val);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="app"></div>
Good Luck...
new Vue({
el: '#term',
data: function() {
return {
term: 'Term',
debounce: 1000
}
},
watch: {
term : _.debounce(function () {
console.log('Debounced term: ' + this.term);
}, this.debounce)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.js"></script>
<div id="term">
<input v-model="term">
</div>

Is there any way to have multiple Vues have a computed listener working on the same value?

Setup:
I have multiple Vue components, and each component has multiple instances in different dialogs in my web app.
For each type of component I have a global state (handrailOptions in the example below) so that each type of component stays in sync across the dialogs.
I'd like for it so that when a component proceeds beyond step 1, I hide the other components in that dialog.
I have achieved this nicely using the computed / watch combo.
However, my problem is that it seems if I try to listen in with computed through more than 1 Vue instance, it hijacks the other listeners.
Problem
Below is a simplified version of what I'm working with, when the app starts up, the console logs 'computed 1' & 'computed 2'. But then when I change handrailOptions.step, only the second fires. ('computed 2' & 'watched 2')
Is there any way to have multiple Vues have a computed listener working on the same value?
handrailOptions = {
step: 1
};
Vue.component( 'handrail-options', {
template: '#module-handrail-options',
data: function() {
return handrailOptions;
},
});
var checkoutDialog = new Vue({
el: '#dialog-checkout',
computed: {
newHandrailStep() {
console.log('computed 1');
return handrailOptions.step;
}
},
watch: {
newHandrailStep( test ) {
console.log('watched 1');
}
}
});
new Vue({
el: '#dialog-estimate-questions',
computed: {
newHandrailStep() {
console.log('computed 2');
return handrailOptions.step;
}
},
watch: {
newHandrailStep( test ) {
console.log('watched 2');
}
}
});
This works as expected. I made handrailOptions responsive by making the data object of a new Vue. Making it the data object of a component, as you did, could also work, but the component would have to be instantiated at least once. It makes more sense to have a single object for your global, anyway.
handrailOptions = {
step: 1
};
// Make it responsive
new Vue({data: handrailOptions});
var checkoutDialog = new Vue({
el: '#dialog-checkout',
computed: {
newHandrailStep() {
console.log('computed 1', handrailOptions.step);
return handrailOptions.step;
}
},
watch: {
newHandrailStep(test) {
console.log('watched 1');
}
}
});
new Vue({
el: '#dialog-estimate-questions',
computed: {
newHandrailStep() {
console.log('computed 2', handrailOptions.step);
return handrailOptions.step;
}
},
watch: {
newHandrailStep(test) {
console.log('watched 2');
}
}
});
setInterval(() => ++handrailOptions.step, 1500);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="dialog-estimate-questions">
Main step {{newHandrailStep}}
</div>
<div id="dialog-checkout">
CD step {{newHandrailStep}}
</div>

Categories