Selecting and unselecting nodes in tree view - vuejs - javascript

I'm a total newbie in VueJS. I've been working on customizing a tree view example from the vuejs docs: Example.
On selecting an item in the treeview, I'm not able to understand how to unselect i.e. unset the class of the previously selected item. Some approaches I've tried include
Setting a global variable using Vue.prototype and accessing it in the computed function in which case the computed function doesn't even run.
I'm aware of the event object that is passed. Using that and jQuery, removing the class of the previously selected div would work but that seems like a hack.
Setting an array of selected items in data on the click event and accessing it in the computed function. This also does not work.
Is there a way that would work or am I not understanding something?
The codepen link that I'm working on: Codepen. For selecting a node, just click on the node and try selecting some other node. The previous node doesn't get cleared.
Thanks!
Update:
The below answer works but it would remove the selected class if clicked somewhere else. I wanted a solution where the selected class would only be removed if I clicked on some other node. All I had to do was create an Event Bus and store the previously selected component object in a parent variable. On clicking a new node, a global event would be emitted which would be listened to by the main instance method. There, it would set a boolean value which would unset the previous component selection and another boolean value to set the selected class to the new component object. I'm not sure if a better way exists.
Updated codepen with some changes: CodePen link

It's nothing to do with VueJS, We have to play with CSS by setting the required css properties when the folder node is focused.
//https://github.com/vuejs/Discussion/issues/356
// demo data
Vue.prototype.$selectedNode = []
var data = {
name: 'My Tree',
children: [{
name: 'hello'
},
{
name: 'wat'
},
{
name: 'child folder',
children: [{
name: 'child folder',
children: [{
name: 'hello'
},
{
name: 'wat'
}
]
},
{
name: 'hello'
},
{
name: 'wat'
},
{
name: 'child folder',
children: [{
name: 'hello'
},
{
name: 'wat'
}
]
}
]
}
]
}
// define the item component
Vue.component('item', {
template: '#item-template',
props: {
model: Object
},
data: function() {
return {
open: false,
selectedNode: []
}
},
computed: {
isFolder: function() {
return this.model.children &&
this.model.children.length
},
setChevronClass: function() {
return {
opened: this.isFolder && this.open,
closed: this.isFolder && !this.open,
folderChevronSpan: this.isFolder
}
},
setSelected: function() {
if (this.selectedNode.length > 0 && this.selectedNode[0].title == this.model.name)
return true;
else
return false;
}
},
methods: {
toggle: function() {
if (this.isFolder) {
this.open = !this.open
this.$refs.toggler.focus();
}
},
changeType: function() {
if (!this.isFolder) {
Vue.set(this.model, 'children', [])
this.addChild()
this.open = true
}
},
addChild: function() {
this.model.children.push({
name: 'new stuff'
})
},
selectNode: function() {
this.selectedNode = [];
this.selectedNode.push({
'title': this.model.name,
'isSelected': true
});
}
}
})
// boot up the demo
var demo = new Vue({
el: '#demo',
data: {
treeData: data
}
})
body {
font-family: Menlo, Consolas, monospace;
color: #444;
}
.item {
cursor: pointer;
}
.folderTitleSpan:hover {
font-weight: bold;
border: 1px solid darkblue;
}
.folderTitleSpan:focus,
li span:nth-child(1):focus+.folderTitleSpan {
background-color: darkblue;
color: white;
}
.node,
.add {
list-style-type: none;
padding-left: 10px !important;
}
.folderChevronSpan::before {
color: #444;
content: '\25b6';
font-size: 10px;
margin-left: -1em;
position: absolute;
transition: -webkit-transform .1s ease;
transition: transform .1s ease;
transition: transform .1s ease, -webkit-transform .1s ease;
-webkit-transition: -webkit-transform .1s ease;
}
.folderChevronSpan.opened::before {
transform: rotate(90deg);
-webkit-transform: rotate(90deg);
}
ul {
padding-left: 1em;
line-height: 1.5em;
list-style-type: dot;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17-beta.0/vue.js"></script>
<!-- item template -->
<script type="text/x-template" id="item-template">
<li>
<span :class="setChevronClass" tabindex="0" ref="toggler" #click="toggle">
</span>
<span #click="selectNode" tabindex="1" :class="{folderTitleSpan: isFolder}">
{{ model.name }}
</span>
<span v-if="isFolder">[{{ open ? '-' : '+' }}]</span>
<ul v-show="open" v-if="isFolder">
<item class="item node" v-for="(model, index) in model.children" :key="index" :model="model">
</item>
<li class="add" #click="addChild">+</li>
</ul>
</li>
</script>
<p>(You can double click on an item to turn it into a folder.)</p>
<!-- the demo root element -->
<ul id="demo">
<item class="item node" :model="treeData">
</item>
</ul>

Related

Vue add class on clicked item in loop and remove from other items

I have a loop where I loop over an Array.
for each item in this array I render a new component. Now when a user clicks on a certain component I only want to add a class to that component to highlight it and remove it from others that have it. Think of it as a menu active item.
<step-icon
v-for="(step, currentStep) in steps"
/>
data() {
return {
steps: [{foo: 'bar'}, {foo2: 'bar2'}]
}
}
my step-icon.vue:
<template>
<div :class="{'selected': selected}" #click="clickStep()">
hello
</div>
</template>
data() {
return {
selected: false
}
},
methods: {
clickStep() {
this.selected = true;
}
}
This works only 1 way, I can only add the selected class but never remove it.
I created a simple example illustrating your use case since you didn't provided enough detail to go with. Below you can find the items selected and unselected. Firstly, we added a key isSelected and set it to false as default. This will act as a status for all items.
steps: [
{key:"0", tec:"foo", isSelected:false},
{key:"1", tec:"bar", isSelected:false},
{key:"2", tec:"foo2", isSelected:false},
{key:"3", tec:"bar2", isSelected:false},
]
Next, we looped over the array and displayed all the items.
<ul>
<li
v-for="l in steps"
id="l.key"
#click="select(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
Here you can se we have set our status property isSelected on v-bind directive which will add or remove the class based on the value of isSelected.
Next, once the item is clicked we will trigger select method.
methods: {
select(key) {
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i].key !== key) {
this.steps[i].isSelected = false
}
}
this.toggleSelection(key)
},
toggleSelection(key) {
const stepsItem = this.steps.find(item => item.key === key)
if (stepsItem) {
stepsItem.isSelected = !stepsItem.isSelected
}
}
}
The select method will firstly unselect all those except the one which is selected and then call toggleSelection which will set the selected Item to true or false.
Complete Code:
new Vue({
el: '#app',
data: {
steps: [
{key:"0", tec:"foo", isSelected:false},
{key:"1", tec:"bar", isSelected:false},
{key:"2", tec:"foo2", isSelected:false},
{key:"3", tec:"bar2", isSelected:false},
]
},
methods: {
select(key) {
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i].key !== key) {
this.steps[i].isSelected = false
}
}
this.toggleSelection(key)
},
toggleSelection(key) {
const stepsItem = this.steps.find(item => item.key === key)
if (stepsItem) {
stepsItem.isSelected = !stepsItem.isSelected
}
}
}
})
.selected {
background: grey;
}
.notselected {
background:transparent;
}
<script src="https://unpkg.com/vue#2.6.10"></script>
<div id="app">
<ul>
<li
v-for="l in steps"
id="l.key"
#click="select(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
</div>
You can keep all the step state in the parent component.
Now the parent component can listen for toggle_selected event from the nested one, and call toggle_selected(step) with the current step as a param.
Toggle_selected method should deselect all steps except the current one, and for the current one just toggle the selected prop.
If You would like to modify more props of the step in the nested component You could use .sync modifier (:step.sync="step") and then this.#emit('update:step', newStepState) in the nested component.
I've also made a snippet (my first). In this example I omitted clickStep and just put #click="$emit('toggle_selected') in the step-icon component.
new Vue({
el: '#app',
// for this example only defined component here
components: {
'step-icon': {
props: { step: Object }
}
},
data: {
steps: [
{ name: 'Alfa', selected: false},
{ name: 'Beta', selected: false},
{ name: 'Gamma', selected: false},
]
},
methods: {
toggle_selected(step) {
this.steps.filter(s => s != step).forEach(s => s.selected = false);
step.selected = true;
}
}
})
#app {
padding: 2rem;
font-family: sans-serif;
}
.step-icon {
border: 1px solid #ddd;
margin-bottom: -1px;
padding: 0.25rem 0.5rem;
cursor: pointer;
}
.step-icon.selected {
background: #07c;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<!-- Ive used inline-template for this example only -->
<div id="app">
<step-icon v-for="(step, currentStep) in steps" :key="currentStep"
:step.sync="step"
#toggle_selected="toggle_selected(step)" inline-template>
<div class="step-icon"
:class="{selected: step.selected}"
#click="$emit('toggle_selected')">
{{ step.name }}
</div>
</step-icon>
</div>

ReactJS changing styles via State breaks CSS

So I've been learning react.
And have been learning about states/props and dynamically changing things. As such I set the states set up on a component as such:
constructor(props) {
super(props);
this.modifyStyle = this.modifyStyle.bind(this);
this.state = {
navigation: [
{ route: "/", title: "Home" },
{ route: "/about", title: "About Me" },
{ route: "/portfolio", title: "Portfolio" },
{ route: "/contact", title: "Contact" },
{ route: "/", title: "Services" },
],
styling: "nav",
};
}
Notice the "Styling" state.
This is used to give the list element style as such:
render() {
return (
<div>
<div className="trigram">
<p>☰</p>
</div>
<ul className={this.state.styling}>
{this.state.navigation.map((items) => (
<NavItem route={items.route} title={items.title} />
))}
</ul>
</div>
);
The css for the "Styling" state is this:
.nav {
width: 100%;
float: left;
list-style: none;
padding: 15px;
transition: 1s;
}
Which produces, along with the relevant li styling the following on the webpage:
[![Screenshot of menu][1]][1]
The idea is to use the following function to change the list style to a smaller one on a "Scroll" event:
componentDidMount() {
document.addEventListener("scroll", this.modifyStyle, true);
}
modifyStyle = () => {
this.setState({
styling: "nav2",
});
};
The "nav2" style which is being assigned to the state should be identical to the main menu style but with lowered padding.
.nav2 {
width: 100%;
float: left;
list-style: none;
padding: 5px;
transition: 1s;
}
The function is called and everything works as intended. The style is changed. Yet for some reason the updated styling breaks completely and is stuck looking like this:
[![screenshot issue][2]][2]
I have no idea why this is happening and it seems no amount of debugging the CSS will resolve the issue.
The Styling will just not play game here.
I expect this is something to do with the way React handles states, but I'm not really sure. Any help would be greatly appreciated.
TIA
[1]: https://i.stack.imgur.com/bK1dt.png
[2]: https://i.stack.imgur.com/w7Wh2.png
Not a React Question, was CSS.
Issue resolved by generalising the "li" tag css. Not specifying it in regards to a specific class

Vue notification when component DOM changed

A prop change with no effect on the component's DOM triggers its updated function, unexpectedly.
https://jsfiddle.net/e5gyuorL/1/
Same result for v-html="markup()" or {{markup()}} or computed: { markup: ... }.
Docs for updated (https://v2.vuejs.org/v2/api/#updated) say:
Called after a data change causes the virtual DOM to be re-rendered and patched.
How does one catch actual DOM re-renders? If this is a FAQ, apologies; I looked at length.
The most straightforward way I can think of is to have the component store its innerHTML in a data item, and on each update check to see whether it has changed:
Vue.component('t-markdown', {
template: '#t-markdown',
data: {
innerHTML: ''
},
props: {src:String},
methods: {
markup: function() { return this.src.slice(0,11) },
},
updated: function() {
if (this.innerHTML !== this.$el.innerHTML) {
this.$parent.count++;
this.innerHTML = this.$el.innerHTML;
}
},
mounted() {
this.innerHTML = this.$el.innerHTML;
}
});
new Vue({
el: "#app",
data: {count:0, inp:'<b>src</b> '},
methods: {
change: function() { this.inp += '#' },
},
mounted() {
setTimeout(() => this.inp = '<i>changed!</i>', 7000);
}
})
body {
background: #20262E;
padding: 20px;
font-family: sans-serif, Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<t-markdown :src="inp"></t-markdown>
<button #click="change">change</button> updated: {{count}}
<div>
{{inp}}
</div>
</div>
<script type="text/x-template" id="t-markdown">
<div v-html="markup()"></div>
</script>

Vue v-for: iterate one element individually in an array

I'm looking to loop through an array of span tags and add is-active to the next one in line, every 3 seconds. I have it working but after the first one, it adds all the rest. How do I just pull that class from the active one and add it to the next array item?
I've read through the official documentation several times and there doesn't seem to be any mention of iterating individual items, just listing them all or pushing an item onto the list.
I'm not sure if 'index' comes in to play here, and how to grab the index of the span element to add/subtract is-active. what am I doing wrong?
var firstComponent = Vue.component('spans-show', {
template: `
<h1>
<span class="unset">Make</span>
<br>
<span class="unset">Something</span>
<br>
<span v-for="(span, index) of spans" :class="{ 'is-active': span.isActive, 'red': span.isRed, 'first': span.isFirst }" :key="index">{{ index }}: {{ span.name }}</span>
</h1>
`,
data() {
return {
spans: [
{
name: 'Magical.',
isActive: true,
isRed: true,
isFirst: true
},
{
name: 'Inspiring.',
isActive: false,
isRed: true,
isFirst: true
},
{
name: 'Awesome.',
isActive: false,
isRed: true,
isFirst: true
}
]
};
},
methods: {
showMe: function() {
setInterval(() => {
// forEach
this.spans.forEach(el => {
if (el.isActive) {
el.isActive = false;
} else {
el.isActive = true;
}
});
}, 3000);
}
},
created() {
window.addEventListener('load', this.showMe);
},
destroyed() {
window.removeEventListener('load', this.showMe);
}
});
var secondComponent = Vue.component('span-show', {
template: `
<span v-show="isActive"><slot></slot></span>
`,
props: {
name: {
required: true
}
},
data() {
return {
isActive: false
};
}
});
new Vue({
el: "#app",
components: {
"first-component": firstComponent,
"second-component": secondComponent
}
});
.container {
position: relative;
overflow: hidden;
width: 100%;
}
.wrapper {
position: relative;
margin: 0 auto;
width: 100%;
padding: 0 40px;
}
h1 {
font-size: 48px;
line-height: 105%;
color: #4c2c72;
letter-spacing: 0.06em;
text-transform: uppercase;
font-family: archia-semibold, serif;
font-weight: 400;
margin: 0;
height: 230px;
}
span {
position: absolute;
clip: rect(0, 0, 300px, 0);
}
span.unset {
clip: unset;
}
span.red {
color: #e43f6f;
}
span.is-active {
clip: rect(0, 900px, 300px, -300px);
}
<div id="app">
<div class="container">
<div class="wrapper">
<spans-show>
<span-show></span-show>
</spans-show>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
To achieve desired result, I'd suggest to change the approach a bit.
Instead of changing value of isActive for individual items, we can create a variable (e.g. activeSpan, that will be responsible for current active span and increment it over time.
setInterval(() => {
// Increment next active span, or reset if it is the one
if (this.activeSpan === this.spans.length - 1) {
this.activeSpan = 0
} else {
this.activeSpan++
}
}, 3000);
In component's template, we make class is-active conditional and dependent on activeSpan variable:
:class="{ 'is-active': index === activeSpan, 'red': span.isRed, 'first': span.isFirst }"
If you still need to update values inside spans array, it can be done in more simple way, via map for example. Also included such case as optional in solution below.
Working example:
JSFiddle
Sidenote: there is no need to add window listeners for load event, as application itself is loaded after DOM is ready. Instead, method can be invoked inside created hook. It is included in solution above.

Add css animation before deleting array item

I would like to do a css animation before deleting an item from my data table. The deletion of an element is triggered by an event #click. So I'd like to see first what my animation does (class delete_animation) and only after delete the element.
var vm = new Vue({
el: '#app',
data: {
addedId: null,
array: [
{ id: 1, text: "lorem ipsum" },
{ id: 2, text: "lorem ipsum" },
]
},
methods: {
add() {
this.addedId = this.array[this.array.length - 1].id + 1;
this.array.push({ id: this.addedId, text: "lorem ipsum"} );
},
remove(item, index) {
this.array.splice(index, 1);
this.addedId = null;
// ???
}
}
});
table {
border-collapse: collapse;
}
table, th, td {
border: 1px solid black;
}
.add_animation {
animation: addItem 1s;
}
#keyframes addItem {
0% {
background-color: green;
}
100% {
background-color: white;
}
}
.deleted_animation {
animation: deleteItem 1s;
}
#keyframes deleteItem {
0% {
background-color: red;
}
100% {
background-color: white;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.11/vue.min.js"></script>
<div id="app">
<table>
<tr v-for="(index, item) in array" :key="item.id" :class="addedId == item.id ? 'add_animation' : ''">
<td>{{ item.text }}</td>
<td> <button type="button" #click="remove(item, index)">remove</button></td>
</tr>
</table>
<button type="button" #click="add()">Add</button>
</div>
I would simply like to do the opposite of what the "add" button does. However, I do not see how to handle events to wait for the animation to display. I think i need to trigger click once my animation was displayed, but i don't know how...
Thanks !
I am not sure,but as i understood,you want to animate the deletion of an item in array using vue.js.
Everything is simple with vue.js so please see Vue.js Transitions
I made a simple example for you,animating items when you delete them.It may help you.
See it in action here
The "html" part
<div id="app">
<transition-group name="fade">
<div v-for="(todo,index) in todos" :key="todo.text" #click="deleteItem(index)">
{{ todo.text}}
</div>
</transition-group>
</div>
The javascript part
new Vue({
el: "#app",
data: {
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
},
methods: {
deleteItem(index) {
this.todos.splice(index, 1);
}
}
})
The css part
.fade-leave-active {
transition: all 1s;
}
.fade-leave-to {
opacity: 0;
}
Add a <transition-group> element around your list, and write your transitions as CSS transitions and Vue.js will take care of setting the correct CSS classes keeping the element there while the exit transition is running. No need to change your logic. For the exact details, check the "List Transitions" section of the documentation.
https://v2.vuejs.org/v2/guide/transitions.html#List-Transitions
If you add the deletedItem class to the clicked item first:
document.querySelectorAll('tr')[index].classList.add('deleted_animation')
(I'm sure you could find a better way to select the clicked item)
And then after that use a setTimeout to delay the action:
setTimeout(() => {
this.array.splice(index, 1);
this.addedId = null;
}, 500)
You will achieve most of this. Depending on the indexes won't be great though since you might quickly click the remove button multiple times. So maybe after the button is clicked you can block the click action on all of the buttons and then add it back after the item is spliced out.

Categories

Resources