How to achieve this kind of Operation in Vue JS? - javascript

I'm Building a tree-like structure in vue. I tried to do this with the data function but in that case, all the elements in the same level of the tree getting these classes although I just want clicked element to achieve this I tried this with the ref but don't know how to achieve that parent element selector part with it as I'm new to Vue till now i don't know how to tackle this .
this.parentElement.querySelector(".nested").classList.toggle("active");
this.classList.toggle("caret-down");

Please take a look at one of solutions (if I understood you correctly) in following snippet
new Vue({
el: '#demo',
data() {
return {
tree: [
{parent: 'AAA', childs: []},
{parent: 'BBB', childs: [{child: 'bbb'}, {child: 'ccc'}]},
{parent: 'CCC', childs: [{child: 'ddd'}, {child: 'eee'}, {child: 'fff'}]},
{parent: 'DDD', childs: []}
],
selected: null,
active: null
}
},
methods: {
select(item, idx) {
if (this.tree[idx].childs.length) {
this.selected = idx
this.active = null
} else {
this.active = item.parent + idx
this.selected = null
}
},
activate(itm, i) {
this.active = itm.child + i
}
}
})
li {
cursor: pointer;
}
.active {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<ul>
<li v-for="(item, idx) in tree" :key="idx">
<p #click="select(item, idx)" :class="active === item.parent + idx ? 'active' : ''">{{ item.parent }}</p>
<ul v-if="selected === idx">
<li v-for="(itm, i) in item.childs" :key="i">
<p #click="activate(itm, i)" :class="active === itm.child + i ? 'active' : ''">{{ itm.child }}</p>
</li>
</ul>
</li>
</ul>
</div>

Related

Setting value for each item in for loop to true or false

I am trying to create a v-for that shows a list of exercises containing several sets. I have created a loop with a row for each set underneath each exercise.
my data looks like this.
const exercises = [
{ id: 1, name: exercise1, sets: 3 },
{ id:2, name: exercise2, sets: 2 }
{ id:3, name: exercise3, sets: 4 }
]
And my component looks something like this:
<template v-for="exercise in exercises" :key="exercise.id">
<span> {{ exercise.name }} </span>
<template v-for="set in exercise.sets" :key="set">
<span #click="completeSet()"> {{ set }} </span>
</template>
</template>
Now I want to be able to mark each set as completed by setting the value on each set to either true or false through a click event. But I am not sure about how to do this since each set doesn't have a property to set a value because it's looping through a number.
What would be the right approach to this problem?
First and foremost, you can't loop through a number. To be able to loop the sets, you'd have to
<template v-for="let set = 0; set < exercise.sets; set++" :key="set">
<span #click="completeSet()"> {{ set }} </span>
</template>
However, setting a property on a number is equally impossible. You have to prepare your data to be able to make that adjustment:
const exercises = [
{ id: 1, name: 'exercise1', sets: 3 },
{ id: 2, name: 'exercise2', sets: 2 } ,
{ id: 3, name: 'exercise3', sets: 4 }
].map(exercise => ({
id: exercise.id,
name: exercise.name,
sets: Array.from(
{ length: exercise.sets },
() => ({ completed: false })
),
}))
You can create array with finished sets and compare it (try the snippet pls):
new Vue({
el: "#demo",
data() {
return {
exercises: [{ id: 1, name: 'exercise1', sets: 3 }, { id: 2, name: 'exercise2', sets: 2 }, { id: 3, name: 'exercise3', sets: 4 }],
finishedSets: []
}
},
computed: {
checkAll() {
return this.exercises.reduce((acc, curr) => acc + curr.sets, 0) === this.finishedSets.length
}
},
methods: {
compareObjects(o1, o2) {
return Object.entries(o1).sort().toString() !== Object.entries(o2).sort().toString()
},
findObject(id, set) {
return this.finishedSets.find(f => f.id === id && f.set === set)
},
completeSet(id, set) {
this.findObject(id, set) ?
this.finishedSets = this.finishedSets.filter(f => {return this.compareObjects(f, this.findObject(id, set))}) :
this.finishedSets.push({id, set})
},
isFinished(id, set) {
return this.findObject(id, set) ? true : false
},
}
})
.set {
width: 70px;
cursor: pointer;
}
.finished {
background-color: seagreen;
}
.finished__not {
background-color: tomato;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div v-for="exercise in exercises" :key="exercise.id">
<span> {{ exercise.name }} </span>
<div v-for="set in exercise.sets" :key="set">
<div #click="completeSet(exercise.id, set)" class="set" :class="isFinished(exercise.id, set) ? 'finished' : 'finished__not'"> {{ set }} <span>
<span v-if="isFinished(exercise.id, set)">finished</div>
</div>
</div>
<button v-if="checkAll">submit</button>
<p>{{finishedSets}}</p>
</div>

Adding 'input' used to edit array object key in selected elements - Vue.js

i need to add an input field used to edit the title in the currently selected element component (selection was done by clicking). The problem is that there should be one input and work for each selected element. I couldn't find a similar task and solving on the Internet. Maybe someone will tell you how to do it?
ItemsList.vue component:
<template>
<input type="text" placeholder="Edit selected items"/>
<div class="items-col">
<ul class="items-list">
<Item v-for="item in items" :key="item" :title="item.title"/>
</ul>
</div>
</template>
<script>
import Item from '#/components/Item.vue'
export default {
data() {
return {
items: [
{ title: 'item 1' },
{ title: 'item 2' },
{ title: 'item 3' },
{ title: 'item 4' },
{ title: 'item 5' },
{ title: 'item 6' }
]
}
},
components: {
Item
}
}
</script>
Item.vue component:
<template>
<li class="item" #click="isActive = !isActive" :class="{ active: isActive }">{{ title }}</li>
</template>
<script>
export default {
name: 'ItemsList',
data() {
return {
isActive: false
}
},
props: {
title: String
}
}
</script>
<style>
.item.active {
color: red;
}
</style>
You might want to reconsider which component should be responsible of knowing which item is active at any point of time: hint: it should be the parent/consuming component. That is because you:
Have only a single input field, which means only one item can be edited at any point of time
You want to let the parent/consuming component to be the single source of truth of which item is actually active
Therefore, the first thing you should do is to ensure that isActive is a prop on the Item component, while the parent ItemList component keeps track of which is active at any point.
Then, it is simply a matter of:
Implementing a toggling logic for the isActive flag. The flag is updated when a native click event is fired from the Item component. For the toggling logic, we can simply toggle between a zero-based index of the click item, or -1, which we used to indicate that nothing is active.
Using v-bind:value and a computed property to reflect the value of the currently active item. We can simply retrieve it using this.items[this.activeIndex] on the parent component
Listening to the onInput event and then updating the correct item
See proof-of-concept below:
Vue.component('item-list', {
template: '#item-list-template',
data() {
return {
items: [{
title: 'item 1'
},
{
title: 'item 2'
},
{
title: 'item 3'
},
{
title: 'item 4'
},
{
title: 'item 5'
},
{
title: 'item 6'
}
],
activeIndex: -1,
}
},
methods: {
onItemClick(index) {
this.activeIndex = this.activeIndex === index ? -1 : index;
},
setActiveItemValue(event) {
const foundItem = this.items[this.activeIndex];
if (!foundItem) return;
return this.items[this.activeIndex].title = event.currentTarget.value;
}
},
computed: {
activeItemValue() {
return this.items[this.activeIndex]?.title ?? '';
}
}
});
Vue.component('item', {
template: '#item-template',
props: {
isActive: Boolean,
title: String
}
});
new Vue({
el: '#app'
});
li.active {
background-color: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<item-list></item-list>
</div>
<script type="text/x-template" id="item-list-template">
<div>
<input type="text" placeholder="Edit selected items" :value="activeItemValue" #input="setActiveItemValue" />
<div class="items-col">
<ul class="items-list">
<item v-for="(item, i) in items" :key="i" :title="item.title" :isActive="activeIndex === i" #click.native="onItemClick(i)" />
</ul>
</div>
</div>
</script>
<script type="text/x-template" id="item-template">
<li class="item" :class="{ active: isActive }">{{ title }}</li>
</script>
If you want a solution with your current components (not the cleanest) , you can actually emit an event to the parent component when you activate an element that event should containe the index of the object in the items array
Then you can use the index to get and set the title variable , here is an example :
Item.vue
<template>
<li class="item" #click="activateItem" :class="{ active: isActive }">{{ title }}</li>
</template>
<script>
export default {
name: 'ItemsList',
data() {
return {
isActive: false
}
},
methods:{
activateItem() {
this.isActive = !this.isActive
this.$emit('activatedItem', this.isActive ? this.index : null)
}
},
props: {
title: String,
index: Number
}
}
</script>
<style>
.item.active {
color: red;
}
</style>
ItemList.vue
<template>
<div>
<input type="text" placeholder="Edit selected items" #input="inputChange" :value="inputValue"/>
<div class="items-col">
<ul class="items-list">
<Item v-for="(item, index) in items" :key="index" :title="item.title" :index="index" #activatedItem="itemSelected"/>
</ul>
</div>
</div>
</template>
<script>
import Item from '#/components/Item.vue'
export default {
data() {
return {
items: [
{ title: 'item 1' },
{ title: 'item 2' },
{ title: 'item 3' },
{ title: 'item 4' },
{ title: 'item 5' },
{ title: 'item 6' }
],
selectedIndex: null,
inputValue: ''
}
},
methods:{
itemSelected(index){
this.selectedIndex = index;
if(this.selectedIndex != null) {
this.inputValue = this.items[this.selectedIndex].title;
}
},
inputChange(event){
this.inputValue = event.target.value;
if(this.selectedIndex != null){
this.items[this.selectedIndex].title = this.inputValue
}
}
},
components: {
Item
}
}
</script>
You should also be aware that with the component Item you have given you can select more than one item !

how to update a value in v-html Vue

I am have an array of items that will be fetched fom an API. First, I display the string replacing ### with ______. Then when I click the buttons I want to replace them with <span>word</span>. so I'am using using v-html. I thought the new element would be shown but it doesn't. what would I have to do?
Code: https://jsfiddle.net/uosm30p9/
var example1 = new Vue({
el: '#example',
data: {
items: [{
str: 'This is ###.',
list: ['Frank', 'Eva']
},
{
str: 'I am not ###.',
list: ['George', 'John', 'Mark']
}
],
},
computed: {},
methods: {
createStr(item) {
item.newStr = item.str;
item.newStr = item.newStr.replace("###", "________")
return item.newStr
},
update(item, word) {
item.newStr = item.str;
var span = "<span class='.span1'>" + word + "</span>";
item.newStr = item.newStr.replace("###", span)
console.log(item.newStr);
}
}
});
.span1 {
color: blueviolet;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="example">
<ul>
<li v-for="(item,i) in items">
<span v-html="createStr(item)"></span>
<ul>
<li v-for="(word,j) in item.list">
<button v-on:click="update(item,word)">{{word}}</button>
</ul>
</li>
</ul>
</div>
Ok, in order to solve your problem more easily and separating concerns, I came to a solution by setting up a template and storing the current value in each element "state".
HTML:
<div id="example">
<ul>
<li v-for="(item,i) in items">
<span v-html="createStr(item)"></span>
<ul>
<li v-for="(word,j) in item.list">
<button v-on:click="update(item, j)">{{word}}</button>
</ul>
</li>
</ul>
</div>
Javascript:
var example1 = new Vue({
el: '#example',
data: {
defaultReplacement: "_________",
items: [{
selectedOption: null,
template: 'This is <b class="span1">###</b>.',
list: ['Frank', 'Eva']
},
{
selectedOption: null,
template: 'I am not <b class="span2">###</b>.',
list: ['George', 'John', 'Mark']
}
],
},
computed: {},
methods: {
createStr(item) {
var listItem = (item.selectedOption != null) ? item.list[item.selectedOption] : this.defaultReplacement;
return item.template.replace("###", listItem);
},
update(item, j) {
item.selectedOption = j;
}
}
});
Here's the working example: https://jsfiddle.net/feload/rhnf8zv4/5/
var example1 = new Vue({
el: '#example',
data: {
items: [{
str: 'This is ###.',
list: ['Frank', 'Eva'],
current: "________"
},
{
str: 'I am not ###.',
list: ['George', 'John', 'Mark'],
current: "________",
}
],
},
computed: {},
methods: {
createStr(item) {
item.newStr = item.str;
item.newStr = item.newStr.replace("###", item.current);
return item.newStr;
},
update(item, word, idx) {
item.current = word;
this.$set(this.items, idx, item);
}
}
});
.span1 {
color: blueviolet;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="example">
<ul>
<li v-for="(item,i) in items">
<span v-html="createStr(item)"></span>
<ul>
<li v-for="(word,j) in item.list">
<button v-on:click="update(item,word, i)">{{word}}</button>
</ul>
</li>
</ul>
</div>
First try making your data object reactive so that alterations to the item array can be observed by Vue:
data: function () {
return {
items: [
{
str: 'This is ###.',
list: ['Frank', 'Eva']
},
{
str: 'I am not ###.',
list: ['George', 'John', 'Mark']
}
]
}
},
Look at the example at the bottom of this page:
https://v2.vuejs.org/v2/guide/reactivity.html

Vuejs highlight color on a clicked item in a list of items?

How can we highlight a item in a list of item when the particular item is clicked? Should we use id as reference?
<li v-for="todo in todos">
<label>
<a href="#"
v-on:click="toggle(todo)"
:style="{color:activeColor}"
>
{{ todo.text }}
</a>
</label>
</li>
toggle: function(todo){
this.activeColor = 'red'
}
I tried here:
https://jsfiddle.net/eywraw8t/110976/
You can add activeIndex to store current active index:
<div id="app">
<h2>Todos:</h2>
<ol>
<li v-for="(todo, idx) in todos">
<label>
<a href="#"
v-on:click="toggle(idx)"
v-bind:checked="todo.done"
:class="{'active': idx == activeIndex}"
>
{{ todo.text }}
</a>
</label>
</li>
</ol>
</div>
new Vue({
el: "#app",
data: {
activeColor:String,
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: false },
{ text: "Build something awesome", done: false }
],
activeIndex: null
},
methods: {
toggle: function(index){
this.activeIndex = index
}
}
and in css
.active {
color: red;
}
Demo: https://jsfiddle.net/Lv7eanru/
This is another solution to highlight selected item in a list using VueJS :
<div id="app">
<ul>
<li v-for="value in objectArray" v-on:click="highlight($event)" >
First name : {{ value.firstName }} -- Last name : {{ value.lastName }}
</li>
</ul>
and in JS file we have:
Vue.createApp({
data() {
return {
objectArray: [{
firstName: 'John',
lastName: 'Doe'
},
{
firstName: 'Amily',
lastName: 'Brown'
},
{
firstName: 'Jack',
lastName: 'London'
},
],
}
},
methods: {
highlight: function (event) {
for (var i = 0; i < event.target.parentElement.children.length; i++) {
event.target.parentElement.children[i].classList.remove('bg-warning');
}
event.target.classList.add('bg-warning');
}
},
}).mount('#app');

Nested menu using parent id in knockoutJs

I am try to create nested menu using given json data by the client.
Data :
var serverData = [
{
Id: "menuColorSearch",
Text: "Color search"
},
{
Id: "menuAncillaryProductMix",
Text: "Ancillary product mix"
},
{
Id: "menuDocuments",
Text: "Documents"
},
{
Id: "menuColorInfo",
ParentId: "menuDocuments",
Text: "Color info"
},
{
Id: "menuReports",
ParentId: "menuDocuments",
Text: "Reports"
},
{
Id: "menuMaintenance",
Text: "Maintenance"
},
{
Id: "menuPriceManagement",
ParentId: "menuMaintenance",
Text: "Price management"
}
];
I am trying like this :
var Menu = function(dept, all) {
var self = this;
this.id = dept.Id;
this.name = ko.observable(dept.Text);
this.parentId = dept.ParentId;
this.children = ko.observableArray();
ko.utils.arrayForEach(all || [], function(menu) {
if(menu.ParentId){
if (menu.ParentId === self.id) {
self.children.push(new Menu(menu, all));
}
}else{
new Menu(menu, all)
}
});
};
var ViewModel = function(data) {
this.root = new Menu(data[0], data);
};
$(function() {
ko.applyBindings(new ViewModel(serverData));
});
Templates :
<div data-bind="with: root">
<ul data-bind="template: 'deptTmpl'">
</ul>
</div>
<script id="deptTmpl" type="text/html">
<li>
<a data-bind="text: name"></a>
<ul data-bind="template: { name: 'deptTmpl', foreach: children }">
</ul>
</li>
</script>
problem is that its only work when 2nd and 3rd object has parent ID. i am trying something like it should make nested menu according to given json data. so id there is no parent id on object it should add on root. and if object has parent id it should add according to parent id.
Please help me to correct my code or guide me if these is another way to do this in KnockoutJS.
Thanks
This should help you http://jsfiddle.net/MCNK8/3/, the main idea is to rebuild main data array, by placing child inside parent
HTML
<script id="nodeTempl" type="text/html">
<li>
<a data-bind="text: Text"></a>
<ul data-bind="template: {name: nodeTemplate, foreach: children }"></ul>
</li>
</script>
<script id="nodeLeafTempl" type="text/html">
<li>
<a data-bind="text: Text"></a>
</li>
</script>
<ul data-bind="template: {name: nodeTemplate, foreach: children }"></ul>
Javascript (#see fiddle)
var serverData = [
{
Id: "menuColorSearch",
Text: "Color search"
},
{
Id: "menuAncillaryProductMix",
ParentId: 'menuColorSearch',
Text: "Ancillary product mix"
},
{
Id: "menuDocuments",
Text: "Documents"
},
{
Id: "menuColorInfo",
ParentId: "menuReports",
Text: "Color info"
},
{
Id: "menuReports",
ParentId: "menuDocuments",
Text: "Reports"
},
{
Id: "menuMaintenance",
ParentId: 'menuReports',
Text: "Maintenance"
},
{
Id: "menuPriceManagement",
ParentId: "menuMaintenance",
Text: "Price management"
}
];
function getNestedMenu(index, all) {
var root = all[index];
if(!root){
return all;
}
if(!all[index].children){
all[index].children = [];
}
for(var i = 0; i < all.length; i++){
//<infinity nesting?>
//put children inside it's parent
if(all[index].Id == all[i].ParentId){
all[index].children.push(all[i]);
all[i].used = true;
}
//this is needed for each item, to determine which template to use
all[index].nodeTemplate = function(node) {
return node.children.length > 0 ? 'nodeTempl' : 'nodeLeafTempl';
}
//</infinity nesting?>
}
return getNestedMenu(++index, all);
};
function getModel(data) {
var items = getNestedMenu(0, data);
//<remove duplicates, for infinity nesting only>
for(var i = 0; i < items.length; i++){
if(items[i].used){
items.splice(i, 1);
i--;
}
}
//</remove duplicates, for infinity nesting only>
//<build root item>
var model = {};
model.children = ko.observableArray(items);
model.nodeTemplate = function(node) {
return node.children.length > 0 ? 'nodeTempl' : 'nodeLeafTempl';
}
//</build root item>
console.log(items);
return model;
};
(function() {
//new ViewModel(serverData);
ko.applyBindings(getModel(serverData));
})();

Categories