Skip to content
Sixtease edited this page Sep 13, 2010 · 39 revisions

Kobayashi — Content By Hash library

Depends on jQuery 1.3+

Synopsis

<script src="kobayashi.js"></script>
<script>Kobayashi.DEFAULT_TARGET = 'content'</script>
<a href="#/foo/">Load /foo/ into #content</a>
<div id="content"></div>

Introduction

Kobayashi enables history and bookmarks with AJAX and automates loading of HTML chunks into a webpage. It simplifies building AJAX web applications in a RESTful fashion.

The typical scenario for employing Kobayashi is when you have a few pages with the same layout, menu, header, footer and just want the menu links to switch content in the “content” div. Without AJAX, you’d have to reload the whole pages with all they have in common. With AJAX, you wouldn’t be able to use the back and forward keys and to bookmark pages.

The problem of many HTML pages sharing a lot of content is often solved with PHP. You can see a lot of URLs like this:
http://example.com/index.php?page=intro
http://example.com/index.php?page=about
http://example.com/index.php?page=links

Here, index.php most likely has the common stuff in it and pages intro.html, about.html and links.html may contain the differing contents. Notice that the URLs are pretty descriptive — you know you’re on the root, accessing a page named intro, about or links. The obvious drawback is that the common stuff is always sent from server to client whenever another page is requested.

Kobayashi does this for you on the client side. If intro.html, about.html and links.html (without the layout) are accessible at http://example.com/intro.html, http://example.com/about.html and http://example.com/links.html, you can use addresses like these to achieve the same thing with Kobayashi:
http://example.com/index.html#/intro.html
http://example.com/index.html#/about.html
http://example.com/index.html#/links.html

Here, index.html could look something like this:

<html> <head>
<script src="kobayashi.js"></script>
<script>Kobayashi.DEFAULT_TARGET = 'content'</script>
<title>Kobayashi Demo</title>
</head> <body>
<ul id="menu" class="js-hashadr-container">
  <li><a href="intro.html">Intro</a></li>
  <li><a href="about.html">About</a></li>
  <li><a href="links.html">Links</a></li>
</ul>
<div id="content">Welcome to Kobayashi demo!</div>
</body> </html>

The tricky parts are

  1. setting Kobayashi.DEFAULT_TARGET to content, which is the ID of the div where the pages are going to be loaded and
  2. setting class="js-hashadr-container" to the menu ul, which changes the behavior of all links within it to merely alter the hash part of the URL.

Kobayashi watches for the URL hash part to change and loads the appropriate page to the “content” div. Thus, links, bookmarks and history work as desired, the page is not unnecessarily reloaded and no extra programming is required.

This is the core functionality of Kobayashi. However, to be of use for building a realistic web application,it provides many more features. Read on to learn about them.

Kobayashi API

Links (<a>) with class="js-hashadr" or inside an element with class="js-hashadr-container" will call function adr passing it their href attribute as parameter and stop following the link.
Class js-nohashadr cancels the effect of js-hashadr-container. This enables a link to be excluded from the hash-address functionality.

This will change location.hash and load stuff into an appropriate element (Kobayashi.DEFAULT_TARGET by default), reflecting this at the URL in the address bar.

Links with class="js-simpleload" or inside an element with class="js-simpleload-container" will call function js-simpleload passing it their href attribute as parameter and stop following the link.

This will load stuff into an appropriate element, not affecting the URL in the address bar.

If the target element is hidden (display: none;), it is shown by default upon injection of content.
Presence of the js-noautoshow class overrides this.

Whenever content is loaded as a result of hashchange event or simple_load function, the custom content_added event is triggered.
When loading content is attempted but the request fails, the load_content_failed event is triggered instead.

Classes:

  • js-hashadr
  • js-hashadr-container
  • js-simpleload
  • js-simpleload-container
  • js-noautoshow

Base URL

Say your web application lives on http://example.com/export/applications/myeshop/. The things you want to load with Kobayashi are at http://example.com/export/applications/myeshop/procucts and http://example.com/export/applications/myeshop/prices. Then the links would have to look like this:

http://example.com/export/applications/myeshop/#/export/applications/myeshop/products/
http://example.com/export/applications/myeshop/#/export/applications/myeshop/prices/

The repetition of the path is not very nice. To please your eye, Kobayashi detects a global variable BASE_URL, which in this case should be /export/applications/myeshop/. If it was set to this value, then you should use the links in this form instead:

http://example.com/export/applications/myeshop/#/products/
http://example.com/export/applications/myeshop/#/prices/

“Loading” Messages

To enable you to note the user that content is being loaded, three events are available:

  • show_loading
  • dec_loading
  • hide_loading

The show_loading event is triggered whenever loading an URL is commenced. It can be triggered repeatedly, when several URLs are being requested from one hash change.

The dec_loading event is triggered whenever loading an URL has terminated — successfully or not.

The hide_loading event is triggered when all loading has been terminated. It is not normally triggered at all, only in exceptional cases.

You may want to condition showing a “loading” message on a counter’s positive value. It should be initialized to zero, increased on show_loading, decreased on dec_loading and set to zero on hide_loading.

