AngularJs lazy loaded list remember data - javascript

I'm facing a problem with a lazy loaded list of books. Initialy I load 10 books. If user is scrolling down, I load the next 10 books.
Let's say the user is now on "page" 4. Then he clicks on the 35th book to get the details of it. After returning to the list, he just sees the first 10 books and has to redo all the lazy loadings.
Is there any "caching" or storing possibiblity to avoid this? Thanks a lot for any hints!
[UPDATE]
thanks for your replys!
routes
.state('app.books.list',{
...
...
resolve: {
books : function(bookFactoryBook) {
return booksFactoryBook.getList({page : 1});
}
}
})
.state('app.books.details',{
// just loads detail
})
ctrl (controller as syntax)
function bookControllerList(_books, ....) {
vm = this;
vm.books = _books;
......
// lazy loader just appends new data on vm.books
// after loading was successful
/**
* init lazy loader
*
* #desc initialize lazy-loader for books
* #return {void}
* #private
*/
function _initLazyLoader() {
if(_isLazyLoadingEnabled() === false) {
return;
}
// config lazy loader
var mapper = {
data : 'books', // vm.books
isEnabled : 'lazyLoadingEnabled',
indicatorId : 'lazyLoadingIndicatorId',
onDataLoaded : _afterLazyLoading
};
if(booksFactoryLazyLoader.setup(vm, mapper) === false) {
// failed to initialize lazy loader - disable feature
vm.lazyLoadingEnabled = false;
} else {
vm.lazyLoadingEnabled = true;
}
}
/**
* after lazy loading
*
* #desc manages data manipulation after lazy loading
* #param books - {Array} - new loaded data to manipulate
* #return {void}
* #private
*/
function _afterLazyLoading(books) {
// user logged in?
if(authFactorySession.isAuthorized() === true) {
// try to flag favorites in received books
books = _addFavoriteFlags(books);
}
// append data to vm
vm.books = vm.books.concat(books);
}
}
Could it be an issue with the controller as syntax? Or how can I extend the lifetime of my scope / vm object?
If I change the order of the states, and connect them as parent / child (app.books.list and app.books.list.detail) it doesn't change anything.
thanks for any hints

Without seeing your code, your controller containing the reference of the list is being reinitialized after viewing the details of the book. If you keep that around, or move the list of books to a scope that has a longer life, you should be fine.

Related

Save params from the first call of a recursive function

I have a function that searches files in folders and recursivly calles itself if a subfolder occurs.
I want to optimize the search algo in that way that i can store the returned data and it's corresponding parameters.
So if a new search is issued. I can check if an equal search was made before and return the saved result instead of doing a new search.
My approach was to push the params into the resulting array at first or last. But this has to happen only one time in the whole recursion process.
This is my function:
/**
* List all files that the matcher has hit
* #param {String} start path from where to start the search
* #param {Object} [options] search options
* #param {RegExp} [options.matcher] expression to match while searching
* #param {Boolean} [options.folders] search in subfolders, default = true
* #returns {Array} files that the matcher has hit
*/
list(start, { matcher, folders = true } = {}) {
if (!fs.existsSync(start)) throw new Error(`${start} doesn't exists.`)
const dir = fs.readdirSync(start)
const files = []
for (let iCnt = 0; iCnt < dir.length; iCnt++) {
const item = path.resolve(start, dir[iCnt])
let stat = fs.statSync(item)
switch (true) {
case stat.isDirectory() && folders:
files.push(...list(item, { matcher, folders }))
break
case matcher && matcher.test(item):
files.push(item)
break
case !matcher:
files.push(item)
break
}
}
return files
}
I thought a lot about it. But can't get my head around.
Does anyone have an idea?
When the first call in a recursive sequence is special, I usually handle it by making the recursive part a worker function, and making the main function a wrapper for it that does the special part.
In your case, that would mean renaming your existing list (perhaps to listWorker) and making a wrapper list function that does the caching. Roughly:
function list(start, { matcher, folders = true } = {}) {
let result = getFromCache(/*...*/);
if (!result) {
result = listWorker(start, {matcher, folders});
putInCache(/*...*/, result);
}
return result;
}

