Vue how to set the active tab - javascript

I have a Vue tab component that console logs out the correct index of the selected tab but I don't know how to set the active tab so it is opened. Any help or direction would be appreciated.
Component:
<Tabs
:tabs="tabs"
:active-tab-index="activeTab"
#tab-changed="changeTab"
/>
Markup
<div v-show="activeTab === 0">
Show tab one content
</div>
<div v-show="activeTab === 1">
Show tab two content
</div>
Data:
data() {
return {
tabs: [
{ id: 0, name: 'Tab One' },
{ id: 1, name: 'Tab Two' },
],
activeTab: 0,
}
},
Method:
changeTab(index) {
console.log('Change activeTab', index)
},

it's not completely clear to me where the method and data is, but I assume it's in the parent, so in that case you'd simply need to change the function to something like this:
changeTab(index) {
this.activeTab = index
},
your component could be even cleaner if it would provide option to use v-model on it like: v-model:activeTabIndex="activeTab". To make this happen you'd just need to emit the data inside the component with update:activeTabIndex instead of tab-changed.

Related

Update the inital array of a recursive treeview in VueJS

I was folling this tutorial for my own tree view with a recursive component in vuejs.
So the input array looks like this:
let tree = {
label: 'root',
nodes: [
{
label: 'item1',
nodes: [
{
label: 'item1.1'
},
{
label: 'item1.2',
nodes: [
{
label: 'item1.2.1'
}
]
}
]
},
{
label: 'item2'
}
]
}
<template>
<div>
...
<tree-menu
v-for="node in nodes"
:nodes="node.nodes"
:label="node.label" />
...
</div>
<template
<script>
export default {
props: [ 'label', 'nodes' ],
name: 'tree-menu'
}
</script>
So basically a label and a subarray of nodes is passed to a child node. Now I want to update or delete a node (e.g. item1.1), but reflect this change in the outmost array (here tree), because I want to send this updated structure to the server. How can I achive this? If I change the label of a node, this will be rendered in the DOM, but the tree array is not updated.
Here's how you can use the .sync modifier to update recursively:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('tree-node', {
template: `
<div style="margin-left: 5px;">
<input :value="label"
type="text"
#input="$emit('update:label', $event.target.value)" />
<tree-node v-for="(node, key) in nodes"
:key="key"
v-bind.sync="node" />
</div>
`,
props: ['label', 'nodes']
});
let tree = {
label: 'root',
nodes: [{
label: 'item 1',
nodes: [
{ label: 'item 1.1' },
{ label: 'item 1.2',
nodes: [
{ label: 'item 1.2.1' }
]
}
]
},
{ label: 'item 2' }
]
};
new Vue({
el: '#app',
data: () => ({
tree
})
})
#app {
display: flex;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<div id="app">
<div>
<tree-node v-bind.sync="tree" />
</div>
<pre v-html="tree" />
</div>
v-bind.sync="node" is shorthand for :label.sync="node.label" :nodes.sync="node.nodes". v-bind unwraps all object members as attributes of the tag, resulting in props for the component.
The other half of the solution is replacing v-model on the input with :value + an $emit('update:propName', $event.target.value) call on #input which updates the .sync-ed property in the parent. To conceptualize it, it's a DIY v-model exposed by Vue so it could be customized (you decide when to call the update and what to update with). You can replace the <input> with any other type of input, depending on what you're binding/modifying (checkboxes, textarea, select, or any fancier input wrapper your framework might feature). Depending on type of input you'll want to customize the listener: #change, #someCustomEvent, etc...
.sync makes everything reactive at each individual level. Since everything is :key-ed, no re-rendering actually happens (Vue only re-renders DOM elements which actually changed). If that wasn't the case, the input would lose focus upon re-rendering.
The update principle is: instead of making the change at child level you update the parent property which, through v-bind, sends it back to the child.
It's the same exact principle used by Vuex. Rather than changing some local prop you call a store mutation which comes back through getters and modifies the local value but it happens for any component using that store data, not just for current one.

