YUI: Filter a datatable client-side - javascript

I have a datasource that gets data from the server. It is then used in a datatable. I want to be able to filter the data in the table client-side, without making another call to the server.
// Data source definition
myDataSource = new YAHOO.util.DataSource("myurl");
myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
myDataSource.connXhrMode = "queueRequests";
myDataSource.responseSchema = {
resultsList: "ResultSet.Result",
fields: ["field1","field2"]
}
// Datatable definition
myDataTable = new YAHOO.widget.DataTable("container", myColumnDefs,myDataSource, {});

Subclass DataSource and override the sendRequest method so that you call the passed-in callback with your own filtered result set as the results argument.
filterDataSource=function(arg) {
filterDataSource.superclass.constructor.call(this,arg);
}
YAHOO.extend(filterDataSource,YAHOO.util.XHRDataSource);
filterDataSource.prototype.sendRequest=function(request, callback) {
var wrapCallBack=function (request,results,error) {
// !!! do filtering on results here !!!
callback.success.call(this,request,results,error);
};
filterDataSource.superclass.sendRequest.call(this,request, {
success: wrapCallBack, argument: callback.argument
});
}
And make your myDataSource a new filterDataSource instead of a new Yahoo.util.DataSource.
Disclaimer: this code probably doesn't work as written; I ripped it out of some old working code and quite likely skipped over some critical piece. Still, I hope it conveys the basic idea.

Related

Bar Chart Renders Before Receiving Updated Data

I have a simple issue relating to function sequence that I'm struggling to solve on my own. Essentially, I'm rendering a bar chart based on parameters selected by the user. This works perfectly on the first go, but when the user updates their parameter selection and hits "render" again, the chart doesn't update. It's only on the second button click that the chart renders with updated data.
I can see in the console that the "generate chart" function IS receiving the updated data, so I know the issue is that the chart is rendering before the new data is loaded. I understand that the common solution to this type of problem would be using JS Promises or callbacks, but I can't figure out how to execute it, and would really appreciate some help.
Here's the code structure. I have to make two API calls to get the data.
const ajaxCallOne = () => {
// Make API call for first data set (data1). On success, invoke the next API call, passing data1.
success: (data1) => {
ajaxCallTwo(data1);
}
}
const ajaxCallTwo = (data1) => {
// Make second API call for another set of data ('data2'). On success, invoke a function to visualize all the data in a bar chart, passing it both data1 and data2.
success: (data2) => {
populateBarChart(data1, data2)
}
}
const populateBarChart = (data1, data2) => {
// Function to create a bar chart and populate it with data1 and data2 from the API calls
// The data retrieved from the API calls is based on parameters selected by the user. When the user selects new parameters and hits the "view results" button again, we kick off another round of API calls, which pass the new data to this populateBarChart function.
// I am using ApexCharts.js library to render the bar chart. The syntax for actually rendering the chart looks like this:
let chart = new ApexCharts(
document.querySelector("#audience-reach-chart-container"),
options
);
chart.render();
}
At first I thought I needed to destroy / empty out the chart at the beginning of the function, but since the function creates a new instance of the chart every time it's invoked, there's nothing to target and destroy.
Also, when I console.log the data being received by the populateBarChart function, I can see the updated data is being received. So the issue must be that the chart is rendering based on the old data, and I need to control the sequence of events?
I can't figure out how to go about this within the code design. Any insights would be greatly appreciated.
You're absolutely right that this can be solved using callbacks, promises and even async/await.
However, this problem statement may have a nuance e.g. if you're using <select> dropdowns, the event listener registered on them could be slightly incorrect. OR, if the ApexCharts library's behavior is to withhold rendering for some reason, then it'll be hard to say so without going through the documentation. Hence, an example on codesandbox or jsfiddle can help us.
Let's look at one possible solution - Ideally, when using promises, we should do something like this:
let container = document.querySelector("#audience-reach-chart-container");
const ajaxCallOne = () => {
// Make API call for first data set (data1).
// No need to invoke the next API call. Just ensure it returns a promise object
}
const ajaxCallTwo = () => {
// Make second API call for another set of data ('data2').
// Sit still! No need to do anything here as long as this function returns a promise
}
// Run this when the user hits 'render' button
Promise.all([ajaxCallOne, ajaxCallTwo]).then((values) => {
console.log(values);
// values[0] holds data1
// values[1] holds data2
let options = doSomethingOnThese(values[0], values[1]);
// Populate the BarChart here
let chart = new ApexCharts(container, options);
chart.render();
});
I've created a sample app(https://codesandbox.io/s/unruffled-cookies-tbcerp?file=/src/index.js) to mimic your usecase here:
// https://codesandbox.io/s/unruffled-cookies-tbcerp?file=/src/index.js
(async function () {
var options = {
chart: {
height: 350,
type: "bar"
},
dataLabels: {
enabled: false
},
series: [],
title: {
text: "Ajax Example"
},
noData: {
text: "Loading..."
}
};
var chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();
try {
const ajaxCallOne = await fetch(
"https://my-json-server.typicode.com/apexcharts/apexcharts.js/yearly"
);
const ajaxCallTwo = await fetch(
"https://my-json-server.typicode.com/apexcharts/apexcharts.js/yearly2"
);
let data1 = await ajaxCallOne.json();
let data2 = await ajaxCallTwo.json();
console.log(data1, data2);
chart.updateSeries([
{
name: "Sales",
data: data1
},
{
name: "Sales2",
data: data2
}
]);
} catch (e) {
console.log(e);
}
})();