Hash Address Language

location.hash is interpreted as a sequence of specifiers delimited by “#” characters.

Each of those specifiers is passed to the adr function, see below.

The requests are done in parallel but inserted into DOM in the order they appear in the hash string, which allows you to load things into an element which is yet to be loaded. An example:

Say you have <div id="talk"> under URL /comment-container/ and you have <p>first comment</p><p>second comment</p> under URL /comments/.

Of course you want the result to be <div id="talk"><p>first comment</p><p>second comment</p></div>. You can achieve this simply by requesting address:

#/comment-container/#talk::/comments/

This will work even though the div #talk isn’t in the DOM yet and even if the request for /comments/ finishes before /comment-container/.

Relative Addresses Beyond Hash Mark in URL

It is assumed here that Kobayashi.DEFAULT_TARGET is set to "content".

Function adr is the cornerstone of handling addresses in the specifiers in location.hash.
It receives two arguments:

  1. A specifier
  2. Options object

Specifiers

Specifiers can be in three distinct formats:

  1. URL
    Example: /articles/article/
  2. target_id::URL
    Example: passwd-container::/password_change/
  3. target_id::relative-address-base::URL
    Example1: history::content::history/
    Example2: history::::history/

Format 1 is a shorthand for content::URL, i.e. omitting the target ID substitutes value of “Kobayashi.DEFAULT_TARGET” as a default.

Specifier in format 2 says that an URL should be loaded from the given URL and the response injected into the element with the given ID.

Format 3 makes it possible to use a relative address to address from another specifier.
Relative address operate on the address up to the first “?” or “#” character in location.href.
Suppose you are at this location: http://example.com/#content::/articles/article/118/
and you want to load a deletion confirmation to the “delete-dialog” element. Say the delete confirmation can be loaded from
/articles/article/118/delete. Using a relative address directly, like at
<a class="hashadr" href="delete-dialog::delete/"> would result in this address being used:
http://example.com/#content::/articles/article/118/#delete-dialog::delete/ and that would load
http://example.com/delete/ into #delete-dialog, which is not what you need.

Enter format 3: saying <a class="hashadr" href="delete-dialog::content::delete/"> means:
Take the address loaded into #content, take it as a base for the relative address “delete/” and load it into “#delete-dialog”.

Leaving the middle part (relative-base) empty is the same as putting content inside, so
delete-dialog::content::delete/ could be equally written as delete-dialog::::delete/.

Relative address particularities

Applying relative addresses is hand-implemented in JavaScript, so it can behave differently than you might expect. Test is before you apply something wild.

Double dot (..) cuts off the last directory, so applying ../foo/ on /a/b/ won’t result to /a/b/../foo/ but to /a/foo/.

Get parameters can be added conveniently: start the address with the & sign.
If you’re at
/foo/?color=green&shape=square and you apply
&color=blue&taste=bitter, you get
/foo/?color=blue&shape=square&taste=bitter

Options for adr()

Second parameter for the adr function — options — is optional. If present, it must be an object. These keys are recognized:

  1. hash
  2. just_get
  3. just_set

Normally, function adr looks at location.hash, modifies it as the specifier dictates and assigns the result back to location.hash.
options.hash and options.just_get override this.

Set options.hash to whatever string you want function adr to look at instead of location.hash.

Example:
You’re at: http://example.com/myapp/#this#is#irrelevant
You call: adr('../oaxaca/', {hash: '/america/mexico/tasco/'})
You’re sent to: http://example.com/myapp/#/america/mexico/oaxaca/

Set options.just_get to ‘hash’ if you want function adr to return the resulting hash (with all specifiers) instead of applying it to location.hash.

Example:
You’re at: http://example.com/myapp/#/index/#gallery::/trips/2009/
You call: adr('gallery::../2008/', {just_get: 'hash'})
You get: #/index/#gallery::/trips/2008/
location will not change.

Set options.just_get to ‘address’ if you want function adr to return just the resulting address of the one affected specifier instead of applying it to location.hash.

Example:
You’re at: http://example.com/myapp/#/index/#gallery::/trips/2009/
You call: adr('gallery::../2008/', {just_get: 'address'})
You get: /trips/2008/
location will not change.

Set options.just_set to true to prevent the hashchange event from being triggered. This will prevent loading any content based on the hash change. Upon next hashchange event, the new state will be recognized though.

Example:
You’re at: http://example.com/myapp/#/america/mexico/tasco/
You call: adr('../oaxaca/', {just_set: true})
location is changed to http://example.com/myapp/#/america/mexico/oaxaca/ but no request is made and the content will stay intact as if you stayed in Tasco.
Later you call: adr('#right-column::/comments/')
You are sent to: http://example.com/myapp/#/america/mexico/oaxaca/#right-column::/comments/
and comments will be loaded to the right column and oaxaca will be loaded into content.

Set options.nohistory to true if you want this change in the location string to be ignored by the browser’s history management. This is useful for redirects.