Vue draggable does not update text input fields when switched, but does update it in the array

I am trying to implement a drag and drop document builder with Vue draggable, you can add elements like headings and rearrange their order. These involve using components with input fields in.
I have got it working ok, except when you type something into heading 1 for example, then swap it with heading 2, the input text you typed is still in the same position where heading 1 was, even though the element swapped. But bizarely, it does switch it round on the array list correctly.
Basically, the input you type doesn't seem to stay with the component when you swap it. It either stays in its ORIGINAL place or just clears, which is obviously very problematic.
It uses a dynamic component looping through the array to display the list, it also involves emitting an object which then updates in the array:
AddNew.vue
<InsertContent #create="createNewElement($event, 0)" />
<draggable :list="userData.packetSections" :options="{animation:750}" handle=".handle">
<div
class="element-wrapper"
v-for="(section, index) in userData.packetSections"
:key="index"
>
<div class="element">
<component :is="section.type + 'Element'" #return="addData($event, index)" />
<i class="remove" #click="removeElement(index)"></i>
</div>
<InsertContent #create="createNewElement($event, index + 1)" />
</div>
</draggable>
<script>
data() {
return {
userData: {
packetSections: [
{
type: "Heading",
text: "test input", //<-- this swaps in the array when switched but does NOT swap in the browser
id: ""
},
{
type: "Heading",
text: "test 2",
id: ""
},
],
}
};
},
methods: {
createNewElement(event, index) {
var element = {
type: event,
text: "",
id: ""
};
this.userData.packetSections.splice(index, 0, element);
},
removeElement(index) {
this.userData.packetSections.splice(index, 1);
},
addData(emittedData, index) {
this.userData.packetSections.splice(index, 1, emittedData);
console.log(emittedData, index);
},
}
};
</script>
HeadingElement.vue
<template>
<div class="grey-box">
<h3>Subheading</h3>
<input type="text" v-model="data.text" #change="emitData" />
</div>
</template>
<script>
export default {
data(){
return{
data: {
type: "Heading",
text: "",
id: ""
}
}
},
methods:{
emitData(){
this.$emit('return', this.data);
}
}
};
</script>
Does anyone know why the text input fields would update in the array but NOT update the position in the browser?
Thanks
For anyone interested, I ended up fixing this. you have to pass down the contents of the array as props when you are working with dynamic components in your list that also pass up data.
So I change it from:
<component :is="section.type + 'Element'" #return="addData($event, index)" />
to:
<component :is="section.type + 'Element'" :data="userData.packetSections[index]" #return="addData($event, index)" />
and it worked fine.

Why is v-model inside a component inside a v-for (using a computed list) changing weirdly when the list changes?

I'm using a computed list to display several forms for changing comments in a database. (backend Symfony / api requests via axios, but unrelated)
The form for the comments itself is in a Vue component.
The computed list is based on a list that gets loaded (and set as data property) when the page is mounted which is then filtered by an input search box in the computed property.
Now when i type different things in the input box and the comment component gets updated the v-model and labels are messing up.
I've tested in several browsers and the behaviour is the same in the major browsers.
I've also searched the docs and haven't found a solution.
Example to reproduce behaviour:
<!DOCTYPE html>
<html>
<div id="app"></app>
</html>
const ChangeCommentForm = {
name: 'ChangeCommentForm',
props: ['comment', 'id'],
data() {
return {
c: this.comment,
disabled: false
};
},
template: `
<form>
<div>{{ comment }}</div>
<input :disabled="disabled" type="text" v-model="c">
<button type="submit" #click.prevent="changeComment">
Change my comment
</button>
</form>
`,
methods: {
changeComment() {
this.disabled = true;
// do the actual api request (should be unrelated)
// await api.changeCommentOfFruit(this.id, this.c),
// replacing this with a timeout for this example
window.setTimeout(() => this.disabled = false, 1000);
}
}
};
const App = {
components: {ChangeCommentForm},
data() {
return {
fruits: [
{id: 1, text: "apple"},
{id: 2, text: "banana"},
{id: 3, text: "peach"},
{id: 4, text: "blueberry"},
{id: 5, text: "blackberry"},
{id: 6, text: "mango"},
{id: 7, text: "watermelon"},
],
search: ''
}
},
computed: {
fruitsFiltered() {
if (!this.search || this.search === "")
return this.fruits;
const r = [];
for (const v of this.fruits)
if (v.text.includes(this.search))
r.push(v);
return r;
}
},
template: `
<div>
<form><input type="search" v-model="search"></form>
<div v-for="s in fruitsFiltered">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
</div>
`
};
const vue = new Vue({
el: '#app',
components: {App},
template: '<app/>'
});
Just type some letters in the search box
Example on codepen: https://codepen.io/anon/pen/KLLYmq
Now as shown in the example the div in CommentChangeForm gets updated correctly, but the v-model is broken.
I am wondering if i miss something or this is a bug in Vue?
In order to preserve state of DOM elements between renderings, it's important that v-for elements also have a key attribute. This key should remain consistent between renderings.
Here it looks like the following might do the trick:
<div v-for="s in fruitsFiltered" :key="s.id">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
See:
https://v2.vuejs.org/v2/guide/list.html#Maintaining-State
https://v2.vuejs.org/v2/api/#key

