Only one button active at a time depending on state - javascript

I am writing an app in Vue.js and cannot find a solution to my problem.
Here's my codeSandbox
I have an array of objects, that I transform into an object with false values for each key.
sizesArray: [
{ number: "30", id: 1 },
{ number: "36", id: 2 },
{ number: "40", id: 3 }
],
sizesObject: {
"30": false,
"36": false,
"40": false
},
I can target each value individually, but how can I toggle the buttons so that only one value is true at a time, instead of being toggled individually as I have now?
It seems that For-in loop or a watcher would be great for that, I'm just not sure how to approach it.
I've been sitting on this problem for a couple of hours, and cannot seem to find something similar on stack overflow. Thank you!

You can simply loop through all entries on any click and set them to false. After that you could set the correct one to true. That's just a pretty rudimentary solution and could be improved. However, it visualizes the way to go.
for (const o in this.sizesObject) {
this.sizesObject[o] = false;
}
this.sizesObject[sizeNumber] = true;
https://codesandbox.io/s/wispy-bird-chk35

Arguably a better approach than the loop method is to just store a reference to the currently active item and reset it on new selection. See CodeSandbox Example.
toggle: function(number) {
if (this.activeIndex !== null && this.activeIndex !== number)
this.sizesObject[this.activeIndex] = false;
this.sizesObject[number] = !this.sizesObject[number];
this.activeIndex = number;
}
And call it:
<button
v-for="size in this.sizesArray"
#click="toggle(size.number)"
:class="{active: sizesObject[size.number]}"
:key="size.key"
>{{size.number}}</button>

You don't need sizesObject in order to implement that logic.
Instead, you can add a data "selected" which stores the selected/clicked button's "id".
And you can choose the active class by checking whether the "selected" value is equal to "id" or not.
This is the full code,
<template>
<div class="hello">
<button
v-for="size in this.sizesArray"
#click="setSelected(size.id)"
:class="{active: selected === size.id}"
:key="size.key"
>{{size.number}}</button>
<br>
<br>
{{sizesObject}}
{{selected}}
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
sizesArray: [
{ number: "30", id: 1 },
{ number: "36", id: 2 },
{ number: "40", id: 3 }
],
selected: null
};
},
methods: {
setSelected: function(value) {
this.selected = value;
}
},
props: {
msg: String
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.active {
background-color: rebeccapurple;
color: white;
}
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

Related

Using vue-virtual-scroller not showing

I'm trying out https://github.com/Akryum/vue-virtual-scroller but I cannot get it to work. What am I doing wrong?
I use pug / jade for server-side HTML rendering. I don't receive any error messages but nothing gets rendered...
Pug / Jade
#test
recyclescroller.scroller(:items='active_projects' :item-size='32' key-field='id' v-slot='{ item }')
.user
| {{ item.name }}
CSS
.user {
height: 32%;
padding: 0 12px;
display: flex;
align-items: center;
}
javascript
Vue.component('RecycleScroller', VueVirtualScroller.RecycleScroller)
test = new Vue({
el: '#test',
data: {
active_projects : [{name : 'test'}]
}
})
When you use Recyclescroller you should inside each item has id
Because the uniqueness of the item is done by using the id
Your list should look like this :
data () {
return {
active_projects: [
{
name: 'test', id: 1
},
{
name: 'test', id: 2
},
{
name: 'test', id: 3
},
{
name: 'test', id: 4
},
]
}
}
Doc - Important notes
If the items are objects, the scroller needs to be able to identify them. By default it will look for an id field on the items. This can be configured with the keyField prop if you are using another field name.

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

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;
},

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.

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>

Pass an Array in ListModel