Example:
You’re at: http://example.com/myapp/#/america/mexico/tasco/
You call: adr('../')
You get to: http://example.com/myapp/#/america/mexico/
Then you call: adr('mexico-city', {nohistory: true})
You get to: http://example.com/myapp/#/america/mexico/mexico-city
You press the back button.
You get to: http://example.com/myapp/#/america/mexico/tasco/

A shorthand for adr(_specifier_, {just_get:true}) is available: get_hashadr.
Function get_adr does the same and prepends BASE_PATH in front of the result.
Both of these accept the same arguments as adr, so you can still set options.hash.

Use function get_adr to get addresses directly usable in conventional hyperlinks.

Redirects

Normal 302 responses from the server affect AJAX requests in the same way as normal requests, so redirecting in this way works normally, however Kobayashi will not be aware of the redirection and will not update the hash. This means that if you come to /~john which redirects to /user/john/, then /user/john/ will load OK but the hash will still show /~john as originally requested.

To remedy this, Kobayashi recognizes special HTTP response header Redirect-To (case sensitive). If your response is a success (e.g. status 200) and in the headers, Redirect-To: /user/john/ is found, then redirection is performed by Kobayashi, reflecting the change to the URL. Optional callbacks as well as chained requests should work normally, i.e. as if the request was atomic.

The value of the Redirect-To header should contain an optional BASE_URL, which will be stripped off for the URL.

Media Loading

Since everything is loaded into a non-reloading page, all needed CSS and JavaScript files must be loaded at the starting page. This is impractical and slows down the initial load. On the other hand, if the JavaScripts were inside the loaded pieces, then they would be loaded and run repeatedly, which is undesired.

To remedy this, the request_media function is provided.

Its only argument is an URL of a medium to be loaded. The type of the medium is recognized by the suffix of the URL. Currently, only .js and .css files are recognized. Re-requesting already loaded media is ignored. When the requested media are loaded, the media_loaded event is fired. Calling the function several times in a row results in sequentially loading the media, waiting for each other to address library dependencies and CSS priorities. The media_loaded event is fired after the whole bunch of media are loaded.
If a library needs to re-run some code when new content is loaded, it should not rely on request_media being called. Rather, it should bind a desired function to the content_added event.

Exported Functions, Constants and Objects

  • Kobayashi

    The object encapsulating the library’s interface
  • Kobayashi.DEFAULT_TARGET

    ID of the element where AJAX’ed stuff is injected if no target is specified.
  • Kobayashi.ADDRESS_POSTPROCESS

    An object mapping addresses (that are to appear in hash specifiers) to their postprocessed variants.

    location.hash will be automatically updated when an address to be postprocessed is requested.
  • Kobayashi.LOADED_URLS

    An object mapping id’s of elements into addresses loaded into them.
  • Kobayashi.URL_LOADED_BY_HASH

    An object mapping IDs of elements where stuff has been loaded to boolean values indicating whether this address is reflected in the URL hash part. False indicates address loaded by simple_load.
  • function Kobayashi.load_content(args)

    Commences an XMLHttpRequest and schedules it to be loaded into DOM when it finishes and when all previous chunks have been inserted.

    The only argument is an object, where some keys are recognized and which is passed intact to the XMLHttpRequest as original_options.

    Recognized fields:

    • target_id: ID of the element where the result should be loaded. This is the only required field.
    • address: The URL (good without http:// and all that). If it is omitted, it is interpreted as a request to return the target element to its original state. This is achieved by loading BASES[ target_id ] or if it is not defined, then reloading the whole page.
    • success_callback: Function to be executed after the loaded content is successfull injected into DOM.
    • error_callback: Function to be executed if loading the content fails. This and the above-mentioned callback receives the arg object passed to load_content as this. It contains a xhr field containing the XMLHttpRequest.
  • function Kobayashi.reload_content(container_id)

    If an URL is loaded into the container given by its id, then it is reloaded,
    otherwise its BASE_URL is loaded. If no BASE_URL is defined either, the whole page is reloaded.

  • function Kobayashi.unload_content(container_id, options)

    Deletes the record of any address being loaded into the corresponding container.
    One effect of this is that subsequent requests for loading are actually executed.
    Another effect is that the container is emptied. This can be prevented by setting options.keep_content to true.

  • function Kobayashi.simple_load(specifier)

    The function called on click on an <a class="js-simpleload">.

  • function closest_loaded(element)

    Given an HTML element, returns the closest ancestor that was loaded via AJAX. Returns null if element has not been dynamically loaded.

  • function adr(address, options)
  • function get_hashadr(address, options)
  • function get_adr(address, options)

    See Relative Addresses Beyond Hash Mark in URL

  • function request_media(url, success_callback, error_callback)

    See Media Loading

  • function arr2map(array)

    A general-purpose function for converting [a,b,c] arrays into {a:1,b:2,c:1} objects.

  • function alert_dump(object, name)

    Debugging function. alerts the keys and values of an object.

  • function carp(anything, …)

    Passes its arguments to console.log and appends them to the #debug div.

  • BASE_URL

    The path to the index page, with trailing slash

  • BASE_PATH

    Like BASE_URL only without trailing slash

  • function prepend_base_path_to(url)