Error on a MasterDetail page

So I'm trying to recreate this app on SAP Web IDE:
But I'm continuously getting this error:
This is my App.Controller.js code:
sap.ui.define([
"pt/procensus/ui5rv/controller/BaseController",
"sap/ui/model/json/JSONModel"
], function (BaseController, JSONModel) {
"use strict";
return BaseController.extend("pt.procensus.ui5rv.controller.App", {
onInit : function () {
var oViewModel,
fnSetAppNotBusy,
oListSelector = this.getOwnerComponent().oListSelector,
iOriginalBusyDelay = this.getView().getBusyIndicatorDelay();
oViewModel = new JSONModel({
busy : true,
delay : 0
});
this.setModel(oViewModel, "appView");
fnSetAppNotBusy = function() {
oViewModel.setProperty("/busy", false);
oViewModel.setProperty("/delay", iOriginalBusyDelay);
};
this.getOwnerComponent().oWhenMetadataIsLoaded.
then(fnSetAppNotBusy, fnSetAppNotBusy);
// Makes sure that master view is hidden in split app
// after a new list entry has been selected.
oListSelector.attachListSelectionChange(function () {
this.byId("idAppControl").hideMaster();
}, this);
// apply content density mode to root view
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
}
});
}
);
This is my Base.Controller.js code:
/*global history */
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/routing/History"
], function(Controller, History) {
"use strict";
return Controller.extend("pt.procensus.ui5rv.controller.BaseController", {
/**
* Convenience method for accessing the router in every controller of the application.
* #public
* #returns {sap.ui.core.routing.Router} the router for this component
*/
getRouter: function() {
return this.getOwnerComponent().getRouter();
},
/**
* Convenience method for getting the view model by name in every controller of the application.
* #public
* #param {string} sName the model name
* #returns {sap.ui.model.Model} the model instance
*/
getModel: function(sName) {
return this.getView().getModel(sName);
},
/**
* Convenience method for setting the view model in every controller of the application.
* #public
* #param {sap.ui.model.Model} oModel the model instance
* #param {string} sName the model name
* #returns {sap.ui.mvc.View} the view instance
*/
setModel: function(oModel, sName) {
return this.getView().setModel(oModel, sName);
},
/**
* Convenience method for getting the resource bundle.
* #public
* #returns {sap.ui.model.resource.ResourceModel} the resourceModel of the component
*/
getResourceBundle: function() {
return this.getOwnerComponent().getModel("i18n").getResourceBundle();
},
/**
* Event handler for navigating back.
* It checks if there is a history entry. If yes, history.go(-1) will happen.
* If not, it will replace the current entry of the browser history with the master route.
* #public
*/
onNavBack: function() {
var sPreviousHash = History.getInstance().getPreviousHash();
if (sPreviousHash !== undefined) {
// The history contains a previous entry
history.go(-1);
} else {
// Otherwise we go backwards with a forward history
var bReplace = true;
this.getRouter().navTo("master", {}, bReplace);
}
}
});
});
My App Folders:
And I can't understand why is this happening. I've already removed the 'then' part and it gives me more errors... :/
Any help will be very much appreciated :)
Can you try this? At least this is how it works in my App.controller:
this.getOwnerComponent().getModel().metadataLoaded()
.then(fnSetAppNotBusy, fnSetAppNotBusy);

How to include js files in header of wordpress pages that are activated on-click

