Git Product home page Git Product logo

hasher's Introduction

hasher-logo.gif

Hasher is a set of JavaScript functions to control browser history for rich-media websites and applications. It works as an abstraction of browsers native methods plus some extra helper methods, it also has the advantage of dispatching Events when the history state change across multiple browsers (since this feature isn't supported by all of them).

Why?

  • Browsers evolved since the other available solutions were created.
  • Some of the alternatives are way too complex, sometimes doing more things automatically than you actually want it to do.
  • Source code of most of the solutions are way too cryptic making it impossible to customize for your need or to debug it in case you find any issue.
  • Some of the solutions require extra markup and/or blank files to make it work.
  • The HTML5 History API is awesome but some for some kinds of applications using the location.hash may still be the recommended solution for saving application state.

Goals

  • Be simple.
  • Work on the main browsers (IE6+, newest versions of Firefox, Safari, Opera and Chrome).
  • Clean source code, making it easy to debug/customize/maintain.
  • Follow best practices/standards.
  • Fully unit tested. (tests)
  • Don't break application if for some reason location.hash can't be updated. (it should still dispatch changed signal at each hasher.setHash())

Dependencies

Basic Example

HTML

Include JS-Signals and hasher to your HTML file:

  <script type="text/javascript" src="signals.js"></script>
  <script type="text/javascript" src="hasher.js"></script>

IMPORTANT: signals.js should be included before hasher.js.

JavaScript

  //handle hash changes
  function handleChanges(newHash, oldHash){
    console.log(newHash);
  }

  hasher.changed.add(handleChanges); //add hash change listener
  hasher.initialized.add(handleChanges); //add initialized listener (to grab initial value in case it is already set)
  hasher.init(); //initialize hasher (start listening for history changes)

  hasher.setHash('foo'); //change hash value (generates new history record)

Advanced Usage

Hash Bang!

Google have a proposal for making Ajax content crawlable by specifying that a certain hash value also have an static snapshot. Those hash values should start with an exclamation mark !:

hasher.prependHash = '!'; //default value is "/"
hasher.setHash('foo'); //will update location.hash to "#!foo" -> htttp://example.com/#!foo

PS: Only use the hashbang if you are generating static snapshots for the hash.

Setting hash value without dispatching changed signal

One of the greatest benefits of Hasher over other solutions is that it uses JS-Signals for the event dispatch, which provides many advanced features. This can be useful when you are setting the hash value and your changed handler doesn't need to be called (e.g. updating hash value during scroll). Use it with care.

function setHashSilently(hash){
  hasher.changed.active = false; //disable changed signal
  hasher.setHash(hash); //set hash without dispatching changed signal
  hasher.changed.active = true; //re-enable signal
}

hasher.init(); //start listening for changes
hasher.changed.add(console.log, console); //log all changes
hasher.setHash('foo');
setHashSilently('lorem/ipsum'); //set hash value without dispatching changed event (will generate history record anyway)
hasher.setHash('bar');

Setting hash value without generating a new history record

Hasher also contains the method replaceHash(). It works very similarly to the setHash() method (will also dispatch a changed signal), the main difference it that it won't keep the previous hash on the history record (similar to location.replace()). It's useful for redirections and any other change that shouldn't be on the browser history.

function onHasherInit(curHash){
  if (curHash == '') {
    // redirect to "home" hash without keeping the empty hash on the history
    hasher.replaceHash('home');
  }
}
hasher.initialized.add(onHasherInit);
hasher.changed.add(console.log, console); // log all hashes
hasher.init();

Routes: Using Hasher together with Crossroads.js

Hasher is only focused on providing a reliable and clear API for setting hash values and listening to hash state change event. If you need an advanced routing system check crossroads.js. Both were designed to work together easily:

//setup crossroads
crossroads.addRoute('home');
crossroads.addRoute('lorem');
crossroads.addRoute('lorem/ipsum');
crossroads.routed.add(console.log, console); //log all routes

//setup hasher
function parseHash(newHash, oldHash){
  crossroads.parse(newHash);
}
hasher.initialized.add(parseHash); // parse initial hash
hasher.changed.add(parseHash); //parse hash changes
hasher.init(); //start listening for history change

How does it work?

Hasher will listen for the browser onhashchange event if it is supported (FF3.6+, IE8+, Chrome 5+, Safari 5+, Opera 10.6+) or it will fallback to pooling the window.location on an interval to check if hash value changed. On IE 6-7 it also uses an hidden iframe to trigger the history state changes (since updating the hash value won't do the trick). This is the same method used by most of the other available solutions like swfaddress, jQuery Address, YUI History, jqBBQ, Really Simple History, etc...

The main difference from the other solutions are the API, code structure and the fact that it doesn't require jQuery/YUI/dojo/moootools/etc to work. It also uses JS-Signals for the events which provides a sane way of handling events and some really useful advanced features.

Why should I use it?

Besides the fact of making history state work across multiple browsers it also normalizes and fixes many bugs, here are a few of the advantges:

  • Normalizes the hash value across browsers (firefox decode hash value and all the other browsers don't).
  • Fix IE8 bug if location.hash contains a "?" character and file is being accessed locally it would break the history stack. [iss #6]
  • Fix Safari 4-5 bug while setting location.hash to a value that contain non-printable ASCII chars (non-latin, accents, etc..). [iss #8]
  • Degrade gracefully if for some reason location.hash isn't available, will dispatch the changed signal at each hasher.setHash() and application can still work, it just won't generate a new history record.
  • Doesn't rely on callbacks so you can add as many listeners as you want and since it uses JS-Signals for the event system it also provides many advanced featured that wouldn't be available through a simple callback system, like disabling the dispatch of an event (so you can change the hash value without affecting your app state), removing all the listeners at once, dispose objects, etc...
  • Option to start/stop pooling/listening for changes on the hash whenever you want giving more control over how you app is supposed to work.
  • Available as an AMD module which can be easily integrated into other projects without polluting the global scope or affecting you aplication structure.
  • Isn't a plugin for a large JS library/framework (so you can use it with any library).
  • Can be easily integrated into a Router like crossroads.js.
  • Sometimes regular URLs doesn't make any sense, specially when you can't provide a fallback to all of them or when you just want to save the state of the application and that change wouldn't make sense on a full page reload (scrolling through the same page, interactive slideshow, etc..), also some content may not need to be indexed by search engines (although you can use hashbangs to make Ajax content crawlable...). Each scenario requires a different approach, be pragmatic.
  • Clean API.

Documentation

Documentation can be found inside the dist/docs folder or at http://millermedeiros.github.com/Hasher/docs/.

Unit Tests

Hasher is usually tested on IE (6,7,8,9), FF (3.6, 4.0, 5.0+ - mac/pc), Chrome (latest stable - mac/pc), Safari Mac (4.3, 5.0) and Opera (latest - mac/pc).

You can also run the test by yourself at http://millermedeiros.github.com/Hasher/test/unit.html

Repository Structure

Folder Structure

dev       ->  development files
|- build        ->  files used on the build process
|- lib          ->  3rd-party libraries
|- src          ->  source files
|- tests        ->  unit tests
dist      ->  distribution files
|- docs         ->  documentation
|- js           ->  javascript files

Branches

master      ->  always contain code from the latest stable version
release-**  ->  code canditate for the next stable version (alpha/beta)
dev         ->  main development branch (nightly)
gh-pages    ->  project page
**other**   ->  features/hotfixes/experimental, probably non-stable code

Distribution Files

Files inside dist/js folder.

  • hasher.js : Uncompressed source code with comments. Works as a plain script or can be loaded by an AMD loader like RequireJS without generating any global variables.
  • hasher.min.js : Compressed code.

Documentation is inside the dist/docs folder.

Building your own

This project uses Apache Ant for the build process. If for some reason you need to build a custom version install Ant and run:

ant compile

This will delete all JS files inside the dist folder, merge/update/compress source files and copy the output to the dist folder.

ant deploy

This will delete all files inside dist folder, is runs ant compile and generate documentation files.

IMPORTANT: dist folder always contain the latest version, regular users should not need to run build task.

License

Released under the MIT license.

Important

  • Weird case scenarios like calling methods from inside (i)frame, wrong doctype, plugins, 3rd party code, etc, MAY prevent script from working properly.
  • Hasher was designed on a way that it will still dispatch the changed signal even if it can't update the browser location.hash, so application should keep working even if back/prev buttons doesn't work as expected.
  • Consider using the new HTML5 history API if normal URLs would make sense on the kind of site/application you are building and you have static fallbacks for all of them (in some cases that may not be possible or even a good option). History.js is probably the most used polyfill for the History API, check it out.

© Miller Medeiros

hasher's People

Contributors

ifandelse avatar millermedeiros avatar techworker avatar th0r avatar winzig avatar xcambar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hasher's Issues

revise and clean source code

some methods are way too big (specially init and stop), extract logic from those methods and create new functions.

also check comments on source code and improve code structure so we don't need comments to explain what the code is doing.

PS: comments used to describe bugfixes shouldn't be removed.

change the way INIT and STOP works

thinking that the best approach to Hasher.stop is to just stop listening to the changes on the hash (manually and back/forward button) but still dispatch changes made using Hasher.setHash.

Add Hasher to npm/ender

Hi there!

First of all, great lib!

It would be nice if it was made available on npm/ender. Crossroads and signals already are, given that Hasher works hand in hand with them, it would be a logical move to make ender package building easier :).

Thanks!

silent change (change without dispatching event)

not sure what is the best approach and if it's really a good idea...

maybe add a public property dispatchChange:Boolean so user can make multiple changes without dispatching change event.. or maybe add a parameter isSilent to the setHash method.

the drawback of using the parameter is that on the AS3 Class setHash is a "real setter", so it can't have parameters.

wrap decodeURIComponent into try/catch

from issue #56:

By the way: Right now setting the fragment identifier to #/%F8 in the browser's address bar while hasher is running causes a URIError because decodeURIComponent is called without being wrapped in a try...catch. I would call that a bug.

IE6 - setting hash manually breaks Hasher.setHash()

events are still dispatched but for some reason hash value on the URL doesn't update. don't think it's a important issue since IE6 is "dying" and nobody sets hash values manually, it also doesn't break applications since the events are still dispatched.

Add option to get/set "query string portion" of the hash

currently there is only option to get the "real query string" (just after file name), Lucas said that some people use query strings inside the hash to make it easier to retrieve params/values..

maybe change structure of queryUtils to allow getting query from any string (not just from "location.search"..)

replaceHash triggered by setHash loses history

If setHash is called, and the resulting changed signal calls replaceHash, then replaceHash replaces the previous history entry instead of the new history entry.

For example: Hash fragment is /, and setHash('/a') is called. Crossroads.js is used to redirect '/a' to '/a/b' (by calling replaceHash). The history after doing this should be '/', '/a/b', but instead, '/' is replaced with '/a/b', so the history is '/a/b', '/a/b'.

It looks like this is caused by setHash and replaceHash updating window.location after calling _registerChange. (setHash's _registerChange triggers replaceHash, and replaceHash replaces window.location before setHash can update window.location.) If they instead updated window.location first, then that would fix this issue (but may raise other issues; I'm not familiar enough with the code and with browser implementations to know).

For now, I'm using this function as a workaround.

function setHashWithPossibleRedirect(hash)
{
    var old_hash = window.location.hash.replace(/^#/, '');
    hasher.changed.active = false;
    hasher.setHash(hash);
    hasher.changed.active = true;
    hasher.changed.dispatch(hash, old_hash);
}

setHashSilently b0rked

So.. w/ out that, it fires 2x for me.
So I added the code from your docs to turn it of.
Does not work. I had to update hasher and signals (to 1.0 - cdnjs has older version only).
Still no work!
I guess since it's async or something. Still fires 2x.

So we need a working setHashSilently.
And it should be a built in function so it's tested.

Please hit me up when you want to work on it. I can hack code to make it happen.

ps: I tried many routers, this works best - I'll show you the private beta site privately.

hasher.reload ?

Hi,

What's the best way to reload the current hash? hasher.replace(hasher.getHash()) doesn't work. I want to rerender the current route without a page reload after an ajax-request has completed.

Thanks

Unnecessary "hasher.changed" events fires

I've found an issue while trying to do some kind of redirects using your lib.
Test code is this:

function log(msg) {
    var elem = document.createElement('div');
    elem.innerHTML = msg;
    document.body.appendChild(elem);
}

var hashChangeHandler = function(newHash) {
    if (newHash === '') {
        log('changing hash to "one"');
        hasher.replaceHash('one');
    } else if (newHash === 'one') {
        log('changing hash to "two"');
        hasher.replaceHash('two');
    } else {
        log('hash changed to "two"');
    }
};

hasher.initialized.add(function(hash) {
    log('inited event fired with hash "' + hash + '"');
    hashChangeHandler(hash);
});
hasher.changed.add(function(hash) {
    log('changed event fired with hash "' + hash + '"');
    hashChangeHandler(hash);
});

window.onload = function() {
    hasher.init();
};

Here i'm doing some redirects in "hasher.changed" event using "hasher.replaceHash" method.
So if we'll go to the page containing this script using empty hash, the output will be this:

inited event fired with hash ""
changing hash to "one"
changed event fired with hash "one"
changing hash to "two"
changed event fired with hash "two"
hash changed to "two"
changed event fired with hash "one"
changing hash to "two"
changed event fired with hash "two"
hash changed to "two"

So you can see there are unnecessary change events fired in this case.
This is because of this line in "hasher.setHash" and "hasher.replaceHash" methods in your code:

_registerChange(path); //avoid breaking the application if for some reason `location.hash` don't change

As for me, there is no need to call "_registerChange" in these functions because it will be called by "hashchange" event listener.
I think there is no circumstances when the hash won't change.

You can see it at http://grunin-ya.ru/sandbox/hasher-issue.html

Is there a way to implement "onChange" that fires before "changed"?

Hi. Is there a way process ALL hash changes before the browser sets the hash? Right now I use the following function:

function parseHash(newHash, oldHash){

hasher.prependHash = '!/';
hasher.replaceHash(newHash);
}

It works but the hash flashes for an instant (mere ms.) in the URL bar; that is, first the original hash is set (e.g., "#25") and then it is prepended and replaced (e.g., "#!/25").

Thanks.

Fails in IE9 in compatibility mode

Hasher fails to work in IE9 (and probably IE8; untested) if IE9's Compatibility View is enabled.

From what I can tell, the problem is that, in compatibility mode, IE8 and IE9 report that 'onhashchange' is enabled but don't actually use it. See https://github.com/greggoryhz/MooTools-onHashChange-Event/issues/1 for more details.

Adding this check just below Hasher's private variables initialization seems to fix it:

    if (_isIE && _isHashChangeSupported && document.documentMode < 8) {
        _isHashChangeSupported = false;
        _isLegacyIE = true;
    }

add `replaceHash()` method

just a proxy to document.location.replace() which accepts paths fragments, it will change hash value without keeping old value in the history record...

how to do it right now: document.location.replace('#/foo/bar')

proposed change: hasher.replaceHash('foo', 'bar') and hasher.replaceHash('foo/bar')

PS: add unit tests (not sure how all browsers behaves when setting hash value through location.replace)

Routes called more than once?

Hi,

I'm trying to replace our current client-side routing with crossroads and hasher but I've run into an issue where the routes are called more than once on click. I'm wrapping crossroads and hasher as follow

parse_hash = (new_hash, old_hash) ->
  console.log("parse_hash: #{new_hash}")
  crossroads.parse(new_hash)

$ ->
  crossroads.routed.add (request, data) -> console.log("crossroads.routed: #{request}", data)

  hasher.initialized.add(parse_hash)
  hasher.changed.add(parse_hash)
  hasher.init()

  console.log('Router initialized!')

The actual routes are configured as follows

crossroads.addRoute 'sites/{site}', (site) -> hasher.replaceHash "sites/#{site}/overview"
crossroads.addRoute 'sites/{site}/{report}', (site, report) -> console.log('done?')

When clicking on on a link that triggers this JQuery snippet

$(document.body).on('click', '#main-menu #context-menu .site-link', function(e) {
    e.preventDefault()

    link = $(e.target)
    href = link.attr('href').substring(3)
    text = link.text()

    console.log('Updating router to ' + href)
    hesher.replaceHash(href)
});

The routing runs more than once as you can see from the following log

Updating router to  sites/example.com
router.update:      sites/example.com
parse_hash:         sites/example.com
parse_hash:         sites/example.com/overview
done?
crossroads.routed:  sites/example.com/overview 
crossroads.routed:  sites/example.com
parse_hash:         sites/example.com
parse_hash:         sites/example.com/overview
done?
crossroads.routed:  sites/example.com/overview 
crossroads.routed:  sites/example.com

I only noticed the issue because our app was making duplicate Ajax requests.

Would appreciate some input about what could be going on. I believe it's a problem with hasher.replaceHash since if I manually update the URL in the browser, it doesn't run twice.

Using hasher with already percent-encoded strings

I have a use case where I'd like my fragment identifiers to represent actual file names on disc, and in some weird cases those can contain octet sequences that don't decode as UTF-8. In other words I'd like to be able to pass an already percent-encoded string to setHash and to somehow be able to access the unprocecessed fragment identifier when it changes. Would you consider that out of scope for this library?

Failure to identify as _isLegacyIE in IE 7 IE 7 Standards doc mode

I'm not sure why it's failing yet, but I did come across this comment on stack overflow: @xkit explained to me a good feature test to work around the fact that although IE7 doesn't support onhashchange it would still return true for existence inference such as if("onhashchange" in window){/code/} when using IE7 Standard Document Mode in IE8.

It is indeed identifying my IE7 app as supporting Hash Change, although its not working.

make "hash" param on `setHash()` a rest param

allow multiple segments at once and merge values later like: hasher.setHash('foo', 'bar', 'ipsum') - since we have appendHash, prependHash and separator it makes way more sense to pass each segment as a different param than to concat the string manually...

revise API

revise API to make sure all methods are useful and still makes sense.

Why not use History.js?

It seems History.js is doing the same thing as your project? Is there anyway anything missing from History.js that you need, as I'll be glad to extend it for your use cases :)

PS. Awesome logo!

events should be dispatched asynchronously

it was a poor design decision to dispatch the signals as soon as the user calls setHash, events should always be async to avoid headaches (avoid cases where user might not expect something to happen in between).

refactor unit tests

unit tests are currently a huge mess... clean it up and split plugins from normal tests.

Nuget package

Have you considered packaging your project as a nuget package?

Change namespace of queryUtils

change code structure so queryUtils also make part of Hasher object.. maybe inside namespace Hasher.queryUtils instead of millermedeiros.queryUtils.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.