Vuejs: method with v-if not working - javascript

I think this might be a typo somewhere, but can't find the issue. I have this Vuejs template, that renders just fine if I remove the v-if verification. However when I use it, it does not render anything at all. Already placed a debugger both in return true, and return false, and the logic test returns true only once, as expected. Can anybody spot what am I doing wrong?
template: `
<div class="workbench container">
<ul class="collapsible popout" data-collapsible="expandable">
<collapsible-cards
v-for="tipo, index in tiposCollapsibles"
v-if="mostraApenasPerfilEspecificado(perfil, tipo)"
v-bind:key=index
v-bind:dados="tipo"
>
</collapsible-cards>
</ul>
</div>`,
mounted: function() {
for (key in this.tiposCollapsibles) {
if (this.tiposCollapsibles[key].perfisQuePodemVer.indexOf(this.perfil) >= 0) {
this.queryTeleconsultorias(key);
}
}
},
methods: {
mostraApenasPerfilEspecificado(perfil, tipo) {
tipo['perfisQuePodemVer'].forEach(function(value) {
if (perfil === value) {
return true;
}
});
return false;
},
...
Update: For anyone who is having the same problem, I ended up using a computed property, rather than a method itself. The v-if/-v-show behaviour to show/hide elements was moved to the computed property. In the end I was not sure if this was an issue with Vuejs. Here is the working code:
template: `
<div class="workbench container">
<ul class="collapsible popout" data-collapsible="expandable">
<collapsible-cards
v-if="showTipoCollapsibles[index]"
v-for="tipo, index in tiposCollapsibles"
v-bind:key="index"
v-bind:object="tipo"
>
</collapsible-cards>
</ul>
</div>`,
mounted: function() {
this.executeQuery(this.perfil);
},
computed: {
showTipoCollapsibles: function() {
let perfisVisiveis = {};
for (tipo in this.tiposCollapsibles) {
perfisVisiveis[tipo] = this.tiposCollapsibles[tipo].enabledForProfiles.includes(this.perfil);
}
return perfisVisiveis;
},
},
methods: {
executeQuery: function(value) {
if (value === 'monitor') {
var query = gql`query {
entrada(user: "${this.user}") {
id
chamadaOriginal {
datahoraAbertura
solicitante {
usuario {
nome
}
}
}
...

Change from v-if to v-show
v-show="mostraApenasPerfilEspecificado(perfil, tipo)"
You can also use template to use v-if outside child component as
template: `
<div class="workbench container">
<ul class="collapsible popout" data-collapsible="expandable">
<template v-for="(tipo, index) in tiposCollapsibles">
<collapsible-cards
v-if="mostraApenasPerfilEspecificado(perfil, tipo)"
v-bind:key="index"
v-bind:dados="tipo">
</collapsible-cards>
</template>
</ul>
</div>`,
If not work, share live demo

There appears to be a similar bug in Bootstrap-Vue, where v-if only works on non-Bootstrap elements.
With otherwise identical code, this element will not appear when this.error = true:
<b-alert v-if="error" variant="danger">Failed!</b-alert>
But this will:
<div v-if="error">Failed!</div>

Actually, you have to use computed rather than method.
computed: {
showTipoCollapsibles: function() {},
executeQuery: function(value) {},
},
methods: {}

Related

Vuejs - add different classes in elements generated with v-for

I need to add a class to a list group create using bootstrap 5 in my vuejs app. I know about class binding but in my case I'm not sure how to proceed. I want that when the user click on an item inside the list, the clicked item get the disabled active class and the other elements gets only the disabled class. At the moment I have this code in my template
<ul class="list-group list-group-flush">
<li class="list-group-item list-group-item-action" v-for="(choice, index) in item.choices" :key="index">
<small class="" #click.prevent="checkAnswer(item.questionIndex, index)">{{ index }}) {{ choice }}</small>
</li>
</ul>
The v-for loop will generate the elements and when an element is clicked a method is called to check the user choice. In my app script I have this code
export default {
name: 'Survey',
data() {
return {
n: 0,
answeredQuestions: [],
}
},
mounted() {
},
computed: {
questions() {
return this.$store.getters.survey;
},
},
methods: {
showNext() {
if( this.n < this.questions.length ){
this.n++
}
},
isAnswered(index) {
return this.n !== index ? 'hide' : '';
},
checkAnswer(questionIndex, choice) {
this.answeredQuestions.push(true);
this.showNext();
...
}
}
}
What's the best way to implement the needed class binding?
There's a lot of unknowns about the rest of your code (how the questions are handled and switched through, etc.), but here's a working example for a single question. So you'll have to adapt this for having multiple questions in your app, but it should push you in the right direction. I used an inline :style attribute in addition to the static styles already present on the <li>, but you could move that to a function as suggeted in Peter's answer, if you prefer.
const app = {
name: 'Survey',
data() {
return {
n: 0,
questions: [],
answeredQuestions: [],
item: {
questionIndex: 1,
choices: ['Lorem', 'Ipsum']
},
selectedChoice: null
}
},
mounted() {
},
computed: {
questions() {
return this.$store.getters.survey;
},
},
methods: {
showNext() {
if (this.n < this.questions.length) {
this.n++
}
},
isAnswered(index) {
return this.n !== index ? 'hide' : '';
},
checkAnswer(questionIndex, choice) {
this.answeredQuestions.push(choice);
this.showNext();
}
}
};
Vue.createApp(app).mount('#app');
<link href="https://cdn.jsdelivr.net/npm/bootstrap#5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
<script src="https://unpkg.com/vue#3.0.11/dist/vue.global.prod.js"></script>
<div id="app">
<ul class="list-group list-group-flush">
<li class="list-group-item list-group-item-action" :class="{disabled: answeredQuestions.length, active: answeredQuestions.includes(index)}" v-for="(choice, index) in item.choices" :key="index" #click.prevent="checkAnswer(item.questionIndex, index)">
<small class="">{{ index }}) {{ choice }}</small>
</li>
</ul>
</div>
If I understand correctly your situation and what you intend to do here I would suggest using the item in the checkAnswer method so that an identifier is used to set a computed property to the current item.questionIndex.
Then you bind the class of each element with a ternary operator condition to check the questionIndex and return the proper classes string: <small :class="questionIndex == item.questionIndex ? 'disabled active':'disabled'" ...
You search the internet for vue class binding and it's the first result that pops up:
https://v2.vuejs.org/v2/guide/class-and-style.html
You can use an plain object, object from your data, a function returning an object or simply a string. You can make any attribute dynamic with v-bind:, or simply :.
Your checkAnswer() function can cause a change in classes by manipulating something in data, for example.
See tutorial above for example code. Keep in mind v-bind:class is the same as :class.
The "best way" changes like every week in Vue, just find a way to do it and learn its advantages and disadvantages.
An example would be:
template: let a function generate the classes
<small
:class="getChoiceClasses(item, choice, index)"
#click.prevent="checkAnswer(item.questionIndex, index)"
>{{ index }}) {{ choice }}</small>
script: add method
getChoiceClasses(item, choice, index) {
let classes = {
active: choice == 1, // for example
disabled: false, // default
even: index % 2 == 0
};
if (whateverYouNeedToCheck) {
classes.disabled = true;
}
return classes;
}
A method is a little slower than a value from data, but it's very minor and only becomes a problem when you have 100s of calls.

Render component template on invoked methods

So while I'm learning vue, I wanted to double check if someone can show me what I'm doing wrong or lead me in the right answer. Below, I will show the code and then explain what I'm attempting to do.
Here is my Vue.js app:
Vue.component('o365_apps_notifications', {
template:
`
<div class="notification is-success is-light">
// Call the name here and if added/removed.
</div>
`,
});
new Vue({
name: 'o365-edit-modal',
el: '#o365-modal-edit',
components: 'o365_apps_notifications',
data() {
return {
list: {},
movable: true,
editable: true,
isDragging: false,
delayedDragging: false,
options: {
group: 'o365apps',
disabled: true,
handle: '.o365_app_handle',
}
}
},
methods: {
add(index, obj) {
console.log(obj.name);
this.$data.list.selected.push(...this.$data.list.available.splice(index, 1));
this.changed();
},
remove(index, obj) {
console.log(obj.name);
this.$data.list.available.push(...this.$data.list.selected.splice(index, 1));
this.changed();
},
checkMove(evt) {
console.log(evt.draggedContext.element.name);
},
},
});
Here is my modal:
<div id="o365-modal-edit" class="modal">
<div class="modal-background"></div>
<div class="modal-card px-4">
<header class="modal-card-head">
<p class="modal-card-title">Applications</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div id="o365-modal-edit-wrapper">
<div class="columns">
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<o365-apps-notifications></o365-apps-notifications>
</footer>
</div>
</div>
Here is what I'm attempting to do:
Inside my modal, I have my o365_apps_notifications html tag called, my add() and remove() methods output a name on each add/remove using console.log(obj.name); and my checkMove method also drags the same name on drag as shown below:
How could I get my component to render and output the name inside the modal footer? I've tried all methods, but I can't seem to figure out how to trigger the component.
Also, would I have to do something special to make the component fade out after a set timeframe?
All help is appreciated!
A couple issues:
You've declared the notification component with underscores (o365_apps_notifications), but used hyphens in the modal's template. They should be consistent (the convention is hyphens).
The notification component is declared globally (with Vue.component), but it looks like you're trying to add it to the modal's components, which is intended for local components. Only one registration is needed (the global component registration should do).
<o365-apps-notifications>
The notification component should have public props that take the item name and state:
Vue.component('o365-apps-notifications', {
props: {
item: String,
isAdded: Boolean
},
})
Then, its template could use data binding to display these props.
Vue.component('o365-apps-notifications', {
template:
`<div>
{{ item }} {{ isAdded ? 'added' : 'removed '}}
</div>`
})
For the fade transition, we want to conditionally render this data based on a local Boolean data property (e.g., named show):
Vue.component('o365-apps-notifications', {
template:
`<div v-if="show">
...
</div>`,
data() {
return {
show: false
}
}
})
...and add the <transition> element along with CSS to style the fade:
Vue.component('o365-apps-notifications', {
template:
`<transition name="fade">
<div v-if="show">
...
</div>
</transition>`,
})
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
To automatically fade out the data, add a watch on item, which sets show=true and then show=false after a delay:
Vue.component('o365-apps-notifications', {
watch: {
item(item) {
if (!item) {
return;
}
this.show = true;
clearTimeout(this._timer);
this._timer = setTimeout(() => this.show = false, 1000);
}
}
})
Usage
In the modal component, declare local data properties that hold the currently added/removed item:
new Vue({
el: '#o365-modal-edit',
data() {
return {
changedItem: null,
changedItemIsAdded: false,
}
},
})
Also update add() and remove() to set these properties:
new Vue({
methods: {
add(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = true;
},
remove(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = false;
},
},
})
Then in the modal component's template, bind these properties to the notification component's props:
<o365-apps-notifications :item="changedItem" :is-added="changedItemIsAdded"></o365-apps-notifications>
demo

Can you pass an external function to a Vue component as a prop?

I have a legacy codebase mostly built in PHP. I'm researching how to turn commonly used parts of the code into re-usable Vue components that can be plugged in as needed.
In one case, I have an onclick event in the html which will need to be individually passed to a child component. onclick="func()"
I want to be able to pass that func to the component from the markup, without defining this one-time use function as a property method either on the component or its parents.
I can't find anything in the Vue docs or elsewhere on how to do that. Every attempt I make gives an error:
Property or method "hi" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
Is there a way to pass externally-defined functions in the global scope to a Vue instance?
Vue tabs:
Vue.config.devtools = true;
Vue.component('tabs', {
template: `
<div class="qv-tabs">
<div class="tab">
<ul>
<li v-for="tab in tabs"
:class="{'is-active' : tab.isActive}"
#click="tab.callHandler"
>
<a :href="tab.href" #click="selectTab(tab)">{{tab.name}}</a>
</li>
</ul>
</div>
<div class="tab-content">
<slot></slot>
</div>
</div>
`,
data(){
return{
tabs: []
};
},
created(){
this.tabs = this.$children;
},
methods:{
selectTab(selectedTab){
this.tabs.forEach(tab => {
tab.isActive = (tab.name == selectedTab.name);
});
},
otherHi() {
alert('other hi');
}
}
});
Vue.component('tab', {
template: `
<div v-show="isActive">
<slot></slot>
</div>
`,
props: {
name: {required: true},
selected: {default: false},
callHandler: Function,
clickHandler: {
type: Function,
default: function() { console.log('default click handler') }
}
},
data(){
return{
isActive: false
}
},
methods: {
callHandler() {
console.log('call handler called');
this.clickHandler();
}
},
computed:{
href(){
return '#' + this.name.toLowerCase().replace(/ /g, '-');
}
},
mounted(){
this.isActive = this.selected;
}
});
new Vue({
el: '.app',
methods: {
vueHi() { alert('hi from vue'); }
}
});
function hi() {
alert('hi!');
}
Markup:
<div class="app">
<tabs>
<tab name="Tab 1" :selected="true" v-bind:call-handler="hi">
<p>Tab content</p>
</tab>
<tab name="Tab 2">
<p>Different content for Tab 2</p>
</tab>
</tabs>
</div>
You could define your methods like this in your component :
...
methods: {
hi
}
...
But you will have to define it in every component where you need this function. Maybe you can use a mixin that define the methods you want to access from yours components, and use this mixins in these components ? https://v2.vuejs.org/v2/guide/mixins.html
Anoter solution ( depending on what you try to achieve ) is to add your method to the Vue prototype like explained here :
https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html
Vue.prototype.$reverseText = function(string) {
return string.split('')
.reverse()
.join('')
}
With this method defined in the Vue prototype, you can use the reverseText method like this in all of your components template :
...
<div> {{ $reverseText('hello') }} </div>
...
Or from yours script with this :
methods: {
sayReverseHello() {
this.$reverseText('hello')
}
}

Component Autocomplete VueJS

I want to create an autocomplete component, so I have the following code.
<Input v-model="form.autocomplete.input" #on-keyup="autocomplete" />
<ul>
<li #click="selected(item, $event)" v-for="item in form.autocomplete.res">
{{item.title}}
</li>
</ul>
autocomplete(e){
const event = e.path[2].childNodes[4]
if(this.form.autocomplete.input.length > 2){
this.Base.get('http://localhost:3080/post/search/post', {
params: {
q: this.form.autocomplete.input
}
})
.then(res => {
this.form.autocomplete.res = res.data
if(this.form.autocomplete.res.length > 0)
event.style.display = 'block'
})
}
},
selected(item, e){
this.form.autocomplete.item = item
console.log(e)
}
however, how would I do to have the return after selecting my item in the main file?
Ex:
Home.vue
<Autocomplete :url="www.url.com/test" />
When selecting the item I want from my autocomplete, how do I get the return from it and store it in that file, as if I were using v-model?
As Vue Guide said:
Although a bit magical, v-model is essentially syntax sugar for
updating data on user input events, plus special care for some edge
cases.
Then at Vue Using v-model in Components,
the <input> inside the component must:
Bind the value attribute to a value prop
On input, emit its own custom input event with the new value
Then follow above guide, one simple demo will be like below:
Vue.config.productionTip = false
Vue.component('child', {
template: `<div>
<input :value="value" #input="autocomplete($event)"/>
<ul v-show="dict && dict.length > 0">
<li #click="selected(item)" v-for="item in dict">
{{item.title}}
</li>
</ul>
</div>`,
props: ['value'],
data() {
return {
dict: [],
timeoutControl: null
}
},
methods: {
autocomplete(e){
/*
const event = e.path[2].childNodes[4]
if(this.form.autocomplete.input.length > 2){
this.Base.get('http://localhost:3080/post/search/post', {
params: {
q: this.form.autocomplete.input
}
})
.then(res => {
this.form.autocomplete.res = res.data
if(this.form.autocomplete.res.length > 0)
event.style.display = 'block'
})
}*/
clearTimeout(this.timeoutControl)
this.timeoutControl = setTimeout(()=>{
this.dict = [{title:'abc'}, {title:'cde'}]
}, 1000)
this.$emit('input', e.target.value)
},
selected(item){
this.selectedValue = item.title
this.$emit('input', item.title)
}
}
})
new Vue({
el: '#app',
data() {
return {
testArray: ['', '']
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<child v-model="testArray[0]"></child>
<child v-model="testArray[1]"></child>
<div>Output: {{testArray}}</div>
</div>

Vue bind multiple v-models to elements in v-for loop

I have a v-for loop that creates sections, all of which have a switch component that is set to ON. It is set to on because it is part of this group. The idea is that when one gets switched to OFF it will make an API call to set the new state and be removed from the group.
The trouble that I'm having is that right now I'm binding the switch with v-model and a computed property and when the sections get built they are connected to the SAME property. So if one gets switched to OFF, they all do. I am not sure how to separate these so that when one is clicked it only affects that one. I will also need data associated with the switch that is clicked to make the API call. PS, a click method on the switch element DOES NOT WORK.
HTML
<div class="col-md-6 col-sm-12" v-for="person in people">
<switcher size="lg" color="green" open-name="ON" close-name="OFF" v-model="toggle"></switcher>
</div>
VUE
computed: {
people() { return this.$store.getters.peopleMonitoring },
toggle: {
get() {
return true;
},
set() {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
});
}
}
}
}
You can change your toggle to an array:
computed: {
people() { return this.$store.getters.peopleMonitoring },
toggle: {
get() {
return Array(this.people.length).fill(true);
},
set() {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
});
}
}
}
}
And your HTML:
<div class="col-md-6 col-sm-12" v-for="(person, index) in people">
<switcher size="lg" color="green" open-name="ON" close-name="OFF" v-model="toggle[index]"></switcher>
</div>

Categories

Resources