Vue.js + Element UI + el-popover - dynamically changing trigger doesn't work - vue.js

I want to change in runtime how my popover is opening (from 'hover' to 'click'). I added code similar to the below:
<el-popover
placement="top-start"
width="200"
:trigger="someCondition ? 'hover' : 'click'"
:title="title"
:content="content">
<el-button slot="reference">Button</el-button>
</el-popover>
The title and content successfully changes dynamically in runtime but the trigger stays with the initial value even if the condition changes.
What am I doing wrong?
PS - I am pretty new with vue.js (but very experienced with programming and other web frameworks - e.g. React).
Thanks!

Use the key modifier on the el-popover as the element should be recreated
The key special attribute is primarily used as a hint for Vue’s
virtual DOM algorithm to identify VNodes when diffing the new list of
nodes against the old list. Without keys, Vue uses an algorithm that
minimizes element movement and tries to patch/reuse elements of the
same type in-place as much as possible.
HTML:
<el-popover
:key="trigger"
placement="top-start"
title="Title"
width="200"
:trigger="trigger"
content="this is content, this is content, this is content">
<el-button slot="reference">Hover to activate</el-button>
</el-popover>
<el-button #click="changebehavior">Change behavior</el-button>
JS:
data() {
return {
visible: false,
trigger: 'hover'
};
},
methods: {
changebehavior() {
this.trigger = 'hover' == this.trigger
? 'click'
: 'hover'
}
}

Using key modifier
Working example
var Main = {
data() {
return {
visible: false,
title: 'Default title',
content: 'Default content',
trigger: 'click',
};
},
methods: {
changeToClick() {
this.title= 'Click title';
this.content= 'Click content will be here';
this.trigger = 'click';
},
changeToHover() {
this.title= 'Hover title';
this.content= 'Hover content will be here';
this.trigger = 'hover';
},
}
};
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
#import url("//unpkg.com/element-ui#2.13.2/lib/theme-chalk/index.css");
<script src="//unpkg.com/vue/dist/vue.js"></script>
<script src="//unpkg.com/element-ui#2.13.2/lib/index.js"></script>
<div id="app">
<template>
<el-popover
:key="trigger"
placement="top-start"
:trigger="trigger"
width="200"
:title="title"
:content="content">
<el-button slot="reference">Button</el-button>
</el-popover>
<el-button type="text" #click='changeToClick'>Change To Click</el-button>
<el-button type="text" #click='changeToHover'>Change To hover</el-button>
</template>
</div>

Related

Bootstrap Popover Content not Reactive Vue2

