I have an issue I can't understand and solve.
I have simple page in vue - set of checkboxes bound via v-model to array.
This is how it looks like:
<template>
<div id="app">
<b-form-checkbox-group
switches
stacked
v-model="selectedFruits"
:options="options"
#change="selectionChanged"
>
</b-form-checkbox-group>
{{ selectedFruits }}
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
selectedFruits: [],
options: [
{
text: 'Apple',
value: {id: 1, val: 'apple'},
},
{
text: 'Banana',
value: {id: 2, val: 'banana'},
},
{
text: 'Pear',
value: {id: 3, val: 'pear'},
},
{
text: 'Plum',
value: {id: 4, val: 'plum'},
}
]
}
},
methods: {
selectionChanged() {
console.log(this.selectedFruits);
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Nothing difficult, as you can see.
It works great on the screen - {{ selectedFruits }} are displayed correctly.
But when I check the value of selectedFuits in console I see different results than displayed - console shows "one click late" results.
Can you explain the issue and direct me, how to solve it, please?
I think you're dumping the value right before Vue updates it. If you want to be in sync with Vue, you should watch the variable instead of implementing your own #change:
<b-form-checkbox-group
switches
stacked
v-model="selectedFruits"
:options="options"
>
...
watch: {
selectedFruits(val) {
console.log(val); // or this.selectedFruits
}
}
You could also probably use Vue.nextTick(() => console.log(this.selectedFruits)) with your current code, but still, when you're using v-model, I think you should not implement your own events for the same thing.
(an aside: v-model just listens for #input - it's possible b-form-checkbox-group just emits this before emitting #change. with watch, you don't have to care about this kind of detail because it is synced with Vue's update cycle)
Related
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.
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>
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.
Here is an example fiddle:
https://jsfiddle.net/40fxcuqd/
Initially, it displays "Carl"
If I select Carol, Clara etc, then an event will fire and data will print to the console.
But if I click the dropdown and choose "Carl", no event will fire, and nothing will print to the console.
The event I'm using is #input:
<select v-model="selectedPerson" #input="myEvent()">
How can I get an event to fire every time something is selected, even if it's the same value?
Edit:
To clarify, when "Carl" is initially selected:
and then the dropdown is opened:
and then Carl is selected again, I would like an event to be triggered and a print to the console. My issue at the moment is no event is triggered, and nothing prints to the console.
That is because the selected option by default is 1, then nothing change when you click on Carl, you must use #change event and if you want to get Carl value when you do click should use placeholder on select option.
new Vue({
el: '#app',
template: `
<div>
<select v-model="selectedPerson" #change="myEvent()">
<option :value="null" disabled hidden>Select option</option>
<option v-for="person in people" :value="person.key" :selected="person.key == selectedPerson">{{person.name}}</option>
</select>
</div>
`,
data: {
people: [
{key: 1, name: "Carl"},
{key: 2, name: "Carol"},
{key: 3, name: "Clara"},
{key: 4, name: "John"},
{key: 5, name: "Jacob"},
{key: 6, name: "Mark"},
{key: 7, name: "Steve"}
],
selectedPerson: null
},
methods: {
myEvent: function() {
console.log(this.selectedPerson);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
Really hacky but does the job, I've used #click and event.detail or event.which:
new Vue({
el: '#app',
template: `
<div>
<select v-model="selectedPerson" #input="myEvent($event)" #click="myEvent($event)">
<option v-for="person in people" :value="person.key" :selected="person.key == selectedPerson">{{person.name}}</option>
</select>
</div>
`,
data: {
people: [{
key: 1,
name: "Carl"
},
{
key: 2,
name: "Carol"
},
{
key: 3,
name: "Clara"
},
{
key: 4,
name: "John"
},
{
key: 5,
name: "Jacob"
},
{
key: 6,
name: "Mark"
},
{
key: 7,
name: "Steve"
}
],
selectedPerson: 1
},
methods: {
myEvent: function(e) {
if (e.detail == 0)//if (e.which == 0)
console.log(e.type, this.selectedPerson);
}
}
});
body {
margin: 20px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.as-console-wrapper {
height: 39px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="app"></div>
A less hacky way using data:
new Vue({
el: '#app',
template: `
<div>
<select v-model="selectedPerson" #input="myEvent($event)" #click="myEvent($event)">
<option v-for="person in people" :value="person.key" :selected="person.key == selectedPerson">{{person.name}}</option>
</select>
</div>
`,
data: {
people: [{
key: 1,
name: "Carl"
},
{
key: 2,
name: "Carol"
},
{
key: 3,
name: "Clara"
},
{
key: 4,
name: "John"
},
{
key: 5,
name: "Jacob"
},
{
key: 6,
name: "Mark"
},
{
key: 7,
name: "Steve"
}
],
selectedPerson: 1,
prev: 0,
isChanged: false
},
methods: {
myEvent: function(e) {
if (e.type == "input" || (e.type == "click" && !this.isChanged && (this.prev == this.selectedPerson || this.prev == 0))) {
this.isChanged = true;
this.prev = 0;
} else if (e.type == "click" && this.isChanged) {
console.log(e.type, this.selectedPerson);
this.prev = this.selectedPerson;
this.isChanged = false;
}
}
}
});
body {
margin: 20px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.as-console-wrapper {
height: 39px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="app"></div>
when you change the dropdown you will get the index of people array and you can do something like this to get value
myEvent: function() {
console.log(this.people[this.selectedPerson].name);
}
}
one workaround is to set selected to a not used value when focus, then change event will fire no matter which option is selected.
<select
v-model="selectedPerson"
ref="s"
#focus="selectedPerson = 0"
#change="myEvent()"
>
see fiddle: https://jsfiddle.net/tne1wp3q/
it's not perfect though, the change event will be fired multiple times with each click, and if no options were selected, it could left blank. Need more code to filter these behaviour.
I have a repeater of items in a Enyo app. In the current setup tapping the repeater item doesn't show a transition or visual cue to show it's been tapped.
So what I tried is setting the class of the repeater item passed in from the ontap event on the repeater.
But stepping into Dev tools tells me that the property addClass is not defined:
IncidentTickets.js:653 Uncaught TypeError: Cannot read property 'addClass' of undefined
Question:
How can you style an Enyo Repeater item background color ontap?
Basically I set a CSS style background color to green:
.tapped {
background: #0CA281;
}
The tried to add the CSS class to that repeater item passed into the onTap event:
tapPopupBundle: function(inSender, inEvent) {
var item = this.$.repeaterBundle.itemAtIndex(inEvent.index);
this.item.addClass('tapped');
},
And this is the definition of the repeater kind:
{kind: "Scroller", name: "scrollerBundle", touch: true, horizontal:"hidden", showing: false, style: "height: 70%", components: [
{content:"All Apps", style: "font-size: 12px; font-weight: bold; margin-left: 20px; text-decoration:underline;",ontap: 'clearFilters'},
{kind: "Repeater", name: "repeaterBundle", count: 0, onSetupItem: "setupBundle", ontap: "tapPopupBundle", components: [
{tag: "br"},
{ kind: "FittableColumns", components: [
{name: "repBundle", style: "font-size: 12px; font-weight: bold; margin-left: 20px;"}
]}
]},
]},
Grabbing the passed in item id and calling appLlyStyle worked in this case:
item.$.repAppName.applyStyle("background-color", 'rgb(12,162,129)');