I am attempting to use wordpress to build a website that integrates google maps. I am doing some overlays with the maps and use the google developers API and Python to make the appropriate javascript. I have successfully written the js files and Python necessary to accomplish this.
My website is built in Worpress and I would like add a page (not the home page) that has n links and each one would populate a box with the corresponding map. I can take care of the layout and design issues but I am at a loss on how to:
a) Include the javascript as a file that
b) gets called upon clicking the link and thus populates that map without calling a new page
That is, the javascript is HUGE because it may include thousands of lat/lon points. Therefore including n of these written into the header is unreasonable. I want to simply call it from filename.js when the link is clicked.
There is a plugin that allows me to include whatever I want in the header. So, if I can find out where to put the *.js files (or txt file) in the directory tree and how to have the corresponding file activated upon click I should be good. Thanks!
This Display different maps with onClick event - Google Maps V3. kind of helps with doing an on-click display but everyone's solution was to make one map. I cannot do that. I am overlaying vast amounts of data.
Here is a way you can get that done. (Jump down to the get started part of the script.)
For brevity, I've included a bunch of scripts in one 'file', but you'll want to break them in to individual files.
You may also need to try the html and js in jsbin js bin example, b/c SO may or may not allow the dynamic loading of js.
(function(undefined) {
/**
* #author (#colecmc)
* #method turn collection into an array
* #param {object} collection - NodeList, HTMLCollection, etc. Should have an "item" method and/or a "length" property
*/
ToArray = collection => Array.prototype.slice.call(collection);
/** \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ **/
Observer = (function(undefined) {
/**
* pub sub
*/
'use strict';
var subUid = -1;
return {
topics: {},
subscribe: function(topic, func) {
/**
* #param {string} topic
* #param {function} func
* #returns {string} - a token such as '3'
* #example Observer.subscribe('any-valid-string',function(name,resp){
console.log(resp.prop);
});
*/
if (!Observer.topics[topic]) {
Observer.topics[topic] = [];
}
var token = (++subUid).toString();
Observer.topics[topic].push({
token: token,
func: func
});
return token;
},
publish: function publish(topic, args) {
/**
* #param {string} topic
* #param {object} args
* #returns {boolean} - true if topic is valid, false otherwise
* #example Observer.publish('any-valid-string',{
prop: 'this is a test'
});
*/
if (!Observer.topics[topic]) {
return false;
}
setTimeout(function() {
var subscribers = Observer.topics[topic],
len = subscribers ? subscribers.length : 0;
while (len--) {
subscribers[len].func(topic, args);
}
}, 0);
return true;
},
unsubscribe: function unsubscribe(token) {
/**
* #param {string} token - value should be saved from the original subscription
* #example Observer.unsubscribe('2');
* #example Observer.unsubscribe(member); - where member is the value returned by Observer.subscribe();
*/
var m,
forEachTopic = function(i) {
if (Observer.topics[m][i].token === token) {
Observer.topics[m].splice(i, 1);
return token;
}
};
for (m in Observer.topics) {
if (Observer.topics.hasOwnProperty(m)) {
Observer.topics[m].forEach(forEachTopic);
}
}
return false;
}
};
}(undefined));
/** \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ **/
SetAttributes = function(el, attrs) {
/**
* #author (#colecmc)
* #method simple for in loop to help with creating elements programmatically
* #param {object} el - HTMLElement attributes are getting added to
* #param {object} attrs - object literal with key/values for desired attributes
* #example SetAttributes(info,{
* 'id' : 'utswFormInfo'
* 'class' : 'my-class-name'
* });
*/
'use strict';
var key;
for (key in attrs) {
if (attrs.hasOwnProperty(key)) {
el.setAttribute(key, attrs[key]);
}
}
return el;
};
/** \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ **/
GetScript = function(url, fullPath) {
/**
* #author (#colecmc)
* #version 1.0.4
* #requires Swlxws.SetAttributes, Swlxws.Observer
* #method dynamically add script tags to the page.
* #param {string} url - relative path and file name - do not include extension
* #param {string} fullPath - absolute path
* #example GetScript('myLocalScript');
* #example GetScript('','https://www.google-analytics.com/analytics.js');
*/
'use strict';
function onLoad(event) {
var result;
if (event.type === 'load') {
result = 1;
} else {
result = -1;
}
Observer.publish('get-script-onload-complete', {
successful: result,
eventData: event
});
}
var JSPATH = '/js/',
/* or where ever you keep js files */
el = document.createElement('script'),
attrs = {
defer: true,
src: null,
type: 'text/javascript'
};
/** look for a string based, protocol agnostic, js file url */
if (typeof fullPath === 'string' && fullPath.indexOf('http') === 0) {
attrs.src = fullPath;
}
/** look for any string with at least 1 character and prefix our root js dir, then append extension */
if (typeof url === 'string' && url.length >= 1) {
attrs.src = JSPATH + url + '.js';
}
SetAttributes(el, attrs);
el.addEventListener('load', onLoad);
el.addEventListener('error', onLoad);
document.body.appendChild(el);
return el;
};
/** \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ **/
/**
* Get Started
*/
function onClick(event) {
GetScript('', event.target.dataset.namespaceUrl);
}
Observer.subscribe('get-script-onload-complete', function(name, resp) {
/** check to make resp is what you expect, ie: the correct script loaded */
/** then it is safe to use */
});
ToArray(document.querySelectorAll('.load-scripts')).map(script => script.addEventListener('click', onClick, false));
}(undefined));
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>How to include js files in header of wordpress pages that are activated on-click</title>
</head>
<body>
Load Google Analytics
</body>
</html>
You can use the function wp_enqueue_script() to load the necessary JS files on only the templates you want.
As for your large data set, I recommend that you cache it in an external .json file and use wp_enqueue_script() to load it only when necessary.
Well if the onclick event suggestion is pretty much what you want and you are just concerned about the large amount of data. Then there are a few ways to tackle it. I am not sure if the dataset is a js file or php/json files but i came across a similar issue on one of my projects, dont remember properly but i was doing something with maxmind's ip/location data set.
So i just splitted the large file into 3 smaller ones. Then i looped through each of the file and if the stuff that i was looking for was found in the file then i just breaked out. And definitely as Brian suggested caching and using a CDN would help a lot.

