Vue2: Why can I not set new value when using computed property? - vue.js

I have a property from an API (romanizationSystem_FK) that sometimes is a null value. If there is a null value, I want to set the value of that property to -1.
I've tried to do this logic via a computed property, but for some reason if the value is null it still does not get set to -1 by the computed property. Anyone spot any errors ?
Template:
<select
v-model="romanizationSystem_FK"
#change="$emit('variation-updates', formValues)"
>
<option value=""></option>
<option
v-for="option in romanizationSystemOptions"
:key="option.code"
:value="option.code"
>
{{ option.system }}
</option>
</select>
script:
props: {
variation: {
type: Object,
default: () => ({})
},
romanizationSystemOptions: {
type: Array,
default: () => []
},
data() {
return {
formValues: {
...this.variation
}
}
},
computed: {
romanizationSystem_FK: {
get() {
return this.formValues.romanizationSystem_FK
},
set(val) {
if (val === null) {
this.formValues.romanizationSystem_FK = -1
} else {
this.formValues.romanizationSystem_FK = val
}
}
}

You can use Nullish coalescing operator (??). I am just creating a very simple demo to show how it works.
new Vue({
el: '#app',
data: {
formValues: {
romanizationSystem_FK: null
}
},
computed: {
romanizationSystem_FK() {
return this.formValues.romanizationSystem_FK ?? '-1'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input v-model="romanizationSystem_FK"/>
</div>

Related

Code works but still getting warning in Vue

i am getting warning "Avoid mutating a prop directly", i know this can be solved through data property or computed usage like mentioned in Vue Official Documentation. But i do not know how to change my code to any of those methods. Please help me with proper code to get rid of this Warning.
My Code looks like this:
<template>
<div class="track-rating">
<span :key="note" v-for="note in maxNotes" :class="{ 'active': note <= rating || note <= hoveredNote }" #mouseover="hoveredNote = note" #mouseleave="hoveredNote = false" #click="rate(note)" class="material-icons mr-1">
audiotrack
</span>
</div>
</template>
<script>
export default {
name: "Rating",
props: {
rating: {
type: Number,
required: true
},
maxNotes: {
type: Number,
default: 3
},
hasCounter: {
type: Boolean,
default: true
},
itemId: {
type: String
}
},
data() {
return {
hoveredNote: false
};
},
methods: {
rate(note) {
if (typeof note === 'number' && note <= this.maxNotes && note >= 0)
this.rating = this.rating === note ? note - 1 : note
this.$emit('onRate', this.itemId, this.rating);
}
}
};
You are modifing rating prop on the rate method, so to avoid the warning you should avoid to modify this prop, one easy solution although not very elegant is to make a local copy of your prop:
<template>
<div class="track-rating">
<span :key="note" v-for="note in maxNotes" :class="{ 'active': note <= copiedrating || note <= hoveredNote }" #mouseover="hoveredNote = note" #mouseleave="hoveredNote = false" #click="rate(note)" class="material-icons mr-1">
audiotrack
</span>
</div>
</template>
<script>
export default {
name: "Rating",
props: {
rating: {
type: Number,
required: true
},
maxNotes: {
type: Number,
default: 3
},
hasCounter: {
type: Boolean,
default: true
},
itemId: {
type: String
}
},
mounted () {
this.copiedrating = this.rating;
},
data() {
return {
hoveredNote: false,
copiedrating: null,
};
},
methods: {
rate(note) {
if (typeof note === 'number' && note <= this.maxNotes && note >= 0)
this.copiedrating = this.copiedrating === note ? note - 1 : note
this.$emit('onRate', this.itemId, this.copiedrating);
}
}
Don't modify this.rating directly, just send the event and make the change in parent component....
methods: {
rate(note) {
if (typeof note === 'number' && note <= this.maxNotes && note >= 0) {
const newRating = this.rating === note ? note - 1 : note
this.$emit('onRate', this.itemId, newRating);
}
}
}
...then in parent handle the event (I don't know how parent component or data looks like so this is more pseudo-code...)
<Rating :itemId="track.itemId" :rating="track.rating" #onRate="changeRating" />
methods:
changeRating: function(id, rating) {
let trackIndex = this.tracks.indexOf(track => track.itemId === id)
this.tracks[trackIndex].rating = rating
}

Vue.js - Watcher for property with variable keypath

Is it possible, in Vue v2, to define a watcher using a keypath containing a variable?
For example, depending on the currentKey, I want to watch either the changes in obj.A or obj.B:
data() {
return {
currentKey: 'A',
obj: { A: { 'a': '' }, B: { 'b' :'' },
}
},
watch: {
'obj[currentKey]'(newItem, oldItem) {}
}
You can make a computed property which returns this.obj[this.currentKey] and then set a watcher on that.
But, if you want to watch changes to the properties of the dynamic object, you'll also need to set the deep property of the watcher to true.
Here's a simple example:
new Vue({
el: '#app',
data () {
return {
currentKey: 'A',
obj: {A: {value: ''}, B: {value:''} },
}
},
computed: {
selected() {
return this.obj[this.currentKey];
}
},
watch: {
selected: {
deep: true,
handler(object) {
console.log('selected object value', object.value);
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.min.js"></script>
<div id="app">
Selected object: {{ selected }}
<select v-model="currentKey">
<option v-for="i in ['A', 'B']" :key="i" :value="i">{{ i }}</option>
</select>
<br><br>
Text for selected object: <input v-model="selected.value">
</div>

How can I make a reusable "CheckAll" checkbox solution in Vue2

I am trying to create a reusable "Check All" solution for displaying a list of objects retrieved from an API.
I really like the get/set methods of computed properties that I use in this example here, https://codepen.io/anon/pen/aLeLOZ but I find that rewriting the same function over and over again and maintaining a seperate checkbox state list is tedious.
index.html
<div id="app">
<input type="checkbox" v-model="selectAll1"> Check All
<div v-for="person in list1">
<input type="checkbox" v-model="checkbox" :value="person.id">
<span>{{ person.name }}</span>
</div>
<hr/>
<input type="checkbox" v-model="selectAll2"> Check All
<div v-for="person in list2">
<input type="checkbox" v-model="checkbox2" :value="person.id">
<span>{{ person.name }}</span>
</div>
</div>
main.js
new Vue({
el: '#app',
data () {
return {
list1: [
{ id: 1, name: 'Jenna1'},
{ id: 2, name: 'Jenna2'},
{ id: 3, name: 'Jenna3'},
{ id: 4, name: 'Jenna4'}
],
list2: [
{ id: 1, name: 'Mary1'},
{ id: 2, name: 'Mary2'},
{ id: 3, name: 'Mary3'},
{ id: 4, name: 'Mary4'}
],
checkbox: [],
checkbox2: []
}
},
computed: {
selectAll1: {
get: function () {
return this.list1 ? this.checkbox.length === this.list1.length : false
},
set: function (value) {
let selected = []
if (value) {
this.list1.forEach(function (bf) {
selected.push(bf.id)
})
}
this.checkbox = selected
}
},
selectAll2: {
get: function () {
return this.list2 ? this.checkbox2.length === this.list2.length : false
},
set: function (value) {
let selected = []
if (value) {
this.list2.forEach(function (bf) {
selected.push(bf.id)
})
}
this.checkbox2 = selected
}
},
}
});
How can I make a resuable selectAll() function that will work in this example that can be included as often as needed?
Is it possible to make a class that can maintain the check box state for each list and still function as a computed property to make use of the v-model directive?
It's not the same at all, but a method based solution would be
methods: {
selectUs: function(){
if (this.checkbox.length <= this.list1.length) {
this.checkbox = Array.from(Array(this.list1.length + 1).keys())
} else {
this.checkbox = []
}
}
}
with #click="selectUs" instead of v-model="selectAll1"
(you could keep the get part of your computed properties to keep track of whether all are selected, and then use if (selectAll1) { etc } in the method)

Vue : Limit characters in text area input, truncate filter?

<textarea name="" id="" cols="30" rows="10" v-model="$store.state.user.giftMessage | truncate 150"></textarea>
I tried creating a custom filter :
filters: {
truncate(text, stop, clamp) {
return text.slice(0, stop) + (stop < text.length ? clamp || '...' : '')
}
}
but that didn't broke the build when I put it on the v-model for the input...
Any advice?
This is one of those cases, where you really want to use a component.
Here is an example component that renders a textarea and limits the amount of text.
Please note: this is not a production ready, handle all the corner cases component. It is intended as an example.
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
This component implements v-model and only emits a change to the data if the length of the text is less than the specified max. It does this by listening to keydown and preventing the default action (typing a character) if the length of the text is equal to or more than the allowed max.
console.clear()
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
new Vue({
el: "#app",
data:{
text: ""
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<limited-textarea v-model="text"
:max="10"
cols="30"
rows="10">
</limited-textarea>
</div>
Another issue with the code in the question is Vuex will not allow you set a state value directly; you have to do it through a mutation. That said, there should be a Vuex mutation that accepts the new value and sets it, and the code should commit the mutation.
mutations: {
setGiftMessage(state, message) {
state.user.giftMessage = message
}
}
And in your Vue:
computed:{
giftMessage:{
get(){return this.$store.state.user.giftMessage},
set(v) {this.$store.commit("setGiftMessage", v)}
}
}
Technically the code should be using a getter to get the user (and it's giftMessage), but this should work. In the template you would use:
<limited-textarea cols="30" rows="10" v-model="giftMessage"></limited-textarea>
Here is a complete example using Vuex.
console.clear()
const store = new Vuex.Store({
state:{
user:{
giftMessage: "test"
}
},
getters:{
giftMessage(state){
return state.user.giftMessage
}
},
mutations:{
setGiftMessage(state, message){
state.user.giftMessage = message
}
}
})
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
new Vue({
el: "#app",
store,
computed:{
giftMessage:{
get(){ return this.$store.getters.giftMessage},
set(v){ this.$store.commit("setGiftMessage", v)}
}
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.4.0/vuex.js"></script>
<div id="app">
<limited-textarea v-model="giftMessage"
:max="10"
cols="30"
rows="10">
</limited-textarea>
Message: {{giftMessage}}
</div>
Sorry to break in. Was looking for a solution. Looked at all of them.
For me they look too complicated. I'm always looking for symplicity.
Therefor I like the answer of #Даниил Пронин. But it has the by #J. Rambo noted potential problem.
To stay as close as possible to the native html textelement. The solution I came up with is:
Vue Template
<textarea v-model="value" #input="assertMaxChars()">
JavaScript
let app = new Vue({
el: '#app',
data: {
value: 'Vue is working!',
maxLengthInCars: 25
},
methods: {
assertMaxChars: function () {
if (this.value.length >= this.maxLengthInCars) {
this.value = this.value.substring(0,this.maxLengthInCars);
}
}
}
})
Here is a REPL link: https://repl.it/#tsboh/LimitedCharsInTextarea
The upside I see is:
the element is as close as possible to the native element
simple code
textarea keeps focus
delete still works
works with pasting text as well
Anyway
happy coding
While I agree with the selected answer. You can also easily prevent the length using a keydown event handler.
Vue Template
<input type="text" #keydown="limit( $event, 'myModel', 3)" v-model="myModel" />
JavaScript
export default {
name: 'SomeComponent',
data () {
return {
myModel: ''
};
},
methods: {
limit( event, dataProp, limit ) {
if ( this[dataProp].length >= limit ) {
event.preventDefault();
}
}
}
}
Doing this way, you can also use regular expression to event prevent the type of keys accepted. For instance, if you only wanted to accept numeric values you can do the following.
methods: {
numeric( event, dataProp, limit ) {
if ( !/[0-9]/.test( event.key ) ) {
event.preventDefault();
}
}
}
I have improved on #J Ws answer. The resulting code does not have to define how to react on which keypress, which is why it can be used with any character in contrast to the accepted answer. It only cares about the string-length of the result. It also can handle Copy-Paste-actions and cuts overlong pastes to size:
Vue.component("limitedTextarea", {
props: {
value: {
type: String,
default: ""
},
max: {
type: Number,
default: 25
}
},
computed: {
internalValue: {
get: function () {
return this.value;
},
set: function (aModifiedValue) {
this.$emit("input", aModifiedValue.substring(0, this.max));
}
}
},
template: '<textarea v-model="internalValue" #keydown="$forceUpdate()" #paste="$forceUpdate()"></textarea>'
});
The magic lies in the #keydown and #paste-events, which force an update. As the value is already cut to size correctly, it assures that the internalValue is acting accordingly.
If you also want to protect the value from unchecked script-changes, you can add the following watcher:
watch: {
value: function(aOldValue){
if(this.value.length > this.max){
this.$emit("input", this.value.substring(0, this.max));
}
}
}
I just found a problem with this easy solution: If you set the cursor somewhere in the middle and type, transgressing the maximum, the last character is removed and the cursor set to the end of the text. So there is still some room for improvement...
My custom directive version. Simple to use.
<textarea v-model="input.textarea" v-max-length="10"></textarea>
Vue.directive('maxlength',{
bind: function(el, binding, vnode) {
el.dataset.maxLength = Number(binding.value);
var handler = function(e) {
if (e.target.value.length > el.dataset.maxLength) {
e.target.value = e.target.value.substring(0, el.dataset.maxLength);
var event = new Event('input', {
'bubbles': true,
'cancelable': true
});
this.dispatchEvent(event);
return;
}
};
el.addEventListener('input', handler);
},
update: function(el, binding, vnode) {
el.dataset.maxLength = Number(binding.value);
}
})
Event() has browser compatibility issue.
Unfortunately for me, keydown approach seems not working good with CJK.
there can be side effects since this method fires input event double time.
Best way is to use watch to string length and set old value if string is longer than you want:
watch: {
'inputModel': function(val, oldVal) {
if (val.length > 250) {
this.inputModel = oldVal
}
},
},
Simply use maxlength attribute like this:
<textarea v-model="value" maxlength="50" />
I used your code and broke it out into a .Vue component, thanks!
<template>
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
</template>
<script>
export default {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
evt.preventDefault();
console.log('keydown');
return
}
}
}
}
I did this using tailwind 1.9.6 and vue 2:
<div class="relative py-4">
<textarea
v-model="comment"
class="w-full border border-gray-400 h-16 bg-gray-300 p-2 rounded-lg text-xs"
placeholder="Write a comment"
:maxlength="100"
/>
<span class="absolute bottom-0 right-0 text-xs">
{{ comment.length }}/100
</span>
</div>
// script
data() {
return {
comment: ''
}
}

I can't set "disabled" parameter in Vue to be persistent

I can't set "disabled" parameter to be persistent. If I set disable: true inside data function, it seems that it doesn't do anything.
You can see inside mounted() that it calls checkCanVote() and in there at first the console.log says it is set to false, then I set it to true but on stars hover (star_over()) it is again false?
http://jsfiddle.net/7unqk49k/1/
Template
<div id="star-app" v-cloak class="col-md-4">
<star-rating value="<?php echo $this->rating_rounded; ?>"></star-rating>
</div>
<template id="template-star-rating">
<div class="star-rating">
<label class="star-rating__star" v-for="rating in ratings" :class="{'is-selected': ((value >= rating) && value != null), 'is-disabled': disabled}" #mouseover="star_over(rating)" #mouseout="star_out">
<input class="star-rating star-rating__checkbox" type="radio" :name="name" :disabled="disabled" :id="id" :required="required" v-model="value" #click="set(rating)"> ★
</label>
</div>
</template>
JS
Vue.component('star-rating', {
template: '#template-star-rating',
data: function data() {
return {
value: null,
temp_value: null,
ratings: [1, 2, 3, 4, 5],
disabled: true
};
},
props: {
'name': String,
'value': null,
'id': String,
'disabled': Boolean,
'required': Boolean
},
methods: {
star_over: function star_over(index) {
console.log(this.disabled);
if (this.disabled == true) {
return;
}
this.temp_value = this.value;
this.value = index;
},
star_out: function star_out() {
if (this.disabled == true) {
return;
}
this.value = this.temp_value;
},
set: function set(value) {
if (this.disabled == true) {
return;
}
this.temp_value = value;
this.value = value;
// On click disable - this works
this.disabled = true;
},
checkCanVote: function() {
console.log('Inside checkCanVote');
console.log(this.disabled);
this.disabled = true;
console.log(this.disabled);
}
},
mounted() {
this.checkCanVote();
}
});
new Vue({
el: '#star-app'
});
The actual problem is in naming things.Try to avoid to use same naming for the props that you pass to child component and data properties that you've declared in child component.
To get this working as expected, you should just remove the prop declaration for disabled prop and It should work.
http://jsfiddle.net/9debdkh1/