Knockout Audio Playlist - javascript

I'm creating a music-based app that uses Knockout for it's bindings. All of the tracks listed in the app are pulled externally from Soundcloud like so:
$.getJSON('http://api.soundcloud.com/users/guy-j/tracks.json?client_id=c4bc3b1a93902abecbaca3fa4582d970', {limit: 200}, function(data) {
vm.tracks($.map(data, function (track) {
return {
artwork: track.artwork_url,
avatar: track.user.avatar_url,
date: track.created_at,
description: track.description,
duration: track.duration,
listens: track.playback_count,
permalink: track.permalink_url,
purhcase: track.purchase_url,
stream: track.stream_url,
track: track.title
};
}));
});
These tracks (once fetched) are pushed into a blank observableArray and the HTML view is then bound to that array and generates a list of tracks. I've got the audio playing/pausing like so:
To select/play a track, each <li> that's generated from the observableArray has a click handler called 'goToTrack' which then passes an observable called 'self.chosenTrackData' the selected track and my audio element is then bound with 'chosenTrackData' to play the chosen track.
My problem now lies with the fact that I'm not quite sure what the best way to approach the next/prev track features using Knockout would be. I'd have to some how distinguish the current point in the observableArray and then ++ or -- depending which option you've selected?
Any help would be greatly appreciated as I'm still learning Knockout!

I'd suggest keeping an internal "current track" number that you increment or decrement and base a computed on that.
function PlayerViewModel() {
var self = this,
currentTrackNo = ko.observable(0);
// data
self.tracks = ko.observableArray();
// computeds
self.currentTrack = ko.computed(function () {
return self.tracks()[currentTrackNo()];
});
self.hasTrack = function (track) {
return !!self.currentTrack();
};
self.hasNextTrack = ko.computed(function () {
return currentTrackNo() < self.tracks().length - 1;
});
self.hasPrevTrack = ko.computed(function () {
return currentTrackNo() > 0;
});
// API
self.setNextTrack = function () {
if (self.hasNextTrack()) {
currentTrackNo(currentTrackNo() + 1);
}
};
self.setPrevTrack = function () {
if (self.hasPrevTrack()) {
currentTrackNo(currentTrackNo() - 1);
}
};
self.setTrack = function (track) {
var trackNo = ko.utils.arrayIndexOf(self.tracks(), track);
currentTrackNo(trackNo);
};
// init
$.getJSON('...', {limit: 200}); etc;
}
Bind your "current track" view component to the currentTrack observable. Make sure that you use hasTrack in some way (as currentTrack might be undefined).
Alternative implementation, avoiding the maintenance of a separate track number:
function PlayerViewModel() {
var self = this;
// data
self.tracks = ko.observableArray();
self.currentTrack = ko.observable();
// computeds
self.currentTrackNo = ko.computed(function () {
var currentTrack = self.currentTrack();
return ko.utils.arrayIndexOf(self.tracks(), currentTrack);
});
self.hasTrack = function (track) {
return !!self.currentTrack();
};
self.hasNextTrack = ko.computed(function () {
return self.currentTrackNo() < self.tracks().length - 1;
});
self.hasPrevTrack = ko.computed(function () {
return self.currentTrackNo() > 0;
});
// API
self.setNextTrack = function () {
if (self.hasNextTrack()) {
self.currentTrack(self.tracks()[self.currentTrackNo() + 1]);
}
};
self.setPrevTrack = function () {
if (self.hasPrevTrack()) {
self.currentTrack(self.tracks()[self.currentTrackNo() - 1]);
}
};
self.setTrack = function (track) {
// make sure that track is actually in self.tracks here
self.currentTrack(track);
};
// init
$.getJSON('...', {limit: 200}); etc;
}

Related

Javascript & knockoutjs: how to refactor the following code to be able to access the properties outside the function

