Syntax for variable class name (or within a v-if) - vue.js

I have a loop and I'm creating many different buttons. In my data property, I have boolean values that I want to use to control whether to display an icon or a spinning loader.
I was thinking of using either a v-if or bind a class so that I apply css. I know the v-if and :class below are incorrect but I just want to show the 2 different approaches I have so far. What is the correct syntax for either of the approaches?
<button
v-for="action in actionTypes"
<div
class="btn btn-shell-grey">
<component
:is="`icon-${ action }`"
v-if="[`${ action }LoadingComplete`]"
:class="{ hidden: ![ action + 'LoadingComplete']}>
</component>
<icon-spin
v-if="![`${ action }LoadingComplete`]"
:class="{ hidden: ![ action + 'LoadingComplete']}>
</icon-spin>
</div>
</button>
data(){
return {
saveLoadingComplete: true,
uploadLoadingComplete: true
}
}

You're very close to having the correct syntax in the snippet you've provided.
To access your data via a dynamically constructed property name in your HTML, use $data, which proxies access to the data, like so:
v-if="$data[`${action}LoadingComplete`]"
And here's a basic working example of this in action.

I never had the need to use css to hide components in vue. I Always use conditional renders(https://v2.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show). In your case it should look something like this.
<button
v-for="action in actionTypes"
<div
class="btn btn-shell-grey">
<component
:is="`icon-${ action }`"
v-if="action.loadingComplete"
</component>
<icon-spin
v-if="!action.loadingComplete"
</icon-spin>
</div>
</button>

Related

b-field value is getting updated only #select and not #input?

I am using the Buefy UI components in my VueJS project. I have a drop-down in a page:
<b-field label="Business Unit">
<b-autocomplete
:data="dataBusinessUnit"
placeholder="select a business unit..."
field="businessUnit"
:loading="isFetching"
:value="this.objectData.businessUnit"
#typing="getAsyncDataBusinessUnit"
#select="(option) => {updateValue(option.id,'businessUnit')}"
>
<template slot-scope="props">
<div class="container">
<p>
<b>ID:</b>
{{props.option.id}}
</p>
<p>
<b>Description:</b>
{{props.option.description}}
</p>
</div>
</template>
<template slot="empty">No results found</template>
</b-autocomplete>
</b-field>
As you can see from the above code, the updateValue function is responsible for updating the value, but it will currently be called only when the user selects something from the drop-down suggestions. I want the value to be updated even when the user starts to type something. Example: #input="(newValue)=>{updateValue(newValue,'businessUnit')}". However, there is already a debounce function called getAsyncDataBusinessUnit that I am calling to fetch the filtered autocomplete results based on what the user has typed during the #typing event.
According to the Buefy Autocomplete API documentation found here, you could probably use v-model instead of using value directly.
Alternatively you could actually implement the #input like you wrote yourself, the #typing event shouldn't interfere with it.
Or you could just handle the value updating in #typing:
#typing="onTyping"
// then later in JS...
methods: {
onTyping(value) {
this.updateValue(value, 'businessUnit')
this.getAsyncDataBusinessUnit(value)
},
}

is special attribute vs v-if / v-show

is special attribute is:
<!-- Component changes when currentTabComponent changes -->
<component v-bind:is="currentTabComponent"></component>
conditional rendering is:
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
I know the different between using v-if and v-show, but I don't know the difference between using a list of v-ifs for different cases vs using the is special attribute. When should I use them?
Does is work like v-if or v-show? I mean, does it render all the components anyways? Is is like a syntactic sugar for a list of subsequent v-ifs?
is would be useful if the list of v-ifs would all render a component if true.
Like so:
<template v-if="component == "firstComponent">
<first-component></first-component>
<template v-else-if="component == "secondComponent">
<second-component></second-component>
</template>
<template v-else-if="component == "thirdComponent">
<third-component></third-component>
</template>
This can then be reduced to:
<component :is="component"></component>
Concerning your second question
Wether component :is works like v-if or v-show depends on wether you wrap it in <keep-alive> or not. Read the documentation on this here. Note though, that using <component> a component only gets created until it is necessary the first time, wether you use <keep-alive> or not.
So:
v-if (re)creates the component everytime the condition is met.
<component :is="..."> creates the component everytime the condition is met (like v-if).
<keep-alive><component :is="..."></keep-alive> creates the
component at most 1 time (but possibly 0).
A component with only v-show on it is created exactly once.

Conditional wrapper rendering in vue

I'm making a link/button component which either can have a button or an anchor wrapper, a text and an optional icon. My template code below is currently rendering either an anchor or a button (with the exact same content) based on an if statement on the wrapper element, resulting in duplicate code.
<template>
<a v-if="link" v-bind:href="url" class="btn" :class="modifier" :id="id" role="button" :disabled="disabled">
{{buttonText}}
<svg class="icon" v-if="icon" :class="iconModifier">
<use v-bind="{ 'xlink:href':'#sprite-' + icon }"></use>
</svg>
</a>
<button v-else type="button" class="btn" :class="modifier" :id="id" :disabled="disabled">
{{buttonText}}
<svg class="icon" v-if="icon" :class="iconModifier">
<use v-bind="{ 'xlink:href':'#sprite-' + icon }"></use>
</svg>
</button>
</template>
Is there a more clean way for wrapping my buttonText and icon inside either an anchor or button?
I've solved my issue by intensive Google-ing! Found this issue regarding Vue on Github which pointed me in the right direction.
Small piece of backstory
I'm using Vue in combination with Storybook to build a component library in which a button can either be a button or an anchor. All buttons look alike (apart from color) and can be used for submitting or linking. To keep my folder structure ordered, I would like a solution that generates a multiple buttons types (with or without link) from one single file.
Solution
Using computed properties I'm able to "calculate" the necessary tag, based on the url property of my component. When a url is passed, I know that my button has to link to another page. If there is no url property it should submit something or preform a custom click handler (not in the sample code below).
I've created the returnComponentTag computed property to avoid placing any complex or bulky logic (like my original solution) in my template. This returns either an a or a button tag based on the existence of the url property.
Next, as suggested by ajobi, using the :is attribute I'm able to define the component tag based on the result of my computed property. Below a stripped sample of my final (and working) solution:
<template>
<component :is="returnComponentTag" v-bind:href="url ? url : ''" class="btn" :class="modifier" :id="id">
{{buttonText}}
</component>
</template>
<script>
export default {
name: "Button",
props: {
id: {
type: Number
},
buttonText: {
type: String,
required: true,
default: "Button"
},
modifier: {
type: String,
default: "btn-cta-01"
},
url: {
type: String,
default: ""
}
},
computed: {
returnComponentTag() {
return this.url ? "a" : "button"
}
}
};
</script>
You could extract the wrapping element into a dedicated component.
<template>
<a v-if="link" v-bind:href="url" class="btn" :class="modifier" :id="id" role="button" :disabled="disabled">
<slot></slot>
</a>
<button v-else type="button" class="btn" :class="modifier" :id="id" :disabled="disabled">
<slot></slot>
</button>
</template>
// You would use it like this
<SomeComponent /* your props here */ >
{{buttonText}}
<svg class="icon" v-if="icon" :class="iconModifier">
<use v-bind="{ 'xlink:href':'#sprite-' + icon }"></use>
</svg>
</SomeComponent>
There are multiple ways of doing this. Two examples would be the following based on the point of view:
You are defining two different components (Button or Anchor) and want to use a wrapper to render either one of them.
You could seperate the Wrapper Content into two components so that the wrapper only decides on which of the components to render (either the Button or the Anchor).
The problem with this approach could be you will have doubled code for methods and styling for the button and anchor component.
You are defining the content as a component and use the wrapper to define what to wrap the content in.
See Answer of https://stackoverflow.com/a/60052780/11930769
It would be great to know, why you would want to achive this. Maybe there are better solutions for your usecase. Cheers!

Attach multiple v-models to the loop of dynamic child components in vuejs 2

Recently I ran into a problem, where I had to display many different components inside a loop, but: each of them should share it's state with parent (kinda knockout.js style). I was digging thru the docs where clearly was pointed out, that Vue.js pass properties one way down to childs, and those can eventually speak with some events. Also, docs says that there can be only one v-model per component, so finally I came up with something like this:
<li :is="field.type" v-for="(field, i) in fields" :key="i" :title="field.title" v-on:title-change="title = $event" :somevalue="field.somevalue" v-on:somevalue-change="somevalue = $event"></li>
And so on... Yet, after fifth parameter I quickly realized that the code is basically messy. Is there some less messy way to attach multiple two-way data bindings to child components?
The solution happened to be a .sync method and proper naming of events. While sync has been deprecated and removed in vue.js 2, since 2.3 version has been rewritten and added again in some similar form. As in fact it's only a syntatic sugar, my component now looks more decent I believe.
<ol>
<li :is="field.type"
v-for="(field, i) in fields"
:key="field.id"
v-bind.sync="field"
v-on:remove="fields.splice(i, 1)"></li>
</ol>
Vue.component('bool', {
template: '\
<li>\
<input type="text" v-bind:value="title" #input="$emit(\'update:title\', $event.target.value)">\
<button v-bind:value="value" #click="$emit(\'update:value\', !$event.target.value)" :class="{active: value}">{{value}}</button>\
<input type="checkbox" value="1" v-bind:checked="istrue" #change="$emit(\'update:istrue\', $event.target.checked)">\
<button #click="$emit(\'remove\')">Remove</button>\
</li>\
',
data () {
return {}
},
props: ['title', 'value', 'availablevalues']
})

Vue v-model not reactive with BS4 radio button group

I'm hoping I'm just missing something simple because I've been looking at this for too long, but I'm stumped.
I have a form with inputs bound to vuejs. I have a group of 2 radio buttons for selecting the "gender", and the binding is working perfectly. If I click on either of the radio buttons, I can see the data change in the vue component inspector.
But I'm trying to change the radio buttons to a Bootstrap 4 button group, and can't seem to get the v-model binding to work. No matter what I try, the gender_id in my vue data is not getting updated when I click either of the buttons in the button group.
The form input values are being fed in through vue component properties, but for simplicity, my data for the radio buttons/button group would look like this:
export default {
data() {
return {
genders: {
1: "Men's",
2: "Women's"
},
gender_id: {
type: Number,
default: null
}
}
}
}
Here is the code I have for the radio button version (which is working properly):
<div class="form-group">
<label>Gender:</label>
<div>
<div class="form-check form-check-inline" v-for="(gender, key) in genders" :key="key">
<input type="radio"
class="form-check-input"
name="gender_id"
:id="'gender_' + key"
:value="key"
v-model.number="gender_id">
<label class="form-check-label" :for="'gender_' + key">
{{ gender }}
</label>
</div>
</div>
</div>
Here is the button group version that is not properly binding to the gender_id data in vue.
<div class="form-group">
<label>Gender:</label>
<div>
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-outline-secondary" v-for="(gender, key) in genders" :key="key">
<input type="radio"
class="btn-group-toggle"
name="gender_id"
:id="'gender_' + key"
:value="key"
autocomplete="off"
v-model.number="gender_id">
{{ gender }}
</label>
</div>
</div>
</div>
I've been using the following Boostrap 4 documentation to try to get this working.
https://getbootstrap.com/docs/4.0/components/buttons/#checkbox-and-radio-buttons
In the documentation for button groups they don't even include the value property of the radio inputs, whereas they do include it in the documentation for form radio buttons.
https://getbootstrap.com/docs/4.0/components/forms/#checkboxes-and-radios
Is this for simplicity or do button groups of radio buttons not even return the value of the checked button?
I see other threads stating that buttons groups are not meant to function as radio buttons, but if that's true for BS4, then why would Bootstrap have button groups with radio buttons as they do in their documentation referenced above? If you can't retrieve the checked state, then why not just use a <button> instead of <label><input type=radio></label>?
Any ideas as to what I'm doing wrong and/or not understanding correctly?
Thanks so much!
Thanks so much to #ebbishop for his helpful insights.
The issue was related to vue and bootstrap both trying to apply javascript to the buttons in the button group.
To get around this issue, it was as simple as removing data-toggle="buttons" from the button group. By removing the data-toggle attribute, the bootstrap js is not applied and vue can manage the button group.
Nothing is actually wrong your use of v-model here.
However: you must add the class "active" to the <label> that wraps each radio-button <input>.
See this fiddle for a working example.
Is that what you're after?