Friday, I posted a long blog article that discussed using KRL to create a simple blogging application. the application writes multiple pages, manages a navigation bar, and allows new posts. (Try it here.) One problem with the implementation is that because it paints the entire app in a single page (sometimes called a single page interface or SPI), the back and forward buttons don't work.
The problem is that the browser doesn't know there's a new page and put it on the history unless the URL changes. The back and forward buttons are just indexes on the browser history. The trick that SPI Web applications use to get around this to write URL fragments (the stuff after the # symbol). Adding a fragment doesn't cause the browser to reload the page because fragments are designed for interpage navigation.
Change the URL Fragment
Rewriting the fragment on the page just takes a little JavaScript. And using JavaScript from within a KRL ruleset is easy with the emit action. We add the emit action to the rules that write pages. Here's the show_contact rule that controls the Contact page:
rule show_contact { select when web click "#siteNavContact" pre { contact_html = << <div id="leftcontainer"> <h2 class="mainheading">Contact</h2> <article class="post">...</article> </div> >>; title = << <title>#{blogtitle} - Contact</title> >>; } { paint_container(title, contact_html); emit << self.document.location.hash='!/contact'; >>; } }
Adding the emit ensures that whenever the Contact page is displayed, the URL will have #!/contact appended:
/kblog/#!/contact
We make similar changes to the Home page so that it has #!/ appended to the URL whenever we visit that page.
Note: technically, I shouldn't be putting the ! in the fragment since that indicates to search engines that the page is available at the non-fragmented URL (with #! removed) for search engine crawlers.
Updating the Page When the Fragment Changes
After we've correctly modified all the rules that control a page, the pages will all be identified with the right fragment as you navigate from page to page using the links in the navigation bar at the top of the blog. The problem is that the back and forward buttons are still broken. If you use them the URL will change, but there's nothing to change the content as the URL changes. As we said before, the browser does not alert the server when the fragment changes and this is as we want it. We want to decide what happens when the URL fragment changes on our blog.
To remedy this problem, we'll use a jQuery plugin called hashchange. Since the KRL Web endpoint has jQuery built in, using jQuery plugins is easy.
First we put the plugin on our server and change the jQuery variable in the final line so that we pass in $KOBJ instead:
... // was --> })(jQuery); })($KOBJ);
The plugin must call the KRL jQuery library, named $KOBJ. The Web endpoint uses jQuery in extreme compatibility mode to ensure that it doesn't interfere with Web pages that have already loaded jQuery.
Second, we have to load the plugin in our ruleset. KRL provides a facility for loading external JavaScript resources as a pragma in the meta section of the ruleset:
use javascript resource "http:/.../kblog/jquery.hashchange.js"
This loads the library once (and only once).
Third, we need to deploy the hashchange watcher so that it can see when the fragment changes and run a function that will raise an appropriate event to the KRL engine. You'll recall that we had a few other watchers in the code that initialized the page. We want to add an emit action to that same rule that watches for the fragment change and raises an event:
rule init_html { select when pageview ".*" setting () { replace_html("#about", about_text); watch("#siteNavHome", "click"); watch("#siteNavContact", "click"); emit << self.document.location.hash='!/'; $KOBJ(window).hashchange(function() { if(KOBJ.a16x88.previous == undefined || KOBJ.a16x88.previous != self.document.location.hash) { var app = KOBJ.get_application("a16x88"); app.raise_event("hash_change", {"newhash": self.document.location.hash}); KOBJ.a16x88.previous = self.document.location.hash; } }); >>; } always { raise explicit event blog_ready } }
The only thing new is the emit action. The JavaScript in that action sets to fragment to the default page fragment and attaches a hashchange JavaScript listener to the window. The function that it calls uses the Web endpoint runtime to get the app object associated with the current ruleset and raise and event called hash_change to the KRL engine with the new fragment as a parameter. The whole thing is wrapped in an if statement to ensure it only runs once per fragment change.
You might be tempted to put the $KOBJ(window).hashchange() call in the global block. As programmers we're used to "global" blocks being evaluated once. And that's true for KRL as well, but in KRL it's once per event. Note that any given interaction with the blog causes multiple events. The init_html rule is only run once per blog interaction--on the initial pageview--so that's the place to put any watchers. After that all the events are the result of clicks, submits, or explicit raises.
Fourth, and last, we need to modify the rules that paint the pages to respond to the hash_change event. Here's the show_contact rule again, with the changes that do that:
rule show_contact { select when web click "#siteNavContact" or web hash_change newhash "/contact$" pre { contact_html = << <div id="leftcontainer"> <h2 class="mainheading">Contact</h2> <article class="post">...</article> </div> >>; title = << <title>#{blogtitle} - Contact</title> >>; } { paint_container(title, contact_html); emit << KOBJ.a16x88.previous = '#!/contact'; self.document.location.hash='!/contact'; >>; } }
There are two changes:
- We added another clause to the select statement that looks for a hash_change event that has a parameter named newhash with the value matched by the appropriate regular expression to ensure we're looking at a contact page.
- The emit has an additional line of JavaScript to set the previous variable to ensure that this event only happens once.
Note: the JavaScript location.hash variable has an inconsistent interface. When you set it, you don't include the #. But when you read it, the string you get back has the # prepended. That took more than a few minutes to figure out.
Event Hierarchy
The event hierarchy diagram is only slightly more complicated (here's the original):
(click to enlarge)
A new Web event, hash_change has been added. The appropriate rules are selected based on the event parameters passed in. These are used to fire existing rules.
With these changes navigating the blog causes the URL to update and, conversely changing the URL changes the blog. Now the back and forward buttons work as we'd expect.