var myStorage = this.myStorage = new Class({ // as a mixin or standalone class that can cache anything into local storage. // in this case, we want to use this in Request.JSONP and have it cache replies // from the twitter API storage: {}, // you can puncture this through classname.storage[key] Implements: [Options], options: { storageMethod: "sessionStorage", // or localStorage privateKey: "myStorage" // sub key for namespacing }, initialize: function(options) { this.setOptions(options); this.storageMethod = this.options.storageMethod; }, setupStorage: function() { this.hasNativeStorage = !!(typeOf(window[this.storageMethod]) == "object" && window[this.storageMethod].getItem); if (this.hasNativeStorage) { try { this.storage = JSON.decode(window[this.storageMethod].getItem(this.options.privateKey)) || this.storage; } catch(e) { // session expired / tabs error (security) } } }, getItem: function(item) { return this.storage[item] || null; }, setItem: function(item, value) { this.storage = JSON.decode(window[this.storageMethod].getItem(this.options.privateKey)) || this.storage; this.storage[item] = value; if (this.hasNativeStorage) { try { window[this.storageMethod].setItem(this.options.privateKey, JSON.encode(this.storage)); } catch(e) { // session expired / tabs error (security) } } }, removeItem: function(item) { delete this.storage[item]; if (this.hasNativeStorage) { try { window[this.storageMethod].setItem(this.options.privateKey, JSON.encode(this.storage)); } catch(e) { // session expired / tabs error (security) } } } }); // allow storage in Request.JSONP first - as mixin. // All instances of Request.JSONP can now benefit from storage class // Request.JSONP.implement(new myStorage); // sample JSONP class that will prefer storage to live data Request.twitterUSER = new Class({ Extends: Request.JSONP, Implements: myStorage, options: { url: "http://api.twitter.com/1/users/show/{user}.json" }, initialize: function(user, options) { this.user = user; // will be used as key in storage object this.parent(options); this.setupStorage(); // initialize storage this.options.url = this.options.url.substitute({user: user}); }, send: function(options) { options = options || {}; // override default send - first check if result under key this.user in storage var data = this.getItem(this.user); if (data && !options.freshen) { // if yes, then fire the oncomplete/success immediately with the data as argument this.fireEvent("success", data); this.fireEvent("complete", data); this.fireEvent("retrieve", data); return; } // otherwise - we use normal send. this.parent(options); }, success: function(args) { // store result into storage for later. var data = Array.prototype.slice.call(args)[0]; this.setItem(this.user, data); this.fireEvent("store", data); this.parent(args); } }); (function() { var testRequest = new Request.twitterUSER("shitmydadsays", { onRequest: function() { document.id("status").set("html", "getting data..."); }, onComplete: function(data) { var el = document.id("userData"); el.removeClass("hide"); data.text = data.status.text; el.set("html", decodeURI(el.get("html")).substitute(data)).removeClass("hide"); }, onStore: function(value) { document.id("status").set("html", "No cache, stored data for {user}".substitute(this)); }, onRetrieve: function(value) { document.id("status").set("html", "Retrieved from cache for {user} - no request sent (API rate saving) + performance".substitute(this)); } }); document.getElements("a.send").addEvent("click", function(e) { e.stop(); document.id("userData").addClass("hide"); testRequest.send(this.hasClass("freshen") ? {freshen:true} : null) }); })();
<a class="send" href="#">Issue a normal JSONP request - cached or live</a> | <a href="#" class="send freshen">Force refresh from twitter API</a> <div id="status" class="baseBox shadowy">waiting... make sure you load it twice to see the caching</div> <div id="userData" class="hide baseBox shadowy"> <img src="{profile_image_url}" class="pimage shadowy" /> <!-- <div style="background:url({profile_image_url}) no-repeat right top"> --> <div class="left legend">Twitter nick:</div> <div class="left"><a href="http://twitter.com/{screen_name}">@{screen_name}</a></div> <div class="clear"></div> <div class="left legend">Full name:</div> <div class="left">{name}</div> <div class="clear"></div> <div class="left legend">Tagline:</div> <div class="left" style="width: 550px;">{description}</div> <div class="clear"></div> <div class="left legend">Location:</div> <div class="left">{location}</div> <div class="clear"></div> <div class="left legend">Homepage:</div> <div class="left"><a href='{url}'>{url}</a></div> <div class="clear"></div> <div class='left legend'>Followers / follows:</div> <div class="left">{followers_count} / {friends_count}</div> <div class="clear"></div> <div class="left legend">Last update:</div> <div class="left" style="width: 550px;"><em>{text}</em></div> <div class="clear"></div> <!-- </div> --> </div> <br /> Real world example: <a target=_blank href="http://dci.uk.net/trends/mootools">twitter hashtag trends thingie</a><br> Using storage for: <ul> <li>followers / profile info</li> <li>bit.ly and other shortened URLs resolution</li> <li>ajax search</li> <li>marking read tweets, votes and reports</li> </ul> If no storage is available, caching only lasts until a page reload. <h1>Pitfalls</h1> Because of the fallback to object properties and the namespacing, your 'key' needs to be a valid object property.<br /> Hence, you cannot do <strong>instance.setItem("http://bit.ly/kQHzsm", someData)</strong> with the URL as key.<br/><br/> Instead, make sure you normalise it first, for example:<br/> <strong>instance.setItem("http://bit.ly/kQHzsm".replace(/[^a-zA-Z0-9]+/g,''), someData);</strong> will work.<br/> This results in "httpbitlykQHzsm" so eg, instance.storage[instance.options.privateKey].httpbitlykQHzsm == someData.
html { font-family: verdana; font-size: 12px; } div.left { float: left; } div.clear { clear: both; } div.legend { width: 150px; font-weight: bold; } #status { color: #fff; margin: 5px 0 15px 0; } .hide { display: none; } em { } div.baseBox { color: #B3AF74; border-bottom: 1px solid #000; background-color: #500; background-image: -moz-linear-gradient(top, #700,#500); background-image: -webkit-linear-gradient(top, #700,#500); background-image: -o-linear-gradient(top, #700,#500); padding: 5px; border: 1px solid #000; } img.pimage { position: absolute; border: 2px solid #000; right: 50px; margin-top: -10px; -moz-transform: skew(7deg) rotate(13deg); -webkit-transform: skew(7deg) rotate(13deg); -ms-transform: skew(7deg) rotate(13deg); } .shadowy { -moz-box-shadow: 8px 8px 15px rgba(0,0,0,.7); -webkit-box-shadow: 8px 8px 15px rgba(0,0,0,.7); -ms-box-shadow: 8px 8px 15px rgba(0,0,0,.7); box-shadow: 8px 8px 15px rgba(0,0,0,.7); -o-box-shadow: 8px 8px 15px rgba(0,0,0,.7); } .baseBox a { color: #fff; }