Toggle of TWO classes in Vuejs - vue.js

I have tried for a few hours to be able to add / remove TWO classes in Vue JS.
The doc examples only show how to toggle one.
I have a button, which I click that I want to change class to either: active toggle-on or toggle-off
I can do the "active" on and off, but I can not seem to be able to add/remove the second class.
The button looks like this:
<button v-on:click="toggleOnTop" id="toggleTopButton" v-bind:class="toggleActive"></button>
And my data:
data: () => ({
toggleActive: {
active: true,
'toggle-on': true
}
})
But it still only applies these two. How can I apply "toggle-off" in reverse?

You may want to use a computed property or object syntax for this, lets say your method toggles a boolean in your data:
data () {
return {
isActive: false
}
}
methods: {
toggleOnTop () {
this.isActive = !this.isActive
}
}
The short form would be to add the following class binding:
< ... v-bind:class="{'active toggle-on': isActive, 'toggle-off': !isActive}">
Another approach would be to use a computed property that will set the classes as you want:
computed: {
toggleActive () {
return {
'active': this.isActive,
'toggle-on': this.isActive,
'toggle-off': !this.isActive
}
}
}

Know too that you are able to pass a ternary operator to the class attribute when bound. For example:
<i :class="['fa', isHappy ? 'fa-smile' : 'fa-frown']"></i>
This just saves from having to repeatedly use the same boolean value in an object as suggested above, especially if you need to have multiple classes bound to each state - like so:
<i :class="['fa', isHappy ? 'fa-smile active' : 'fa-frown']"></i>

Related

Why it is hard to use vue-i18n in vue data() (why it is not reactive)

I am using vue-i18n in a vue project. And I found it really confusing when using some data in vue data with i18n. Then if I change locale, that data is not reactive. I tried to return that data from another computed data but anyways it is not reactive because i18n is written in data. *My situation - * I want to show table with dropdown(list of columns with checkbox) above it. When user checks a column it will be showed in table if unchecks it won't. It is working fine until I change locale. After changing locale table columns is not translated but dropdown items is reactively translated and my code won't work anymore. Here is some code to explain better: In my myTable.vue component I use bootstrap-vue table -
template in myTable.vue
<vs-dropdown vs-custom-content vs-trigger-click>
<b-link href.prevent class="card-header-action btn-setting" style="font-size: 1.4em">
<i class="fa fa-th"></i>
</b-link>
<vs-dropdown-menu class="columns-dropdown">
<visible-columns :default-fields="columns" #result="columnListener"></visible-columns>
</vs-dropdown-menu>
</vs-dropdown>
<b-table class="generalTableClass table-responsive" :fields="computedFieldsForTable">custom content goes here</b-table>
script in myTable.vue
data(){
return {
fieldsForTable: [];
}
},
computed: {
computedFieldsForTable () {
return this.fieldsForTable;
},
columns() {
return [
{
key: 'id',
label: this.$t('id'),,
visible: true,
changeable: true
},
{
key: 'fullName',
label: this.$t('full-name'),,
visible: true,
changeable: true
},
{
key: 'email',
label: this.$t('email'),,
visible: true,
changeable: true
}
]
}
},
mounted () {
this.fieldsForTable = this.filterColumns(this.columns);
},
methods: {
filterColumns(columns = []) {
return columns.filter(column => {
if (column.visible) {
return column
}
})
},
columnListener ($event) {
this.fieldsForTable = this.filterColumns($event)
}
}
Can someone give me some advice for this situation ?
*EDIT AFTER SOME DEBUGGING: I think when filtering columns(in computed) and returning it for fieldsForTable inside filterColumns(columns) method, it actually returning array(of objects) with label='Label Name' not label=this.$t('labelName'). So after filtering the new array has nothing to do with vue-i18n. My last chance is reloading the page when locale changes.
Trying modify computedFieldsForTable as follows. You need to reference this.columns in computedFieldsForTable, so that Vue can detect the change of labels in this.columns.
computedFieldsForTable () {
return this.filterColumns(this.columns);
},
EDITED: put your this.columns in data. Then
columnListener ($event) {
this.columns = $event;
}
I hope i didn't misunderstand what you mean.
EDITED (again):
Maybe this is the last chance that I think it can work. Put columns in computed() still and remove computedFieldsForTable. Finally, just leave fieldsForTable and bind it on fields of <b-table>.
watch: {
columns(val) {
this.fieldsForTable = this.filterColumns(val)
}
},
method: {
columnListener ($event) {
this.fieldsForTable = this.filterColumns($event)
}
}
However, I think it is better and easier to reload page whenever local change. Especially when your columns have a more complex data structure.

