25 4 / 2012
Simple HTML5 details polyfill
Although there are many HTML5 <details> polyfills already available, I didn’t like the complexity behind them. Here is how you can create your own from scratch in just few lines of css and js.
The idea was to put as much “logic” as possible in the stylesheet itself. It should hide/show the content based on the presence of open attribute. Here is the sass file:
.no-details details {
> * {
position: absolute;
visibility: hidden;
}
> summary, &[open] > * {
position: static;
visibility: visible;
}
> summary {
display: block;
&:before {
content: "►";
padding-right: 5px;
font-size: 11px;
}
}
&[open] > summary:before {
content:"▼"
}
}
All children of details tag (except summary) will be by default hidden. If details element has open attribute set, its content will be shown. We could use css :not selector to simplify the css a whole lot, but it is only supported by IE9+, and I’d like polyfill to target IE8+.
And here is the coffeescript:
jQuery ->
return if Modernizr.details
$(document).on 'click', 'summary', (event) ->
$summary = $(this)
$details = $summary.parent()
if $details.attr('open')
$details.removeAttr('open')
else
$details.attr('open', 'open')
It simply toggles the open attribute when user clicks on summary. Its as simple as that.
Permalink 2 notes
14 4 / 2012
Highlighting navigation links with javascript
Recently I blogged about Tagging the HTML tag. The general idea is to apply some data- attributes to html tag in order to more easily target the page from css and js files. I also blogged about Javascript Processors, technique to keep your “DOM enhancers” organized.
Using the two, here is how you can very easily implement processor that would highlight active link in your navigation. Lets say that we have following nav element:
<nav class="main">
<a class="home">Home</a>
<a class="projects">Projects</a>
<a class="account">My Account</a>
</nav>
Here is how our NavHighlighter may look like:
@NavHighlighter =
mapping:
"nav.main .home": [ /^\/welcome/ ]
"nav.main .projects": [ /^\/projects/ ]
"nav.main .account": [ /^\/registrations\/(edit|update)/, /^\/profile/ ]
process: ->
for elem, paths of @mapping
$(elem).addClass("active") if App.onPath.apply(App, paths)
We’re using mapping hash to define highlight conditions for each link. Hash key is just css selector, so you can use anything you want. Hash value is an array representing list of paths for which the given link is active.
Can’t we do it on server? Sure. There are two reasons I’m doing it on the client: cleaner and faster erb template, and fragment caching.
What about users with js disabled? Well, they could probably live without highlighted menu items. If you really want to do this on server and still use caching, read more on Cache Personalization.
01 4 / 2012
Tagging the HTML tag
Before rails asset pipeline was introduced, I was loading bunch of separate css and js files on every page. Depending on my needs, I would load different set of assets. For example, when I am on the articles/index path, I loaded articles.css and articles.js files (among default ones).
Asset pipeline changed things (for better, of course). Now, all assets are bundled into single application.css and application.js files. This approach was good for a lot of reasons including less requests made to the server (load once, use everywhere) and higher compression rate (when using gzip) all resulting in faster page loads and better ux.
I like to keep things organized. When styling specific page, I want to be very explicit on what I’m styling. Likewise, when writing a script, I want to be very specific when that script should run. Thats why I decided to tag every page in a way that I can reference that info within my css and js files.
In past I tried to do this with html classes, but html5 data- attributes now seem like a way to go.
The idea is to have this:
<html data-controller="articles" data-action="index">
...
</html>
This is very easy to achieve by creating custom rails helper method:
module ApplicationHelper
def html_tag(attributes = {})
attributes[:data] ||= {}
attributes[:data][:controller] = controller_name
attributes[:data][:action] = action_name
tag(:html, attributes, true)
end
end
And in the application.html.erb layout file, we simply include
<!DOCTYPE html>
<%= html_tag lang: 'en' %>
<head>...</head>
<body>...</body>
</html>
Although a bit controversial, I prefer to generate only <html> opening tag with html_tag method in order to avoid wrapping entire layout file in a block.
Now, if we want to set #content’s background color to yellow only for the articles/index page, we could do this:
html[data-controller=articles][data-action=index] {
#content { background-color: yellow; }
}
Likewise, we could do this in our coffeescript:
jQuery ($) ->
return unless $('html').data('controller') == 'articles'
alert "Welcome to Articles controller"
We can easily add javascript page detection helper method to our App module to simplify this process. It can look something like this:
@App =
data: (name) -> $('html').data(name)
controller: -> @data('controller')
action: -> @data('action')
path: ->
p = []
p.push @controller()
p.push @action()
"/" + p.join("/")
onPath: ->
for arg in arguments
return true if @path() == arg
false
We can rewrite the previous script:
jQuery ($) ->
return unless App.onPath '/articles/index'
alert "Welcome to Articles controller, index action"
We can store whatever we like in these attributes. I store vars like module name, current environment, current user’s id and even the public api keys (e.g. for google maps).
22 3 / 2012
Deferring Google API
I just love jQuery Deferreds. There are so many things that can be greatly simplified with this concept.
Here is how we can write simple wrapper around google api to make it less painful to load and work with.
@GoogleAPI =
dfd: null,
load: ->
script = document.createElement("script")
script.src = "https://www.google.com/jsapi"
script.src+= "?key=API-KEY"
script.src+= "&callback=GoogleAPI.loading_complete"
script.type = "text/javascript"
document.getElementsByTagName("head")[0].appendChild(script)
loading_complete: ->
GoogleAPI.dfd.resolve()
loaded: ->
if !GoogleAPI.dfd
GoogleAPI.dfd = $.Deferred()
GoogleAPI.load()
GoogleAPI.dfd.promise()
First time GoogleAPI.loaded method is called, it will return the promise object and start loading jsapi script by appending it to document head. Once the script is loaded, the promise will be resolved and the callback will fire.
The beauty of this is that you can use these promises anywhere you want regardless of whether jsapi is loaded, is loading, or is yet to load. As soon as its available, all callbacks will fire.
Another cool thing is that Google jsapi will never load if its not required by your code.
The usage is really simple. It goes something like this:
$.when(GoogleAPI.loaded()).then -> console.log("jsapi loaded!")
It really reads nice: when GoogleAPI is loaded, then do the dance :)
Whats even more cool is that we can stack such modules one on top of the another. This is how GoogleMaps module would look like:
@GoogleMaps =
dfd: null,
load: ->
google.load 'maps', '3',
callback: GoogleMaps.loading_complete,
other_params: "sensor=false"
loading_complete: ->
GoogleMaps.dfd.resolve()
loaded: ->
if !GoogleMaps.dfd
GoogleMaps.dfd = $.Deferred()
$.when(GoogleAPI.loaded()).then -> GoogleMaps.load()
GoogleMaps.dfd.promise()
This one will load GoogleAPI first, then maps package. Once both load, you’re good to go. Once again, usage is real candy:
$.when(GoogleMaps.loaded()).then -> console.log("maps loaded!")
We can now even create GoogleCharts module with similar implementation. The awesome thing is that it won’t reload GoogleAPI if GoogleMaps already loaded it.
It all just works, and it works nicely!
22 3 / 2012
JavaScript Processors
Often times we use jQuery plugins and other scripts to enhance the user experience. These plugins are usually initialized when the page loads and they process html and add some fancy new behavior. But when we start adding content via ajax, things start to get messy.
Some of the plugins can be developed to use live event handlers and that they can handle the problem elegantly (with more or less performance impact), but most of them cannot.
General idea is to have these blocks of code declared once and re-applied whenever needed. To do so, we need all of them to share same interface. Lets call these blocks processors and created a module for each of them. Here are few written in CoffeeScript:
@TimeagoProcessor =
process: ->
$('time.ago').timeago()
@SimpleTooltipsProcessor =
process: ->
$('[data-tooltip]').qtip
position:
at: 'bottom center'
my: 'top left'
Next, we need to store these in a collection, so that we can run them all once the page loads, and after every ajax call. For that purpose, lets create new module called App.
@App =
processorData: []
registerProcessor: (processor, options) ->
@processorData.push { processor: processor, options: options }
bindEventHandlers: ->
$('body').ajaxComplete => @runProcessors(ajax: true)
runProcessors: (options) ->
for data in @processorData
if !options || options.ajax && data.options.ajax
data.processor.process()
init: ->
@runProcessors()
@bindEventHandlers()
The App enables us to register new processors through registerProcessor method. We can also specify other custom options for each processor. Currently, only ajax option is implemented to denote whether processor should run after each ajax call, but other options can be added easily.
Finally, lets hook it up:
jQuery ($) ->
App.registerProcessor TimeagoProcessor, ajax: true
App.registerProcessor SimpleTooltipsProcessor, ajax: false
App.init()
That’s it! We no longer have to worry about initializing various plugins and other blocks of code after every ajax call.
20 3 / 2012
Preserving scroll position across pages
When you want to edit specific record, typical rails behavior would be to go to edit page, make your changes, and after saving the record, automatically redirect to model index page. This usually makes user loose its focus on large index pages (where scrolling happens).
One way to solve this problem would be to store scroll position for given page in users browser, and then just scroll to that position automatically when user returns to page. Here is the javascript module that does just that:
@ScrollProcessor =
process: ->
return unless Modernizr.localstorage
@restore() if location.hash == '#scroll'
$(window).on 'scroll', => @onscroll()
onscroll: (event) ->
localStorage.setItem @key, $(window).scrollTop()
restore: ->
$(window).scrollTop localStorage.getItem(@key)
key: "scroll#{location.pathname}"
The only thing you need to do is to call ScrollProcessor.process on document ready callback and it will automatically store the scroll position in browser’s localStorage. It also jumps to saved position when it is loaded if path has ‘#scroll’ anchor. Please note that I’m using Modernizr to detect whether localstorage is supported by the browser.
Now, wherever you wish to link to specific page (or redirect to it), but preserve last scrolled position, just use something like this:
articles_path(anchor: 'scroll')
Its really simple, but it does job well.