I'm just beginning to understand how the Observer Pattern works. But now I want to put it to use. I see lots of examples of the Observer Pattern, but most simply demonstrate the pattern and don't show it being implemented to complete a task.
I understand that an observer gets notified by a subject. But, how can I then get the observer to then do something as a result?
The code below is a common Observer Pattern program. I see that it executes a console.log (console.log("Observer " + number + " is notified!");) when an observer is notified. Is this where I could, instead, return other things: values, function calls, etc.?
I've seen visual examples with simple math that updates a calculation when an observer is notified (i.e., cost + tax = total). But I can't find an example that shows how the code is making this happening. 1. how/where is the new cost being returned and 2. so an observer (a function?) gets notified that the cost has changed; how/where does the function receive the updated cost?
I'm a novice and the pattern, itself, is a bit baffling on its own. I'd love to see a super basic example program.
var Subject = function() {
let observers = [];
return {
subscribeObserver: function(observer) {
observers.push(observer);
},
unsubscribeObserver: function(observer) {
var index = observers.indexOf(observer);
if(index > -1) {
observers.splice(index, 1);
}
},
notifyObserver: function(observer) {
var index = observers.indexOf(observer);
if(index > -1) {
observers[index].notify(index);
}
},
notifyAllObservers: function() {
for(var i = 0; i < observers.length; i++){
observers[i].notify(i);
};
}
};
};
var Observer = function(number) {
return {
notify: function() {
console.log("Observer " + number + " is notified!");
}
}
}
var subject = new Subject();
var observer1 = new Observer(1);
var observer2 = new Observer(2);
subject.subscribeObserver(observer1);
subject.subscribeObserver(observer2);
subject.notifyObserver(observer2);
subject.unsubscribeObserver(observer2);
subject.notifyAllObservers();
In the simplest form, an Observer is nothing but a function that is invoked by the Subject. More "complex" Observers would be objects with a "notify" or similar function, an error handler and maybe a "done" notification, depending on the Subject.
So here's a very basic example to demonstrate this:
var Subject = function() {
let observers = [];
return {
subscribe: function(observer) {
observers.push(observer);
},
unsubscribe: function(observer) {
var index = observers.indexOf(observer);
if(index > -1) {
observers.splice(index, 1);
}
},
nextValue: function(value) {
// call every registered observer
for(var i = 0; i < observers.length; i++){
observers[i](value);
}
}
};
};
// now simply pass an observer function to the subject
const subject = new Subject();
subject.subscribe(value => {
// do whatever you want with the value, call functions etc.
console.log(value);
});
subject.nextValue('hello world!');
Just use the RxJs library.
https://www.learnrxjs.io/
const { Subject } = rxjs;
const subject$ = new Subject();
subject$.subscribe(val => { console.log(val); });
subject$.next('Hello');
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.1/rxjs.umd.min.js"></script>
The code below is a common Observer Pattern program. I see that it executes a console.log (console.log("Observer " + number + " is notified!");) when an observer is notified. Is this where I could, instead, return other things: values, function calls, etc.?
Yes when the subject calls notify it can pass data instead of an index or pass itself to the observer. In this way the observer can inspect Subject's state and do something with it. The method notifyObserver becomes:
notifyObserver: function(observer) {
var index = observers.indexOf(observer);
if(index > -1) {
observers[index].notify(this);
}
},
And the observer is something like:
var Observer = function(number) {
return {
notify: function(subject) {
// do something with subject
...
}
}
}
Related
Trying to build a small component that sends you notifications via the Notifications Object on a small web app.
So, once a private message has been received in a group, if the value of the field has incremented or changed, then display a notification.
This works fine if you refresh the page but it is not running asynchronously.
With Object.observe() being deprecated, would somebody please explain how I would implement this? I don't quite understand how to do so with Proxies.
Thanks a lot!!
shortened for brevity
var myGroup = 0;
var notificationCount = [];
notificationCount.push({'myGroup': myGroup});
localStorage.setItem('notificationCount', JSON.stringify(notificationCount));
var storedCounts = JSON.parse(localStorage.getItem("notificationCount");
setTimeout(function() {
var newCountMyGroup = parseInt($('.myGroup .wrapper').text());
if(newCountMyGroup > 0 && storedCounts[0].myGroup !== newCountMyGroup) {
notify('New Post in My Groups', 'linkHere')
}
}, 800);
function notify(alertMessage, alertLink) {
if(Notification.permission !== "granted") {
Notification.requestPermission();
} else {
var notificationMyGroup = new Notification(...);
notificationMyGroup.onclick = function(){
window.open(alertLink);
}
}
}
From your comment:
I want a Notification to display when the div's value changes due to a message. The setTimeout function isn't running properly. The desired outcome is for it to run async as soon as a message is received. This currently only works once you refresh the page.
I read past it three times, but your current code schedules a single timed callback on page load for 800ms later. setTimeout just sets a one-off timer. If you want that to be repeated, you use setInterval instead.
So if you want to do this with polling, setInterval instead of setTimeout will do it.
If you want to do it without polling, you don't need Object.observe or Proxy because what you want to observe is a DOM element (the div), not a JavaScript object. Fortunately, there's a tool for that: Mutation observers. You could watch for childList changes to all .myGroup elements, which would tell you when a new .wrapper was added, and/or watch for characterData/childList notifications on the .wrapper elements, which will tell you when their text is changed.
Here's a quick example of both, see comments:
var wrapperId = 0;
// Our function for when a .myGroup's child list changes
function myGroupModificationCallback(records) {
// (Your real code would go here)
console.log("Saw a modification to " + records[0].target.id);
// If you want to watch wrappers, you'd set them up by calling hookUpWrapperObservers
hookUpWrapperObservers();
}
// Hook up obervers to any `.myGroup` elements that don't have them yet
function hookUpMyGroupObservers() {
$(".myGroup").each(function() {
var group = $(this);
var ob = group.data("ob");
if (!ob) {
ob = new MutationObserver(myGroupModificationCallback);
ob.observe(this, {
childList: true
});
group.data("ob", ob);
}
});
}
// Our function for when a .wrapper's character data changes
function wrapperNotificationCallback(records) {
// (Your real code would go here. Note you may get multiple records for the same wrapper.)
var changes = Object.create(null);
records.forEach(function(record) {
changes[record.target.id] = record.target;
});
Object.keys(changes).forEach(function(id) {
console.log(id + " changed: " + $(changes[id]).text());
});
}
// Hook up observers to any .wrapper elements that don't have them yet
function hookUpWrapperObservers() {
$(".myGroup .wrapper").each(function() {
var wrapper = $(this);
var ob = wrapper.data("ob");
if (!ob) {
var ob = new MutationObserver(wrapperNotificationCallback);
ob.observe(this, {
characterData: true,
childList: true
});
wrapper.data("ob", ob);
console.log(this.id + " received: " + $(this).text());
}
});
}
// Initial setup
hookUpMyGroupObservers();
hookUpWrapperObservers();
// Testing/demo: Add two wrappers to the first group and one to the
// second Update all three of them three times, then stop
setTimeout(function() {
addWrapper("#group1");
setTimeout(function() {
addWrapper("#group2");
setTimeout(function() {
addWrapper("#group1");
}, 300);
}, 300);
function addWrapper(selector) {
var wrapper = $("<div class=wrapper>1</div>");
wrapper[0].id = "wrapper" + (++wrapperId);
$(selector).append(wrapper);
var counter = 0;
var timer = setInterval(function() {
wrapper.text(parseInt(wrapper.text()) + 1);
if (++counter == 3) {
clearInterval(timer);
}
}, 300);
}
}, 300);
<div class="myGroup" id="group1"></div>
<div class="myGroup" id="group2"></div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Mutation observer support is good in modern browsers. IE9 and IE10 implemented the previous mutation events, and so there are polyfills that use events to provide a subset of observer behavior on those browers. For IE8 and earlier, you'll need to poll.
Here's a dead simple demo of using a proxy.
let o = {a: 1, b: 2};
let p = new Proxy(o, {
get: (target, name) => {
console.log(`getting ${name} on ${target}: ${target[name]}`)
return target[name];
},
set: (target, name, value) => {
console.log(`setting ${name} to ${value} on ${target}`)
target[name] = value;
}
});
p.a; // getting a on [object Object]: 1
p.a = 5; // setting a to 5 on [object Object]
p.a; // getting a on [object Object]: 5
Instead of doing a console.log, connect this to whatever event handler you'd like.
You could do something like ...
set: (target, name, value) => {
event.emit(`set:${name}`, target, value, target[name]);
target[name] = value;
}
... or whatever works for you.
I want to use Proxy on a customized class called ObservableList which contains an Array. Since Proxy is available only after ES6, I wonder if there is any alternative implementation.
My requirement is to get updated (rather than get noticed) for observers once ObservableList changes, so that the observers are always consist with observable with some filtering or mapping method.
var activities = new ObservableList(['reading', 'swimming']);
var sAct = activities.filter(function(v) {
return v[0] === 's';
});
// expect sAct.list to be ['swimming']
var meAct = activities.map(function(v) {
return 'I am ' + v;
});
// expect meAct.list to be ['I am reading', 'I am swimming']
activities.list.push('smiling');
console.log(sAct.list, meAct.list);
// expect sAct.list to be ['swimming', 'smiling']
// expect meAct.list to be ['I am reading', 'I am swimming', 'I am smiling']
activities.list[1] = 'snoopying';
console.log(sAct.list, meAct.list);
// expect sAct.list to be ['swimming', 'snoopying']
// expect meAct.list to be ['I am reading', 'I am snoopying', 'I am smiling']
My implementation with Proxy is available at https://jsfiddle.net/ovilia/tLmbptr0/3/
used defineProperty.
not exactly with what you want. i just implemented a "reactive array". but i think it maybe works in your problems.
bad parts:
defined tons of getters/setters on the target.
accessing indexers not defined will be not reactive.
update() is to-be optimized.
good parts:
ES5 friendly.
if no indexers needed, use set(i, val)/get(i) will be reactive.
https://jsfiddle.net/jimnox/jrtq40p7/2/
As described in questions, I only need ObservableList to contain an Array, rather than to extend it, as Jim did in his complicated answer. And surprisingly enough, I found this could be easily achieved by wrapping the original Array operations.
One limitation is that index operation is not reactive in my implementation, in that I failed to find a proper way to capture index operations. If you have a better idea, feel welcomed to tell me! XD
Here's the full implementation.
export class ObservableList {
list: Array<any>;
private _observer: Array<ObserverList>;
constructor(list?: Array<any>) {
this.list = list || [];
this._initList();
this._initMethods();
this._observer = [];
}
notify(): void {
for (let o of this._observer) {
o.update();
}
}
private _initList(): void {
var that = this;
var operations = ['push', 'pop', 'shift', 'unshift', 'splice',
'sort', 'reverse'];
for (let operation of operations) {
this.list[operation] = function() {
var res = Array.prototype[operation].apply(that.list, arguments);
that.notify();
return res;
}
}
}
private _initMethods(): void {
var that = this;
var methods = ['filter', 'map'];
for (let method of methods) {
this[method] = (formatter: Function): ObserverList => {
var observer = new ObserverList(that, formatter, method);
this._observer.push(observer);
return observer;
}
}
}
}
export class ObserverList {
public list: Array<any>;
constructor(public observable: ObservableList,
public formatter: Function,
public method: string) {
this.list = [];
this.update();
}
update(): void {
var list = [];
var master = this.observable.list;
for (var i = 0, len = master.length; i < len; ++i) {
if (this.method === 'filter') {
if (this.formatter(master[i])) {
list.push(master[i]);
}
} else if (this.method === 'map') {
list.push(this.formatter(master[i]));
} else {
console.error('Illegal method ' + this.method + '.');
}
}
this.list = list;
}
}
Is using proxies a hard requirement? I wouldn't recommend proxies for
general programming tasks as you can end up with unpredictable and
hard-to-spot side effects.
If you keep to data and functions to transform it, avoiding mutable
state where possible, I think you'll end up with simpler code that's
easier to maintain.
var activities = ['reading', 'swimming'];
var sfilter = function(activities){
return activities.filter(function(v){
return v[0] === 's';
});
};
console.log(sfilter(activities));
var memap = function(activities){
return activities.map(function(v){
return 'I am ' + v;
});
};
console.log(memap(activities));
activities.push('smiling');
console.log(sfilter(activities));
console.log(memap(activities));
// Yes, I know this doesn't work in quite the same way,
// but you're asking for trouble here since in your
// code you're appending to one list, but overwriting
// an element in the other.
activities[1] = 'snoopying';
console.log(sfilter(activities));
console.log(memap(activities));
Stick to a Single Source of Truth and observe that. With each copy you are multiplying state complexity. That will make debugging, testing, and extending the code difficult.
Durring some experiments with protractorJS i noticed that there is no easy way to extend (inherit) ElementFinder object from protractor to add own functions.
For example, i want to create object Checkbox, that would have additional method - check() - should switch checkbox depending on result of isSelected().
I come up with the code -
var ElementFinder = require('protractor/lib/element.js').ElementFinder;
var ElementArrayFinder = require('protractor/lib/element.js').ElementArrayFinder;
class CheckBox extends ElementFinder {
constructor(loc) {
var getWebElements = function () {
var ptor = browser;
var locator = loc;
return ptor.waitForAngular().then(function() {
if (locator.findElementsOverride) {
return locator.findElementsOverride(ptor.driver, null, ptor.rootEl);
} else {
return ptor.driver.findElements(locator);
}
});
}
var ArrayFinderFull = new ElementArrayFinder(browser, getWebElements, loc);
super(browser, ArrayFinderFull);
}
check() {
return this.isSelected().then(selected => selected? this.click() : null)
}
}
But getWebElements is copy-paste from protractor/element.js -
https://github.com/angular/protractor/blob/3.1.0/lib/element.js#L131
This copy-paste flustrating me. I think there should be more proper way to extend ElementFinder.
Does anyone inherited ElementFinder in protractorJS?
I'm not sure this would help, but here is something we did recently to have a takewhile() method available on an ElementArrayFinder. We've put the following into onPrepare():
protractor.ElementArrayFinder.prototype.takewhile = function(whileFn) {
var self = this;
var getWebElements = function() {
return self.getWebElements().then(function(parentWebElements) {
var list = [];
parentWebElements.forEach(function(parentWebElement, index) {
var elementFinder =
protractor.ElementFinder.fromWebElement_(self.ptor_, parentWebElement, self.locator_);
list.push(whileFn(elementFinder, index));
});
return protractor.promise.all(list).then(function(resolvedList) {
var filteredElementList = [];
for (var index = 0; index < resolvedList.length; index++) {
if (!resolvedList[index]) {
break;
}
filteredElementList.push(parentWebElements[index])
}
return filteredElementList;
});
});
};
return new protractor.ElementArrayFinder(this.ptor_, getWebElements, this.locator_);
};
And now we can use takewhile on the result of element.all():
element.all(by.repeater("row in rows")).takewhile(function (elm) {
return elm.getText().then(function (text) {
return some_condition_to_be_true;
});
});
Now it is much simplier to extend ElementFinder, i calling this - page fragments.
I even created lib to solve this issue (PRs welcome!) - https://github.com/Xotabu4/protractor-element-extend
For now it only works with ElementFinder, but i want to be able to extend
ElementArrayFinders as well (planned for 2.0.0 version)
UPDATE
Support for ElementArrayFinder inheritance is added.
Is there a way to know when a user has pushed (via push()) an item onto an array?
Basically I have an asynchronous script that allows the user to push commands onto an array. Once my script loads, it execute the commands. The problems is, the user may push additional commands onto the array after my script has already run and I need to be notified when this happens. Keep in mind this is just a regular array that the user creates themselves. Google Analytics does something similar to this.
I also found this which is where I think Google does it, but I don't quite understand the code:
Aa = function (k) {
return Object.prototype[ha].call(Object(k)) == "[object Array]"
I also found a great example which seems to cover the bases, but I can't get my added push method to work correctly:
http://jsbin.com/ixovi4/4/edit
You could use an 'eventify' function that overrides push in the passed array.
var eventify = function(arr, callback) {
arr.push = function(e) {
Array.prototype.push.call(arr, e);
callback(arr);
};
};
In the following example, 3 alerts should be raised as that is what the event handler (callback) does after eventify has been called.
var testArr = [1, 2];
testArr.push(3);
eventify(testArr, function(updatedArr) {
alert(updatedArr.length);
});
testArr.push(4);
testArr.push(5);
testArr.push(6);
The only sensible way to do this is to write a class that wraps around an array:
function EventedArray(handler) {
this.stack = [];
this.mutationHandler = handler || function() {};
this.setHandler = function(f) {
this.mutationHandler = f;
};
this.callHandler = function() {
if(typeof this.mutationHandler === 'function') {
this.mutationHandler();
}
};
this.push = function(obj) {
this.stack.push(obj);
this.callHandler();
};
this.pop = function() {
this.callHandler();
return this.stack.pop();
};
this.getArray = function() {
return this.stack;
}
}
var handler = function() {
console.log('something changed');
};
var arr = new EventedArray(handler);
//or
var arr = new EventedArray();
arr.setHandler(handler);
arr.push('something interesting'); //logs 'something changed'
try this:
var MyArray = function() { };
MyArray.prototype = Array.prototype;
MyArray.prototype.push = function() {
console.log('push now!');
for(var i = 0; i < arguments.length; i++ ) {
Array.prototype.push.call(this, arguments[i]);
}
};
var arr = new MyArray();
arr.push(2,3,'test',1);
you can add functions at after pushing or before pushing
Why not just do something like this?
Array.prototype.eventPush = function(item, callback) {
this.push(item);
callback(this);
}
Then define a handler.
handler = function(array) {
console.log(array.length)
}
Then use the eventPush in the place that you want a specific event to happen passing in the handler like so:
a = []
a.eventPush(1, handler);
a.eventPush(2, handler);
I'd wrap the original array around a simple observer interface like so.
function EventedList(list){
this.listbase = list;
this.events = [];
}
EventedList.prototype.on = function(name, callback){
this.events.push({
name:name,
callback:callback
});
}
//push to listbase and emit added event
EventedList.prototype.push = function(item){
this.listbase.push(item);
this._emit("added", item)
}
EventedList.prototype._emit = function(evtName, data){
this.events.forEach(function(event){
if(evtName === event.name){
event.callback.call(null, data, this.listbase);
}
}.bind(this));
}
Then i'd instantiate it with a base array
//returns an object interface that lets you observe the array
var evtList = new EventedList([]);
//attach a listener to the added event
evtList.on('added', function(item, list){
console.log("added event called: item = "+ item +", baseArray = "+ list);
})
evtList.push(1) //added event called: item = 1, baseArray = 1
evtList.push(2) //added event called: item = 2, baseArray = 1,2
evtList.push(3) //added event called: item = 3, baseArray = 1,2,3
you can also extend the observer to observe other things like prePush or postPush or whatever events you'd like to emit as you interact with the internal base array.
This will add a function called onPush to all arrays, by default it shouldn't do anything so it doesn't interfere with normal functioning arrays.
just override onPush on an individual array.
Array.prototype.oldPush = Array.prototype.push;
Array.prototype.push = function(obj){
this.onPush(obj);
this.oldPush(obj);
};
//Override this method, be default this shouldnt do anything. As an example it will.
Array.prototype.onPush = function(obj){
alert(obj + 'got pushed');
};
//Example
var someArray = [];
//Overriding
someArray.onPush = function(obj){
alert('somearray now has a ' + obj + ' in it');
};
//Testing
someArray.push('swag');
This alerts 'somearray now has a swag in it'
If you want to do it on a single array :
var a = [];
a.push = function(item) {
Array.prototype.push.call(this, item);
this.onPush(item);
};
a.onPush = function(obj) {
// Do your stuff here (ex: alert(this.length);)
};
Sometimes you need to queue things up before a callback is available. This solves that issue. Push any item(s) to an array. Once you want to start consuming these items, pass the array and a callback to QueuedCallback(). QueuedCallback will overload array.push as your callback and then cycle through any queued up items. Continue to push items to that array and they will be forwarded directly to your callback. The array will remain empty.
Compatible with all browsers and IE 5.5+.
var QueuedCallback = function(arr, callback) {
arr.push = callback;
while (arr.length) callback(arr.shift());
};
Sample usage here.
Untested, but I am assuming something like this could work:
Array.prototype.push = function(e) {
this.push(e);
callbackFunction(e);
}
A lot better way is to use the fact that those methods modify array length.
The way to take advantage of that is quite simple (CoffeeScript):
class app.ArrayModelWrapper extends Array
constructor: (arr,chName,path)->
vl = arr.length
#.push.apply(#,arr)
Object.defineProperty(#,"length",{
get: ->vl
set: (newValue)=>
console.log("Hello there ;)")
vl = newValue
vl
enumerable: false
})
for debugging purpose you can try. And track the calling function from the call stack.
yourArray.push = function(){debugger;}
We can prototype Array to add a MyPush function that does push the rec to the array and then dispatches the event.
Array.prototype.MyPush = (rec) =>
{
var onArrayPush = new Event("onArrayPush",{bubbles:true,cancelable:true});
this.push(rec);
window.dispatchEvent(onArrayPush);
};
and then we need an eventhandler to capture the event, here I am capturing the event to log the event and then indexing the record for example:
addEventListener("onArrayPush",(e)=> {
this.#Log(e);
this.#IndexRecords();
});
But in 2022 you may also go with callback as:
Array.prototype.MyPush = function(rec,cb){
this.push(rec);
cb(rec);
};
here cb is the callback that is invoked after rec is pushed to the Array. This works at least in the console.
I come from the land of Java, C#, etc. I am working on a javascript report engine for a web application I have. I am using jQuery, AJAX, etc. I am having difficulty making things work the way I feel they should - for instance, I have gone to what seems like too much trouble to make sure that when I make an AJAX call, my callback has access to the object's members. Those callback functions don't need to be that complicated, do they? I know I must be doing something wrong. Please point out what I could be doing better - let me know if the provided snippet is too much/too little/too terrible to look at.
What I'm trying to do:
On page load, I have a select full of users.
I create the reports (1 for now) and add them to a select box.
When both a user and report are selected, I run the report.
The report involves making a series of calls - getting practice serieses, leagues, and tournaments - for each league and tournament, it gets all of those serieses, and then for each series it grabs all games.
It maintains a counter of the calls that are active, and when they have all completed the report is run and displayed to the user.
Code:
//Initializes the handlers and reports
function loadUI() {
loadReports();
$("#userSelect").change(updateRunButton);
$("#runReport").click(runReport);
updateRunButton();
return;
$("#userSelect").change(loadUserGames);
var user = $("#userSelect").val();
if(user) {
getUserGames(user);
}
}
//Creates reports and adds them to the select
function loadReports() {
var reportSelect = $("#reportSelect");
var report = new SpareReport();
engine.reports[report.name] = report;
reportSelect.append($("<option/>").text(report.name));
reportSelect.change(updateRunButton);
}
//The class that represents the 1 report we can run right now.
function SpareReport() {
this.name = "Spare Percentages";
this.activate = function() {
};
this.canRun = function() {
return true;
};
//Collects the data for the report. Initializes/resets the class variables,
//and initiates calls to retrieve all user practices, leagues, and tournaments.
this.run = function() {
var rC = $("#rC");
var user = engine.currentUser();
rC.html("<img src='/img/loading.gif' alt='Loading...'/> <span id='reportProgress'>Loading games...</span>");
this.pendingOperations = 3;
this.games = [];
$("#runReport").enabled = false;
$.ajaxSetup({"error":(function(report) {
return function(event, XMLHttpRequest, ajaxOptions, thrownError) {
report.ajaxError(event, XMLHttpRequest, ajaxOptions, thrownError);
};
})(this)});
$.getJSON("/api/leagues", {"user":user}, (function(report) {
return function(leagues) {
report.addSeriesGroup(leagues);
};
})(this));
$.getJSON("/api/tournaments", {"user":user}, (function(report) {
return function(tournaments) {
report.addSeriesGroup(tournaments);
};
})(this));
$.getJSON("/api/practices", {"user":user}, (function(report) {
return function(practices) {
report.addSerieses(practices);
};
})(this));
};
// Retrieves the serieses (group of IDs) for a series group, such as a league or
// tournament.
this.addSeriesGroup = function(seriesGroups) {
var report = this;
if(seriesGroups) {
$.each(seriesGroups, function(index, seriesGroup) {
report.pendingOperations += 1;
$.getJSON("/api/seriesgroup", {"group":seriesGroup.key}, (function(report) {
return function(serieses) {
report.addSerieses(serieses);
};
})(report));
});
}
this.pendingOperations -= 1;
this.tryFinishReport();
};
// Retrieves the actual serieses for a series group. Takes a set of
// series IDs and retrieves each series.
this.addSerieses = function(serieses) {
var report = this;
if(serieses) {
$.each(serieses, function(index, series) {
report.pendingOperations += 1;
$.getJSON("/api/series", {"series":series.key}, (function(report) {
return function(series) {
report.addSeries(series);
};
})(report));
});
}
this.pendingOperations -= 1;
this.tryFinishReport();
};
// Adds the games for the series to the list of games
this.addSeries = function(series) {
var report = this;
if(series && series.games) {
$.each(series.games, function(index, game) {
report.games.push(game);
});
}
this.pendingOperations -= 1;
this.tryFinishReport();
};
// Checks to see if all pending requests have completed - if so, runs the
// report.
this.tryFinishReport = function() {
if(this.pendingOperations > 0) {
return;
}
var progress = $("#reportProgress");
progress.text("Performing calculations...");
setTimeout((function(report) {
return function() {
report.finishReport();
};
})(this), 1);
}
// Performs report calculations and displays them to the user.
this.finishReport = function() {
var rC = $("#rC");
//snip a page of calculations/table generation
rC.html(html);
$("#rC table").addClass("tablesorter").attr("cellspacing", "1").tablesorter({"sortList":[[3,1]]});
};
// Handles errors (by ignoring them)
this.ajaxError = function(event, XMLHttpRequest, ajaxOptions, thrownError) {
this.pendingOperations -= 1;
};
return true;
}
// A class to track the state of the various controls. The "series set" stuff
// is for future functionality.
function ReportingEngine() {
this.seriesSet = [];
this.reports = {};
this.getSeriesSet = function() {
return this.seriesSet;
};
this.clearSeriesSet = function() {
this.seriesSet = [];
};
this.addGame = function(series) {
this.seriesSet.push(series);
};
this.currentUser = function() {
return $("#userSelect").val();
};
this.currentReport = function() {
reportName = $("#reportSelect").val();
if(reportName) {
return this.reports[reportName];
}
return null;
};
}
// Sets the enablement of the run button based on the selections to the inputs
function updateRunButton() {
var report = engine.currentReport();
var user = engine.currentUser();
setRunButtonEnablement(report != null && user != null);
}
function setRunButtonEnablement(enabled) {
if(enabled) {
$("#runReport").removeAttr("disabled");
} else {
$("#runReport").attr("disabled", "disabled");
}
}
var engine = new ReportingEngine();
$(document).ready( function() {
loadUI();
});
function runReport() {
var report = engine.currentReport();
if(report == null) {
updateRunButton();
return;
}
report.run();
}
I am about to start adding new reports, some of which will operate on only a subset of user's games. I am going to be trying to use subclasses (prototype?), but if I can't figure out how to simplify some of this... I don't know how to finish that sentence. Help!
$.getJSON("/api/leagues", {"user":user}, (function(report) {
return function(leagues) {
report.addSeriesGroup(leagues);
};
})(this));
Can be written as:
var self = this;
$.getJSON("/api/leagues", {"user":user}, (function(leagues) {
self.addSeriesGroup(leagues);
});
The function-returning-function is more useful when you're inside a loop and want to bind to a variable that changes each time around the loop.
Provide "some" comments where necessary.
I'm going to be honest with you and say that I didn't read the whole thing. However, I think there is something about JavaScript you should know and that is that it has closures.
var x = 1;
$.ajax({
success: function () {
alert(x);
}
});
No matter how long time it takes for the AJAX request to complete, it will have access to x and will alert "1" once it succeeds.
Understand Closures. This takes some getting used to. (which, many will use, and is certainly the typical way of going about things, so it's good if you understand how that's happening)
This is a good thread to read to get a simple explanation of how to use them effectively.
You should use prototypes to define methods and do inheritance:
function Parent(x) {
this.x = x; /* Set an instance variable. Methods come later. */
}
/* Make Parent inherit from Object by assigning an
* instance of Object to Parent.prototype. This is
* very different from how you do inheritance in
* Java or C# !
*/
Parent.prototype = { /* Define a method in the parent class. */
foo: function () {
return 'parent ' + this.x; /* Use an instance variable. */
}
}
function Child(x) {
Parent.call(this, x) /* Call the parent implementation. */
}
/* Similar to how Parent inherits from Object; you
* assign an instance of the parent class (Parent) to
* the prototype attribute of the child constructor
* (Child).
*/
Child.prototype = new Parent();
/* Specialize the parent implementation. */
Child.prototype.foo = function() {
return Parent.prototype.foo.call(this) + ' child ' + this.x;
}
/* Define a method in Child that does not override
* something in Parent.
*/
Child.prototype.bar = function() {
return 'bar';
}
var p = new Parent(1);
alert(p.foo());
var ch = new Child(2);
alert(ch.foo());
alert(ch.bar());
I'm not familiar with jQuery, but I know the Prototype library (worst name choice ever) has some functionality that make it easier to work with inheritance.
Also, while coming up with the answer to this question, I found a nice page that goes into more detail on how to do OO right in JS, which you may want to look at.