How to render react component from a json object?

I made a menu component that accepts a json object with all menu itens.
For icons i use react-icons/io.
The json object is something like this:
const menus = {
Item1: { buttonText: 'Item 1 text', icon: { IoLogoAndroid }, path: 'item 1 path' },
Item2: { buttonText: 'Item 2 text', icon: { IoLogoAndroid }, path: 'item 2 path'},
};
This is the Menu function that will render the menu items as buttons:
const buttons = Object.keys(this.props.menus).map((menu) => {
return (
<a key={menu} href={this.props.menus[menu].path}
onClick={this.changeMenu.bind(this, menu)}>
{...this.props.menus[menu].icon} <- fail here
{this.props.menus[menu].buttonText}
</a>
);
})
I tried many ways to render the icon, but i am clueless on how this could work. Not sure if this is even possible. Any pointers?
If you are importing the icon from where you are defining the object then just tag it <IoLogoAndroid/>;, so react knows it should treat it as an element to render.
const menus = {
Item1: { buttonText: 'Item 1 text', icon: <IoLogoAndroid/> , path: 'item 1 path' },
Item2: { buttonText: 'Item 2 text', icon: <IoLogoAndroid/>, path: 'item 2 path'},
};
And then just call it directly (remove the ...)
<a key={menu} href={this.props.menus[menu].path}
onClick={this.changeMenu.bind(this, menu)}>
{this.props.menus[menu].icon}
{this.props.menus[menu].buttonText}
</a>
Alternatively you could just call React.createElement if you don't want to tag it in your object definition.
<a key={menu} href={this.props.menus[menu].path}
onClick={this.changeMenu.bind(this, menu)}>
{React.createElement(this.props.menus[menu].icon)}
{this.props.menus[menu].buttonText}
</a>
Here is a sample showing the 2 implementations https://codesandbox.io/s/pmvyyo33o0
I was just working with a similar project, and I managed to make it work, with a syntax like this
I here have an array of objects (like yours)
links: [
{
name: 'Frontend',
link: 'https://github.com/Thomas-Rosenkrans-Vestergaard/ca-3-web',
icon: <FaCode size={40} />,
id: 1
},
{
name: 'Backend',
link: 'https://github.com/Thomas-Rosenkrans-Vestergaard/ca-3-backend',
icon: <FaCogs size={40}/>,
id: 2
},
{
name: 'Mobile',
link: 'https://github.com/Thomas-Rosenkrans-Vestergaard/ca-3-app',
icon: <FaMobile size={40} />,
id: 3
}]
I then render my component by mapping, where I pass in the entire object as a prop
const projects = this.state.projects.map((project, i) => {
return(
<Project key={`Project key: ${i}`} project={project} />
)
})
I then use objectdestructing to get the prop
const { logo } = this.props.project
then it can just be displayed
//in my case I use antd framework, so I pass the FAIcon component in as a prop
<Meta
avatar={logo}
title={title}
description={description}
/>
I suppose you could do the same thing, by just passing the entire menu object in as a prop, and then accessing the icon?
You need to change the following:
icon: <IoLogoAndroid />
And in the code (remove the spread operator):
this.props.menus[menu].icon
Also, a few refactoring suggestions.
Do you really need to have an object of objects? Why not an array of objects, where every item has a "name" prop? It will be easier to iterate through, as you can access props directly from map, unlike with object keys.
You are creating a button list, so you should have ul and li tags aswell.
Consider passing only a reference to onClick such as:
onClick={this.changeMenu}
If you need to pass data around, you should use dataset for that. Pass a name/path then find it inside the change handler to avoid rebinding inside every re-render.
Refactored suggestion with an array of objects
changeMenu = e => {
const { menus } = this.props;
const { menuName } = e.target.dataset;
const menu = menus.find(menu => menu.name === menuName);
// Do something with menu
return;
};
renderMenu() {
return (
<ul>
{this.props.menus.map(menu => (
<li key={menu.name} style={{ listStyle: "none" }}>
<a
data-menu-name={menu.name}
href={menu.path}
onClick={this.changeMenu}
>
{menu.icon}
{menu.buttonText}
</a>
</li>
))}
</ul>
);
}

