How to arrow toggle up and down by clicked index? Vue - javascript

How do I rotate just that arrow icon based on the clicked item?
new Vue({
el: "#app",
data() {
return {
isToggled: false,
items: [{
id: 1,
name: "Test1"
},
{
id: 2,
name: "Test2"
},
{
id: 3,
name: "Test3"
},
{
id: 4,
name: "Test4"
},
]
}
},
methods: {
arrowToggle() {
this.isToggled = !this.isToggled;
},
getItems() {
return this.items;
}
},
mounted() {
this.getItems();
}
});
i {
border: solid black;
border-width: 0 3px 3px 0;
display: inline-block;
padding: 3px;
}
.down {
transform: rotate(45deg);
}
.up {
transform: rotate(-155deg);
}
.accordion {
display: flex;
background: lightblue;
align-items: center;
width: 100%;
width: 1000px;
justify-content: space-between;
height: 30px;
padding: 0 20px;
}
.arrow {
transform: rotate(-135deg);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" style="display: flex; justify-content: center; align-items: center;">
<div v-for="(item, index) in items" :key="index">
<div class="accordion" #click="arrowToggle()">
<p> {{ item.name }}</p>
<i :class="{ 'down': item.isToggled }" class="arrow"> </i>
</div>
</div>
</div>
Based on the clicked element do I want my arrow to rotate?
If i have 10 items and click on 2 items i want the icon to rotate there.
Failing to bind id to the clicked item and to bind that class to rotate the item
One thing is very important, I cannot set the isOpen parameter in my json ITEMS which is false which everyone recommends to me. I get it from a database and I don't have a condition for it.

You will have to toggle at individual item level. Note that I have used isToggled per item. Here is full code at: https://jsfiddle.net/kdj62myg/
Even if you get your items from DB, you can iterate through array and add a key named isToggled to each item.
HTML
<div id="app" style="display: flex; justify-content: center; align-items: center;">
<div v-for="(item, index) in items" :key="index">
<div class="accordion" #click="arrowToggle(item)">
<p> {{ item.name }}</p>
<i :class="{ 'down': item.isToggled, 'up': !item.isToggled }"> </i>
</div>
</div>
</div>
Vue
new Vue({
el: "#app",
data() {
return {
isToggled: false,
items: [{
id: 1,
name: "Test1",
isToggled: false
},
{
id: 2,
name: "Test2",
isToggled: false
},
{
id: 3,
name: "Test3",
isToggled: false
},
{
id: 4,
name: "Test4",
isToggled: false
},
]
}
},
methods: {
arrowToggle(item) {
return item.isToggled = !item.isToggled;
},
getItems() {
return this.items;
}
},
mounted() {
this.getItems();
}
});

You have to map your items and attach a custom data on it to solve your problem.
Items data should be like this
items: [{
id: 1,
name: "Test1",
isToggled: false
},
{
id: 2,
name: "Test2",
isToggled: false
},
{
id: 3,
name: "Test3",
isToggled: false
},
{
id: 4,
name: "Test4",
isToggled: false
},
]
and your toogle function should look like this.
arrowToggle(item) {
return item.isToggled = !item.isToggled;
},
Now, after you fetched the items from the server. You have to map it to attach a isToggled data on every item you have. like this.
getItems() {
axios.get('api/for/items')
.then(({data}) => {
this.items = data.map(item => ({
return {
name:item.name,
id:item.id,
isToggled:false
}
}))
});
}

The above arrowToggle function breaks vue reactivity (google vue reactivity for docs). According to the docs, changing an object property directly will break reactivity. To keep reactivity, the function should change to:
arrowToggle(item) {
this.$set(this.item, 'isToggled', item.isToggled = !item.isToggled)
return item.isToggled;
},

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>

How to detect if an element is outside of its container?

So I've created the the following codesandbox. I got a webapp that relies heavily on user input. For demonstration purposes I've kept it simple by displaying a bunch of authors on a a4 formatted page. The page and font-size both use vw unit to make it responsive.
As you can see in the codesandbox, the last few authors are forced off the page because it no longer fits inside the container. Ideally I'd like to detect the content that doesn't fit on the page anymore, and generate a second identical a4 page to display that particular content.
Currently in my webapp I've just added overflow: scroll; to the page div where all the content is placed in, so that it at least looks somewhat 'ok'. But it isn't a very good User Experience and I'd like to improve it.
I don't have a clue where to start so any help in the right direction would be very much appreciated.
Thanks in advance.
CSS
#app {
-webkit-font-smoothing: antialiased;
font: 12pt "Tahoma";
}
.book {
margin: 0;
padding: 0;
background-color: #FAFAFA;
font: 3vw "Tahoma";
}
* {
box-sizing: border-box;
-moz-box-sizing: border-box;
}
.page {
/* overflow: scroll; */
display: block;
width: calc(100 / 23 * 21vw);
height: calc(100 / 23 * 29.7vw);
margin: calc(100 / 23 * 1vw) auto;
border: 1px #D3D3D3 solid;
border-radius: 5px;
background: white;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.subpage {
margin: calc(100 / 23 * 1vw);
width: calc(100 / 23 * 19vw);
height: calc(100 / 23 * 27.7vw);
line-height: 2;
border: 1px solid red;
outline: 0cm #FAFAFA solid;
}
.subpage-content {
height: 100%;
}
Javascript
export default {
name: "App",
data() {
return {
authors: [
{ id: 1, name: "Smith" },
{ id: 2, name: "Johnson" },
{ id: 3, name: "Williams" },
{ id: 4, name: "Jones" },
{ id: 5, name: "Brown" },
{ id: 6, name: "Davis" },
{ id: 7, name: "Miller" },
{ id: 8, name: "Wilson" },
{ id: 9, name: "Moore" },
{ id: 10, name: "Taylor" },
{ id: 11, name: "Anderson" },
{ id: 12, name: "Thomas" },
{ id: 13, name: "Jackson" },
{ id: 14, name: "White" },
{ id: 15, name: "Harris" },
{ id: 16, name: "Martin" },
{ id: 17, name: "Thomspson" },
{ id: 18, name: "Garcia" },
{ id: 19, name: "Martinez" },
{ id: 20, name: "Robinson" },
{ id: 21, name: "Clark" },
{ id: 22, name: "Rodeiquez" },
{ id: 23, name: "Lewis" },
{ id: 24, name: "Lee" }
]
};
}
};
HTML
<template>
<div id="app">
<div class="container-fluid">
<div class="book">
<div class="page">HEADER
<div class="subpage" id="editor-container">Authors:
<!-- <div class="subpage-content">The real content</div> -->
<div v-for="item in authors" :key="item.id">{{ item.name }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
You can view a fork of your code sandbox here.
I changed the data structure (and template) to have a pages array in which each page has an authors array, instead of a single one. Initially, the first page holds all the authors.
data() {
return {
pages: [
{
authors: [
{ id: 1, name: "Smith" },
...
]
}
]
}
}
<div class="page" v-for="(page, pageIndex) in pages" :key="pageIndex">HEADER
<div class="subpage" id="editor-container">
<template v-if="pageIndex < 1">Authors:</template>
<!-- <div class="subpage-content">The real content</div> -->
<div v-for="item in page.authors" :key="item.id" class="author">{{ item.name }}</div>
</div>
</div>
I then created a method recalcPages that gets called when the component is mounted:
methods: {
recalcPages() {
let pageElements = this.$el.querySelectorAll(".page");
Array.from(pageElements).some((p, pi) => {
let authors = p.querySelectorAll(".author");
if (authors.length) {
return Array.from(authors).some((a, ai) => {
let offPage = a.offsetTop + a.offsetHeight > p.offsetHeight;
if (offPage) {
let currentAuthors = this.pages[pi].authors;
var p1 = currentAuthors.slice(0, ai);
var p2 = currentAuthors.slice(ai);
this.pages[pi].authors = p1;
this.pages.push({ authors: p2 });
}
return offPage;
});
}
return false;
});
}
},
It iterates the actual DOM nodes and uses offsetTop + offsetHeight to calculate whether an author is off the page or not. As soon as an element leaves the page, it and all following elements are split from the current page's authors and a second page is inserted.
You'll also need to call this.recalcPages() after updating the contents deleting all pages and set a new authors array on the first one to be split up automatically again, unless you're only adding to the last page. You could also try to use the updated hook to achieve this automatically, I haven't tried that.
Of course it's quite a heavy operation, as it renders the component just to trigger re-rendering again by modifying the data. But unless you don't know the exact height of every element, there's no way around it (at least none I'm aware of).
By the way (although your final data will probably look different, but just for the sake of completeness of this demonstration) I also wrapped your Authors: headline in <template v-if="pageIndex < 1">Authors:</template> in order to display it only on the first page.

Vue button with 3-way toggle based on array value

I have a vue project where I'm loading an array on page load and looking at the line item of each, checking the status and trying to show a button for each status with a 3-way toggle.
I think I have the main idea right but the structure is off with the mounting and it being an array. I'm a bit stuck on how to fully get this working but quite simply I want only one button to show for each subItem row based on status.
If subItem status = 'A' I want a button that says Accept, if it's 'B' then pause, and if 'C' then resume. I need the toggle to work and I can then work on calling an axios call based on the status, but I think I just need this working first to get the idea.
subItems is an array like this:
array(2)
0:
id: 123,
status: 'A'
1:
id: 234,
status: 'B'
This is my template/vue code:
<div class="row" v-for="(subItem, key) in subItems" v-cloak>
<button :class="['btn', 'btn-block', subItem.status == 'H' ? 'accept' : 'resume' : 'pause']" :style="{ border:none, borderRadius: .15 }" v-on:click="pause(subItem)" type="button" role="button" id="" aria-expanded="false">
{{ subItem.status == 'A' ? 'Accept' : 'Resume' : 'Pause' }}
</button>
</div>
data() {
return {
subItems: [],
}
}
You can use a computed property to extend the property on the data object, or you could do this is the mounted method. A computed property will be better as it will change when the data object does.
new Vue({
el: '#app',
computed: {
formattedSubItems() {
return this.subItems.map(si => {
if (si.status === 'A') {
return { ...si,
text: 'Accept',
class: 'accept'
}
} else if (si.status === 'B') {
return { ...si,
text: 'Pause',
class: 'pause'
}
} else if (si.status === 'C') {
return { ...si,
text: 'Resume',
class: 'resume'
}
}
})
}
},
data() {
return {
subItems: [{
id: 123,
status: 'A'
},
{
id: 234,
status: 'B'
}
],
}
}
})
.accept {
color: green
}
.pause {
color: yellow
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="row" v-for="(subItem, key) in formattedSubItems" v-cloak>
<button class="btn btn-block" :class="subItem.class" :style="{ border:none, borderRadius: .15 }" v-on:click="pause(subItem)" type="button" role="button" id="" aria-expanded="false">
{{ subItem.text}}
</button>
</div>
</div>
You could also create a button object that contain your button name with based on your key. Like below example
buttons: {
A: 'Accept',
B: 'Pause',
C: 'Resume'
}
And this buttons object you can use when you looping your subItems.
Please check below working code snippet.
new Vue({
el: '#app',
methods: {
getClass(subItem) {
return this.buttons[subItem.status].toLocaleLowerCase();
},
pause(subItem) {
console.log(this.buttons[subItem.status])
}
},
data() {
return {
subItems: [{
id: 123,
status: 'A'
}, {
id: 234,
status: 'B'
}, {
id: 235,
status: 'C'
}],
buttons: {
A: 'Accept',
B: 'Pause',
C: 'Resume'
}
}
}
})
.accept {
color: green
}
.pause {
color: violet
}
.resume {
color: red
}
.btn-block {
cursor: pointer;
border: 1px solid;
padding: 5px 10px;
margin: 10px;
font-size: 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="row" v-for="(subItem, key) in subItems">
<button class="btn btn-block" :class="getClass(subItem)" #click="pause(subItem)" role="button" aria-expanded="false">
{{ buttons[subItem.status]}}
</button>
</div>
</div>

How can I create dynamic filtering in Vue from a graphql dataset?

How do I create a dynamical filter using a computed property from vue when the dataset is from a graphql-query?
I've looked at several articles that all use the array.filter()-method, but I can't get it to work on my dataset (dummy data below):
books: [{
node: {
title: 'Elon Musk',
by:'Ashlee Vance',
},
node: {
title: 'Steve Jobs',
by:'George Llian',
},
node: {
title: 'Face of Facebook',
by: 'Sandip Paul',
},
node: {
title: 'Tim Cook',
by:'Andy Atkins',
url:'http://www.voidcanvas.com/'
},
node: {
title: 'Abdul Kalam',
by:'Arun Tiwari',
},
node: {
title: 'Story of Elon Musk',
by:'BP John',
},
node: {
title: 'Story of Bill Gates',
by:'Russel Crook',
},
node: {
title: 'Becoming Steve Jobs',
by:'Andrew Russel',
}
}]
Method:
computed: {
filteredBooks: function () {
var books_array = this.books,
searchString = this.searchString;
if(!searchString) {
return books_array;
}
searchString = searchString.trim().toLowerCase();
books_array = books_array.filter(function(item) {
if(item.node.title.toLowerCase().indexOf(searchString) !== -1) {
return item;
}
});
return books_array;
}
HTML:
<div id="app">
<input type="text" v-model="searchString" placeholder="search" />
<ul style="list-style: none;">
<li v-for="book in filteredBooks">
<p>{{book.title}} -by- {{book.by}}</p>
</li>
</ul>
</div>
This is my first coding project since early 2000, so please feel free to point me in the right direction if this is the wrong forum for this question.
I set up a jsfiddle to play with the case.
Here is the code with some modifications:
var app = new Vue({
el: '#app',
data: {
searchString: '',
books: [{
title: 'Elon Musk',
by: 'Ashlee Vance'
},
{
title: 'Steve Jobs',
by: 'George Llian'
},
{
title: 'Face of Facebook',
by: 'Sandip Paul'
},
{
title: 'Tim Cook',
by: 'Andy Atkins',
url: 'http://www.voidcanvas.com/'
},
{
title: 'Abdul Kalam',
by: 'Arun Tiwari'
},
{
title: 'Story of Elon Musk',
by: 'BP John'
},
{
title: 'Story of Bill Gates',
by: 'Russel Crook'
},
{
title: 'Becoming Steve Jobs',
by: 'Andrew Russel'
}
]
},
computed: {
filteredBooks: function() {
return this.books.filter(e => this.searchString === '' ? false : e.title.toLowerCase().indexOf(this.searchString.toLowerCase()) !== -1 ? true : false);
}
}
});
body {
background-color: #dbd8d8;
padding: 20px;
}
input {
width: 300px;
height: 30px;
padding: 0.2rem;
}
.design {}
p {
position: relative;
display: block;
padding: .4em .4em .4em 2em;
margin: .5em 0;
border: 3px solid white;
background: #FC756F;
color: #444;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="searchString" placeholder="search" />
<ul style="list-style: none;">
<li v-for="book in filteredBooks">
<p>{{book.title}} -by- {{book.by}}</p>
</li>
</ul>
</div>
Remove the node: from before the objects in the books data array - books array should contain a bunch of plain objects. If you put node: before each object, then you "say" that the every node is the key of key-value pair of an object (so the keynames will be identical - node!!!)
Simplify filteredBooks computed - no need to store all the variables. This function (filteredBooks) doesn't change the inputs, so you can use this here. The filter() functions doesn't change the array it filters - rather it returns a new array, containing only values that the iteratee function "saw" as true
You check for !searchString and that's never the case - searchString is always going to be true as you initialize it with searchString: '' (an empty value - but a value), so I changed it checking for the empty value in the filteredBooks computed.
I modified the code so that it compares lowercase to lowercase. With your code if someone typed a search string in uppercase, then there'd have been no match.

How to create a favorite list with Vue js. Allowing the user to select maximun 5 options

First of all, I'm new to programming and new to this channel so I'm sorry if the structure of my question is not correct. I can provide more details if needed. Thanks in advance for your help.
Context: I have a view with multiple options (buttons) where the user can select up to 5 of them. Once the user selects the fifth button all the other buttons should get disabled. If the user clicks again on one of the selected buttons, all the other buttons should get enabled again. How can I implement this logic?
I'm using vue js and vuetify.
It's probably not the best solution but I once the user clicks on a button I change the class of that button so it looks as if it was active. Then I count the amount of buttons that have been clicked to disable the rest of the buttons (this is not working).
<v-layout>
<v-flex row wrap>
<v-card class="interests__content">
<!-- looping through interests array -->
<!-- with :class i'm binding classes to change the color of the buttons, with #click="increase()" i'm calling the increase method -->
<v-tooltip color="primary" top v-for="interest in interests" :key="interest.name">
<v-btn
v-model="selected"
flat
slot="activator"
:class="{blue:interest.checked, interest__btn: !interest.checked}"
#click="increase(interest)"
:disabled="disabled"
:checked="checked"
>{{interest.name}}</v-btn>
<span>{{interest.name}}</span>
</v-tooltip>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
data() {
return {
checked: false,
selected: "",
count: 0,
disabled: false,
interests: [
{ name: "Interest1", checked: false },
{ name: "Interest2", checked: false },
{ name: "Interest3", checked: false },
{ name: "Interest4", checked: false },
{ name: "Interest5", checked: false },
{ name: "Interest6", checked: false },
{ name: "Interest7", checked: false },
{ name: "Interest8", checked: false },
{ name: "Interest9", checked: false },
]
};
},
methods: {
increase: function(interest) {
this.count += 1;
//changing the value of checked to add the blue class, with this class I add the background color to the button as if it was active.
interest.checked = !interest.checked;
if (this.count > 4) {
this.disabled = !this.disabled;
}
},
// I'm trying to check if the button has a class to implement the logic
checkIfClass: function(interest) {
interest.checked = !interest.checked;
if (interest.classList.contains("blue"));
}
},
computed: {}
};
</script>
<style scoped>
.interest__btn {
font-family: Arial;
font-size: 1em;
background: white;
color: #333333;
border: 1px solid #0091da;
text-transform: none;
}
.interest__btn:hover {
color: black;
background-color: rgba(172, 196, 221, 0.7);
}
.interests__content {
padding: 1.7em;
}
.blue {
background-color: #0091da;
color: white !important;
text-transform: none;
}
</style>
The Vue way of doing this is to have all the state of your application in js (your interests array), then to make everything that handles the display of this state reactive. In practice this means using computed rather than methods for everything that turns state into pixels on screen.
In da old days, we would have looped through interests to count the number checked. In 2019 we'll use reduce(), so...
computed:{
numChecked(){
return this.interests.reduce((acc,curr)=>acc += curr.checked?1:0,0)
}
}
Then in your template...
:disabled="(numChecked > 5 && !interest.checked)?'disabled':''"
I finally came up with a solution, basically using a watcher to know when the counter was greater than 5 and adding another value-pair to handle the disable property:
`
{{interest.name}}
<span>{{interest.name}}</span>
</v-tooltip>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
data() {
return {
checked: false,
selected: "",
count: 0,
disabled: false,
interests: [
{ name: "Interest1", checked: false, disabled: false },
{ name: "Interest2", checked: false, disabled: false },
{ name: "Interest3", checked: false, disabled: false },
{ name: "Interest4", checked: false, disabled: false },
{ name: "Interest5", checked: false, disabled: false },
{ name: "Interest6", checked: false, disabled: false },
{ name: "Interest7", checked: false, disabled: false },
]
};
},
methods: {
increase: function(interest) {
//changing the value of checked to add the blue class, with this class I add
the background color to the button as if it was active.
interest.checked = !interest.checked;
if (interest.checked) {
this.count++;
} else {
this.count--;``
}
}
},
watch: {
count: function(n, o) {
if (this.count > 4) {
for (let i = 0; i < this.interests.length; i++) {
if (!this.interests[i].checked) {
this.interests[i].disabled = true;
} else {`
this.interests[i].disabled = false;
}
}
} else {
for (let i = 0; i < this.interests.length; i++) {
this.interests[i].disabled = false;
}
}
}
}
};
</script>
<style scoped>
.interest__btn {
font-family: Arial;
font-size: 1em;
background: white;
color: #333333;
border: 1px solid #0091da;
text-transform: none;
}
.interest__btn:hover {
color: black;
background-color: rgba(172, 196, 221, 0.7);
}
.interests__content {
padding: 1.7em;
}
.blue {
background-color: #0091da;
color: white !important;
text-transform: none;
}
</style>

Categories