I tried to make my Load More data when my page scroll to the bottom. The first thing is I make a div element that I put at the end of the data loop.
<div class="products">
<p>{{ status }}</p>
<div class="product" v-for="(item, index) in items">
<div>
<div class="product-image"><img :src="item.link" alt=""></div>
</div>
<div>
<h4 class="product-title">{{ item.title }}</h4>
<p>Price : {{ price }}</p>
<button class="add-to-cart btn" #click="addItem(index)">Add Item To Cart</button>
</div>
</div>
<div id="product-list-bottom"></div>
</div>
Div element with id product-list-bottom I will detect it using scrollMonitor.js
My default data :
data: {
status: 'Empty product',
total: 0,
items: [],
cart: [],
newSearch: 'anime',
lastSearch: '',
price: STATIC_PRICE,
result: []
}
Inside mounted I detected scroll to bottom :
mounted: function() {
this.onSubmit()
var vueInstance = this
var elem = document.getElementById('product-list-bottom')
var watcher = scrollMonitor.create(elem)
watcher.enterViewport(function() {
vueInstance.appendItems()
})
}
Inside mounted I call onSubmit :
onSubmit: function() {
this.items = ''
this.status = "Searching keyword '" + this.newSearch + "' on server ..."
this.$http.get('/search/'.concat(this.newSearch))
.then(function(response) {
this.lastSearch = this.newSearch,
this.status = 'Find ' + response.data.length + ' data'
this.result = response.data
this.appendItems()
})
}
And inside onSubmit I call appendItems function :
appendItems: function() {
if(this.items.length < this.result.length) {
var start = this.items.length
var end = parseInt(this.items.length + 5)
var append = this.result.slice(start, end)
this.items = this.items.concat(append)
console.log(append)
}
}
All goes well, but when I scroll down I get an error message :
This is because this line :
this.items = this.items.concat(append)
How do I make the data on xxx change (always added five new data from the array) according to the command on the line :
var end = parseInt(this.items.length + 5)
Thanks
it seems '/search/'.concat(this.newSearch) gets evaluated into function and not an actual string value
Try this if you are using babel/webpack
this.$http.get(`/search/`${this.newSearch}`)
Or if not
this.$http.get('/search/' + this.newSearch)
I think since Vue 2.3+ or so you can get this done without any jQuery stuff or any other dependencies:
<style>
.scrollbar{
overflow-y: scroll;
//...
}
.styled-scrollbar::-webkit-scrollbar
.styled-scrollbar::-webkit-scrollbar-thumb
.styled-scrollbar::-webkit-scrollbar-track{
//styling
}
</style>
<template>
//...
<div #scroll="scroll" class="scrollbar">
<div v-for="item in items" :key="item.id">
//TODO: item content
</div
</div>
//...
</template>
<script>
{
data: {
//..
lastScrollUpdate:0
}
//..
mounted: {
scroll:function (e) {
var scrollBar=e.target;
if((scrollBar.scrollTop + scrollBar.clientHeight >= scrollBar.scrollHeight-20)){
var t=new Date().getTime();
if((t-this.lastScrollUpdate)>3000){
this.lastScrollUpdate=t;
console.log('reached end: '+scrollBar.scrollTop+' '+scrollBar.clientHeight+' '+scrollBar.scrollHeight);
//TODO: load more data
}else{
console.log("< 3sec between scoll. no update");
}
}
},
//..
}
}
</script>
You may also want to adjust this to "#scroll.passive", in order to let the scroll-function be executed parallel to the UI (https://v2.vuejs.org/v2/guide/events.html#Event-Modifiers)
Related
It is not like it is slow on rendering many entries. The problem is that whenever the $scope.data got updated, it adds the new item first at the end of the element, then reduce it as it match the new $scope.data.
For example:
<div class="list" ng-repeat="entry in data">
<h3>{{entry.title}}</h3>
</div>
This script is updating the $scope.data:
$scope.load = function() {
$scope.data = getDataFromDB();
}
Lets say I have 5 entries inside $scope.data. The entries are:
[
{
id: 1,
title: 1
},
{
id: 2,
title: 2
},
......
]
When the $scope.data already has those entries then got reloaded ($scope.data = getDataFromDB(); being called), the DOM element for about 0.1s - 0.2s has 10 elements (duplicate elements), then after 0.1s - 0.2s it is reduced to 5.
So the problem is that there is delay about 0.1s - 0.2s when updating the ng-repeat DOM. This looks really bad when I implement live search. Whenever it updates from the database, the ng-repeat DOM element got added up every time for a brief millisecond.
How can I make the rendering instant?
EDITED
I will paste all my code here:
The controller:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = [];
$scope.loadViewModels($scope.orderBy, table);
}
$scope.loadViewModels = function (orderBy, table, cb) {
if (!$scope.endOfPage) {
let searchKey = $scope.page.searchString;
let skip = ($scope.currentPage - 1) * $scope.itemsPerPage;
let searchClause = '';
if (searchKey && searchKey.length > 0) {
let searchArr = [];
$($scope.vmKeys).each((i, key) => {
searchArr.push(key + ` LIKE '%` + searchKey + `%'`);
});
searchClause = `WHERE ` + searchArr.join(' OR ');
}
let sc = `SELECT * FROM ` + table + ` ` + searchClause + ` ` + orderBy +
` LIMIT ` + skip + `, ` + $scope.itemsPerPage;
sqlite.query(sc, rows => {
$scope.$apply(function () {
var data = [];
let loadedCount = 0;
if (rows != null) {
$scope.currentPage += 1;
loadedCount = rows.length;
if (rows.length < $scope.itemsPerPage)
$scope.endOfPage = true
for (var i = 0; i < rows.length; i++) {
let item = rows.item(i);
let returnObject = {};
$($scope.vmKeys).each((i, key) => {
returnObject[key] = item[key];
});
data.push(returnObject);
}
$scope.viewModels = $scope.viewModels.concat(data);
}
else
$scope.endOfPage = true;
if (cb)
cb(loadedCount);
})
});
}
}
The view:
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
<div class="row note-list" ng-if="showList">
<h3>Notes</h3>
<input ng-model="page.searchString" id="search"
ng-keyup="search('notes')" type="text" class="form-control"
placeholder="Search Notes" style="margin-bottom:10px">
<div class="col-12 note-list-item"
ng-repeat="data in viewModels track by data.id"
ng-click="edit(data.id)"
ontouchstart="touchStart()" ontouchend="touchEnd()"
ontouchmove="touchMove()">
<p ng-class="deleteMode ? 'note-list-title w-80' : 'note-list-title'"
ng-bind-html="data.title"></p>
<p ng-class="deleteMode ? 'note-list-date w-80' : 'note-list-date'">{{data.dateCreated | displayDate}}</p>
<div ng-if="deleteMode" class="note-list-delete ease-in" ng-click="delete($event, data.id)">
<span class="btn fa fa-trash"></span>
</div>
</div>
<div ng-if="!deleteMode" ng-click="new()" class="add-btn btn btn-primary ease-in">
<span class="fa fa-plus"></span>
</div>
</div>
<div ng-if="!showList" class="ease-in">
<div>
<div ng-click="back()" class="btn btn-primary"><span class="fa fa-arrow-left"></span></div>
<div ng-disabled="!isDataChanged" ng-click="save()" class="btn btn-primary" style="float:right">
<span class="fa fa-check"></span>
</div>
</div>
<div contenteditable="true" class="note-title"
ng-bind-html="selected.title" id="title">
</div>
<div contenteditable="true" class="note-container" ng-bind-html="selected.note" id="note"></div>
</div>
</div>
<script src="../js/pages/note.js"></script>
Calling it from:
$scope.loadViewModels($scope.orderBy, 'notes');
The sqlite query:
query: function (query, cb) {
db.transaction(function (tx) {
tx.executeSql(query, [], function (tx, res) {
return cb(res.rows, null);
});
}, function (error) {
return cb(null, error.message);
}, function () {
//console.log('query ok');
});
},
It is apache cordova framework, so it uses webview in Android emulator.
My Code Structure
<html ng-app="app" ng-controller="pageController">
<head>....</head>
<body>
....
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
....
</div>
</body>
</html>
So there is controller inside controller. The parent is pageController and the child is noteController. Is a structure like this slowing the ng-repeat directives?
Btw using track by is not helping. There is still delay when rendering it. Also I can modify the entries as well, so when an entry was updated, it should be updated in the list as well.
NOTE
After thorough investigation there is something weird. Usually ng-repeat item has hash key in it. In my case ng-repeat items do not have it. Is it the cause of the problem?
One approach to improve performance is to use the track by clause in the ng-repeat expression:
<div class="list" ng-repeat="entry in data track by entry.id">
<h3>{{entry.title}}</h3>
</div>
From the Docs:
Best Practice: If you are working with objects that have a unique identifier property, you should track by this identifier instead of the object instance, e.g. item in items track by item.id. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones. For large collections, this significantly improves rendering performance.
For more information, see
AngularJS ngRepeat API Reference -- Tracking and Duplicates
In your html, try this:
<div class="list" ng-repeat="entry in data">
<h3 ng-bind="entry.title"></h3>
</div>
After thorough research, I found my problem. Every time I reset / reload my $scope.viewModels I always assign it to null / empty array first. This what causes the render delay.
Example:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = []; <------ THIS
$scope.loadViewModels($scope.orderBy, table);
}
So instead of assigning it to null / empty array, I just replace it with the new loaded data, and the flickering is gone.
I'm making a blog and would like the user to be able to create new textareas when they hit enter and for it to autofocus on the newly created textarea. I've tried using the autofocus attribute, but that doesn't work. I've also tried using the nextTick function, but that doesn't work. How do I do this?
<div v-for="(value, index) in content">
<textarea v-model="content[index].value" v-bind:ref="'content-'+index" v-on:keyup.enter="add_content(index)" placeholder="Content" autofocus></textarea>
</div>
and add_content() is defined as follows:
add_content(index) {
var next = index + 1;
this.content.splice(next, 0, '');
//this.$nextTick(() => {this.$refs['content-'+next].contentTextArea.focus()})
}
You're on the right path, but this.$refs['content-'+next] returns an array, so just access the first one and call .focus() on that
add_content(index) {
var next = index + 1;
this.content.splice(next, 0, {
value: "Next"
});
this.$nextTick(() => {
this.$refs["content-" + next][0].focus();
});
}
Working Example
var app = new Vue({
el: '#app',
data() {
return {
content: [{
value: "hello"
}]
};
},
methods: {
add_content(index) {
var next = index + 1;
this.content.splice(next, 0, {
value: "Next"
});
this.$nextTick(() => {
this.$refs["content-" + next][0].focus();
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(value, index) in content">
<textarea v-model="content[index].value" v-bind:ref="'content-' + index" v-on:keyup.enter="add_content(index);" placeholder="Content" autofocus></textarea>
</div>
</div>
Also, your value in the array seems to be an object rather than a string, so splice in an object rather than an empty string
I'm trying to change the HTML received from a database to respond to custom onClick handlers. Specifically, the HTML I pull has divs called yui-navsets which contain yui_nav page selectors and yui_content page contents. I want to click an li in yui_nav, set that li's class to "selected", set the existing content to display:none, and set the new content to style="".
To do this, I have created a function updateTabs which inputs the index of the chosen yui and the new page number, set that li's class to "selected", set the existing content to display:none, and set the new content to style="". This function works: I tried running updateTabs(2, 3) in componentDidUpdate, and it worked fine, changing the content as requested. I want to assign updateTabs to each of the lis, and I attempt to do so in my componentDidMount after my axios request.
However, I keep getting the error: TypeError: this.updateTabs is not a function. Please help?
Page.js:
import React, { Component } from 'react';
import axios from 'axios';
class Page extends Component {
constructor(props) {
super(props);
this.state = {
innerHTML: "",
pageTags: [],
};
console.log(this.props.url);
}
componentDidMount() {
console.log(this.props.url);
axios
.get(
this.props.db_address + "pages?url=" + this.props.url,
{headers: {"Access-Control-Allow-Origin": "*"}}
)
.then(response => {
this.setState({
innerHTML: response.data[0].html,
pageTags: response.data[1]
});
console.log(response);
// Check for yui boxes, evade the null scenario
var yui_sets = document.getElementsByClassName('yui-navset');
if (yui_sets !== null) {
let yui_set, yui_nav, yui_content;
// Iterate through the navs of each set to find the active tabs
for (var yui_set_count = 0; yui_set_count < yui_sets.length; yui_set_count ++) {
yui_set = yui_sets[yui_set_count];
yui_nav = yui_set.getElementsByClassName('yui-nav')[0].children;
yui_content = yui_set.getElementsByClassName('yui-content')[0].children;
let tab_count;
// Give each nav and tab and appropriate ID for testing purposes
for (tab_count = 0; tab_count < yui_nav.length; tab_count ++) {
yui_nav[tab_count].onclick = function() { this.updateTabs(yui_set_count); }
yui_nav[tab_count].id = "nav-"+ yui_set_count.toString() + "-" + tab_count.toString()
yui_content[tab_count].id = "content-"+ yui_set_count.toString() + "-" + tab_count.toString()
}
}
}
})
.catch(error => {
this.setState({ innerHTML: "ERROR 404: Page not found." })
console.log(error);
});
}
updateTabs(yui_index, tab_index){
// Get all yuis
var yui_sets = document.getElementsByClassName('yui-navset');
let yui_set, yui_nav, yui_content
yui_set = yui_sets[yui_index];
yui_nav = yui_set.getElementsByClassName('yui-nav')[0].children;
yui_content = yui_set.getElementsByClassName('yui-content')[0].children;
// Identify the current active tab
var current_tab_found = false;
var old_index = -1;
while (current_tab_found == false) {
old_index += 1;
if (yui_nav[old_index].className === "selected") {
current_tab_found = true;
}
}
// Identify the new and old navs and contents
var yui_nav_old = yui_nav[old_index]
var yui_nav_new = yui_nav[tab_index]
var yui_content_old = yui_content[old_index]
var yui_content_new = yui_content[tab_index]
// Give the new and old navs and contents their appropriate attributes
yui_nav_old.className = "";
yui_nav_new.className = "selected";
yui_content_old.style = "display:none";
yui_content_new.style = "";
}
render() {
return (
<div className="Page">
<div className="Page-html col-12" dangerouslySetInnerHTML={{__html:this.state.innerHTML}} />
<div className="Page-footer">
<div className="d-flex flex-wrap btn btn-secondary justify-content-around">
{this.state.pageTags.map(function(pageTag){return(
<div className="pd-2" key={pageTag.id}>
{pageTag.name}
</div>
)})}
</div>
<div className="d-flex justify-content-center" >
<div className="p-2">Discuss</div>
<div className="p-2">Rate</div>
<div className="p-2">Edit</div>
</div>
<div className="d-flex justify-content-around App">
<div className="p-2">
Unless otherwise stated, the content
of this page is licensed under <br />
<a href="http://creativecommons.org/licenses/by-sa/3.0/"
target="_blank" rel="noopener noreferrer">
Creative Commons Attribution-ShareAlike 3.0 License
</a>
</div>
</div>
</div>
</div>
)
}
}
export default Page
Instead of function with function keyword use arrow functions and it will be solved as follows
You have
yui_nav[tab_count].onclick = function() { this.updateTabs(yui_set_count); }
But use
yui_nav[tab_count].onclick = () => { this.updateTabs(yui_set_count); }
Use this in componentDidMount method
You have to bind the updateTabs method in the constructor:
constructor(props) {
super(props);
...
this.updateTabs = this.updateTabs.bind(this);
}
You should use arrow functions in order to call this method with the correct contetxt:
yui_nav[tab_count].onclick = () => { this.updateTabs(yui_set_count); }
I have this Orders array and on the inner div order, I check which status it has.
<div class="list-group" v-for="(order, key) in orders">
<div class="order" v-if="checkOrderStatus(order.status)">
<div class="dishes">
<ul id="dishes" v-for="dish in order.dishes" >
<li v-if="checkDishOnOrder(dish)">{{checkDishOnOrder(dish).quantity}} x {{checkDishOnOrder(dish).dishName}}</li>
</ul>
</div>
<div class="notInclude">
<ul id="notInclude" v-for="dish in order.dishes" >
<li v-if="checkForNotInclude(dish)">{{checkForNotInclude(dish)}}</li>
</ul>
</div>
<div class="table">
<center><span id="table">{{order.tableID}}</span></center>
</div>
<div class="hour">
<center><span id="hour">{{order.hour}}</span></center>
</div>
<div class="status">
<center><button type="button" id="status" :class="{'doingOrder' : order.status == 'Pedido pronto', 'orderDone' : order.status == 'Pronto para entrega'}" #click="changeStatus(order)">{{order.status}}</button></center>
</div>
</div>
</div>
On the beforeCreate: I binded this array with a firebase ref:
this.$bindAsArray('orders', database.ref('orders/' + user.uid).orderByChild('hourOrder'))
The problem is, every time I change a order status the last element of the array changes together and it should not happen.
Here is my checkOrderStatus: function:
checkOrderStatus: function(orderStatus) {
if(this.orderType == 'Em andamento') {
if(orderStatus != "Pronto para entrega") {
return true
}
} else if (this.orderType == 'Pedidos feitos') {
if(orderStatus == "Pronto para entrega") {
return true
}
}
},
Here is changeStatus: function:
changeStatus: function(order) {
var that = this;
var database = Firebase.database();
var loggedUser = Firebase.auth().currentUser;
if (order.status == 'Em andamento') {
order.status = 'Pedido pronto';
var orderKey = order['.key'];
delete order['.key'];
var updates = {};
updates['orders/' + loggedUser.uid + '/'+ orderKey] = order;
database.ref().update(updates).then(function() {
console.log('order Updated');
})
}
else if(order.status == 'Pedido pronto') {
order.status = 'Pronto para entrega';
var orderKey = order['.key'];
delete order['.key'];
var updates = {};
updates['orders/' + loggedUser.uid + '/'+ orderKey] = order;
database.ref().update(updates).then(function() {
console.log('order Updated');
})
}
},
I found a way to avoid this behavior using computed properties:
computed: {
fixedOrders() {
var database = Firebase.database();
this.$bindAsArray('orders', database.ref('orders/' + this.userID).orderByChild('hourOrder'))
return this.orders
},
}
I've binded the array again on a computed property, so that way it always have the right values for orders.
I'm just concerned about the performance loss because I'm biding the orders array again every time it changes.
I have added a server side pagination with table sorter successfully. I just would like to know how can I refresh it? I would like to create a button to call a refresh function. Does anyone know if there is any method to do it? I do not want to reload the page for it.
UPDATE:
ajaxProcessing: function(data){
if (data && data.hasOwnProperty('rows')) {
var r, row, c, d = data.rows,
total = data.total_rows,
headers = data.headers,
rows = [],
len = d.length;
for ( r=0; r < len; r++ ) {
row = []; // new row array
// cells
for (c in d[r]) {
if (typeof(c) === "string") {
row.push(d[r][c]); //add each table cell data to row array
}
}
rows.push(row); // add new row array to rows array
}
var items="";
$("#tabelaTickets tr:has(td)").remove();
if (rows!==null && rows.length!== 0) {
$.each(rows,function(index,item) {
$("#tabelaTickets").append('<tr class="danger"><td align="center" style="width: 70px"><a type="button" class="btn btn-primary btn-xs" data-placement="right" title="Visualizar ticket" data-toggle="modal" class="btn btn-primary" href="visualizar.php?ticket='+item[3]+'"> #' + item[3] + '</a></td><td><div style="text-overflow:ellipsis;overflow:hidden;width:250px">' + item[4] + '</div></td><td><div style="text-overflow:ellipsis;overflow:hidden;width:350px;">' + item[5] + '</div></td><td><div style="text-overflow:ellipsis;overflow:hidden;width:250px;">' + item[6] + '</div></td><td><div style="text-overflow:ellipsis;overflow:hidden;width:60px;">' + item[7] + '</div></td><td><div style="text-overflow:ellipsis;overflow:hidden;width:70px;">' + item[8] + '</div></td></tr>');
});
}else{
$("#tabelaTickets").append('<tr><td colspan = "6" align="center">SEM RESULTADO A SER EXIBIDO</td></tr>');
}
$("#tabelaTickets").trigger("update");
$("#tabelaTickets").trigger("appendCache");
$("#pleaseWaitDialog").modal('hide');
// in version 2.10, you can optionally return $(rows) a set of table rows within a jQuery object
return [ total];
}
},
Thanks since now,
Erik
your repsonse is JSON, it's easy with a little AJAX function.
example your HTML is look like :
<div class="wrapper">
<div class="item">
<span>item 01</span>
</div>
<div class="item">
<span>item 02</span>
</div>
<div class="item">
<span>item 03 </span>
</div>
</div>
<button class="btn refresh-btn" type="submit"></button>
your response JSON maybe look like :
response = {
{ content : item11 },
{ content : item12 },
{ content : item13 }
};
your HTML render function with AJAX will be look like :
$('.refresh-btn').on('click', function() {
var url = 'yourUrl/?param=refresh&example=true';
var $wrapper = $('.wrapper'); // a div that wrap your new HTML.
$.get(url, {}) //call AJAX GET new item.
.done(function(data) {
$wrapper.html(''); // clear old list;
var $template = $('<div/>', {class : 'item'} ); // create item's HTML.
data.arrayItemList.forEach(function(item) {
var itemTemplate = $template.clone();
itemTemplate.append($('<span/>').text(item.content));
$wrapper.append(itemTemplate); // add new item in list.
});
});
})
that's mean : you create new HTML, and fill it with your data, everything worked fine.
Some time I create a empty template some where in view and clone it.
<div class="sample-template">
<div class="item">
<span> </span>
</div>
</div>
when I need it, I call the jQuery var $template = $('.sample-template').clone(); then fill data with $template.find('span').text(item.content);