shoreleave/shoreleave-browser

0.3.0


A smarter client-side with ClojureScript : Shoreleave's enhanced browser utilities

dependencies

org.clojure/clojure
1.4.0
shoreleave/shoreleave-core
0.3.0

dev dependencies

lein-marginalia
0.7.1



(this space intentionally left almost blank)
 

An idiomatic interface to Blobs

(ns shoreleave.browser.blob)

Blobs

HTML5 File API supports the creation of Blobs.

Blobs allow you to take arbitrary text (like functions) and create file-like objects, that get their own unique URL wuth a blob://... schema.

This is useful if you're making an app that wants to use on-demand assets, or you need to build something like embedded web workers. It also comes in handy if you want to build dynamic content on the fly, like streaming images.

The Blob API is now a stable spec in HTML5.

To create blobs, you pass a vector of file contents (or parts) to (blob ...) Optionally, you can set the content-type of the blob by passing in the content-type string. For example, "text\/xml"

(defn- window-url-prop
  []
  (or (.-URL js/window) (.-webkitURL js/window)))

Build a new Blob object, but don't muck with the args. This is for low-level interop stuff - when needed.

(defn raw-blob
  ([file-parts]
   (js/Blob. file-parts))
  ([file-parts prop-bag]
   (js/Blob. file-parts prop-bag)))

Build the file-contents into a Blob and return it. Optionally set the content-type via a string

(defn blob
  ([file-parts]
   (js/Blob. (clj->js file-parts)))
  ([file-parts content-type-str]
   (js/Blob. (clj->js file-parts) (js-obj "type" content-type-str))))