I'm wondering how can I pass an array in ListModel?
ok, In QML I have a ListView and I set it's ListModel like so:
model: ListModel
{
id: myList
ListElement
{
name: ""
card: 0
books: []
}
}
I can append to it by using:
myList.append({name:"terry", card:00100, books:["024589","865976","879582","215645"]});
but when I try to output it on screen I get this.
{
"card": 00100
"books": {
"objectName": "",
"count": 4,
"dynamicRoles": false
},
"name": "terry",
"name": "terry"
}
I'm not sure why I'm getting 2 names though! and how can I get the value of books?
I look up the QML documentation of ListModel and ListElement couldn't find anything related to passing an array, all the examples are integer or string.
Any idea how I can get the date?
I did work around it by calling the array in Delegate with Component.onCompleted:{} but I believe that's not a good/correct way since Delegate is not responsible for holding the data and should be done in Model, please do correct me if I'm wrong.
Thanks for your time.
Edit01: Thanks for the reply, here is the reason I need array:
I have a ComboBox in Delegate like so:
delegate: Rectangle
{
id: rowID
width: 50
height: 40
color: "#323232"
Row
{
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
Label{
id: nameID
text: name
font.pixelSize: 12
width: 200
wrapMode: Text.WrapAnywhere
anchors.verticalCenter: parent.verticalCenter
color: "#999"
}
Label{
anchors.verticalCenter: parent.verticalCenter
text: "out:"
font.pixelSize: 12
color: "#999"
}
ComboBox{
id: booksID
height: 20
width: 50
model: books
anchors.verticalCenter: parent.verticalCenter
}
}
}
as you can see I'm feeding the name to Label (id: nameID) and I want to feed the books to ComboBox (id: booksID) that has model, if I make books key as ListElement how can I feed all the values?
in QML ListModel or ListElement documentation didn't mention anything about getting all the key's value right? it only supports get(int index) which is based on an index number.
You did that wrong. Array members must be ListElement:
ListModel {
id: mod
ListElement {
name: "ali"
dic: [ ListElement{text:"asad-o-llah"; code: 14}, ListElement{text:"aboo torab"; code: 72}, ListElement{text:"amir al-momenin"; code: 110}]
}
}
ListView {
model: mod
anchors.fill: parent
delegate: Component {
Rectangle {
width: parent.width; height: 50
Row {
Text {
text: name
}
ComboBox {
width: 100; height: 30
model: dic //<-- set dic as model for combo box
textRole: "text" //<-- important!
onCurrentIndexChanged: {
console.log("current code is "+model.get(currentIndex).code); //<-- get code value
}
}
}
}
}
}
Component.onCompleted: {
var v = mod.get(0).dic.get(0).value; //<-- sample usage
console.log(v);
}
Do you want some thing similar to this:
Rectangle {
id: root
visible: true
width: 360
height: 360
ListModel
{
id: myList
ListElement {
name: "Story"
card: 3
books: [
ListElement { bookName: "Story 1" },
ListElement { bookName: "Story 2" },
ListElement { bookName: "Story 3" }
]
}
ListElement {
name: "Novel"
card: 3
books: [
ListElement { bookName: "Novel 1" },
ListElement { bookName: "Novel 2" },
ListElement { bookName: "Novel 3" }
]
}
}
Component {
id: displayDelegate
Rectangle
{
id: rowID
width: 300 //50
height: 40
color: "#323232"
border.color: "white"
Row
{
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
Text{
id: nameID
text: name
font.pixelSize: 12
width: 50 //200
wrapMode: Text.WrapAnywhere
/*anchors.verticalCenter: parent.verticalCenter*/
color: "white"//"#999"
}
Text{
/*anchors.verticalCenter: parent.verticalCenter*/
text: "out:"
font.pixelSize: 12
color: "white"//"#999"
}
/*ComboBox{
id: booksID
height: 20
width: 50
model: books
anchors.verticalCenter: parent.verticalCenter
}*/
Repeater {
model: books
Text { text: bookName + "\t"; color: "white" }
}
}
}
}
ListView {
id: disp
anchors.fill: parent
model: myList
delegate: displayDelegate
}
}
I have modified few line of the code which you have shared. I am not sure about your ComboBox implementation. Therefore, I have used my own implementation of Repeater. You can try to execute and check the result.
As an alternative to working with ListModel and ListElement, you can also have a look at the QSyncable JsonListModel QML type. (It is an open-source component by Ben Lau, you can find it on GitHub here: https://github.com/benlau/qsyncable)
The JsonListModel is a specialized ListModel type, that can handle JSON Arrays that you e.g. create in your QML or fetch from a REST service. It automatically synchronizes the JSON to a QML ListModel, so its very convenient to use:
ListView {
id: listView
anchors.fill: parent
// property for json data, used as source for JsonListModel
property var jsonData: []
// use JsonListModel as model
model: JsonListModel {
source: listView.jsonData
keyField: "id"
}
// delegate
delegate: DelegateItem { /* ... */ }
}
You can also find a comprehensive guide how it works here: JsonListModel guide

Categories