I am trying to create a component for a popover using Bootstrap4 in Vue:
<template>
<div>
<div :id="uid + '_Content'" class="d-none">
<slot name="content">
</slot>
</div>
<div :class="'call' + uid">
<slot name="caller">
</slot>
</div>
</div>
</template>
<script>
module.exports = {
data() {
return {
uid: this.genUID()
}
},
props: {
title: {
type: String,
required: false,
default: ''
}
},
mounted() {
_this = this;
// PopOver Definition
const popover = new bootstrap.Popover(document.querySelector('.call' + this.uid), {
container: 'body',
title: this.title,
html: true,
placement: 'auto',
sanitize: false,
customClass: 'noselect',
content: () => {
return document.querySelector("#" + _this.uid + "_Content").innerHTML;
}
});
},
methods: {
genUID() {
return "Popover_" + Math.random().toString(16).slice(2);
}
}
}
</script>
However, when passing content to <slot name="content"> from another component, the data is not reactive. Is there any config information that I am missing to make it reactive? Is this even possible with Vue and (regular) Bootstrap (not Bootstrap-Vue).
You're losing reactivity because your content option to bootstrap.Popover is returning a string of your element's HTML, not the element itself. The popover just copies the HTML as it exists when it is opened. If you pass the element, Bootstrap will reparent the element itself into the popover, so changes to the element's children should be reflected. (Note that this could still be disrupted by a virtual DOM change that rewrote the element itself, which is why Bootstrap-Vue would still be better here.) If the popover might be reused, you'll need to reparent the element back into your component's own tree each time the popover is closed. You'll also need to make provision for the _Content element to only be hidden while it isn't reparented.
Here's how it all would look:
<template>
<div ref="container" class="Popover__Container">
<div ref="content" class="Popover__Content">
<slot name="content">
</slot>
</div>
<div ref="caller" class="Popover__Caller">
<slot name="caller">
</slot>
</div>
</div>
</template>
<script>
module.exports = {
props: {
title: {
type: String,
required: false,
default: ''
}
},
mounted() {
const content = this.$refs.content;
// PopOver Definition
const popover = new bootstrap.Popover(this.$refs.caller, {
container: 'body',
title: this.title,
html: true,
placement: 'auto',
sanitize: false,
customClass: 'noselect',
content: content
});
$(this.$refs.caller).on('hidden.bs.popover', () =>
{
this.$refs.container.prepend(content);
});
}
}
</script>
<style scoped>
.Popover__Container > .Popover__Content {
display: none;
}
</style>
(I've also replaced the UID approach with refs, since that is a more Vue-like approach.)

vuejs semantic ui - drop down not displaying on arrow click

I'm faced with an issue where my semantic drop down in my vue project won't activate when clicking on the arrow icon but works when I click on the rest of the element. The drop down also works when I set the dropdown to activate on hover, but just not on click. Solutions I've tried:
tested if the dynamic id are at fault
tested if the back ticks are confusing things
placed the values directly into the semantic drop down
Aside from the dropdown not activating, the code below works as intended and brings back the selected value to the parent component and can be displayed.
Dropdown.vue:
<template>
<div class="ui selection dropdown" :id="`drop_${dropDownId}`">
<input type="hidden" name="gender" v-model="selected">
<i class="dropdown icon"></i>
<div class="default text">Gender</div>
<div class="menu">
<div class="item" v-for="option in options" v-bind:data-value="option.value">
{{ option.text }}
</div>
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
selected: {}
}
},
watch: {
selected: function (){
this.$emit("dropDownChanged", this.selected)
}
},
props: {
options: Array, //[{text, value}]
dropDownId: String
},
mounted () {
let vm = this;
$(`#drop_${vm.dropDownId}`).dropdown({
onChange: function (value, text, $selectedItem) {
vm.selected = value;
},
forceSelection: false,
selectOnKeydown: false,
showOnFocus: false,
on: "click"
});
}
}
</script>
The component usage:
<vue-drop-down :options="dropDownOptions" dropDownId="drop1" #dropDownChanged="dropDownSelectedValue = $event"></vue-drop-down>
The data in the parent:
dropDownOptions: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
],
dropDownSelectedValue: ""
Here is a fiddle of the above but simplified to use a flatter project. However the problem doesn't reproduce :(
https://jsfiddle.net/eywraw8t/210520/
I'm not sure what is causing your issue (as the examples on the Semantic Ui website look similar), but there is a workaround. For you arrow icon:
<i #click="toggleDropDownVisibility" class="dropdown icon"></i>
And then in the methods section of your Vue component:
methods: {
toggleDropDownVisibility () {
$(`#drop_${this.dropDownId}`)
.dropdown('toggle');
}
},

Vue: How to switch between displaying input and label with v-if

