I have a todo list in vue and I'm using pop() to clear out/delete list items. See the code in question below:
// components
Vue.component('todoitem', {
template: "<li>Test Item</li>"
})
// app code
var app = new Vue({
el: '#app',
data: {
todos: [
{ text: 'Sample Item 1' },
{ text: 'Sample Item 2' },
{ text: 'Sample Item 3' }
],
button: {
text: 'Hide'
},
seen: true
},
methods: {
addItem: function() {
let item = document.getElementById("list-input").value;
let error = document.getElementById("error");
if (item == "") {
error.style.display = "block";
} else {
app.todos.push({ text: item });
error.style.display = "none";
}
},
removeItem: function() {
this.todos.pop();
},
toggleSeen: function() {
app.seen = !app.seen;
app.button.text = app.seen ? 'Hide' : 'Show';
}
}
});
.todo-list {
list-style-type: square;
}
.todo-list__delete {
display: none;
}
li:hover .todo-list__delete {
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="app">
<ul class="todo-list">
<li v-for="todo in todos">
{{ todo.text }}
<a v-on:click="removeItem" class="todo-list__delete" href="#">Delete</a>
</li>
</ul>
<input type="text" id="list-input">
<input type="submit" id="list-submit" v-on:click="addItem">
<span id="error" style="color: red; display: none;">Please Enter Text</span>
<ul>
<todoitem></todoitem>
</ul>
<h2 v-if="seen">SEEN</h2>
<button id="hide-seen" v-on:click="toggleSeen">{{ button.text }}</button>
</div>
The expected behavior is that when delete is clicked, it invokes the removeItem function and with the usage of this in that function it deletes the selected item. However what actually happens is it just deletes nodes starting from the bottom.
I thought the issue is that with this I'm actually referencing the delete link and not actually the <li> element I'm trying to remove. So I tried both:
removeItem: function() {
this.todos.parentElement.pop();
}
And:
removeItem: function() {
this.parentElement.todos.pop();
}
With no luck.
How does this work in Vue?
In that context, this refers to the Vue component (not the DOM element). this.todos is the todos array inside the component's data object, and pop removes the last item of an array. So that's why the last element is being removed.
If you want to remove a specific element you'll need to pass some information to the removeItem function about which element you want removed and then have removeItem() drop that specific element from the todos array instead of popping the last element. One simple way to do this would be to pass the array index to the removeItem function, and then splice that index out of the todos array:
<li v-for="(todo, index) in todos">
...
<a v-on:click="removeItem(index)">Delete</a>
</li>
removeItem: function(index) {
this.todos.splice(index, 1);
},
Your full snippet with that change applied is below:
// components
Vue.component('todoitem', {
template: "<li>Test Item</li>"
})
// app code
var app = new Vue({
el: '#app',
data: {
todos: [
{ text: 'Sample Item 1' },
{ text: 'Sample Item 2' },
{ text: 'Sample Item 3' }
],
button: {
text: 'Hide'
},
seen: true
},
methods: {
addItem: function() {
let item = document.getElementById("list-input").value;
let error = document.getElementById("error");
if (item == "") {
error.style.display = "block";
} else {
app.todos.push({ text: item });
error.style.display = "none";
}
},
removeItem: function(index) {
this.todos.splice(index, 1);
},
toggleSeen: function() {
app.seen = !app.seen;
app.button.text = app.seen ? 'Hide' : 'Show';
}
}
});
.todo-list {
list-style-type: square;
}
.todo-list__delete {
display: none;
}
li:hover .todo-list__delete {
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="app">
<ul class="todo-list">
<li v-for="(todo, index) in todos">
{{ todo.text }}
<a v-on:click="removeItem(index)" class="todo-list__delete" href="#">Delete</a>
</li>
</ul>
<input type="text" id="list-input">
<input type="submit" id="list-submit" v-on:click="addItem">
<span id="error" style="color: red; display: none;">Please Enter Text</span>
<ul>
<todoitem></todoitem>
</ul>
<h2 v-if="seen">SEEN</h2>
<button id="hide-seen" v-on:click="toggleSeen">{{ button.text }}</button>
</div>
Related
I've been trying to add a .active class to the current link and I know it's been asked here before, but, none of the answers seems to match my need exactly.
It's a Vue component. I don't want to add vue-router. I want to add the active class dynamically to whichever link is clicked.
<template>
<nav>
<ul>
<li v-for="(item, $index) in items" :key="$index" >
<a :href="item.link">{{ item.name }}</a> //how do I dynamically add the active class here?
</li>
</ul>
</nav>
</template>
<script>
export default {
name: "Navbar",
props: {
items: {
type: Array,
default: null,
},
},
}
</script>
<style>
ul li .active {
background-color: red;
cursor: pointer;
}
</style>
The list of links:
const items = [
{
name: "Home",
link: "#home",
},
{
name: "About",
link: "#about"
},
{
name: "Contacts",
link: "#contact",
},
];
You can make use of the hashchange event and Vue's class binding:
<template>
<nav>
<ul>
<li v-for="(item, $index) in items" :key="$index">
<a :href="item.link" :class="{ active: hash === item.link }">{{ item.name }}</a>
</li>
</ul>
</nav>
</template>
<script>
export default {
name: 'Navbar',
props: {
items: {
type: Array,
default: null,
},
},
data() {
return {
hash: window.location.hash,
};
},
created() {
window.addEventListener('hashchange', () => {
this.hash = window.location.hash;
});
},
};
</script>
<style>
ul li .active {
background-color: red;
cursor: pointer;
font-weight: bold;
}
</style>
Another solution is to use a click event and setTimeout.
Using setTimeout without a delay will simply push the evaluation to the back of the queue, meaning it waits for the URL to update before reading it. Without this, the method will always be one step behind.
Here's an example:
<template>
<nav>
<ul>
<li v-for="(item, $index) in items" :key="$index">
<a
#click="setUrlSection(item.section)"
:href="item.link"
:class="{ active: urlSection === item.link }"
>{{ item.name }}</a
>
</li>
</ul>
</nav>
</template>
<script>
export default {
name: 'Navbar',
data() {
return {
items: [
{
name: 'Home',
link: '#home',
section: 'hash',
},
{
name: 'About',
link: '/about',
section: 'pathname',
},
{
name: 'Contacts',
link: '#contact',
section: 'hash',
},
],
urlSection: window.location.hash || window.location.pathname,
};
},
methods: {
setUrlSection(section) {
setTimeout(() => (this.urlSection = window.location[section]));
},
},
};
</script>
<style>
ul li .active {
background-color: red;
cursor: pointer;
font-weight: bold;
}
</style>
Keep in mind, you have to add a section property to each item so it knows which window.location property to read.
You can use the VueJs :class
<template>
<nav>
<ul>
<li v-for="(item, $index) in items" :key="$index" >
<a :href="item.link" :class="{'active': current_page_name == item.name }">{{ item.name }}</a>
</li>
</ul>
</nav>
</template>
<script>
export default {
computed: {
current_page_name() {
return this.$route.name;
}
}
}
</script>
If you do not want to use vue-router you'll have to write more code.
Here is example
<script>
export default {
data(){
return {
url: '',
}
},
computed: {
current_page_name() {
if(this.url){
var URL = this.url;
var arr=URL.split('/');
var route_name = arr[1]; // i think zero is the domain, then 1 is the page name,
return route_name;
}
return '';
}
},
mounted(){
this.url = window.location.href;
}
}
</script>
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>
I'm making a simple todo list app, and wondering on how to apply styles on only specific dynamic v-for elements.
<f7-list-item v-for="(listItem, index) in activeList.listItems" :key="index" :class="(checked) ? 'checked':'not-checked'">
{{ index+1 }}. {{ listItem }}
<span #click="checkTaskDone(index)">
<i class="f7-icons" id="check-task-btn">check_round</i>
</span>
</f7-list-item>
export default {
data() {
return {
checked: false
}
},
methods: {
checkTaskDone(item) {
if (this.checked == false) {
this.checked = true;
} else if (this.checked == true) {
this.checked = false;
}
}
}
}
.checked {
text-decoration: line-through;
color: #444;
}
With this code it adds the class to every single v-for list element regardless of which one is clicked, as expected. I'm wondering what's the best approach to deal with this. I've experimented with making a prop from the index and trying to target that to apply the styles but I couldn't make it work.
Thanks in advance!
Typically you want to have a "done" or "checked" flag on the individual to-do items, something like:
const todoList = [
{
name: 'Grab some food',
done: false
},
{
name: 'Start coding',
done: false
}
];
And in Vue.js, you could do the class toggling with v-bind:class rather than ternary operator:
export default {
data() {
return {
//checked: false,
activeList: {
listItems: [
{
name: 'Grab some food',
done: false
},
{
name: 'Start coding',
done: false
}
]
}
}
},
methods: {
checkTaskDone(item) {
//if (this.checked == false) {
// this.checked = true;
//}
//else if (this.checked == true) {
// this.checked = false;
//}
// Check/uncheck
item.done = !item.done;
}
}
}
<f7-list-item
v-for="(listItem, index) in activeList.listItems"
:key="index"
:class="{ 'checked': listItem.done }">
{{ index + 1 }}. {{ listItem }}
<span #click="checkTaskDone(listItem)">
<i class="f7-icons" :id="`check-task-btn${index}`">check_round</i>
</span>
</f7-list-item>
BTW, I'm appending an index on the individual i.f7-icons elements because ID should be unique, otherwise please use class instead.
First you need to create dynamic checked array according to activeList.listItems length ! Then you can check with index and you can update array data by this.$set(array,index,value) ...
new Vue({
el: "#app",
data: {
checked: [],
activeList : {listItems:[1,2,3,5]}
},
created: function() {
for(var i = 0; i < this.activeList.listItems.length; i++) {
this.checked.push(false);
}
},
methods: {
checkTaskDone(item) {
if (this.checked[item] == false) {
this.$set(this.checked,item, true);
} else if (this.checked[item] == true) {
this.$set(this.checked,item, false);
}
}
}
});
.checked {
text-decoration: line-through;
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(listItem, index) in activeList.listItems" :key="index" :class="{checked: checked[index]}">
{{ index+1 }}. {{ listItem }}
<span #click="checkTaskDone(index)">
<i class="f7-icons" id="check-task-btn">check_round</i>
</span>
</div>
</div>
I'm playing with vue.js for learning purposes consisting of different components, one of them being a classic to do list. For now, everything is within one component.
I want to change the text of a button after it is clicked to hide an element from "hide" to "show" - I'm going about this by setting a text data object and then changing it in a function.
See below:
<div id="app">
<ul>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ul>
<input type="text" id="list-input">
<input type="submit" id="list-submit" v-on:click="addItem">
<span id="error" style="color: red; display: none;">Please Enter Text</span>
<ul>
<todoitem></todoitem>
<todoitem></todoitem>
<todoitem></todoitem>
</ul>
<h2 v-if="seen">SEEN</h2>
<button id="hide-seen" v-on:click="toggleSeen">{{ button.text }}</button>
</div>
<script type="text/javascript">
// components
Vue.component('todoitem', {
template: "<li>Test Item</li>"
})
// app code
var app = new Vue({
el: '#app',
data: {
todos: [
{ text: 'Sample Item 1' },
{ text: 'Sample Item 2' },
{ text: 'Sample Item 3' }
],
button: [
{ text: 'Hide'}
],
seen: true
},
methods: {
addItem: function() {
let item = document.getElementById("list-input").value;
let error = document.getElementById("error");
if (item == "") {
error.style.display = "block";
} else {
app.todos.push({ text: item });
error.style.display = "none";
}
},
toggleSeen: function() {
app.seen = false
app.button.push({ text: 'Show' });
}
}
})
</script>
Unexpectedly, the button is blank on both hide and show states. Being new to vue, this seems like a strange way to go about doing it. Is changing data in this context bad practice? I don't understand how to fix this, as I have no errors in my console.
Here you have your code in a snipplet.
I change your button by a plain object instead of an array and small adaptation in method toggleSeen.
// components
Vue.component('todoitem', {
template: "<li>Test Item</li>"
})
// app code
var app = new Vue({
el: '#app',
data: {
todos: [
{ text: 'Sample Item 1' },
{ text: 'Sample Item 2' },
{ text: 'Sample Item 3' }
],
button: {
text: 'Hide'
},
seen: true
},
methods: {
addItem: function() {
let item = document.getElementById("list-input").value;
let error = document.getElementById("error");
if (item == "") {
error.style.display = "block";
} else {
app.todos.push({ text: item });
error.style.display = "none";
}
},
toggleSeen: function() {
app.seen = !app.seen;
app.button.text = app.seen ? 'Hide' : 'Show';
}
}
});
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<ul>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ul>
<input type="text" id="list-input">
<input type="submit" id="list-submit" v-on:click="addItem">
<span id="error" style="color: red; display: none;">Please Enter Text</span>
<ul>
<todoitem></todoitem>
<todoitem></todoitem>
<todoitem></todoitem>
</ul>
<h2 v-if="seen">SEEN</h2>
<button id="hide-seen" v-on:click="toggleSeen">{{ button.text }}</button>
</div>
You can achieve this by using refs in vuejs:
<body>
<div id = 'app'>
<button #click="changeState" ref="btnToggle">Hide</button>
<div v-show="show">
<h1>1 to 100</h1>
<p v-for="i in 100">{{i}}</p>
</div>
</div>
<script>
const app = new Vue({
el:'#app',
data: function(){
return{
show: true
}
},
methods: {
changeState: function(){
this.show = !this.show;
this.$refs.btnToggle.innerText = this.show?'Hide':'Show';
}
},
});
</script>
</body>
How to use the computed to realize It? When I input in the input the ul only show the li which inclue my input. For example, if I am inputting the Ad, then only show the "Add some todos" li. And if I give up input, the ul will goback, all the li will show again.
This is my code, and I use Vue.
div id="app">
<input v-model="newTodo" v-on:keyup.enter="addTodo">
<ul>
<li v-for="todo in todos">
<span>{{ todo.text }}</span>
<button v-on:click="removeTodo($index)">X</button>
</li>
</ul>
<script>
new Vue({
el: '#app',
data: {
newTodo: '',
todos: [
{text: 'Add some todos'}
]
},
methods: {
addTodo: function () {
var text = this.newTodo.trim()
if (text) {
this.todos.push({text: text})
this.newTodo = ''
}
},
removeTodo: function (index) {
this.todos.splice(index, 1)
}
}
})
</script>
Define a computed to do the filtering you want done.
Use the computed instead of todos in your v-for.
new Vue({
el: '#app',
data: {
newTodo: '',
todos: [{
text: 'Add some todos'
}]
},
computed: {
filteredTodos: function() {
const re = new RegExp(this.newTodo, 'i');
return this.todos.filter((item) => re.test(item.text));
}
},
methods: {
addTodo: function() {
var text = this.newTodo.trim()
if (text) {
this.todos.push({
text: text
})
this.newTodo = ''
}
},
removeTodo: function(index) {
this.todos.splice(index, 1)
}
}
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div id="app">
<input v-model="newTodo" v-on:keyup.enter="addTodo">
<ul>
<li v-for="todo in filteredTodos">
<span>{{ todo.text }}</span>
<button v-on:click="removeTodo($index)">X</button>
</li>
</ul>
</div>