JavaScript code design async/callback

this is a kind of embarrassing question but I'm stuck.
My background is managed code and I never learned JavaScript but yet I want to implement a tiny project.
The script is running on SharePoint 2010, queries items from a custom list using the JavaScript Object Model and populates a Google chart or table respectively.
With the help of MSDN and Google Developer I was able to query data from one list and visualize it.
However, I'm unable to transfer the concept to query multiple lists, combine result sets and finally pass to Google API.
In my code I created a chain of callbacks like showChart->loadListData->drawChart. This proves to be bad design since it's inflexible and cannot be extended. All API methods are asynchronous and don't have return values but expect method names to call once finished. This is what get's me stuck and where I lack knowledge.
I'm very happy for every comment and answer, also I can provide actual source code if requested. Thank you in advance, Toby
UPDATE as asked for by #Utkanos:
var listItems;
$(document).ready(function() {
ExecuteOrDelayUntilScriptLoaded(loadChartData, "sp.js");
});
function loadChartData() {
var camlQuery = SP.CamlQuery.createAllItemsQuery();
camlQuery.set_viewXml("<View><Query><Where><Eq><FieldRef Name='Year'/><Value Type='Text'>2015</Value></Eq></Where></Query></View>");
loadListData('CustomList', camlQuery, drawChart, readListItemFailed);
}
function loadListData(listTitle, camlQuery, onSuccess, onFail) {
context = SP.ClientContext.get_current();
var list = context.get_web().get_lists().getByTitle(listTitle);
var listItems = list.getItems(camlQuery);
context.load(listItems);
context.executeQueryAsync(function(sender, args){onSuccess(listItems);}, onFail);
}
function drawDpOverviewChart(listItems) {
var data;
var enumerator = listItems.getEnumerator();
data = new google.visualization.DataTable();
data.addColumn('string', 'Column1');
data.addColumn('number', 'Column2');
var listItem;
while (enumerator.moveNext()) {
listItem = enumerator.get_current();
data.addRow([listItem.get_item('Title'), Math.round(listItem.get_item('Balance')/10000)/100]);
}
var options = {'title':'Pretty Chart'};
var chart = new google.visualization.PieChart(document.getElementById('chart_div'));
chart.draw(data, options);
}
function readListItemFailed(sender, args) {
alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
}
if using SP 2010 on a typical .aspx page, you have some tools available,
such as MicrosoftAjax.js and _spPageContextInfo
using the REST API, you can join lists on lookup fields and include fields from both lists in one query
following is an example url for a call to rest...
'/_vti_bin/ListData.svc/MI_Projects?$expand=ModifiedBy&$filter=ModifiedBy/Id eq 738'
this call actually "joins" the list MI_Projects to the UserInformationList by "expanding" ModifiedBy
so when the data returns, you can access any of the user info fields, i.e.
row.ModifiedBy.Name
this can be done with lookup fields on custom lists as well
to make the call, you can use the Sys.Net.WebRequest class from MicrosoftAjax
this class also allows you to pass variables to the callback
see following snippet...
function makeCall() {
// Sys.Net.WebRequest is from MicrosoftAjax.js
var webRequest = new Sys.Net.WebRequest();
webRequest.get_headers()['Cache-Control'] = 'no-cache';
webRequest.get_headers()['Accept'] = 'application/json';
webRequest.get_headers()['Content-Type'] = 'application/json';
webRequest.set_url(_spPageContextInfo.webServerRelativeUrl + '/_vti_bin/ListData.svc/MI_Projects?$expand=ModifiedBy&$filter=ModifiedBy/Id%20eq%20738');
// use the 'user context' to pass variables you want available in the callback
webRequest.set_userContext({
Title: 'variable to pass to completed callback'
});
webRequest.add_completed(restComplete);
webRequest.invoke();
}
// the first argument of callback is the Sys.Net.WebRequestExecutor class
function restComplete(executor, eventArgs) {
if (executor.get_responseAvailable()) {
if (executor.get_statusCode() === 200) {
// get variable passed via user context
var variablePassed = executor.get_webRequest().get_userContext().Title;
// i.e. -- build google table
// add rows received from rest (forEach is from MicrosoftAjax.js)
// list results array = executor.get_object().d.results
Array.forEach(executor.get_object().d.results, function (row) {
data.addRow(row.Title, row.Id, row.ModifiedBy.Name);
}, this);
}
}
}