Calling a function from a conditional variable - ReactJS

I have this Button A, with content "All categories". When I click this button, I want a div with content "show more" above Button A to display 6 other buttons, each consisting of "category 1", "category 2" up to 6. (and "show more" to disappear). Clicking any of these 6 buttons would hide the 6 buttons, and go back to being the "show more" div.
CODE
var React = require('react');
module.exports = React.createClass({
getInitialState: function() {
return ({
category: false,
selected: 'Total',
categories: [
{
name: 'Button 1',
id: 1,
},
{
name: 'Button 2',
id: 2,
},
{
name: 'Button 3',
id: 3,
},
{
name: 'Button 4',
id: 4,
},
{
name: 'Button 5',
id: 5,
},
{
name: 'Button 6',
id: 6,
}
]
})
},
selectCategory: function(event) {
(this.state.category) ? this.setState({category: false}) : this.setState({category: true})
},
render: function() {
if(this.state.category == true) {
var category = this.state.categories.map(function(category){
return (
<div
className="category-container"
onClick={this.selectCategory}>
<span> {category['name']} </span>
</div>
)
})
} else {
var category = (
<span id="showplease" onClick={this.selectCategory}> Show more </span>
)
}
console.log(this.state.selected)
return (
<div id="landing">
<div id="search-container">
<div id="category-selector-container">
{category}
<div id="selector" onClick={this.selectCategory}> Button A </div>
</div>
</div>
</div>
)
}
})
Simple enough. HERE IS MY ISSUE: Clicking any of the 6 buttons does not change the state, as if after being mapped, the individual components lost the 'onClick={this.selectCategory}' part. However, clicking the "Show more" div will run the selectCategory function, and will show the 6 buttons.
Any help? I can simply avoid the mapping and individually repeat the buttons, but I would like to know why this is not working.
Thank you in advance!
Your problem involves the this variable. Whenever you create a new function, a new context for this is created and the old one is lost.
There are a few ways around this. A common one is to assign this to a different variable, e.g. var self = this;. Another way is to use bind to ensure this is passed from the outer scope. i.e.
var category = this.state.categories.map(function (category) {
return (
<div
className="category-container"
onClick={this.selectCategory}
>
<span>{category['name']}</span>
</div>
);
}.bind(this));
If you're using ES6, the best way is to use arrow functions, which automatically bind this.
var category = this.state.categories.map((category) => (
<div
className="category-container"
onClick={this.selectCategory}
>
<span>{category['name']}</span>
</div>
));

Categories

Resources