I need to be able to switch between an input field and a label. When the button "Add Location" is clicked (which create a new div), the input field must be visible. But when the div "Expandable" is maximized it must be hidden and the label visible instead!
The input field should only be visible right after the mentioned button is clicked, else the label has to take its place. What is the best way to achieve this? I was thinking about using some sort of toggle since I am using that in other places.
The label and the input field is placed in the div class "switch".
You can also see the code in this jsFiddle!
Html
<div id="lotsOfDivs">
<addingdivs></addingdivs>
</div>
Vue
var gate = 0;
Vue.component('addingdivs', {
template: `
<div>
<div id="header">
<button class="addDiv" type="button" #click="createDiv">ADD LOCATION</button>
</div>
<div class="parent" v-for="div in divs" :style=" div.height ? { 'height': div.height }: null">
<div class="big" v-if="div.expanded" :key="'expanded' + div.id">
<div class="switch">
<input type="text" v-if="inputFieldInfo">
<label class="propertyLabel" v-else>
<div class="firstChild">
<button class="done" #click="increaseLimit">INCREASE</button>
</div>
<div class="secondChild">
<button class="done" #click="expand(div)">EXPAND</button>
</div>
</div>
<div class="small" v-else :key="'collapsed' + div.id">
<button class="done" #click="expand(div)">EXPAND</button>
</div>
</div>
</div>
`,
data: function() {
return {
gate: gate,
height: "",
count: 0,
locationsArr: ["one", "two", "three"],
divs: [],
InputFieldInfo: false
}
},
methods: {
expand: function(div) {
if (div.expanded) {
div.expanded = false
this.height = ''
} else {
div.expanded = true
this.height = '7vh'
}
},
createDiv: function() {
if (this.count <= gate) { // Here you can decide how many divs that will be generated
// this.count++;
this.divs.push({
id: this.count,
expanded: true,
inputFieldInfo: true,
height: '',
});
this.count++
}},
increaseLimit: function() {
// Here you can increase the number of divs that it's possible to generate
gate++;
}
}
});
new Vue({
el: '#lotsOfDivs',
});
The template had a few compilation errors:
The <label> needs a closing tag (and text content to be useful)
The <div class="big"> needs a closing tag
The v-if was bound to inputFieldInfo, but that variable was declared as InputFieldInfo (note the uppercase I), but based on your behavior description, this field should be unique per location container, so a single data property like this wouldn't work (if I understood your description correctly).
Each location container should have a variable to contain the location name (e.g., locationName) and another variable to contain the show/hide Boolean for the <input> and <label> (i.e., inputFieldInfo):
createDiv: function() {
this.divs.push({
// ...
inputFieldInfo: true,
locationName: ''
});
}
Then, we could bind div.inputFieldInfo and div.locationName to the <input>. We bind to v-model so that the user's text is automatically reflected to the div.locationName variable:
<input v-if="div.inputFieldInfo" v-model="div.locationName">
The <label>'s content should be div.locationName so that it contains the text from the <input> when shown:
<label class="propertyLabel" v-else>{{div.locationName}}</label>
To switch the <input> with the <label> when the expand-button is clicked, we update expand() to set div.inputFieldInfo to false but only when div.locationName is not empty (this gives the user a chance to revisit/re-expand the container to fill in the location later if needed):
expand: function(div) {
if (div.expanded) {
div.expanded = false
if (div.locationName) {
div.inputFieldInfo = false
}
// ...
updated jsfiddle
You had some missing closing tags and an error with InputFieldInfo, it should have a lowercase i.
var gate = 0;
Vue.component('addingdivs', {
template: `
<div>
<div id="header">
<button class="addDiv" type="button" #click="createDiv">ADD LOCATION</button>
</div>
<div class="parent" v-for="div in divs" :style=" div.height ? { 'height': div.height }: null">
<div class="big" v-if="div.expanded" :key="'expanded' + div.id">
<div class="switch">
<input type="text" v-if="inputFieldInfo">
<label class="propertyLabel" v-else>Label</label>
<div class="firstChild">
<button class="done" #click="increaseLimit">INCREASE</button>
</div>
<div class="secondChild">
<button class="done" #click="expand(div)">EXPAND</button>
</div>
</div>
</div>
<div class="small" v-else :key="'collapsed' + div.id">
<button class="done" #click="expand(div)">EXPAND</button>
</div>
</div>
</div>
`,
data: function() {
return {
gate: gate,
height: "",
count: 0,
locationsArr: ["one", "two", "three"],
divs: [],
inputFieldInfo: true
}
},
methods: {
expand: function(div) {
this.inputFieldInfo = false
if (div.expanded) {
div.expanded = false
this.height = ''
} else {
div.expanded = true
this.height = '7vh'
}
},
createDiv: function() {
this.inputFieldInfo = true
if (this.count <= gate) { // Here you can decide how many divs that will be generated
// this.count++;
this.divs.push({
id: this.count,
expanded: true,
inputFieldInfo: true,
height: '',
});
this.count++
}
},
increaseLimit: function() {
// Here you can increase the number of divs that it's possible to generate
gate++;
}
}
});
new Vue({
el: '#lotsOfDivs',
});
You just basically toggle the inputFieldInfo data, whenever each button is pressed.
You can do that by using toggle variable like this
Vue.component('addingdivs', {
template: `
<div>
<div>
<input type="text" v-if="takeinput">
<label v-if="!takeinput">
<button #click="toggleInput()">
</div>
</div>
`,
data: function() {
return {
takeinput:true,
}
},
methods: {
toggleInput: function(){
let vm = this;
vm.takeinput = ( vm.takeinput == true) ? false : true
}
}
});
new Vue({
el: '#lotsOfDivs',
});
In this example, we are just toggeling value of takeinput on click , so according the value either label or input will be showed.
This is very basic exmpale. But you can extend it as your need

Move elements passed into a component using a slot

I'm just starting out with VueJS and I was trying to port over a simple jQuery read more plugin I had.
I've got everything working except I don't know how to get access to the contents of the slot. What I would like to do is move some elements passed into the slot to right above the div.readmore__wrapper.
Can this be done simply in the template, or am I going to have to do it some other way?
Here's my component so far...
<template>
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>
<script>
export default {
name: 'read-more',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
}
</script>
You can certainly do what you describe. Manipulating the DOM in a component is typically done in the mounted hook. If you expect the content of the slot to be updated at some point, you might need to do the same thing in the updated hook, although in playing with it, simply having some interpolated content change didn't require it.
new Vue({
el: '#app',
components: {
readMore: {
template: '#read-more-template',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
mounted() {
const readmoreEl = this.$el.querySelector('.readmore__wrapper');
const firstEl = readmoreEl.querySelector('*');
this.$el.insertBefore(firstEl, readmoreEl);
}
}
}
});
.readmore__wrapper {
display: none;
}
.readmore__wrapper.active {
display: block;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id='app'>
Hi there.
<read-more>
<div>First div inside</div>
<div>Another div of content</div>
</read-more>
</div>
<template id="read-more-template">
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>

Nested Components and Proper Wrapping Techniques in VueJS

I'm trying to put Bootstrap Select2 in a Vue wrapper. Somehow, my code works. But It doesn't seem proper to me.
I found a reference in here https://v2.vuejs.org/v2/examples/select2.html
In the VueJS website example, the el is empty. I didn't stick to this concept where in the new Vue part, they included a template because my el have sidebars and other stuffs also. What I did was I added a
<admin-access></admin-access>
to a section in the HTML.
I think I made my component nested which I'm skeptical if it is proper in VueJS.
Is there a better way to code this?
Templates
<template id="select2-template">
<select>
<slot></slot>
</select>
</template>
<template id="admin-access">
<div>
<transition name="access" enter-active-class="animated slideInRight" leave-active-class="animated slideOutRight" appear>
<div class="box box-solid box-primary" v-if="admin" key="create">
<div class="box-header with-border">
<i class="fa fa-text-width"></i>
<h3 class="box-title">Create Admin</h3>
</div>
<div class="box-body">
<form action="" class="search-form">
<div class="form-group">
<label for="search_user_admin">Search User</label>
<select2 name="search_user_admin" id="search-user-admin" class="form-control select2" :options="options" v-model="selected">
<option disabled value="0">Select one</option>
</select2>
</div>
</form>
</div>
</div>
</transition>
</div>
</template>
Script
Vue.component('admin-access', {
template: '#admin-access',
props: ['options', 'value'],
created: function() {
$('#search-user-admin').select2({
placeholder: "Select User",
allowClear: true
});
},
data: function() {
return {
admin : true,
selected : false
}
},
});
Vue.component('select2', {
template: '#select2-template',
data: function() {
return {
selected: 0,
options: [
{ id: 1, text: 'Hello' },
{ id: 2, text: 'Darkness' },
{ id: 3, text: 'My' },
{ id: 4, text: 'Old' },
{ id: 5, text: 'Friend' }
]
}
},
mounted: function() {
var vm = this;
$(this.$el).select2({
data: this.options,
placeholder: "Select an option",
})
.val(this.value)
.trigger('change')
.on('change', function () {
vm.$emit('input', this.value);
});
},
watch: {
value: function (value) {
$(this.$el).val(value).trigger('change');
},
options: function (options) {
$(this.$el).select2({ data: options });
}
},
destroyed: function () {
$(this.$el).off().select2('destroy');
}
});
var admin = new Vue({
el: '#app'
});
There's nothing wrong with nesting components in the first place. But you have to keep in mind that nesting them, or just in general, just wrapping generic html components like a select into a Vue Component is very costly when it comes to rendering, especially when you are nesting them. Typically when all your component does is just wrapping a simple HTMl element with some styles, you should probably just use generic HTML with classes here, for performance sake.
Also, I'd try to get rid of all the jQuery code inside of Vue Components. Vue offers all the functionality that jQuery has and it'll just increase loading times further.