Assemble paginated ajax data in a Bacon FRP stream

I'm learning FRP using Bacon.js, and would like to assemble data from a paginated API in a stream.
The module that uses the data has a consumption API like this:
// UI module, displays unicorns as they arrive
beautifulUnicorns.property.onValue(function(allUnicorns){
console.log("Got "+ allUnicorns.length +" Unicorns");
// ... some real display work
});
The module that assembles the data requests sequential pages from an API and pushes onto the stream every time it gets a new data set:
// beautifulUnicorns module
var curPage = 1
var stream = new Bacon.Bus()
var property = stream.toProperty()
var property.onValue(function(){}) # You have to add an empty subscriber, otherwise future onValues will not receive the initial value. https://github.com/baconjs/bacon.js/wiki/FAQ#why-isnt-my-property-updated
var allUnicorns = [] // !!! stateful list of all unicorns ever received. Is this idiomatic for FRP?
var getNextPage = function(){
/* get data for subsequent pages.
Skipping for clarity */
}
var gotNextPage = function (resp) {
Array.prototype.push.apply(allUnicorns, resp) // just adds the responses to the existing array reference
stream.push(allUnicorns)
curPage++
if (curPage <= pageLimit) { getNextPage() }
}
How do I subscribe to the stream in a way that provides me a full list of all unicorns ever received? Is this flatMap or similar? I don't think I need a new stream out of it, but I don't know. I'm sorry, I'm new to the FRP way of thinking. To be clear, assembling the array works, it just feels like I'm not doing the idiomatic thing.
I'm not using jQuery or another ajax library for this, so that's why I'm not using Bacon.fromPromise
You also may wonder why my consuming module wants the whole set instead of just the incremental update. If it were just appending rows that could be ok, but in my case it's an infinite scroll and it should draw data if both: 1. data is available and 2. area is on screen.
This can be done with the .scan() method. And also you will need a stream that emits items of one page, you can create it with .repeat().
Here is a draft code (sorry not tested):
var itemsPerPage = Bacon.repeat(function(index) {
var pageNumber = index + 1;
if (pageNumber < PAGE_LIMIT) {
return Bacon.fromCallback(function(callback) {
// your method that talks to the server
getDataForAPage(pageNumber, callback);
});
} else {
return false;
}
});
var allItems = itemsPerPage.scan([], function(allItems, itemsFromAPage) {
return allItems.concat(itemsFromAPage);
});
// Here you go
allItems.onValue(function(allUnicorns){
console.log("Got "+ allUnicorns.length +" Unicorns");
// ... some real display work
});
As you noticed, you also won't need .onValue(function(){}) hack, and curPage external state.
Here is a solution using flatMap and fold. When dealing with network you have to remember that the data can come back in a different order than you sent the requests - that's why the combination of fold and map.
var pages = Bacon.fromArray([1,2,3,4,5])
var requests = pages.flatMap(function(page) {
return doAjax(page)
.map(function(value) {
return {
page: page,
value: value
}
})
}).log("Data received")
var allData = requests.fold([], function(arr, data) {
return arr.concat([data])
}).map(function(arr) {
// I would normally write this as a oneliner
var sorted = _.sortBy(arr, "page")
var onlyValues = _.pluck(sorted, "value")
var inOneArray = _.flatten(onlyValues)
return inOneArray
})
allData.log("All data")
function doAjax(page) {
// This would actually be Bacon.fromPromise($.ajax...)
// Math random to simulate the fact that requests can return out
// of order
return Bacon.later(Math.random() * 3000, [
"Page"+page+"Item1",
"Page"+page+"Item2"])
}
http://jsbin.com/damevu/4/edit