Flux Pagination

I'm anxious to see an example of pagination in a flux environment, I can't wrap my mind around how that would work. I've taken a look at redux's example, but that's not really pagination, just a "load more" button. What I'm looking for is a way to paginate possibly millions of records (so you must use lazy loading).
Here are a few of the pitfalls I'm running into:
1) Someone could load page 20 without loading pages 1-19 (by clicking on a hyperlink, for example).
2) If someone edited a record inline, and then that record no longer satisfied the filter used to include in that list, we'll need to load more data to fill in the empty space left behind.
3) Monitoring props for changes to the page number, you'll need to load more data if that page hasn't been loaded yet.
I would love some examples that note how to overcome these pitfalls. Let me know if you have any suggestions. Thanks!
I added an example based on your title and the last sentence. To address your specific questions:
Of course you can load from page 20, just start from higher request params
Editing the record wouldn't change the state until committed right? So once you committed the edit change the filter would apply and your record would be removed if you edited the criteria that had been set by your filter
You would have your result set fetched further ahead as you paginated - if you clicked next it would load the next one in sequence forward, or if you clicked result start at 40 it would fetch 40-X where X is the count per fetch that you specify. The code example I found uses 10, like most applications, so you would fetch starting at 40, but would get 40 to 50.
This page. Basically Use an Event List Store to hold the data for the child objects to access, the pagination component peice itself and finally, there's the Pagination store that
"updates the current State of the active page and provide a function to calculate the number of pages available to the Pagination based on the Total amount of items and how many of those items are to be displayed per page"
I believe to implement this code you would need an api request with the query parameters such as search keywords and result set preferences. This code was designed to send an api call which returns a json response that could be broken down and presented accordingly (this example does in sets of 10.)
For another working example and there are probably many others, but off hand here is one that I know of personally. This code provided below was Posted from Adam Ellsworth and credits go to him for the code:
EventListStore.js
// requires go here ...
var events = [], // Default Event listing
total = 0, // Default number of Available Events
start = 0, // Default start
end = 9, // Default end
amt = 9; // Number of Events to list per page (0-based)
processTurnPage: function (page) {
start = (page - 1) * amt;
end = start + amt;
}
var EventListStore = assign({}, EventEmitter.prototype, {
...
getTotal: function () {
return total;
},
getStart: function () {
return start;
},
getEnd: function () {
return end;
},
getAmountPerPage: function () {
return amt;
},
...
// emitChange, addChangeListener, removeChangeListener
});
EventListStore.dispatchToken = EventListDispatcher.register(function (payload) {
var action = payload.action,
data = payload.action.data;
switch (action.actionType) {
case PageConstants.TURN_PAGE:
processTurnPage(data);
// Omitted:
// Call the API to get new event data based on our new Page params
EventListStore.emitChange();
break;
}
});
Pagination.Jsx
// requires go here ...
var LIMIT = 12; // The amount of clickable <li> to show
function getNavigation (count, per) {
/**
* This is where we build our <li /> elements. I'm omitting our code because
* there are too many ways in which pagination can be displayed, and ours
* is specific to our needs.
*
* What is returned below is just the gist of it.
*/
var pages = []; // what we'll store our JSX <li /> elements in.
var pages = PaginationStore.getTotalPageCount(count, per);
/**
* Translate our 0-based pagination data to a user-friendly representation
* by starting at 1
*/
for (var i = 1; i <= pages; i++) {
pages.push(
<li className="page" data-value={i} key={i}>
{i}
</li>
);
}
return pages;
}
var Pagination = React.createClass({
componentDidUpdate: function (prevProps, prevState) {
var self = this;
$('.page').unbind().on('click', function (e) {
var value = $(this).data('value');
if (value != self.state.page) {
EventViewActions.turnPage(value);
}
});
},
/**
* Note here that in our EventList.jsx Component we're instantiating our
* <Pagination /> component thusly:
*
* <Pagination total={this.state.total} per={this.state.amount} />
*/
render: function () {
var navigation = getNavigation(this.props.total, this.props.per);
return (
<ul>
{navigation}
</ul>
);
}
});
Pagination.jsx
// requires ...
var _page = 1; // Default page
updatePage: function (page) {
console.log('changing page: ' + _page + ' -> ' + page);
_page = page;
}
var PaginationStore = assign({}, EventEmitter.prototype, {
getPage: function () {
return _page;
},
getTotalPageCount: function (total, per) {
var pages = total - 1;
if (pages > 0) {
if (per < pages) {
return Math.ceil(pages / per);
}
return 1; // only one page of items
} else {
return 0; // no items
}
},
...
});
PaginationStore.dispatchToken = EventListDispatcher.register(function (payload) {
var action = payload.action,
data = payload.action.data;
switch (action.actionType) {
case PageConstants.TURN_PAGE:
updatePage(data)
PaginationStore.emitChange();
break;
}
});