How to directly modify v-model value using directives?

I've got form with dynamic number of input fields and i need to transliterate data, passed to this fields in 'live'. I wrote custom directive which do all job, but there is an a error -> it converts all chars except last one (should be привет->privet, while привет->priveт). This is my source code
directives: {
transliterate: {
update(element, binding) {
element.value = tr(element.value)
}
}
}
This is PUG (Jade)
input(v-model='requestHotels.travellers[index].first_name', v-transliterate='true')
tr - just function, which transliterate from ru to en
I knew why this happening, but i can't solve it by myself. Any ideas?
1) Consider using computed property instead of directive. Personally, I don't like directives because they can add alot of useless complexity to your code. But there are some complex cases where they can be really useful. But this one is not one of them.
export default {
data: () => ({
tranliteratedValue: ""
}),
computed: {
vModelValue: {
get() {
return this.tranliteratedValue;
},
set(value) {
this.tranliteratedValue = transl.transform(value);
}
}
}
};
Full example: https://codesandbox.io/s/039vvo13yv?module=%2Fsrc%2Fcomponents%2FComputedProperty.vue
2) You can use filter and transliterate during render
filters: {
transliterate(value) {
return transl.transform(value);
}
}
Then in your template:
<p>{{ value | transliterate }}</p>
Full example: https://codesandbox.io/s/039vvo13yv?module=%2Fsrc%2Fcomponents%2FFilter.vue
3) Transparent wrapper technique (using custom component)
The idea behind transparent wrapper is that you should create custom component that behave as build-in input (and accepts the same arguments) but you can intercept events and change behaviour as you'd like. In your example - tranliterate input text.
<textarea
v-bind="$attrs"
:value="value"
v-on="listeners"
/>
computed: {
listeners() {
return {
...this.$listeners,
input: event => {
const value = transl.transform(event.target.value + "");
this.$emit("input", value);
}
};
}
}
Full example: https://codesandbox.io/s/039vvo13yv?module=%2Fsrc%2Fcomponents%2Finc%2FTransliteratedInput.vue
Read more about Transparent wrapper technique here https://github.com/chrisvfritz/7-secret-patterns/blob/master/slides-2018-03-03-spotlight-export.pdf
You can check all 3 working approaches here https://codesandbox.io/s/039vvo13yv

Access component instance from vue watcher

I'm working on a project, similar as a bill manager, so I want that the subtotal get recalculated every time that quantity or unit value change, I have tried and searched to accomplish this using watcher or computed properties, but I don't find the right approach, cause I need to access the whole scope of the element when another change, like this.
Model structure:
detail
quantity
unit value
subtotal (should be a computed or updated)
So I think I should be able of doing something like this:
Vue.component('item', {
template: '#item',
props: {
item: Object,
},
computed:{
total: function(){
return this.quantity*this.unit_value;
}
},
watch:{
'item.quantity':()=>{
this.subtotal = this.quantity*this.unit_value;
}
}
});
I have several components being read from a list
I merged the approach using watcher and computed in the same code to make it shorter.
The problem is that I haven't found a way to access the hole element from inside itself, anyone could pls explain the right way? thanks
You shouldn't use arrows functions there, use method declarations.
If you want to watch for a property of the item object, you'll have to watch for the item object itself, and additionally use the deep: true flag of the watcher.
Final detail, you are using several properties that are not declared in your data. Declare them, otherwise they will not be reactive, that is, the computed will not recalculate when they change.
See code:
Vue.component('item', {
template: '#item',
props: {
item: Object,
},
data() {
return {
subtotal: null, // added data properties
quantity: null,
unit_value: null
}
},
computed: {
total: function() {
return this.quantity * this.unit_value;
}
},
watch: {
item: { // watching for item now
deep: true, // using deep: true
handler() { // and NOT using arrow functions
this.subtotal = this.quantity * this.unit_value;
}
}
}
});

Vue 2 - change model in main instance from the component