Waiting on collection data prior to rendering a subview

I am building what should be a fairly simple project which is heavily based on Ampersand's starter project (when you first run ampersand). My Add page has a <select> element that should to be populated with data from another collection. I have been comparing this view with the Edit page view because I think they are quite similar but I cannot figure it out.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
Below you can see that I am trying to fetch the app.brandCollection and set its value to this.model, is this correct? I will need to modify the output and pass through the data to an ampersand-select-view element with the correct formatting; that is my next problem. If anyone has suggestions for that I would also appreciate it.
var PageView = require('./base');
var templates = require('../templates');
var ProjectForm = require('../forms/addProjectForm');
module.exports = PageView.extend({
pageTitle: 'add project',
template: templates.pages.projectAdd,
initialize: function () {
var self = this;
app.brandCollection.fetch({
success : function(collection, resp) {
console.log('SUCCESS: resp', resp);
self.brands = resp;
},
error: function(collection, resp) {
console.log('ERROR: resp', resp, options);
}
});
},
subviews: {
form: {
container: 'form',
waitFor: 'brands',
prepareView: function (el) {
return new ProjectForm({
el: el,
submitCallback: function (data) {
app.projectCollection.create(data, {
wait: true,
success: function () {
app.navigate('/');
app.projectCollection.fetch();
}
});
}
});
}
}
}
});
This is only the add page view but I think that is all that's needed.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
This string represents path in a current object with fixed this context. In your example you've waitFor: 'brands' which is nothing more than PageView.brands here, as PageView is this context. If you'd have model.some.attribute, then it'd mean that this string represents PageView.model.some.attribute. It's just convenient way to traverse through objects.
There's to few informations to answer your latter question. In what form you retrieve your data? What do you want to do with it later on?
It'd be much quicker if you could ping us on https://gitter.im/AmpersandJS/AmpersandJS :)

Javascript Data Layer Architecture Assistance

