Toggle button in dynamic/recursive structure - javascript

I'm looping an array of element and I'd want to recursively display that element with given template
and then inside that template use button with toggle to show/hide deeper level template of Childs of given element (Child is also an Element)
<div v-for="(element in elements)">
<MyTemplate :element="element"></MyTemplate>
</div>
Here's my template:
<template>
<div>element.Name</div>
<button #click="toggleSomehow">
// I'd want to display it under that button when he's "showing"
<MyTemplate :element="element.Child"></MyTemplate>
</button>
</template>
But I'm not really sure how to do that SHOW/HIDE button without binding it with some property or array, but I'd rather want to avoid it because everything has to be kind of dynamic

You should add toggleable data to your MyComponent component like visible
See example below
Vue.component('my-template', {
template: '#my-template',
props: {
element: {
type: Object,
required: true
}
},
data() {
return {
visible: false
}
},
methods: {
toggleVisible() {
this.visible = !this.visible
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script type="text/x-template" id="my-template">
<div>
<div>{{element.name}}</div>
<button #click="toggleVisible" v-if="element.child">toggle</button>
<my-template v-if="visible" :element="element.child" />
</div>
</script>
<div id="app">
<my-template :element="{name: 'test', child: {name: 'child test'}}" />
</div>

Related

Toggle sidebar from Vue method?

In a <b-table> I would like to create an action on each items so I have a button:
<b-table :items="data" :fields="fields">
<template v-slot:cell(actions)="data">
<b-button v-on:click="doIt(data.index)">Do It</b-button>
</template>
</b-table>
Then I have a Form in a sidebar
<b-sidebar id="do-it-form" title="Do it" right>
...
</b-sidebar>
In my methods I would like to respond to the action:
methods: {
doIt(id) {
sidebar.form.id = id
sidebar.show().onSubmit(() => {
axio...
refresh(<b-table>)
})
}
}
Of course, this last part is not valid. On Bootstrap Vue manual I didn't find how to interact from Vue to Bootstrap components. Any clue?
You can emit an event on $root, which can be used to toggle the sidebar. The second parameter being the id of the sidebar you wish to open.
this.$root.$emit('bv::toggle::collapse', 'my-sidebar-id')
<b-collapse> and <b-sidebar> listens for the same event, which is why it says collapse in the event.
new Vue({
el: '#app',
methods: {
openSidebar() {
this.$root.$emit('bv::toggle::collapse', 'my-sidebar')
}
}
})
<link href="https://unpkg.com/bootstrap#4.5.2/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#2.17.1/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.17.1/dist/bootstrap-vue.js"></script>
<div id="app">
<b-sidebar id="my-sidebar" right>
My sidebar
</b-sidebar>
<b-btn #click="openSidebar">
Open sidebar
</b-btn>
</div>
Alternatively you can bind a boolean property to the v-model on the sidebar and set the boolean to true when you want to open the sidebar.
new Vue({
el: '#app',
data() {
return {
isSidebarOpen: false
}
},
methods: {
openSidebar() {
this.isSidebarOpen = true
}
}
})
<link href="https://unpkg.com/bootstrap#4.5.2/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#2.17.1/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.17.1/dist/bootstrap-vue.js"></script>
<div id="app">
<b-sidebar v-model="isSidebarOpen" right>
My sidebar
</b-sidebar>
<b-btn #click="openSidebar">
Open sidebar
</b-btn>
</div>
Also, you can use one of the sidebar built-in public methods show, hide or toggle. All you need is add a reference to your sidebar
<b-sidebar ref="mySidebar" id="do-it-form">
...
</b-sidebar>
and then in any of your methods where/when it's needed, you can simply call any of them
this.$refs.mysidebar.show();
this.$refs.mysidebar.hide();
this.$refs.mysidebar.toggle();
You can also just assign a boolean value to the visible prop on the b-sidebar component and toggle the boolean value as you like.
<b-sidebar ref="mySidebar" id="do-it-form" :visible="showSidebar">
...
</b-sidebar>
And toggling it:
data: {
showSidebar: false, //starts off invisible
},
methods: {
toggleSidebar(){
this.showSidebar = !this.showSidebar
}
}
This approach at first glance is not the best in an approach where you have other components updating the sidebar visiblity. This use case is for situations when all updates to the sidebar visibility are made from a central store by using a central boolean value.
e.g.
const state = {
showSidebar: null
}
const mutations: {
toggleSidebar(state, payload){
if (payload) { //payload incase you want to force a value and not just toggle
state.showSidebar = payload;
} else {
state.showSidebar = !state.showSidebar;
}
}
}
And in your components:
computed: {
showSidebar(){
return this.$store.state.showSidebar
}, //starts off invisible
},
methods: {
toggleSidebar(){
this.$store.commit("toggleSidebar");
}
}
Your updated sidebar component would look like this:
<b-sidebar ref="mySidebar" id="do-it-form" :visible="showSidebar" #change="updateSidebar">
...
</b-sidebar>
And the method:
methods: {
updateSidebar(value){
this.$store.commit("toggleSidebar", value);
}
}

Can't modify top level data of slot object in Vue

If you run this demo and click "modify in child", the text gets updated. But if you click "modify top level through slot", then it doesn't get updated, and after clicking it clicking the other button no longer works.
How can I update a top-level property of the slot? For example a boolean or string. Doing it directly in the child works, but I can't do it through the slot.
If the child data contains an object, I can modify a sub property of that data object through the slot (see the original version of this question before the edits for a demo of that), but I can't modify a top level property.
const Child = {
template: `<div>
{{ object }}
<slot name="named" v-bind="object">
</slot>
<button #click="click">child</button>
</div>`,
data() {
return {
object: {
string: "initial"
}
}
},
methods: {
click() {
this.object.string = "modify in child"
}
}
}
new Vue({
components: {
Child,
},
template: `
<div class="page1">
<Child>
<template v-slot:named="slot">
<button #click="click(slot)">modify top level through slot</button>
</template>
</Child>
</div>`,
methods: {
click(slot) {
slot.string = "updated top level through slot"
}
}
}).$mount('#app')
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11/dist/vue.min.js"></script>
As to why you are seeing this behavior where you can't modify the root object, but can modify properties underneath it, the exposed slot variable is a shallow clone, so the top level reference is not the same as the object reference in the child component. I've added a console.log and wrapped .string in an object to show it. Click child and then modify buttons.
So it looks like you are trying to expose the state of the child component to the parent component, so you can reach in from the parent and mutate the state of the child. This is generally not the way you are supposed to use Vue. The idea is that state should be moved higher up in the tree, and deterministic props are propogated down through your tree of components.
Directly referencing and mutating state on child components is an antipattern. This is to encourage designing components that have deterministic behavior and to maintain decoupling of the components (to keep them standalone and reusable). There's also some performance benefits.
This guy explains it well: https://stackoverflow.com/a/31756470/120242
Vue and React are based on similar concepts.
const Child = {
template: `<div>
{{ object }}
<slot name="named" v-bind="object">
</slot>
<button #click="click">child</button>
</div>`,
data() {
return {
object: {
string: {x: "initial"}
}
}
},
methods: {
click() {
window.ChildObjectReference = this.object;
this.object.string.x = "modify in child"
}
}
}
new Vue({
components: {
Child,
},
template: `
<div class="page1">
<Child>
<template v-slot:named="slot">
<button #click="click(slot)">modify top level through slot</button>
</template>
</Child>
</div>`,
methods: {
click(slot) {
console.log( `slot: `,slot, `\nobject = `, window.ChildObjectReference,
`\nslot !== ref as object: `, slot === window.ChildObjectReference,
`\nslot.string === ref object.string: `, slot.string === window.ChildObjectReference.string)
}
}
}).$mount('#app')
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11/dist/vue.min.js"></script>
Displaying interaction between slot and parent component:
const Child = {
template: `<div>
{{ object }}
<slot name="named" v-bind="object">
</slot>
<button #click="click">child</button>
</div>`,
data() {
return {
object: {
string: "initial"
}
}
},
methods: {
click() {
this.object.string = "modify in child"
}
}
}
new Vue({
data: {
topLevelObject: { property: "top level initial" }
},
components: {
Child,
},
template: `
<div class="page1">
<Child>
<template v-slot:named="slot">
<div>this is v-bind:'object' on slot 'named' put into variable slot: {{ slot }}</div>
<button #click="click(slot)">modify top level through slot</button>
</template>
</Child>
Top Level state: {{ topLevelObject }}
</div>`,
methods: {
click(slot) {
this.topLevelObject.property = "slot.string pushed to top level: " + slot.string
}
}
}).$mount('#app')
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11/dist/vue.min.js"></script>

Passing data from Child to Parent using only Javascript on VueJS

I know use $emit to pass data from child to parent components on VueJS but I want get that value on javascript function. My scenario is:
Parent
created () {
this.$on('getValue', function (params) {
console.log('PARAMS: ' + params)
})
},
Child
methods: {
checkBoxChanged (index) {
this.$emit('getValue', 'some value')
},
}
But it ins't works. Using html I can set on Parent using something like: (I'VE REASONS TO CAN'T DO IT!)
<template>
<div>
<h1>{{ message }}</h1>
<child v-on:listenerChild="listenerChild"/>
</div>
</template>
But I need do this using just javascript.
That's how you can pass data from child to parent:
Vue.component('child-component', {
template:
`<button type="button" #click="emitClick">
Click to emit event to parent component
</button>`,
methods: {
emitClick() {
this.$emit('buttonClicked', 'Magic from child component!');
},
},
});
Vue.component('parent-component', {
template:
`<div>
<div>Child data is: {{ childData }}</div>
<child-component #buttonClicked="handleClick" />
</div>`,
data() {
return {
childData: 'empty',
};
},
methods: {
handleClick(data) {
this.childData = data;
},
},
});
new Vue({
el: '#app',
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<parent-component />
</div>

How to bind whole component in Vue.js?

I just want to ask how to bind whole component (for example div) in Vue.js. Is there anything like innerHTML? Here is example:
Parent.vue
<template>
<div id="parent">
//some elements
</div>
</template>
Child.vue
<template>
<div id="child">
//some different elements
</div>
</template>
Now how to innerHTML child in parent? I've tried something like v-html:component and then data(){ return{ component: and here I dont know how to pass whole vue Component like Child.vue div. Should I use refs or something?
Now I use visibility attribute from css and I change it but I don't think that is good way to do this.
If you want to switch between components, then check out VueJS dynamic components:
https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components
You can use the component element and the :is prop to send down what component to render.
I have a working demo here: https://codepen.io/bergur/pen/bPEJdB
Imagine the following simple Vue component
Vue.component('menuList', {
data() {
return {
list: ['Menu item A', 'Menu item B']
}
},
template: `
<ul>
<li v-for="item in list">{{ item}}</li>
</ul>
`
})
This is a simple component rendering a unordered list of menu items. Lets create another similiar component that renders ordered list of products. Note that just to make them a little different, the menuList that has ul and the productList has ol
Vue.component('productList', {
data() {
return {
list: ['Product item A', 'Product item B']
}
},
template: `
<ol>
<li v-for="item in list">{{ item}}</li>
</ol>
`
})
Now we can create a main VueJS that renders these components depending on which button I press. You can have what ever trigger/action you want to change the component.
new Vue({
name: 'main',
el: '#main',
data() {
return {
header: 'Component switching',
selectedComponent: 'menuList'
}
},
methods: {
setComponent(name) {
this.selectedComponent = name
}
},
template: `<div>
<button #click="setComponent('menuList')">Menu List</button>
<button #click="setComponent('productList')">Products</button>
<component :is="selectedComponent" />
</div>`
})
So here the magic begins.
We create a app with some data properties. The header property is just a string value, and selectedComponent tells us which component is beeing rendered.
In our template we use the <component :is="selectedComponent /> so initially the menuList component is the active one.
We create a method called setComponent that takes in a string value and sets that as a new value for selectedComponent. By pressing a button a new value for selectedComponent is set and the component is rendered. Voila

With Vuejs, how to use a modal component inside a v-for loop the right way

In my vue.js app, I need to display a list of items which the user can click.
When clicked, each of these items should then fire a modal, containing additional information about the item that the user just clicked.
What I have so far in my Items.vue component is:
<template>
<div id="content">
<li v-for="item in items" class="list-item pa2 ba">
<div class="f5 pt2 pb2">
<span>{{item.name}}</span>
</div>
</li>
</div>
</template>
<script>
import Items from '#/api/items';
export default {
name: 'items',
asyncComputed: {
items: {
async get() {
const items = await Items.getAll();
return items.data;
},
default: []
}
},
components: {}
}
</script>
Now, I could simply add a modal component to the v-for loop, thus creating one modal for each item, but this does not seem ideal if, for example, I have a list of thousands of items.
This led me to believe that the modal should probably be placed at the root of the app (in my case App.vue), like this:
<template>
<div id="app">
<modal></modal>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
and then somehow fired with custom data whenever I needed it.
However, I'm not sure how to proceed. How do I fire this modal with custom information from inside the v-for loop, which is in a child component relative to App.vue?
These two links helped me figure this out:
https://v2.vuejs.org/v2/examples/modal.html
https://laracasts.com/discuss/channels/vue/passing-data-form-v-for-loop-to-modal-component
#In your Parent Component
You don't have to create a modal for each item within the v-for loop, simply include the modal-component at the beginning of your parent and then work with v-if="..." and props.
<template>
<div>
<modal v-if="modalVisible" #close="modalVisible = false" :data="modalData"/>
<div v-for="item in items">
<button type="button" #click="openModal(item)">Open Modal</button>
</div>
</div>
</template>
and then in your script:
import modal from './Modal'
export default {
components: {
modal
},
data() {
return {
modalVisible: false,
modalData: null
}
},
methods: {
openModal(data) {
this.modalData = data
this.modalVisible = true
},
}
#In your child (modal) component
In your modal you can now do the following:
Template:
<template>
{{ data.foo }}
<button #click="$emit('close')">Cancel</button>
</template>
Script
<script>
export default {
props: ['user']
};
</script>
Hope that helps :-)

Categories