Create a unique object URL (ala blob://...) for a Blob object, as returned from (blob ...)

(defn object-url!
  [file-or-blob]
  (let [url (window-url-prop)]
    (when url
      (.createObjectURL url file-or-blob))))
(defn revoke-object-url!
  [obj-url]
  (let [url (window-url-prop)]
    (when url
      (.revokeObjectURL url obj-url))))
 

An idiomatic interface to cookies

(ns shoreleave.browser.cookies
  (:require [goog.net.Cookies :as gCookies]
            [goog.string :as gstr]))

Cookie support

Shoreleave's cookie support is built upon Closure's Cookies.

The base object is extended to support the following calls:

  • map-style lookup - (:csrf-token cookies "default value")
  • get lookups
  • seqable behavior - (map identity cookies)
  • (count cookies)
  • (keys cookies)
  • (persistent! cookies) - a PersistentHashMap of the cookies
  • (assoc! cookies :new-key "saved") - updating the cookies
  • (dissoc! cookies :csrf-token) - removing things from the cookies
  • (empty! cookies) - delete all cookies
(declare as-hash-map)

TODO: Consider making Cookies extend IWatchable

(extend-type goog.net.Cookies

  ILookup
  (-lookup
    ([c k]
      (-lookup c k nil))
    ([c k not-found] ;gstr/urlDecode
      (let [v (.get c (name k) not-found)]
        (if (string? v)
          (gstr/urlDecode v)
          v))
      #_(.get c (name k) not-found)))

  ISeqable
  (-seq [c]
    (map vector (.getKeys c) (.getValues c)))

  ICounted
  (-count  [c] (.getCount c))

  IFn
  (-invoke
    ([c k]
      (-lookup c k))
    ([c k not-found]
      (-lookup c k not-found))) 

  ITransientCollection
  (-persistent! [c] (as-hash-map c))
  ;(-conj! [c v] nil)

  ITransientAssociative
  (-assoc! [c k v & opts]
    (when-let [k (and (.isValidName c (name k)) (name k))]
      (let [{:keys [max-age path domain secure?]} (apply hash-map opts)]
        (.set c k v max-age path domain secure?))))

  ITransientMap
  (-dissoc! [c k & opts]
    (when-let [k (and (.isValidName c (name k)) (name k))]
      (let [{:keys [path domain]} (apply hash-map opts)]
        (.remove c k path domain))))

  IAssociative
  (-assoc [c k v]
    (-assoc (-persistent! c) k v))
  (-contains-key? [c k]
    (.containsKey c (name k)))

  ;IPrintable
  ;(-pr-seq  [c opts]
  ;  #_(let  [pr-pair  (fn  [keyval]  (pr-sequential pr-seq "" " " "" opts keyval))]
  ;    (pr-sequential pr-pair "{" ", " "}" opts c))
  ;  (-pr-seq (-persistent! c) opts))
  IPrintWithWriter
  (-pr-writer [c writer opts]
    (-write writer (-persistent! c)))

  ;; TODO: using the persistent version here might be a bad idea
  IHash
  (-hash [c]
    (-hash (-persistent! c))))
(def cookies (goog.net.Cookies. js/document))
(defn as-hash-map
  ([]
   (as-hash-map cookies))
  ([cks]
   (zipmap (.getKeys cks) (.getValues cks))))

Returns a boolean, true if cookies are currently enabled for the browser

(defn cookies-enabled?
  ([]
   (cookies-enabled? cookies))
  ([cks]
   (.isEnabled cks)))
(defn empty! [cks]
  (.clear cks))
 

An idiomatic interface to browser history

(ns shoreleave.browser.history
  (:require [goog.events :as gevents]
            [goog.History :as ghistory]
            [goog.history.EventType :as history-event]
            [goog.history.Html5History :as history5]))

This is the history object - the interface the browser's history

You can initialize your own history object, but you're encouraged to us the one provided

(declare history)

Navigation Events

Adding History support to your application allows you to correctly handle location-bar state and enables correct usage of a browser's navigation buttons (like the back button).

Every point of "navigation" within your app can be added to the browsers history, allowing you to go backwards and forwards in that history

History events are packaged up as a map with the keys: :token :type :navigation?

:token is the location URL associated with this point in history. :type is the type of history event that was captured. You can ignore this. :navigation? is a boolean, True if the event was initiated by a browser, or false otherwise

Add a function to be called when a navigation event happens. The function should accept a single map, with keys :token, :type, :navigation?

(defn navigate-callback
  ([callback-fn]
   (navigate-callback history callback-fn))
  ([hist callback-fn]
   (gevents/listen hist history-event/NAVIGATE
                  (fn [e]
                    (callback-fn {:token (keyword (.-token e))
                                  :type (.-type e)
                                  :navigation? (.-isNavigation e)})))))

Initialize the browser's history, with HTML5 API support if available

(defn init-history
  []
  (let [history (if (history5/isSupported)
                  (goog.history.Html5History.)
                  (goog.History.))]
                   (.setEnabled history true)
                   (gevents/unlisten (.-window_ history) (.-POPSTATE gevents/EventType) ; This is a patch-hack to ignore double events
                                     (.-onHistoryEvent_ history), false, history)
                   history))

Get the current token/url string in the browser history

(def history (init-history))
(defn get-token
  [hist]
  (.getToken hist))

Add a new token to the brower's history. This will trigger a nvaigate event

(defn set-token!
  [hist tok]
  (.setToken hist tok))
(defn replace-token! [hist tok] (.replaceToken hist tok))

Raw access to the HTML5 History API

This is advantageous when you want to use the stateobj for partial view or data caching

For most applications this is not needed (nor is it advised). It's useful for storing remote URLs the page needed, or small pieces of page specific state that need to be restored for the page to be functional.

(defn push-state [hist state-map]
  (let [{:keys [state title url]
         :or {state nil
              title js/document.title}} state-map]
    (apply js/window.history.pushState (map clj->js [state title url]))
    (.dispatchEvent hist (goog.history.Event. url false))))
 

An idiomatic interface to the browser's local storage

(ns shoreleave.browser.storage.localstorage
  (:require [cljs.reader :as reader]
            [goog.storage.mechanism.HTML5LocalStorage :as html5ls]
            [shoreleave.browser.storage.webstorage]))

Watchers

In most applications, you want to trigger actions when data is changed. To support this, Shoreleave's local storage use IWatchable and maintains the watchers in an atom.

(def ls-watchers (atom {}))

`localStorage` support

For general information on localStorage, please see Mozilla's docs

Shoreleave's localStorage support is built against Closure's interface

The extension supports the following calls:

  • map-style lookup - (:search-results local-storage "default value")
  • get lookups
  • (count local-storage) - the number of things/keys stored
  • (assoc! local-storage :new-key "saved") - update or add an item
  • (dissoc! local-storage :saved-results) - remove an item
  • (empty! local-storage) - Clear out the localStorage store

Using localStorage in Pub/Sub

The apprpriate IWatchable support is attached to Google's HTML5LocalStorage to allow it to participate in Shoreleave's pub/sub system

To enable it, you need to (publishable/include-localstorage!) in the file where you setup and wire together your bus and publishables.

(extend-type goog.storage.mechanism.HTML5LocalStorage

  IWatchable
  (-notify-watches [ls oldval newval]
    (doseq  [[key f] @ls-watchers]
      (f key ls oldval newval)))
  (-add-watch [ls key f]
    (swap! ls-watchers assoc key f))
  (-remove-watch [ls key]
    (swap! ls-watchers dissoc key)))

Get the browser's localStorage

Usage

You'll typically do something like: (def local-storage (localstorage/storage)

(defn storage
  []
  (goog.storage.mechanism.HTML5LocalStorage.))

Much like how you can easily get "cookies/cookies" you can get "localstorage/localstorage"

(def localstorage (storage))
 

An idiomatic interface to the browser's session storage

(ns shoreleave.browser.storage.sessionstorage
  (:require [cljs.reader :as reader]
            [goog.storage.mechanism.HTML5SessionStorage :as html5ss]
            [shoreleave.browser.storage.webstorage]))

Watchers

In most applications, you want to trigger actions when data is changed. To support this, Shoreleave's session storage use IWatchable and maintains the watchers in an atom. This is identical to techniques used in local storage.

(def ss-watchers (atom {}))

`sessionStorage` support

For general information on sessionStorage, please see Mozilla's docs

Shoreleave's sessionStorage support is built against Closure's interface

The extension supports the following calls:

  • map-style lookup - (:search-results session-storage "default value")
  • get lookups
  • (count session-storage) - the number of things/keys stored
  • (assoc! session-storage :new-key "saved") - update or add an item
  • (dissoc! session-storage :saved-results) - remove an item
  • (empty! session-storage) - Clear out the localStorage store

Using sessionStorage in Pub/Sub

The apprpriate IWatchable support is attached to Google's HTML5SessionStorage to allow it to participate in Shoreleave's pub/sub system

To enable it, you need to (publishable/include-sessionstorage!) in the file where you setup and wire together your bus and publishables.

(extend-type goog.storage.mechanism.HTML5SessionStorage

  IWatchable
  (-notify-watches [ss oldval newval]
    (doseq  [[key f] @ss-watchers]
      (f key ss oldval newval)))
  (-add-watch [ss key f]
    (swap! ss-watchers assoc key f))
  (-remove-watch [ss key]
    (swap! ss-watchers dissoc key)))

Get the browser's sessionStorage

Usage

You'll typically do something like: (def session-storage (sessionstorage/storage)

(defn storage
  []
  (goog.storage.mechanism.HTML5SessionStorage.))

Much like how you can easily get "cookies/cookies" you can get "sessionstorage/sessionstorage"

(def sessionstorage (storage))
 

An idiomatic interface to the browser's storage mechanisms (local and sessions)

(ns shoreleave.browser.storage.webstorage
  (:require [cljs.reader :as reader]
            [goog.storage.mechanism.HTML5WebStorage :as html5webstorage]
            [goog.iter :as g-iter]))

Google Closure attaches a common prototype to all browser storage systems called, WebStorage. Shoreleave extends this type, to extend ClojureScript functionality/interop to all browsers storages.

WebStorage support

For general information on localStorage, please see the docs in localstorage.cljs For general information on sessionStorage, please see the docs in sessionstorage.cljs

Shoreleave's generic storage support is built against Closure's interface

The extension supports the following calls:

  • map-style lookup - (:search-results storage "default value")
  • get lookups
  • (count storage) - the number of things/keys stored
  • (assoc! storage :new-key "saved") - update or add an item
  • (dissoc! storage :saved-results) - remove an item
  • (empty! storage) - Clear out the localStorage store

Using storage in Pub/Sub

There is PubSub support for the specific storage types. Please see the details in those files. You'll need to require them directly to get support.

(defn storage-keys [ls]
  (g-iter/toArray (.__iterator__ ls true)))
(defn storage-values [ls]
  (g-iter/toArray (.__iterator__ ls false)))
(defn as-hash-map
  ([storage]
   (zipmap (storage-keys storage) (storage-values storage))))
(extend-type goog.storage.mechanism.HTML5WebStorage
  
  ILookup
  (-lookup
    ([ls k]
      (-lookup ls k nil))
    ([ls k not-found]
      (let [read-value (if-let [v (not-empty (.get ls (name k)))]
                        v
                        (pr-str not-found))]
        (reader/read-string read-value))))

  ISeqable
  (-seq [ls]
    (map vector (storage-keys ls) (storage-values ls)))

  ICounted
  (-count  [ls] (.getCount ls))

  IFn
  (-invoke
    ([ls k]
      (-lookup ls k))
    ([ls k not-found]
      (-lookup ls k not-found))) 

  ITransientCollection
  (-persistent! [ls] (as-hash-map ls))
  ;(-conj! [c v] nil)

  ITransientAssociative
  (-assoc! [ls k v]
    (let [old-val (-lookup ls k)]
      (.set ls (name k) (pr-str v))
      (-notify-watches ls {k old-val} {k v})
      ls))

  ITransientMap
  (-dissoc! [ls k]
    (do
      (.remove ls (name k))
      ls))

  ;IPrintable
  ;(-pr-seq  [ls opts]
   ; #_(let  [pr-pair  (fn  [keyval]  (pr-sequential pr-seq "" " " "" opts keyval))]
   ;   (pr-sequential pr-pair "{" ", " "}" opts ls))
   ; (-pr-seq (-persistent! ls) opts))
  IPrintWithWriter
  (-pr-writer [ls writer opts]
    (let [pers-st (-persistent! ls)]
     (-write writer (-persistent! ls)))))

Clear the storage

(defn empty!
  [ls]
  (.clear ls))