I am creating a Vue app, where a list of jobs will be displayed and this data is coming from a JSON object. In this app I also am adding filtering functionality as well as pagination. So what I have so far is:
<div id="app" v-cloak>
<h2>Location</h2>
<select v-model="selectedLocation" v-on:change="setPages">
<option value="">Any</option>
<option v-for="location in locations" v-bind:value="location" >{{ location }}</option>
</select>
<div v-for="job in jobs">
<a v-bind:href="'/job-details-page/?entity=' + job.id"><h2>{{ job.title }}</h2></a>
<div v-if="job.customText12"><strong>Location:</strong> {{ job.customText12 }}</div>
</div>
<div class="paginationBtns">
<button type="button" v-if="page != 1" v-on:click="page--">Prev</button>
<button type="button" v-for="pageNumber in pages.slice(page-1, page+5)" v-on:click="page = pageNumber"> {{pageNumber}} </button>
<button type="button" v-if="page < pages.length" v-on:click="page++">Next</button>
</div>
<script>
var json = <?php echo getBhQuery('search','JobOrder','isOpen:true','id,title,categories,dateAdded,externalCategoryID,employmentType,customText12', null, 200, '-dateAdded');?>;
json = JSON.parse(json);
var jsonData = json.data;
var app = new Vue({
el: '#app',
data() {
return {
//assigning the jobs JSON data to this variable
jobs: jsonData,
locations: ['Chicago', 'Philly', 'Baltimore'],
//Used to filter based on selected filter options
selectedLocation: '',
page: 1,
perPage: 10,
pages: [],
}
},
methods: {
setPages () {
this.pages = [];
let numberOfPages = Math.ceil(this.jobs.length / this.perPage);
for (let i = 1; i <= numberOfPages; i++) {
this.pages.push(i);
}
},
paginate (jobs) {
let page = this.page;
let perPage = this.perPage;
let from = (page * perPage) - perPage;
let to = (page * perPage);
return jobs.slice(from, to);
},
}
watch: {
jobs () {
this.setPages();
}
},
})
computed: {
filteredJobs: function(){
var filteredList = this.jobs.filter(el=> {
return el.customText12.toUpperCase().match(this.selectedLocation.toUpperCase())
});
return this.paginate(filteredList);
}
}
</script>
So the issue I am running into is that I want the amount of pages to change when the user filters the list using the select input. The list itself changes, but the amount of pages does not, and there ends up being a ton of empty pages once you get past a certain point.
I believe the reason why this is happening is the amount of pages is being set based on the length of the jobs data object. Since that never changes the amount of pages stays the same as well. What I need to happen is once the setPages method is ran it needs to empty the pages data array, then look at the filteredJobs object and find the length of that instead of the base jobs object.
The filteredJobs filtering is a computed property and I am not sure how to grab the length of the object once it has been filtered.
EDIT: Okay so I added this into the setPages method:
let numberOfPages = Math.ceil(this.filteredJobs.length / this.perPage);
instead of
let numberOfPages = Math.ceil(this.jobs.length / this.perPage);
and I found out it is actually grabbing the length of filteredJobs, but since I am running the paginate method on that computed property, it is saying there is only 10 items in the filteredJobs array currently and will only add one pagination page. So grabbing the length of filteredJobs may not be the best route for this. Possibly setting a data variable to equal the filtered jobs object may be better and grab the length of that.
Related
I have this piece of code that reads data from an excel sheet, turns them into objects and then display their details in a neat product card
let allHoodies = [
['Hoodie', 'Purple', 'Cotton', '$39.99', 'image/items/hoodies/hoodie(1).jpg'],
['Hoodie', 'Blue', 'Cotton', '$39.99', 'image/items/hoodies/hoodie(2).jpg'],
['Hoodie', 'Green', 'Cotton', '$39.99', 'image/items/hoodies/hoodie(3).jpg']
]
allHoodies.forEach((element, index) => {
let obj = {}
obj.id = index
obj.type = element[0]
obj.color = element[1]
obj.material = element[2]
obj.price = element[3]
obj.imagesrc = element[4]
allHoodies[index] = obj
})
//Evaluating each hoodie and displaying its information in HTML
allHoodies.forEach(function(hoodie) {
let card = `
<div class="card">
<img class="product-image" src="${hoodie.imagesrc}">
<h1 class="product-type">${hoodie.type}</h1>
<p>Color: ${hoodie.color}</p>
<p>${hoodie.material} Read more </p>
<p class="price">${hoodie.price}</p>
<p><button>Buy</button></p>
</div>
`;
// Add the card to the page
document.getElementById('product-container').innerHTML += card;
});
What I'm trying to do is, upon clicking "Buy", it adds multiple items to the local storage although I'm struggling to do it and add multiple ones, it keeps on adding only 1 of them and overwriting the previous one (I'm assuming due to the fact that they have the same key)
Here's what I've tried (which works, but its not my goal):
function addToCart(id){
let hoodie = hoodies[id];
localStorage.setItem('item', JSON.stringify(hoodie));
}
and then I simply add the addToCart() function to the button, would someone guide me and help me figure out how I could actually add multiple ones to the local storage and not just keep overwriting?
Expected result:
Runnable JSFiddle snippet
You can use localStorage#getItem to get the current list, and JSON#parse to convert it to an array of objects. Then, use Array#push to add the current item, and finally, use localStorage#set and JSON#stringify to save the updated list:
function addToCart(id) {
try {
const hoodie = allHoodies[id];
if(hoodie) {
const items = JSON.parse(localStorage.getItem('items') || "[]");
items.push(hoodie);
localStorage.setItem('items', JSON.stringify(items));
}
} catch(e) {
console.log('error adding item');
}
}
Function to show the saved list:
function displayProductsinCart() {
const products = JSON.parse(localStorage.getItem("item") || "[]");
document.getElementById("item-container").innerHTML = products.reduce((cards, product) =>
cards + `<div class="card">
<img class="item-image" src="${product.image}">
<h1 class="product-type">${product.type}</h1>
<p>Color: ${product.color}</p>
<p>${product.description}</p>
<p class="price">${product.price} </p>
<p><button>Buy</button></p>
</div>
`, '');
}
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;ve built a CMS that allows users to build static pages of images and text content solely for displaying on television screens throughout our building. I've completed this to the point of viewing the display with it's pages but only if I call it explicitly in the url. The problem is, I want to load by display which is stored in the URL.
For instance, the url now if you click on Display 3 is http://local.CMSTest.com/showDisplay.php?display=3
and this calls a function using 3 as the display ID that grabs all pages with that display ID. This works, but in my html where I throw a foreach in there, it loads both pages associated with that display into a crammed page, half and half. So I know it's working but I need to store these pages into an array for javascript as well I believe.
I'm thinking there may be a way where I can load it with the first page on default and append it to the url like http://local.CMSTest.com/showDisplay.php?display=3&page_id = 2 and then after the time is up, it can go to the next one and change the URL http://local.CMSTest.com/showDisplay.php?display=3&page_id = 3
So the PHP and HTML is working at this moment:
<div class="row top">
<?php include 'banner.php'?>
</div>
<div class="row middle" style="background-image: url(<?php echo $showDisplays['background_img']?>);">
<div class="col-lg-12">
<?if($showDisplays['panel_type_id'] == 1){?>
<div class="fullContent" style=" height: 100%; ">
<?php echo $showDisplays['content']?>
</div>
<?}?>
</div>
</div>
<div class="row bottom">
<?php include 'ticker.php';?>
</div>
<?php }?>
I want to try some javascript like this that will take each page from the array, replacing the following URLs with page IDs from my previous array. How can I do this properly?
<script type="text/javascript">
var Dash = {
nextIndex: 0,
dashboards: [
{url: "http://www.google.com", time: 5},
{url: "http://www.yahoo.com", time: 10},
{url: "http://www.stackoverflow.com", time: 15}
],
display: function()
{
var dashboard = Dash.dashboards[Dash.nextIndex];
frames["displayArea"].location.href = dashboard.url;
Dash.nextIndex = (Dash.nextIndex + 1) % Dash.dashboards.length;
setTimeout(Dash.display, dashboard.time * 1000);
}
};
window.onload = Dash.display;
</script>
UPDATE:
Current array
Array ( [pageID] => 104 [page_type_id] => 1 [display_id] => 3 [slide_order] => [active] => 1 [background_img] => [panel_id] => 96 [panel_type_id] => 1 [page_id] => 104 [cont_id] => 148 [contID] => 148 [content] =>This is full content)
First you would need to access your php array from javascript and you can do that by using php's json_encode which will convert the array to a JSON object.
Then you can use URL search params to alter the query parameters for the next URL which you can call. See the below example
function setDisplay() {
let params = new URL(document.location).searchParams;
let pageID = params.get("pageID");
let disply = params.get("display");
// set the html based on pageID and display
}
function getNextURL() {
// encode your php array to json
// let obj = <?php echo json_encode($myArray); ?>;
// probably what obj will look like after above line
// this is just for this example, you should use the line commented out above
var obj = {
"pageID": 104,
"page_type_id": 1,
"display_id": 3,
}
let params = new URL(document.location).searchParams;
params.set("pageID", obj.pageID);
params.set("display", obj.display_id);
let url = window.location.href.split('?')[0];
let nextURL = url + "?" + params.toString();
console.log(nextURL);
return nextURL
}
getNextURL();
You would need to
parse the frames.displayArea.location.href for its query parameters.
assign to Dash.nextIndex either (params.page_id + 1) % Dash.dashboards.length because the page_id is the currently diplaying page and we want the next page... or 0 if params.page_id is undefined.
build your URL from dashboard.url appending the display_id and page_id parameters.
For cleanliness, I recommend creating a var called newURL since we need to build a custom string.
display: function() {
// step 1
let queryString = frames.displayArea.location.href.split("?")[1];
let paramArr = queryString.split("&");
let params = {};
paramArr.forEach((param) => {
let [name, value] = param.split("=");
params[name] = value;
})
console.log("params: ", params)
// step 2
Dash.nextIndex = (params.page_id)
? (params.page_id + 1) % Dash.dashboards.length
: 0;
var dashboard = Dash.dashboards[Dash.nextIndex];
// step 3
let newURL = dashboard.url + "?display_id=" + params.display_id
+ "&page_id=" + Dash.nextIndex;
// Unrelated nit: bracket access IMHO should only be used when specifying a
// property by the string value contained in a variable, or when the
// property name would be an invalid javascript variable name (starting
// with a number, including spaces, etc)
frames.displayArea.location.href = newURL;
setTimeout(Dash.display, dashboard.time * 1000);
}
Here's a simplified version of my code :
<template>
/* ----------------------------------------------------------
* Displays a list of templates, #click, select the template
/* ----------------------------------------------------------
<ul>
<li
v-for="form in forms.forms"
#click="selectTemplate(form)"
:key="form.id"
:class="{selected: templateSelected == form}">
<h4>{{ form.name }}</h4>
<p>{{ form.description }}</p>
</li>
</ul>
/* --------------------------------------------------------
* Displays the "Editable fields" of the selected template
/* --------------------------------------------------------
<div class="form-group" v-for="(editableField, index) in editableFields" :key="editableField.id">
<input
type="text"
class="appfield appfield-block data-to-document"
:id="'item_'+index"
:name="editableField.tag"
v-model="editableField.value">
</div>
</template>
<script>
export default {
data: function () {
return {
editableFields: [],
}
},
methods: {
selectTemplate: function (form) {
/* ------------------
* My problem is here
*/ ------------------
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
}
}
}
</script>
Basically I want to update the array EditableFields each time the user clicks on a template. My problem is that Vuejs does not update the display because the detection is not triggered. I've read the documentation here which advise to either $set the array or use Array instance methods only such as splice and push.
The code above (with push) works but the array is never emptied and therefore, "editable fields" keep pilling up, which is not a behavior I desire.
In order to empty the array before filling it again with fresh data, I tried several things with no luck :
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
==> Does not update the display
for (let i = 0; i < form.editable_fields.length; i++) {
this.$set(this.editableFields, i, form.editable_fields[i]);
}
==> Does not update the display
this.editableFields = form.editable_fields;
==> Does not update the display
Something I haven't tried yet is setting a whole new array with the fresh data but I can't understand how I can put that in place since I want the user to be able to click (and change the template selection) more than once.
I banged my head on that problem for a few hours now, I'd appreciate any help.
Thank you in advance :) !
I've got no problem using splice + push. The reactivity should be triggered normally as described in the link you provided.
See my code sample:
new Vue({
el: '#app',
data: function() {
return {
forms: {
forms: [{
id: 'form1',
editable_fields: [{
id: 'form1_field1',
value: 'form1_field1_value'
},
{
id: 'form1_field2',
value: 'form1_field2_value'
}
]
},
{
id: 'form2',
editable_fields: [{
id: 'form2_field1',
value: 'form2_field1_value'
},
{
id: 'form2_field2',
value: 'form2_field2_value'
}
]
}
]
},
editableFields: []
}
},
methods: {
selectTemplate(form) {
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<ul>
<li v-for="form in forms.forms"
#click="selectTemplate(form)"
:key="form.id">
<h4>{{ form.id }}</h4>
</li>
</ul>
<div class="form-group"
v-for="(editableField, index) in editableFields"
:key="editableField.id">
{{ editableField.id }}:
<input type="text" v-model="editableField.value">
</div>
</div>
Problem solved... Another remote part of the code was in fact, causing the problem.
For future reference, this solution is the correct one :
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
Using only Array instance methods is the way to go with Vuejs.
I am trying to implement a simple favorites system. On the page load posts are listed on the home page and any previously favorited posts called nubs will show up with the FAVED tag underneath them.
<div class="list-group" ng-repeat="nub in nubs">
<a href="#" class="list-group-item active">
<h4 class="list-group-item-heading">{{nub.title}}</h4>
<p class="list-group-item-text">{{nub.description}}</p>
<p class="list-group-item-text">{{nub.synopsis}}</p>
<li ng-repeat="url in nub.attachmentsUrls">
<p class="list-group-item-image">
<img ng-src={{url}} />
</p>
</li>
</a>
<button ng-click="toggleFav(nub)">favorite</button>
<p ng-show="getFaved(nub.$id)">FAVED</p>
</div>
This is working but when I add something to my favorites the page doesn't update to reflect the newly favorited post. I would like to make my page respond actively to the toggleFav function.
Here is my controller
var ref = new Firebase("https://xxxxx.firebaseio.com");
var auth = ref.getAuth();
var nubRef = new Firebase("https://xxxxx.firebaseio.com/Nubs");
var nubs = $firebaseArray(nubRef);
$scope.nubs = nubs;
var userRef = new Firebase("https://xxxxx.firebaseio.com/users");
var users = $firebaseArray(userRef);
$scope.users = users;
// Array of booleans for favorites
$scope.favedArray = [];
// Array of user ids for
$scope.userIdArray = [];
var userFavs = $firebaseArray(userRef.child(auth.uid).child("favorites"));
$scope.userFavs = userFavs;
userFavs.$loaded()
.then
(
function()
{
nubs.$loaded()
.then
(
function()
{
$scope.tempFaved = [];
$scope.tempId = [];
console.log(userFavs);
angular.forEach
(
nubs,
function(nub)
{
$scope.tempFaved.push(false);
$scope.tempId.push(nub.$id);
console.log($scope.tempId);
angular.forEach
(
userFavs,
function(favs)
{
console.log($scope.tempFaved);
if(favs.nub == nub.$id)
{
$scope.tempFaved.pop();
$scope.tempFaved.push(true);
console.log($scope.tempFaved);
}
}
);
}
);
while($scope.tempFaved.length > 0)
{
$scope.favedArray.push($scope.tempFaved.pop());
$scope.userIdArray.push($scope.tempId.pop());
}
$scope.getFaved = function(nubId)
{
console.log($scope.favedArray[$scope.userIdArray.indexOf(nubId)]);
$scope.faved = $scope.favedArray[$scope.userIdArray.indexOf(nubId)];
return $scope.faved;
}
$scope.toggleFav = function(nub)
{
var nubFavRef = nubRef.child(nub.$id).child("favorites");
var nubFavs = $firebaseArray(nubFavRef);
var faved = $scope.getFaved(nub.$id)
console.log(faved);
if (faved == false)
{
nubFavs.$add
(
{
user: auth.uid
}
);
userFavs.$add
(
{
nub: nub.$id
}
)
console.log("favorited");
}
else
{
nubFavs.$remove(auth.uid);
userFavs.$remove(nub.$id);
console.log("unfavorited");
}
};
}
)
}
);
Essentially it is looping through the nubs or posts displayed on the page and checking them against the nubs the user has favorited to display the FAVED tag and toggle the functionality of the favorite button. If the user doesn't have the nub favorited the button will add the nub to their list of favorites as well as adding them to the list of users that have the nub favorited and if the user does have the post favorited it will remove them.
The unfavorite functionality of the toggleFav doesn't work either so help with that would also be appreciated, but that's a matter of being able to access the right child of the faved arrays which I'm not sure how to do.
What I think needs to happen for the page to update with the right information when something is favorited is some kind of $on listener, but I'm not sure how to implement it.
/* How store data in fire base:
{
"home" : {
"room1" : {
"status" : "true",
"switch_name" : "light 2",
"user_id" : "-Kvbk-XHqluR-hB8l2Hh"
}
}
}
*/
//select element in which you want real time data.
const preObject = document.getElementById('tbl_switch_list');
//select your root table name
const dbRefObject = firebase.database().ref().child('home');
//Change Value in Firebase and view in your console.
dbRefObject.on('value',snap => console.log('Response : ',snap.val());
<h3>Switch List</h3>
<table id="tbl_switch_list" border="1">
<thead>
<tr>
<td>#ID</td>
<td>#switchName</td>
<td>#status</td>
</tr>
<thead>
<tbody id="list"></tbody>
</table>