Im struggling to find a way to get the properties Override & Justification available outside of the function. The code is:
self.CasOverridesViewModel = ko.observable(self.CasOverridesViewModel);
var hasOverrides = typeof self.CasOverridesViewModel === typeof(Function);
if (hasOverrides) {
self.setupOverrides = function() {
var extendViewModel = function(obj, extend) {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
extend(obj[property]);
}
}
};
extendViewModel(self.CasOverridesViewModel(), function(item) {
item.isOverrideFilledIn = ko.computed( function() {
var result = false;
if (!!item.Override()) {
result = true;
}
return result;
});
if (item) {
item.isJustificationMissing = ko.computed(function() {
var override = item.Override();
var result = false;
if (!!override) {
result = !item.hasAtleastNineWords();
}
return result;
});
item.hasAtleastNineWords = ko.computed(function() {
var justification = item.Justification(),
moreThanNineWords = false;
if (justification != null) {
moreThanNineWords = justification.trim().split(/\s+/).length > 9;
}
return moreThanNineWords;
});
item.isValid = ko.computed(function() {
return (!item.isJustificationMissing());
});
}
});
}();
}
I've tried it by setting up a global variable like:
var item;
or
var obj;
if(hasOverrides) {...
So the thing that gets me the most that im not able to grasp how the connection is made
between the underlying model CasOverridesviewModel. As i assumed that self.CasOverridesViewModel.Override() would be able to fetch the data that is written on the screen.
Another try i did was var override = ko.observable(self.CasOverridesViewModel.Override()), which led to js typeError as you cannot read from an undefined object.
So if anyone is able to give me some guidance on how to get the fields from an input field available outside of this function. It would be deeply appreciated.
If I need to clarify some aspects do not hesitate to ask.
The upmost gratitude!
not sure how far outside you wanted to go with your variable but if you just define your global var at root level but only add to it at the moment your inner variable gets a value, you won't get the error of setting undefined.
var root = {
override: ko.observable()
};
root.override.subscribe((val) => console.log(val));
var ViewModel = function () {
var self = this;
self.override = ko.observable();
self.override.subscribe((val) => root.override(val));
self.load = function () {
self.override(true);
};
self.load();
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

Trying to access JavaScript object properties, getting different results

Here is the pseudo-code in question: https://jsfiddle.net/yzps2gef/40/
I'm trying to understand why I cannot access an object's properties directly in one scenario (see ISSUE #1 in comments) but I can in another scenario (see ISSUE #2 in comments). I'm failing to see the difference between the two. Thanks!
Here's the fiddle code:
window.DataStore = function () {
var url = new Url(),
filters = new Filters(),
orderBy,
orderByDir,
setOrderBy = function (x, y) {
orderBy = x;
orderByDir = y;
},
getOrderBy = function () {
return orderBy;
},
getOrderByDir = function () {
return orderByDir;
};
return {
url: url,
filters: filters,
orderBy: orderBy,
orderByDir: orderByDir,
setOrderBy: setOrderBy,
getOrderBy: getOrderBy,
getOrderByDir: getOrderByDir
};
};
window.Url = function () {
var get = function (ds) {
var url = 'xyz.php';
console.log(ds);
// ISSUE #1: These do not work. It results in: xyz.php?orderby=undefined&orderbydir=undefined.
// Why can't I access them directly like I do below with the dataStore.filters.someFilterOption?
url = url + '?orderby=' + ds.orderBy;
url = url + '&orderbydir=' + ds.orderByDir;
// These work when I use the "get" functions.
// url = url + '?orderby=' + ds.getOrderBy();
// url = url + '&orderbydir=' + ds.getOrderByDir();
return url;
}
return {
get: get
};
};
window.Filters = function () {
var someFilterOption = 0;
return {
someFilterOption: someFilterOption
};
};
window.Grid = function () {
var dataStore = new DataStore(),
doSearch = function () {
console.log(dataStore.url.get(dataStore));
},
render = function () {
doSearch();
// ISSUE #2: Why can I access this one directly but not the order bys?
if (dataStore.filters.someFilterOption) {
console.log('Why was I able to read this one (dataStore.filters.someFilterOption) directly and not have to have a getSomeFilterOption() function to read it? But when it comes to the orderBy and orderByDir above I cannot read them directly.');
}
}
return {
dataStore: dataStore,
render: render
};
};
window.MyReUsableGrid = function () {
var grid = new Grid(),
showSomeFilterOption = function () {
grid.dataStore.filters.someFilterOption = 1;
},
render = function () {
grid.render();
};
grid.dataStore.setOrderBy(4, 'asc');
return {
showSomeFilterOption: showSomeFilterOption,
render: render
};
};
// The Screen
var myGridScreen = new MyReUsableGrid();
myGridScreen.showSomeFilterOption();
myGridScreen.render();
Because when your object gets returned from the function this line gets evaluated:
orderBy: orderBy,
And as the variable orderBy isnt set yet it is actually:
orderBy: undefined
Now later you call setOrderBy and set the internal variable orderBy to a value which you can expose through the getter, but that doesnt get reflected to the objects property.
IMO the whole thing should be restructured so that the methods work with their context:
window.DataStore = () => ({
url: new Url(),
filters: new Filters(),
applyOrder(order, dir) {
this.orderBy = order;
this.orderByDir = dir;
},
});
That way you dont need getters at all.

What is the difference between returning a function and returning an object in a Durandal viewmodel?

I'm looking at implementing a wizard type system in my application and looking at the first wizard example on the dfiddle-2.0 project on GitHub. The step viewmodels are all functions though and I'm trying to understand why.
Here is what the dfiddle is using for the index.js of the wizard:
define(['durandal/activator', './step1', './step2', './step3', 'knockout'], function( activator, Step1, Step2, Step3, ko ) {
var steps = [new Step1(), new Step2(), new Step3()];
var step = ko.observable(0);
var activeStep = activator.create();
var stepsLength = steps.length;
var hasPrevious = ko.computed(function() {
return step() > 0;
});
var hasNext = ko.computed(function() {
return (step() < stepsLength - 1);
});
// Start with first step
activeStep(steps[step()]);
return {
showCodeUrl: true,
steps: steps,
step: step,
activeStep: activeStep,
next: next,
previous: previous,
hasPrevious: hasPrevious,
hasNext: hasNext
};
function next () {
if ( step() < stepsLength ) {
step(step() + 1);
activeStep(steps[step()]);
}
}
function previous () {
if ( step() > 0 ) {
step(step() - 1);
activeStep(steps[step()]);
}
}
});
And here is what it's using for step1.js
define(function() {
return function() {
this.name = 'Step 1';
this.s1one = 'Unique to' + this.name;
this.s1two = 'Another property unique to' + this.name;
};
});
Here is what I'm currently using for index.js.
define(['knockout'],
function (ko) {
var rootPath = "viewmodels/wizards/steps/";
var steps = ["step1", "step2", "step3"];
var step = ko.observable(0);
var activeStep = ko.observable();
var stepLength = steps.length;
var hasPrevious = ko.computed(function () { return step() > 0 });
var hasNext = ko.computed(function () { return step() < stepLength - 1 });
var activate = function () {
return activeStep(rootPath + steps[step()]);
};
return {
steps: steps,
step: step,
activeStep: activeStep,
next: next,
previous: previous,
hasPrevious: hasPrevious,
hasNext: hasNext,
activate: activate
}
function next() {
if (hasNext()) {
step(step() + 1);
activeStep(rootPath + steps[step()]);
}
}
function previous() {
if (hasPrevious()) {
step(step() - 1);
activeStep(rootPath + steps[step()]);
}
}
});
And my step1.js
define(function () {
var name = ko.observable("Step 1");
var s1one = ko.observable("Unique to " + name());
var s1two = ko.observable("Another property unique to " + name());
var returnVm = {
name: name,
s1one: s1one,
s1two: s1two
};
return returnVm;
});
The bindings are the same so how are these two approaches different? What am I losing by just returning an object instead of using functions?
The difference is subtle, but important. Modules that return an object are singletons. The same object will be shared among all other modules that depend on it. Modules that return a function are termed constructor functions. Dependant modules will instantiate this constructor function with the new keyword. Therefore, each instance is unique.
Here's some more information gleaned from the Durandal documentation:
A module's define is only exeucted once, at the time the module is first required. As a result, if you return an object instance, you have created a singleton which will stay in memory for the lifetime of your application. If this is not desired, return a constructor function to retain greater control of the lifetime of your objects by allowing consumers to create/release them as needed.
In your example, you aren't losing anything. Either approach works. Which is more correct depends on a number of things. If you do not require unique instances of your module each time it is required, then a singleton is the best choice. However, if say you need multiple instances of the same dialog module, but each with their own data, a constructor function is the way to go.
I hope this helps.

Clean and Rebind Data using KnockoutJs Template

So I bind my Knockout template as follows:
First ajax, get data then I pass the data can call a function named bindKo:
function bindKo(data) {
var length = data.length;
var insertRecord = {};
if (length > 0) {
insertRecord = data[data.length - 1]; //last record is an empty PremlimViewModel for insert
insertRecord.Add = true;
data.splice(data.length - 1, 1); //remove that blank insert record
}
function prelims(data) {
var self = this;
var model = ko.mapping.fromJS(data, { copy: ["_destroy"] }, self);
self.BidPriceFormatted = ko.computed({
read: function () {
var bidPrice = this.BidPrice();
if (bidPrice) {
if (!isNaN(bidPrice)) {
var input = '<input type="text" value="' + bidPrice + '"/>';
return $(input).currency({ decimals: 0 }).val();
}
}
},
write: function (value) {
value = value.replace(/\D/g, '');
this.BidPrice(value);
},
owner: this
});
return model;
}
var mapping = {
create: function (options) {
return new prelims(options.data);
}
};
function viewModel(prelimData) {
var self = this;
self.prelims = ko.mapping.fromJS(prelimData, mapping);
self.remove = function (prelim) {
self.prelims.destroy(prelim);
};
self.addOption = function () {
var clone = jQuery.extend(true, {}, insertRecord);
self.prelims.push(ko.mapping.fromJS(clone));
};
}
ViewModel = new viewModel(data);
ko.applyBindings(ViewModel);
}
I have a template defined where you can add and remove records, and user does just that:
<script type="text/html" id="PrelimsTemplate">
<!--Template Goodness-->
</script>
Then, ajax call, records updated in datanbase, latest results returned and I do:
ko.mapping.fromJS(newestData, ViewModel)
But this does not work because my ViewModel is complex.
So I would just like to reBind the template entirely. Make is disappear and reappear with latest data.
Wrap your template in a container than you can hook onto with jQuery.
When you need to trash it use ko.cleanNode and jQuery .empty()
emptyTemplate: function(){
ko.cleanNode($('#template-container')[0]);
$('#template-container').empty();
}
Load your template back up
fillTemplate: function(){
$('#template-container').html('<div data-bind="template: {name:\'templateId\', data: $data}"></div>');
ko.applyBindings(data,$('#template-container')[0])
},
See my fiddle

How can I make this javascript easier to read, maintain, and understand from an OO background?

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.

Categories