I have the following component:
Vue.component('visible-filter', {
template: `
<span class="text-muted"
#mouseenter="changeClassMouseenter($event)"
#mouseout="changeClassMouseout($event)"
#click="countryTest(filter)"
#clicked="clicked = true"
v-model="clicked"
><slot></slot></span>
`,
props: ['filter', 'clicked'],
methods: {
changeClassMouseenter(event) {
console.log(this.clicked);
event.target.classList.remove('text-muted')
},
changeClassMouseout(event) {
event.target.classList.add('text-muted')
},
countryTest(filter) {
Event.$emit('clicked');
Event.$emit('country-filter', filter);
}
}
});
The Event in the main instance:
Event.$on('clicked', () => {
this.clicked = true;
})
The data in the main instance:
data: {
clicked: false
},
The thing I want to do:
When I click on the element, I want to set the clicked property to true (for that element), and for the rest of the elements I want to set it to false; Also I want to check if the clicked is true/false when the mouseenter/mouseout event is fired.
How can I achive this?
You should not change the parent from child.
To make communication between parent and child (and child->parent as well) you can/should set up the events.
Emit the event in child component. Nice examples here: https://v2.vuejs.org/v2/guide/components.html#Using-v-on-with-Custom-Events
Listen to event in your parent using <child-component #yourEventName='eventHandler(data)'>
Handle data in your parent, so add eventHandler(data) into your methods and do whatever you want with your data.

Using $refs in a computed property

How do I access $refs inside computed? It's always undefined the first time the computed property is run.
Going to answer my own question here, I couldn't find a satisfactory answer anywhere else. Sometimes you just need access to a dom element to make some calculations. Hopefully this is helpful to others.
I had to trick Vue to update the computed property once the component was mounted.
Vue.component('my-component', {
data(){
return {
isMounted: false
}
},
computed:{
property(){
if(!this.isMounted)
return;
// this.$refs is available
}
},
mounted(){
this.isMounted = true;
}
})
I think it is important to quote the Vue js guide:
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
It is therefore not something you're supposed to do, although you can always hack your way around it.
If you need the $refs after an v-if you could use the updated() hook.
<div v-if="myProp"></div>
updated() {
if (!this.myProp) return;
/// this.$refs is available
},
I just came with this same problem and realized that this is the type of situation that computed properties will not work.
According to the current documentation (https://v2.vuejs.org/v2/guide/computed.html):
"[...]Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed"
So, what (probably) happen in these situations is that finishing the mounted lifecycle of the component and setting the refs doesn't count as a reactive change on the dependencies of the computed property.
For example, in my case I have a button that need to be disabled when there is no selected row in my ref table.
So, this code will not work:
<button :disabled="!anySelected">Test</button>
computed: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
What you can do is replace the computed property to a method, and that should work properly:
<button :disabled="!anySelected()">Test</button>
methods: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
For others users like me that need just pass some data to prop, I used data instead of computed
Vue.component('my-component', {
data(){
return {
myProp: null
}
},
mounted(){
this.myProp= 'hello'
//$refs is available
// this.myProp is reactive, bind will work to property
}
})
Use property binding if you want. :disabled prop is reactive in this case
<button :disabled="$refs.email ? $refs.email.$v.$invalid : true">Login</button>
But to check two fields i found no other way as dummy method:
<button :disabled="$refs.password ? checkIsValid($refs.email.$v.$invalid, $refs.password.$v.$invalid) : true">
{{data.submitButton.value}}
</button>
methods: {
checkIsValid(email, password) {
return email || password;
}
}
I was in a similar situation and I fixed it with:
data: () => {
return {
foo: null,
}, // data
And then you watch the variable:
watch: {
foo: function() {
if(this.$refs)
this.myVideo = this.$refs.webcam.$el;
return null;
},
} // watch
Notice the if that evaluates the existence of this.$refs and when it changes you get your data.
What I did is to store the references into a data property. Then, I populate this data attribute in mounted event.
data() {
return {
childComps: [] // reference to child comps
}
},
methods: {
// method to populate the data array
getChildComponent() {
var listComps = [];
if (this.$refs && this.$refs.childComps) {
this.$refs.childComps.forEach(comp => {
listComps.push(comp);
});
}
return this.childComps = listComps;
}
},
mounted() {
// Populates only when it is mounted
this.getChildComponent();
},
computed: {
propBasedOnComps() {
var total = 0;
// reference not to $refs but to data childComps array
this.childComps.forEach(comp => {
total += comp.compPropOrMethod;
});
return total;
}
}
Another approach is to avoid $refs completely and just subscribe to events from the child component.
It requires an explicit setter in the child component, but it is reactive and not dependent on mount timing.
Parent component:
<script>
{
data() {
return {
childFoo: null,
}
}
}
</script>
<template>
<div>
<Child #foo="childFoo = $event" />
<!-- reacts to the child foo property -->
{{ childFoo }}
</div>
</template>
Child component:
{
data() {
const data = {
foo: null,
}
this.$emit('foo', data)
return data
},
emits: ['foo'],
methods: {
setFoo(foo) {
this.foo = foo
this.$emit('foo', foo)
}
}
}
<!-- template that calls setFoo e.g. on click -->