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.
Related
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)
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.
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
I have one ListView and one GridView. Imagine it like this: The first view presents the categories and the second view presents article in each category. I want to dynamically change the data model of the GridView when the current index of the ListView change by javascript. How do we do that?
You simply need to assign a new model. Here's one example, based on the ListModel docs. This model shows the fruit in the model in the ListView on the left. When a delegate is clicked, it sets the model for the GridView on the right to the list defined by the attributes role.
import QtQuick 1.0
Item {
width: 600; height: 400
ListView {
width: 300; height: 400
model: fruitModel
delegate: Text {
font.pixelSize: 20
text: name
width: 300
MouseArea {
anchors.fill: parent
onClicked: grid.model = attributes
}
}
}
GridView {
id: grid
x: 300; width: 300; height: 400
delegate: Text {
text: description
font.pixelSize: 20
}
}
ListModel {
id: fruitModel
ListElement {
name: "Apple"
cost: 2.45
attributes: [
ListElement { description: "Core" },
ListElement { description: "Deciduous" }
]
}
ListElement {
name: "Orange"
cost: 3.25
attributes: [
ListElement { description: "Citrus" }
]
}
ListElement {
name: "Banana"
cost: 1.95
attributes: [
ListElement { description: "Tropical" },
ListElement { description: "Seedless" }
]
}
}
}
This is an example of a nested model, but there are other possibilities. For example, if you're sourcing your data from a database, perhaps you just need to change the query used by the GridView's model, rather than setting a different model.
Depends on your models. Assuming CategoriesModel has category role, and ArticlesModel has setCategory method:
ListView {
model: CategoriesModel {}
delegate: Item {
MouseArea {
anchors.fill: parent
onClicked: {
grid_view.model.setCategory(model.category)
}
}
// ...
}
}
GridView {
id: grid_view
model: ArticlesModel {}
// ...
}