How do I generalize this set of Javascript methods, including promises?

I have the following code setup to initialize a single field's autosuggest feature using jQuery and MagicSuggest. It's relatively straight forward. I have modularized a bit of it because I intend on using it to initialize other fields as well with MagicSuggest. One extraneous part is the canonical name conversion, but it's a necessary function for this particular data set I'm working with. (Problem I'm having trouble with explained below ...)
/**
* Initialize Flights From autosuggest feature
* #return {void}
*/
function initFlightsFromAutosuggest() {
// Flights From Typeahead *************************************
var msField = $('#magicsuggest.direct_flights_from');
var ms = msField.magicSuggest({
id : 'direct_flights_from',
name : 'direct_flights_from',
minChars : 1,
highlight : false,
valueField : 'id',
displayField : 'name',
placeholder : getMSPlaceholder(msField, 'City'),
resultAsString: true,
useTabKey : true,
useCommaKey : true,
useZebraStyle : true,
hideTrigger : true,
sortOrder : 'canonical_name',
maxDropHeight : 500,
data : '/api/v1/cities',
defaultValues : msField.attr('data-default').split(','),
renderer : function(data) { return convertCanonical(data.canonical_name) }
});
// Once loaded, add pre-selected values if there are any
$(ms).on('load', addDefaults(ms, msField));
}
/**
* Gets placeholder value for MagicSuggest instances
* #param {element} el DOM element
* #param {string} defaultString Default string to use
* #return {string}
*/
function getMSPlaceholder(el, defaultString) {
if (el.attr('data-default').length > 0) {
return '';
}
return defaultString;
}
/**
* Converts canonical name into city, state string (dropping country, fixing spacing)
* #param {string} canonical_name Full canonical name
* #return {string} Short name, without country
*/
function convertCanonical(canonical_name) {
if (typeof canonical_name !== 'undefined') {
canonical_name = canonical_name.replace(',United States', '');
canonical_name = canonical_name.replace(',', ', ');
return canonical_name;
}
// Not sure what to do if it's undefined
return;
}
That all said, below is what I have to do to pre-populate this one field with data previously submitted.
/**
* Adds pre-selected values (ids) loaded into the 'data-default' attribute into the input field
* #param {object} ms MagicSuggest instantiation
* #param {element} msField DOM element used by MagicSuggest
*/
function addDefaults(ms, msField) {
// Get the default attribute value as an array
var defaultIds = msField.attr('data-default').split(',');
// Setup array of requests
var requests = [];
// Push all the requests into an array
$.each(defaultIds, function(index, id) {
requests.push($.getJSON('/api/v1/cities/' + id));
});
// Create a promise, and when all the requests are done (promises fulfilled)
// Send the args (json) to the .done callback
var promise = $.when.apply($, requests).then(function () {
var args = Array.prototype.slice.call(arguments);
return args.map(function(arg) { return arg[0] });
});
// Setup the callback function for 'done'
promise.done(function(json) {
// Setup results array
var results = [];
// Got an auth error from the API, so return early. No results.
if (typeof(json[0].auth) === 'object') {
return false;
}
// For each item, add the proper structure to the results array
$.each(json, function (index, id) {
results.push({
value: json[index][0]['city']['id'],
name: json[index][0]['city']['name']
});
});
var resultPromise = $.when.apply($, results).then(function () {
var args = Array.prototype.slice.call(arguments);
return args.map(function(arg) { return arg });
});
resultPromise.done(function(results) {
ms.setValue(results);
ms.setDataUrlParams({});
$('.input')
});
});
}
There has to be a way to generalize this, but I'm new at promises and $.Deferred so I've been hitting a wall of understanding.
The other fields I'll be instantiating with MagicSuggest will be using different URLs for the $.getJSON() method (probably all using IDs though) (used for finding what the user had previously chosen, thus what to pre-populate the field with), and will obviously have different JSON responses for those calls. So, the trouble spots for me are how to get this all to work together and still DRY.
As soon as I start breaking apart addDefaults() I hit problems because ms is undefined in resultPromise.done, the URLs with the IDs in them, and the json structure inside the $.each command.
How would you refactor this to be more re-usable? Comments/explanations on promises and deferred are always helpful too.
With a fresh head after a few days rest, and with the insight of this post I realized I didn't need to do all this just to add default values. Thankfully, just adding the following to the init worked perfectly: value: msField.attr('data-default').split(','), (I'm adding the values into the HTML under the data-default attribute via PHP.
Code: deleted.
Problem: solved.

Categories

Resources