I'm making a fairly complex HTML 5 + Javascript game. The client is going to have to download images and data at different points of the game depending on the area they are at. I'm having a huge problem resolving some issues with the Data Layer portion of the Javascript architecture.
The problems I need to solve with the Data Layer:
Data used in the application that becomes outdated needs to be automatically updated whenever calls are made to the server that retrieve fresh data.
Data retrieved from the server should be stored locally to reduce any overhead that would come from requesting the same data twice.
Any portion of the code that needs access to data should be able to retrieve it easily and in a uniform way regardless of whether the data is available locally already.
What I've tried to do to accomplish this is build a data layer that has two main components:
1. The portion of the layer that gives access to the data (through get* methods)
2. The portion of the layer that stores and synchronizes local data with data from the server.
The workflow is as follows:
When the game needs access to some data it calls get* method in the data layer for that data, passing a callback function.
bs.data.getInventory({ teamId: this.refTeam.PartyId, callback: this.inventories.initialize.bind(this.inventories) });
The get* method determines whether the data is already available locally. If so it either returns the data directly (if no callback was specified) or calls the callback function passing it the data.
If the data is not available, it stores the callback method locally (setupListener) and makes a call to the communication object passing the originally requested information along.
getInventory: function (obj) {
if ((obj.teamId && !this.teamInventory[obj.teamId]) || obj.refresh) {
this.setupListener(this.inventoryNotifier, obj);
bs.com.getInventory({ teamId: obj.teamId });
}
else if (typeof (obj.callback) === "function") {
if (obj.teamId) {
obj.callback(this.team[obj.teamId].InventoryList);
}
}
else {
if (obj.teamId) {
return this.team[obj.teamId].InventoryList;
}
}
}
The communication object then makes an ajax call to the server and waits for the data to return.
When the data is returned a call is made to the data layer again asking it to publish the retrieved data.
getInventory: function (obj) {
if (obj.teamId) {
this.doAjaxCall({ orig: obj, url: "/Item/GetTeamEquipment/" + obj.teamId, event: "inventoryRefreshed" });
}
},
doAjaxCall: function (obj) {
var that = this;
if (!this.inprocess[obj.url + obj.data]) {
this.inprocess[obj.url + obj.data] = true;
$.ajax({
type: obj.type || "GET",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: obj.data,
url: obj.url,
async: true,
success: function (data) {
try {
ig.fire(bs.com, obj.event, { data: data, orig: obj.orig });
}
catch (ex) {
// this enables ajaxComplete to fire
ig.log(ex.message + '\n' + ex.stack);
}
finally {
that.inprocess[obj.url + obj.data] = false;
}
},
error: function () { that.inprocess[obj.url + obj.data] = false; }
});
}
}
The data layer then stores all of the data in a local object and finally calls the original callback function, passing it the requested data.
publishInventory: function (data) {
if (!this.inventory) this.inventory = {};
for (var i = 0; i < data.data.length; i++) {
if (this.inventory[data.data[i].Id]) {
this.preservingUpdate(this.inventory[data.data[i].Id], data.data[i]);
}
else {
this.inventory[data.data[i].Id] = data.data[i];
}
}
// if we pulled this inventory for a team, update the team
// with the inventory
if (data.orig.teamId && this.team[data.orig.teamId]) {
this.teamInventory[data.orig.teamId] = true;
this.team[data.orig.teamId].InventoryList = [];
for (var i = 0; i < data.data.length; i++) {
this.team[data.orig.teamId].InventoryList.push(data.data[i]);
}
}
// set up the data we'll notify with
var notifyData = [];
for (var i = 0; i < data.data.length; i++) {
notifyData.push(this.inventory[data.data[i].Id]);
}
ig.fire(this.inventoryNotifier, "refresh", notifyData, null, true);
}
There are several problems with this that bother me constantly. I'll list them in order of most annoying :).
Anytime I have to add a call that goes through this process it takes too much time to do so. (at least an hour)
The amount of jumping and callback passing gets confusing and seems very prone to errors.
The hierarchical way in which I am storing the data is incredibly difficult to synchronize and manage. More on that next.
Regarding issue #3 above, if I have objects in the data layer that are being stored that have a structure that looks like this:
this.Account = {Battles[{ Teams: [{ TeamId: 392, Characters: [{}] }] }]}
this.Teams[392] = {Characters: [{}]}
Because I want to store Teams in a way where I can pass the TeamId to retrieve the data (e.g. return Teams[392];) but I also want to store the teams in relation to the Battles in which they exist (this.Account.Battles[0].Teams[0]); I have a nightmare of a time keeping each instance of the same team fresh and maintaining the same object identity (so I am not actually storing it twice and so that my data will automatically update wherever it is being used which is objective #1 of the data layer).
It just seems so messy and jumbled.
I really appreciate any help.
Thanks
You should consider using jquery's deferred objects.
Example:
var deferredObject = $.Deferred();
$.ajax({
...
success: function(data){
deferredObject.resolve(data);
}
});
return deferredObject;
Now with the deferredObject returned, you can attach callbacks to it like this:
var inventoryDfd = getInventory();
$.when(inventoryDfd).done(function(){
// code that needs data to continue
}
and you're probably less prone to errors. You can even nest deferred objects, or combine them so that a callback isn't called until multiple server calls are downloaded.
+1 for Backbone -- it does some great heavy lifting for you.
Also look at the Memoizer in Douglas Crockford's book Javascript the Good Parts. It's dense, but awesome. I hacked it up to make the memo data store optional, and added more things like the ability to set a value without having to query first -- e.g. to handle data freshness.

Categories

Resources