5th August, 2016 - Posted by david
A general rule I follow when using KnockoutJS is that there should be no DOM manipulation in the viewModel. The viewModel should be completely independent of any markup, while any changes to the DOM (via jQuery or otherwise) should be handled in the binding handler. This makes your viewModels much more portable and testable.
As I’m sure you’re aware if you’re reading this article(!), KnockoutJS’s binding handlers are applied to an element and have init
and update
functions that get called when a certain value in your viewModel changes. Within your init
function, you can set up various DOM-element-specific jQuery handlers, while within your update
function you can perform various DOM manipulations, trigger events etc., as well as reading from/updating your viewModel and much more.
A common situation I’ve come across a number of times is: say you have a big div
with plenty of buttons and links that are tied into external jQuery plugins and DOM elements and you want to perform certain actions when they’re clicked or when other changes happen in your viewModel. You don’t really want to have loads of binding handlers for each separate change that might happen in your viewModel, your codebase could get quite big quite quickly. What I’m about to propose is a structure of how to apply 1 binding handler to the entire div, then call various functions to manipulate the DOM outside of your update
binding handler function, via the viewModel.
So, I’ll start with the viewModel. I’m going to have an observable action
attribute and 2 functions linkClicked
and buttonClicked
. (Please bear in mind, this is a very simple example for illustration purposes, you wouldn’t really call viewModel functions linkClicked
etc.!) There’ll also be a resetAction
function, which will be explained shortly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| exampleViewModel = (function($) {
var viewModel = function() {
this.action = ko.observable('');
};
viewModel.prototype.resetAction = function() {
this.action('');
};
viewModel.prototype.linkClicked = function() {
this.action('jQLinkClicked'); // prepended "jQ" to the function name to help the reader later
};
viewModel.prototype.buttonClicked = function() {
this.action('jQButtonClicked');
};
//JS Module Pattern
return viewModel;
}(jQuery));
ko.applyBindings(new exampleViewModel(), $('#example')[0]); |
And now our HTML markup:
1 2 3 4
| <div id="example" data-bind="exampleBindingHandler: action()">
<a href="void(0)" data-bind="click: linkClicked"><br />
<button data-bind="click: buttonClicked">
</div> |
So now we can see that whenever we click either the link or the button, our action
attribute will be updated and thus trigger the update
function in the exampleBindingHandler
binding handler that’s applied to the div
. Let’s look at that binding handler now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ko.bindingHandlers.exampleBindingHandler = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
// do whatever initial set up you need to do here, e.g.
$('body').addClass('whatever');
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
// so this will be called whenever our observable 'action' changes
// get the value
var action = valueAccessor();
// reset to empty
viewModel.resetAction();
switch (action) {
case 'jQLinkClicked':
alert('link');
break;
case 'jQButtonClicked':
alert('button');
break;
}
}
}; |
So you can see from the above how we can move from various different viewModel changes out to the binding handler and maniuplate the DOM in there. We read and save action
from the valueAccessor
, then reset it via the viewModel’s resetAction
function, just to keep things clean.
At this point we have very simple alert
s for each of our actions but of course in real life you’ll want to call your jQuery plugins, change the DOM etc. To keep things clean what we can do is have a simple JSON object with functions for each of the actions and within those functions do our heavy jQuery lifting, something along the lines of:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| var _ = {
jQLinkClicked: function() {
// e.g.
$('.class').parent().remove();
},
jQButtonClicked: function() {
// e.g.
$.plugin.foo();
}
}
ko.bindingHandlers.exampleBindingHandler = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
// do whatever initial set up you need to do here, e.g.
$('body').addClass('whatever');
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
// so this will be called whenever our observable 'action' changes
// get the value
var action = valueAccessor();
// reset to empty
viewModel.resetAction();
switch (action) {
case 'jQLinkClicked':
_.jQLinkClicked();
break;
case 'jQButtonClicked':
_.jQButtonCliked();
break;
}
}
}; |
So, in summary:
- Have an observable
action
attribute in your viewModel
- Apply the binding handler to your main
div
, with the observable action
variable as the valueAccessor
parameter
- Set
action
accordingly in your viewModel
- In your binding handler, figure out what jQuery/DOM manipulation etc. you want to do based on the value of
action
Read more...
14th February, 2015 - Posted by david
Sometimes in work we can be asked to do things we don’t like and recently I was asked to look into implementing one of those homepage takovers. Personally, I think these are awful and would like to think I wouldn’t degrade my site by implementing one, but they do make money and have a high click rate, so I can see why sites like to use them.
Normally they’re done using a fixed background wallpaper that’s clickable all the way to the edge of the page. However, I was asked to simulate this look using 2 existing skyscraper ads, 170px in width, to be positioned either side of the main content and fixed to the top of the page. Since it wasn’t entirely straightforward, I thought I’d block about it here, to help anyone else in a similar situation. I’m not going to go into the specifics of displaying the ads, simply the CSS and Javascript involved in positioning them where you want them.
I should point out, this might be possible with just CSS, but changing a site’s fundamental structure to accomodate the new columns isn’t always possible. Also, you might only want the takeover on the homepage and not other pages. This solution should have minimal impact, as it simply adds 2 div
s, that can go anywhere in the HTML.
So, to describe the set-up, let’s say our main content is 1000px in width, centred in the page and we want 2 170px x 1068px div
s to contain our ads and line up on the right and left of that content, as well as for the 2 ads to remain fixed at the top of the page, no matter how far we scroll down. We’ll give each of these div
s a class of side-banner
, with left-banner
and right-banner
IDs. Since these are going to be positioned explicitly, it doesn’t really matter where in the HTML you put them, maybe just inside your main content div
. Initially, we’re simply going to position them in the extreme top corners of each side. I’m also going to give them different background colours, so we can know they’re positioned correctly without having to worry about loading the ads (which can come later).
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| .side-banner {
width: 170px;
height: 1068px;
position: fixed;
top: 0;
}
#right-banner {
right: 0;
background: red;
}
#left-banner {
left: 0;
background: green;
} |
To align these alongside the content, I needed to write a Javascript function (position_banners()
) to position them correctly. This function is called when the page finishes loading, as well as when the window is resized. It simply gets the body’s width, subtracts the width of our main content (remember 1000px), divides the result by 2 (as we’ve 2 sides), then further subtracts the width of our banners. This fairly basic formula works out the amount to move each div in from their corresponding edge, to line up with our main content. Then, we just use CSS left
and right
to position them nicely.
1 2 3 4 5 6 7 8 9 10 11 12
| $(document).ready(function() {
position_banners();
$(window).resize(position_banners);
});
function position_banners() {
var margin = ($('body').width() - 1000) / 2 - $('#left-banner').width(),
left = Math.floor(margin),
right = Math.ceil(margin);
$('#left-banner').css({left: left + 'px'});
$('#right-banner').css({right: left + 'px'});
} |
I know this code isn’t the tidiest, but should be enough to get the idea of what you need to do.
To further enhance the ‘takeover’ effect, you could display a 970px x 250px ‘billboard’ right at the top of your main content.
Read more...
23rd May, 2011 - Posted by david
UPDATE: I’ve recently been asked by entertainment.ie to stop scraping thier licensed data, to which I’ve duly agreed. Thus, the app is no longer live. However, the lessons learned below are still valid. END
Haven’t posted for a while, mainly because I’ve been busy trying to teach myself Ruby on Rails, so haven’t created anything new in my spare time. Have come up with a few interesting fixes/ways to do things in relation to Facebook code in work though, so will hopefully do a post on those in the near future.
In the meantime, I wrote a very small little web application last week using the pretty cool jQuery Mobile framework. The app, called What’s on now (link removed, see UPDATE above), is simply a list of what’s on now and next on Ireland’s 17 basic cable channels. The data is pulled from a similar page on entertainment.ie, with the unnecessary channels filtered out. At the moment the app is pretty simple, but I plan to add to it over time (e.g. to add an option for the non-basic channels), updating this article as I go.
I did this mainly to have a quick go with jQuery Mobile, to see what it could do. I could’ve used PHP and built a mobile-sized HTML page, but it’s always good to try new things! Most of the development was pretty straight forward; however, because the data is retrieved dynamically every time the page is loaded, there’s a couple of tricks you need to apply to get the jQuery Mobile framework to do it’s magic.
The main page you’re greeted with is simply a glorified un-ordered list of programmes, with separator list items to distinguish the channels. I’m not going to go into the details of how you need to structure a page (see links at the end of this post) but here’s a snippet of the HTML:
1 2 3 4 5 6 7
| <ul id="channels" data-role="listview">
<li data-role="list-divider">RTE One</li>
<li><a href="#RTE-One-1">21:00: 9 O'Clock News</a></li>
<li><a href="#RTE-One-2">21:35: Prime Time</a></li>
<li data-role="list-divider">RTE Two</li>
.. etc.
</ul> |
When the page loads, ul#channels
is empty. The data is called via a jQuery GET, which gets over cross-domain restrictions by using YUI, thanks to the Cross-Domain AJAX mod. The relevant data is filtered out and formatted and each of the li
‘s are built and inserted into #channels
. At this point, if you look at the page in your browser, it’ll still look like an ordinary list – you need to tell jQuery to work it’s magic on the dynamically created data. In this instance it’s done as follows:
1
| $("#channels").listview("refresh"); |
Once I had my list of programmes, I thought I may as well add the info for each program, seeing as I already had the data at my disposal. The route I decided to go down here was to create new ‘page’ div
‘s for each program, each one having it’s own ID, then link to each of these pages from the li
‘s. Again, the scope of building one of these pages is beyond this blog post and well documented elsewhere, but here’s a quick sample:
1 2 3 4 5 6 7 8 9 10 11 12
| <div data-role="page" id="RTE-One-1" data-url="RTE-One-1">
<div data-role="header">
<h1>RTE one</h1>
</div>
<div data-role="content">
<h2>9 O'Clock News: 21:00</h2>
An update on the latest domestic and international events of the day.
</div>
<div data-role="footer">
© David Coen 2011</div>
</div>
</div> |
This is simply added to the body using $('body').append(page);
(where the variable page is a string of HTML such as the above). So, again here you need to tell jQuery Mobile that you’ve added a new page, so it can do it’s magic. This is achieved by the one simple line:
Hopefully this post will clear up a couple of things for anyone using jQuery Mobile with dynamically generated data. As I promised, here are some links of articles that helped me get a better understanding of the framework:
Full implementation code (UPDATE 2)
I was requested by @danny to post the full source code, seeing as the app is actually no longer available, so I’ve decided to put most of it here. I excluded some of the scrape JS (as indicated in the code comments) to prevent the app being re-used somewhere else.
So, first up, is the initial HTML page, with header and footer blocks, an empty content block and the JS and CSS includes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <title>What's on now</title>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.0a1/jquery.mobile-1.0a1.min.css">
<script type="text/javascript" src="./jquery.min.js"></script>
<script type="text/javascript" src="./jquery-mobile.min.js"></script>
<script type="text/javascript" src="./whatson.js"></script>
<div data-role="page" id="home">
<div data-role="header">
<h1>What's on now</h1>
</div>
<div data-role="content">
<ul id="channels" data-role="listview"></ul>
</div>
<div data-role="footer" style="text-align: center;">
<a href="http://www.drcoen.com">© David Coen 2011</a></div>
</div> |
Next up is the javascript file, whatson.js
in the above. Don’t forget, the $.ajax
call has been overwritten by the Cross-Domain AJAX plug-in I mentioned earlier. Addtionally, I’ve used a few functions from php.js. to replicate this functionality in JS, namely str_replace
, in_array
and trim
. I’ve excluded them here but they can be found in the php.js website.
Also, just to re-clarify, the page i was scraping had a list of channels and programs that were on now (prog1) and next (prog2).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| $(document).ready(function() {
var cable_channels = [
'RTE One',
'RTE Two',
'BBC One',
// etc......
];
$.mobile.pageLoading();
$.ajax({
url: // URL for the data
type: 'GET',
success: function(data) {
html = $(data['responseText']);
// for each channel scrapped
$('td.listing-channel', html).each(function(){
var channel = // code omitted
var prog1_start, prog2_start, prog1, prog2, prog1_name, prog2_name;
// if it's a channel I'm interested in
if (in_array(channel, cable_channels))
{
// get the name, start time and description of the program currently being shown on the channel
prog1_start = // start time of 1st program - code omitted
prog1_name = // name of 1st program - code omitted
prog1 = // description of 1st program - code omitted
// do the same for the one on after it
prog2_start = // start time of 2nd program - code omitted
prog2_name = // name of 2nd program - code omitted
prog2 = // description of 2nd program - code omitted
// replace spaces with '-' for a valid #id
var id = str_replace(' ', '-', channel);
//create new block on the main page for our channel and it's 2 programs
var li = '<li data-role="list-divider">' + channel + '</li>' +
'<li><a href="#' + id + '-1">' + prog1_start + ': ' + prog1_name + '</a></li>' +
'<li><a href="#' + id + '-2">' + prog2_start + ': ' + prog2_name + '</a></li>';
$('#channels').append(li);
// create a new page for the program description - clicking on the program in the <li> above will
// bring you to this new description page
var page = '<div data-role="page" id="'+id+'-1" data-url="'+id+'-1">' +
'<div data-role="header">' +
'<h1>' + channel + '</h1>' +
'</div>' +
'<div data-role="content">' +
'<h2>' + prog1_name + ': ' + prog1_start + '</h2>' + prog1 +
'</div>' +
'<div data-role="footer" style="text-align: center;">' +
'<a href="http://www.drcoen.com">© David Coen 2011</a>' +
'</div></div>';
$('body').append(page);
$('#'+id+'-1').page();
// Do same again for 2nd program
page = '<div data-role="page" id="'+id+'-2" data-url="'+id+'-2">' +
'<div data-role="header">' +
'<h1>' + channel + '</h1>' +
'</div>' +
'<div data-role="content">' +
'<h2>' + prog2_name + ': ' + prog2_start + '</h2>' + prog2 +
'</div>' +
'<div data-role="footer" style="text-align: center;">' +
'<a href="http://www.drcoen.com">© David Coen 2011</a>' +
'</div></div>';
$('body').append(page);
$('#'+id+'-2').page();
}
});
$("#channels").listview("refresh");
$.mobile.pageLoading(true); // kill the page loading modal
}
});
}); |
I realise this code could be alot cleaner, but the app was still in it’s early stages before I was asked to take it down, thus I haven’t spent time tidying it up. Hopefully there’s enough here to figure out how to do what you need!