Giter VIP home page Giter VIP logo

secretary's Introduction

secretary

A client-side router for ClojureScript.

Clojars Project cljdoc badge CircleCI

Contents

Installation

Add secretary to your project.clj :dependencies vector:

[clj-commons/secretary "1.2.4"]

For the current SNAPSHOT version use:

[clj-commons/secretary "1.2.5-SNAPSHOT"]

Guide

To get started :require secretary somewhere in your project.

(ns app.routes
  (:require [secretary.core :as secretary :refer-macros [defroute]]))

Note: starting ClojureScript v0.0-2371, :refer cannot be used to import macros into your project anymore. The proper way to do it is by using :refer-macros as above. When using ClojureScript v0.0-2755 or above, if (:require [secretary.core :as secretary]) is used, macros will be automatically aliased to secretary, e.g. secretary/defroute.

Basic routing and dispatch

Secretary is built around two main goals: creating route matchers and dispatching actions. Route matchers match and extract parameters from URI fragments and actions are functions which accept those parameters.

defroute is Secretary's primary macro for defining a link between a route matcher and an action. The signature of this macro is [name? route destruct & body]. We will skip the name? part of the signature for now and return to it when we discuss named routes. To get clearer picture of this let's define a route for users with an id.

(defroute "/users/:id" {:as params}
  (js/console.log (str "User: " (:id params))))

In this example "/users/:id" is route, the route matcher, {:as params} is destruct, the destructured parameters extracted from the route matcher result, and the remaining (js/console.log ...) portion is body, the route action.

Before going in to more detail let's try to dispatch our route.

(secretary/dispatch! "/users/gf3")

With any luck, when we refresh the page and view the console we should see that User: gf3 has been logged somewhere.

Route matchers

By default the route matcher may either be a string or regular expression. String route matchers have special syntax which may be familiar to you if you've worked with Sinatra or Ruby on Rails. When secretary/dispatch! is called with a URI it attempts to find a route match and it's corresponding action. If the match is successful, parameters will be extracted from the URI. For string route matchers these will be contained in a map; for regular expressions a vector.

In the example above, the route matcher "/users/:id" successfully matched against "/users/gf3" and extracted {:id "gf3} as parameters. You can refer to the table below for more examples of route matchers and the parameters they return when matched successfully.

Route matcher URI Parameters
"/:x/:y" "/foo/bar" {:x "foo" :y "bar"}
"/:x/:x" "/foo/bar" {:x ["foo" "bar"]}
"/files/*.:format" "/files/x.zip" {:* "x" :format "zip"}
"*" "/any/thing" {:* "/any/thing"}
"/*/*" "/n/e/thing" {:* ["n" "e/thing"]}
"/*x/*y" "/n/e/thing" {:x "n" :y "e/thing"}
#"/[a-z]+/\d+" "/foo/123" ["/foo/123"]
#"/([a-z]+)/(\d+)" "/foo/123" ["foo" "123"]

Parameter destructuring

Now that we understand what happens during dispatch we can look at the destruct argument of defroute. This part is literally sugar around let. Basically whenever one of our route matches is successful and extracts parameters this is where we destructure them. Under the hood, for example with our users route, this looks something like the following.

(let [{:as params} {:id "gf3"}]
  ...)

Given this, it should be fairly easy to see that we could have have written

(defroute "/users/:id" {id :id}
  (js/console.log (str "User: " id)))

and seen the same result. With string route matchers we can go even further and write

(defroute "/users/:id" [id]
  (js/console.log (str "User: " id)))

which is essentially the same as saying {:keys [id]}.

For regular expression route matchers we can only use vectors for destructuring since they only ever return vectors.

(defroute #"/users/(\d+)" [id]
  (js/console.log (str "User: " id)))

Query parameters

If a URI contains a query string it will automatically be extracted to :query-params for string route matchers and to the last element for regular expression matchers.

(defroute "/users/:id" [id query-params]
  (js/console.log (str "User: " id))
  (js/console.log (pr-str query-params)))

(defroute #"/users/(\d+)" [id {:keys [query-params]}]
  (js/console.log (str "User: " id))
  (js/console.log (pr-str query-params)))

;; In both instances...
(secretary/dispatch! "/users/10?action=delete")
;; ... will log
;; User: 10
;; "{:action \"delete\"}"

Named routes

While route matching and dispatch is by itself useful, it is often necessary to have functions which take a map of parameters and return a URI. By passing an optional name to defroute Secretary will define this function for you.

(defroute users-path "/users" []
  (js/console.log "Users path"))

(defroute user-path "/users/:id" [id]
  (js/console.log (str "User " id "'s path"))

(users-path) ;; => "/users"
(user-path {:id 1}) ;; => "/users/1"

This also works with :query-params.

(user-path {:id 1 :query-params {:action "delete"}})
;; => "/users/1?action=delete"

If the browser you're targeting does not support HTML5 history you can call

(secretary/set-config! :prefix "#")

to prefix generated URIs with a "#".

(user-path {:id 1})
;; => "#/users/1"
This scheme doesn't comply with URI spec

Beware that using :prefix that way will make resulting URIs no longer compliant with standard URI syntax – the fragment must be the last part of the URI after the query). Indeed, the syntax of a URI is defined as:

scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]

secretary adds a # after the path so it makes the fragment hide the query. For instance, the following URL is comprehended in different ways by secretary and the spec:

https://www.example.com/path/of/app#path/inside/app?query=params&as=defined&by=secretary
  • the fragment is "path/inside/app?query=params&as=defined&by=secretary" for standard libraries, but is "path/inside/app" according to Secretary

  • the query is "" for standard libraries, but is "query=params&as=defined&by=secretary" according to Secretary

Available protocols

You can extend Secretary's protocols to your own data types and records if you need special functionality.

IRenderRoute

Most of the time the defaults will be good enough but on occasion you may need custom route rendering. To do this implement IRenderRoute for your type or record.

(defrecord User [id]
  secretary/IRenderRoute
  (render-route [_]
    (str "/users/" id))

  (render-route [this params]
    (str (secretary/render-route this) "?"
         (secretary/encode-query-params params))))

(secretary/render-route (User. 1))
;; => "/users/1"
(secretary/render-route (User. 1) {:action :delete})
;; => "/users/1?action=delete"

IRouteMatches

It is seldom you will ever need to create your own route matching implementation as the built in String and RegExp routes matchers should be fine for most applications. Still, if you have a suitable use case then this protocol is available. If your intention is to is to use it with defroute your implementation must return a map or vector.

Example with goog.History

(ns example
  (:require [secretary.core :as secretary :refer-macros [defroute]]
            [goog.events :as events])
  (:import [goog History]
           [goog.history EventType]))

(def application
  (js/document.getElementById "application"))

(defn set-html! [el content]
  (aset el "innerHTML" content))

(secretary/set-config! :prefix "#")

;; /#/
(defroute home-path "/" []
  (set-html! application "<h1>OMG! YOU'RE HOME!</h1>"))

;; /#/users
(defroute users-path "/users" []
  (set-html! application "<h1>USERS!</h1>"))

;; /#/users/:id
(defroute user-path "/users/:id" [id]
  (let [message (str "<h1>HELLO USER <small>" id "</small>!</h1>")]
    (set-html! application message)))

;; /#/777
(defroute jackpot-path "/777" []
  (set-html! application "<h1>YOU HIT THE JACKPOT!</h1>"))

;; Catch all
(defroute "*" []
  (set-html! application "<h1>LOL! YOU LOST!</h1>"))

;; Quick and dirty history configuration.
(doto (History.)
  (events/listen EventType.NAVIGATE #(secretary/dispatch! (.-token %)))
  (.setEnabled true))

Contributors

Committers

License

Distributed under the Eclipse Public License, the same as Clojure.

secretary's People

Contributors

anmonteiro avatar avfonarev avatar bbbates avatar borkdude avatar cloojure avatar danielsz avatar ddeaguiar avatar ddellacosta avatar dfuenzalida avatar dtulig avatar gf3 avatar jarohen avatar jntn avatar joelash avatar mfikes avatar noprompt avatar p-b-west avatar piotr-yuxuan avatar riwsky avatar rranelli avatar sgrove avatar slipset avatar the-kenny avatar wagjo 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  avatar  avatar  avatar  avatar  avatar

secretary's Issues

Dispatch! to adjust url hash

Following from goog.history example, if I do

(secretary/dispatch! "/777")

The dispatch doesn't adjust the url hash to #/777
Is there an option to have it adjust it?


For more context outside of the history example:

I am working with this bit of logic from Bruce Hauman's async todo's post:

(defn example2-loop [start-state]
  (let [ new-todo-click         (click-chan "a.new-todo" 
                                            :new-task)
         cancel-new-form-click  (click-chan "a.cancel-new-todo" 
                                            :cancel-new-form)
         input-chan             (async/merge [ new-todo-click 
                                               cancel-new-form-click])]
    (go
     (loop [state start-state]
       (render-templates state)
       (<! (get-next-message #{:new-task} input-chan))
       (render-templates (assoc state :mode :add-todo-form))
       (<! (get-next-message #{:cancel-new-form} input-chan))
       (recur (dissoc state :mode)))))

The program would ideally go through the following:

  • User clicks on an item
    1. A message is put on a channel that blocks a go loop like the one above
    2. Dispatch is fired for a route (/test/route)
    3. The blocking statement shoots an goog.xhrio request
    4. When the xhr request is done the loop gets unblocked

This way if another item is clicked during the request:

  • Url does not get adjusted
  • No new xhr request is initiated

The road-block currently is that dispatch doesn't adjust the hash which ends up preventing the user from being able to deep link.

The fix I tried was to use a elements with href="#/test/route" but that creates the problem where even if the loop is blocked, the user can still go to another url hash by clicking more links. Which can create a discrepancy between the data that eventually loads and what the hash is actually showing.

Currently my thinking is that this would be solved by having dispatch adjust the url hash to reflect where it has been dispatched to. Is there a better solution to this?

Hope that makes sense and thanks for reading through it!

Query params in named routes

Given this route definition:

(defroute exchange-link "/exchange" [z]
  (println "route params" z))

and this invokation:

(sec/dispatch! "/exchange?a=b")

I'm getting:

route params nil

instead of

route params {:query-params ... }
(defroute exchange-link "/exchange" {:keys [query-params]}
  (println "route params" query-params))

gives the same result.

dispatch! not dispatching

Using [org.clojure/clojurescript "0.0-2280"] the following works in [secretary "1.1.1"] but not in [secretary "1.2.0"]

(def history (Html5History.))
(.setUseFragment history false)

(defroute "test" [] 
  (log "TEST")
  (set-html! (js/document.getElementById "main-container") "TEST"))

(events/listen history EventType.NAVIGATE
  (fn [e] (secretary/dispatch! (.-token e))))

(.setEnabled history true)

Redirect with history to another page

I'm using goog.History as you suggested in the readme. I've been using (set! (.-href window.location) url) to redirect user (like ) to another page. But it always flashes the homepage first then go there. I also tried (.setToken (History.) "/add") but that only changes the address bar and not actually load the new page so it gives a blank page.

Is there a correct way of redirecting the browser please?

Check if a route exists v2.0.0

Hi,
what is the best way to check if a route is handled in version 2?
In the previous version there was a function called locate-route that was very helpful.
Thanks

Storing secretary named routes in a map.

I'm trying to store named routes in a map and use as part of a look-up, such as...

(navigate! :submit-page)

I have two routes (/ and /submit for now) and I'm currently trying:

(defroute sec (str route) []
      (log/info "Routing to" route)
      (launch-route! route))
    (swap! routes assoc-in [(route-to-keyword route)] sec)

This produces the following atom:

#<Atom: {:r #<function venue$core$add_view_BANG__$_sec(){
var argseq__17077__auto__ = ((((0) < arguments.length))?(new cljs.core.IndexedSeq(Array.prototype.slice.call(arguments,(0)),(0))):null);
return venue.core.sec.cljs$core$IFn$_invoke$arity$variadic(argseq__17077__auto__);
}>, :r-submit #<function venue$core$add_view_BANG__$_sec(){
var argseq__17077__auto__ = ((((0) < arguments.length))?(new cljs.core.IndexedSeq(Array.prototype.slice.call(arguments,(0)),(0))):null);
return venue.core.sec.cljs$core$IFn$_invoke$arity$variadic(argseq__17077__auto__);
}>}>

You'll notice that both values are identical. And sure enough...

venue.core> ((:r- @routes))
"#/submit" ;; we were expecting "#/"

Fair enough, the symbols are called the same thing. So I've also tried using a macro but the result was the same (gensym returns the same symbol venue$core$add_view_BANG__$_G__49633? What's going on here?)

(defmacro defroute!
  [route routes funcn]
  (let [sec-obj (gensym)]
    `(do
       (secretary/defroute ~sec-obj ~route []
         (~funcn ~route))
       (swap! ~routes assoc-in [(route-to-keyword ~route)] ~sec-obj))))

#<Atom: {:r- #<function venue$core$add_view_BANG__$_G__49633(){
var argseq__17077__auto__ = ((((0) < arguments.length))?(new cljs.core.IndexedSeq(Array.prototype.slice.call(arguments,(0)),(0))):null);
return venue.core.G__49633.cljs$core$IFn$_invoke$arity$variadic(argseq__17077__auto__);
}>, :r-submit #<function venue$core$add_view_BANG__$_G__49633(){
var argseq__17077__auto__ = ((((0) < arguments.length))?(new cljs.core.IndexedSeq(Array.prototype.slice.call(arguments,(0)),(0))):null);
return venue.core.G__49633.cljs$core$IFn$_invoke$arity$variadic(argseq__17077__auto__);
}>}>

Is there something about defroute that makes this impossible or is there actually a way to do this? Is it ClojureScript scuppering my plans? Thanks for your help

com.cemerick/clojurescript.test deprecated

Hello,

com.cemerick/clojurescript.test is deprecated. Is it worth while converting to doo and CLJS's built-in clojurescript.test suite?

In addition to being linked to an actively maintained project it would have some additional benefits like eliminating a SNAPSHOT dependency.

Is this something you'd be willing to entertain as a PR?

Kind regards,
Nathan

Default segment specification in named routes

I'd like to be able to specify a route similar to this:

(defroute my-path "/design/:filter-name/:id" [filter-name id]
  ; do something
  )

(my-path {:id 5}) ; currently yields "/design/:filter-name/5", but I'd like to be able to specify a default for filter-name

And have the generated named path be able to specify default values to the parameters, rather than a keyword being used. I'm not really sure how this could be accomplished.

This is pretty closely related to having trailing slashes being elided, too... these two use-cases seem to be common enough that a solution would be really great to have in secretary itself, perhaps.

Question: clicking on links does not trigger dispatch?

This is probably PEBCAK.
I have the following tiny application using secretary and reagent. Calls to secretary/dispatch! like the one at the bottom of the file update the application as expected. Clicking on the links, however, does not result in a call to dispatch.
Any pointers you can provide will be appreciated. Thanks in advance!

(ns askit-client.main
    (:require [reagent.core :as reagent]
              [secretary.core :as secretary :refer-macros [defroute]]))

(enable-console-print!)


;; Define reagent components
(defn home []
    [:div [:h1 "Welcome, friend!"]
        [:div [:a {:href "#/about"} "about us"]]])
(defn about []
    [:div "Now you know all about us!"
        [:div [:a {:href "#/"} "go home"]]])
(defn current-page [] [(@app-state :current-page)])




;; Set up application state
(def app-state (reagent/atom {}))

(defn put! [k v] (swap! app-state assoc k v))

(put! :current-page home)



;; Define secretary routes
(secretary/set-config! :prefix "#")
(defroute "/" []
    (println "time to go home")
    (put! :current-page home))
(defroute "/about" []
    (println "time to go about")
    (put! :current-page about))



(println "Let's mount this thing and go home!")
(reagent/render-component [current-page] (.getElementById js/document "app"))

(secretary/dispatch! "/about")

Suggested improvements to History example

From the secretary history example:

;; Quick and dirty history configuration.
(let [h (History.)]
  (goog.events/listen h EventType.NAVIGATE #(secretary/dispatch! (.-token %)))
  (doto h
    (.setEnabled true)))

In my experience, the first event from goog.History.Event has a target of "" by default. I think for many applications, it is useful to handle this differently. Maybe a few quick changes to the example would be in order?

Here is some sample code that I use:

(let [h (History.)
      f (fn [he] ;; goog.History.Event
          (.log js/console "navigate %o" (clj->js he))
          (let [token (.-token he)]
            (if (seq token)
              (secretary/dispatch! token)))))]
  (events/listen h EventType/NAVIGATE f)
  (doto h (.setEnabled true)))

Another way might be to test isNavigation. From history.Event:

isNavigation : boolean
True if the event was triggered by a browser action, such as forward or back, clicking on a link, editing the URL, or calling window.history.(go|back|forward). False if the token has been changed by a setToken or replaceToken call.

Support named routes

Allow defroute to optionally be passed a symbol which will both create the route and define a function. This function can then be used to create routes.

(defroute user-path "/users/:id" {:as params}
  ...)

(user-path {:id 1}) ;; => "/users/1"

defroute is undefined when compiled with sourcemap build settings

I'm trying to use secretary in conjunction with om and it works fine using the release cljsbuild settings, however the development settings don't work. I'm not sure if it's the source map that's messing it up or the js include in the index.html.

https://github.com/swannodette/om#using-it

Using the following dev build settings I get an error that defroute is undefined:

:cljsbuild {
  :builds [{:id "dev"
            :source-paths ["src"]
            :compiler {
              :output-to "main.js"
              :output-dir "out"
              :optimizations :none
              :source-map true}}]}
    <script src="main.js" type="text/javascript"></script>
    <script type="text/javascript">goog.require("main.core");</script>

Non-encoded URIs

It's probably a long shot but is there a way to use secretary without using encoded URIs. Like being able to dispatch to utf-8 without encoding first and named routes returning utf-8 URIs.

I'm not sure if there actually are any browsers that require encoded URIs but man are they ugly and awkward as permalinks.

version 1.2.2 has public/index.html, public/out/* etc.....

I'm actually wondering if this was intentional or not because 1.2.1 only had secretary/*

upgrading to it broke my app because my app serves the index.html dynamically from a template but I believe the ring.middleware.resource.wrap-resource is picking up the index.html from 1.2.2 instead of my handler.

I could probably work around this, but I suspect, 1.2.2 was probably not supposed to be packaged with all the public/out/* stuff

Add route globbing

This will require some extra plumbing but has obvious uses cases. For example

(defroute not-found "*" [] ...)

Matching routes with trailing slashes

If we define the following route:

(defroute "/foo" []
  (.log js/console "matched /foo"))

it will match calls to "/foo" but not "/foo/" (notice the trailing slash). This is something that is also reported in weavejester/compojure#68

I was able to find a workaround by defining my route using a regex, like the following:

(defroute #"/foo(/)*" []
  (.log js/console "matched /foo"))

but I don't quite like this solution.

Is this an actual issue or is secretary supposed to behave like this by design?

Is this defroute supposed to trigger and evaluate its contents?

I must be missing a fundamental understanding of how defroute works. I have this:

(.log js/console "running") 

(defroute "*" {:as r}
  (.log js/console "route: " r))

When the app loads, the "running" prints to the screen. I would expect that this route would also run regardless of which URI I visited on the server which is loading this page. But there is never any other logging that shows.

I also tried with more specific routes with wildcards but the same behavior, when I visit the URL, while the JS file itself clearly loads and runs, the defroutes aren't triggering in their bodies based on the URL they are supposed to match.

Stupid question, but updating the URL?

Sorry if I've missed this in the docs somewhere, however:

I've got things setup nicely with my routes and reagent for rendering. The secretary/dispatch! works great for navigating around, and my HTML5 navigation on the server is correctly loading the application index and providing the correct paths to secretary on deep links when the page loads.

My question is, how can I update the location in the URL bar on dispatch? Is this something I should be looking into in the Google Closure docs or have I misconfigured secretary somehow?

No params when route is matched after user presses the "back" button

Not sure if this is a secretary bug or me misunderstanding how the browser is supposed to work.

I have 2 routes:

(defroute "/a" {:as m}  (.log js/console m) (show-page-a))
(defroute "/b" {:as m}  (show-page-b))

If I load a route like /a?foo=bar I see those params logged as expected, if I follow a link to /b then press back button, I get an empty map logged even though the url is still /a?foo=bar.

I'm using accountant to call dispatch! when following links, so there isn't a full page reload at that point.

Make Route Dispatching a Little Less Imperative

Allow routes to be dispatched from (and optionally defined to?) different collections. This'll make things like testing and optional routes easier to work with.

E.g.:

(secretary/dispatch! "/users/gf3")
;; To ↓ 
(secretary/dispatch! some-defed-routes "/users/gf3")

Thanks for the suggestion, @sgrove.

ClojureScript dependency leaks into projects including secretary

org.clojure/clojurescript dependency leaks into projects including secretary. This is a problem when this project is also a regular clojure project as org.clojure/clojurescript ends up packaged in final uberjar.

A simple workaround is to use :exclusions [org.clojure/clojurescript].
Probably the proper way to handle this is to define org.clojure/clojurescript dependency in the dev profile.

No such namespace for goog.history.EventType

I am getting a WARNING: No such namespace: goog.history.EventType at line 59 src/cljs/core.cljs when I just copy and paste the basic history example.

Is it something on my end?

Dispatching on empty string

I think the following dispatch should go into the "catch all" route, but it is going to the "users" route:

(defroute "/users/:id" [id]
  (println "User" id))

(defroute "*" []
  (println "catch all"))

(secretary/dispatch! "") ; => prints "User nil"

I suppose the use-case here is to catch the default home route, but I don't see the ability here.

Restoring any-matches? functionality

There used to be any-matches? which is IMO necessary in the secretary API.
locate-route is a private function. Maybe expose it, or alias it with a name that indicates it can be used as a predicate. Thank you.

(defn- locate-route [route]
  (some
   (fn [[compiled-route action]]
     (when-let [params (route-matches compiled-route route)]
       [action (route-matches compiled-route route)]))
   @*routes*))

Circular Dependencies -- Suggestions?

Hi

I have an app which uses Secretary to define the routes for the system.

so routes.cljs has the route definitions.

routes.cljs references some controller namespaces in order to route people to the right controller when things change.

Additionally, controllers sometimes need to send people places.

routes -> controller -> routes

This is a circular dependency.
I believe there's no way around it since in my case the things which are accepting being routed-to also want to route people to other places.

In my earlier home-grown efforts I had split paths and routes into two files so that I could avoid this.

e.g.

routes -> paths  (to generate the link btwn URL paths and behaviors)
routes -> controllers (to shuttle control to a controller)
controllers -> paths (to generate paths to things)

Is there something like this in Secretary / has someone come up with any solutions other than "reorganize your code"?

I looked and there seemed to only be the "mother-macro" defroute, and I wasn't clear on how to split that apart.

This seems like such a common issue that I hope there is something simple...
like:

paths.cljs
(defpath article  "/article/:id")
;; Returns a (defn article-path which just generates a "string")
;; This can perform the machinations to generate URLs correctly

routes.cljs
(defroute (article-path :id)
   (do-whatever id))
;; So here, the article-path is handing back a stirng with the first parameter set to :id
;; e.g. /articles/:id 

Something along those lines.
Thanks!

Render route containing dash

To demonstrate the issue:

(defroute good "/:a:b/:c" [])
(good {:a 1 :b 2 :c 3}) => "/12/3"

(defroute bad "/:a-:b/:c" [])
(bad {:a 1 :b 2 :c 3}) => "/:a-2/3"

Is there any way to render "bad" as "/1-2/3"?

Undefine route?

What is the right way to abandon any/every route definition? I'm using secretary in component which I want to be able to start/stop clean.

Routes without prefix?

The readme mentions that "If the browser you're targeting does not support HTML5 history you can call (secretary/set-config! :prefix "#") to prefix generated URIs with a "#"."

It seems that should a call to set-config! be omitted, the :prefix will be set to an empty string. That leads me to believe that generated routes would have no kind of prefix in the url -- yet all of mine do by default.

Is the # prefix supposed to always be present by default, or is there a way to disable it?

Decode query params keys

Collection values are encoded into query params with brackets attached as a suffix. When parsing back the url query string, the keys are not decoded. As brackets in query keys tend to be percent encoded in several tools, parser should decode them.

Example of such query, github percent encodes brackets in the href of the following link automatically:
http://example.com/search?terms[]=foo&terms[]=bar

Wrong route is dispatched

In https://github.com/borkdude/sliders-cljs I have tried to incorporate [secretary "1.0.2"] and the example given on the README page of secretary, combined with Om.

Problem: when I browse directly to sliders-cljs/resources/public/index.html#/users I should see USERS!, but I'm seeing OMG! YOU'RE HOME!. See this screenshot:
screenshot 2014-03-16 08 57 32

When I change the uri to /777 I see the correct screen.
When I change the uri to /users I also see the correct screen. Upon refresh I'm seeing the OMG! YOU'RE HOME! screen again, with the same uri /users.

Accessing by URL does not trigger event in defroute

For example, when user clicks <a href="#/users/jim">Jim</a>, it will trigger
(defroute user-path "/users/:id" [id] ...). However, the event is not triggered when I directly type in a new window index.html#/users/jim.

In Emberjs, the state is associated with each URL, so bookmarks in the browser can reference the exact state which the user want to save.

No dispatch upon page load

First time a page loads there is no dispatch. For example my SPA has a certain component to display if they are on /login, the app determines that by a dispatch on secretary navigate. When you navigate directly to /login secretary fails to trigger the dispatch.

Login wall via middleware?

What's a recommended way to build a login wall to a Secretary routed app? Currently I'm doing this up top on my routes namespace.

(defn enforce-login []
  "Check authentication of user then redirect to login page if not"
  (when-not (re-seq #"/#/login$" (.-href js/window.location)) ;; not at login page
    (if-let [token (c/get-token)]
      (go-authenticate token)
      (.replace window.location "/#/login"))))

It sort of work but user can still jump into any route if they know the URL. I'm thinking I could wrap each route with a authorisation func first?

(defroute "/foo" [] (auth? (run-foo)))

That's basically a middleware.

Is there middleware for Secretary? If not, could you point me the right way to making one please?

defroute macro not found

I get the following warning while compiling my application:

WARNING: Referred var secretary.core/defroute does not exist at line 1 src-cljs/myproject/main.cljs

And running the app in browser yields:

Uncaught TypeError: Cannot read property 'call' of undefined

from a line:

(defroute "/" [] (render-view dashboard-view))

The namespace declaration is:

(ns myproject.main
  (:require [secretary.core :as secretary :include-macros true :refer [defroute]]))

And project configuration:

(defproject myproject "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [org.clojure/clojurescript "0.0-2371"]
                 [om "0.8.0-alpha1"]
                 [secretary "1.2.1"]
                 [prismatic/om-tools "0.3.6"]
                 [cljs-ajax "0.2.6"]]
  :plugins [
            [lein-cljsbuild "1.0.3"]]
  :cljsbuild {:builds [{:source-paths ["src-cljs"]
                        :compiler {:output-to "resources/public/js/app.js"
                                   :output-dir "resources/public/js/out"
                                   :source-map true
                                   :optimizations :none
                                   :pretty-print true}}]})

Using Java8 (update25) on OSX.

Encode/decode data structures in query params

This doesn't always come up but occasionally it's necessary.

(secretary/encode-query-params {:foo [1 2 3]})
;; => "foo[]=1&foo[]=2&foo[]3"
(secretary/decode-query-params "foo[]=1&foo[]=2&foo[]3")
;; => {:foo ["1" "2" "3"]}
(secretary/encode-query-params {:foo {:a 1 :b 2}})
;; => "foo[a]=1&foo[b]=2"
(secretary/decode-query-params "foo[a]=1&foo[b]=2")
;; => {:foo {:a "1" :b "2"}}

Expect a patch for this today.

Invalid URL handling

When invalid URL is entered by the user, such as:

http://host/some/path?param=val%%ue

encodeURIComponent and/or decodeURIComponent throw an unhandled exception and Secretary stops working. It would be nice if Secretary would be able to let app know such thing happened (it might be a bug in the app itself, invalid route, or something else) so that an app could do whatever it thinks makes sense.

So something like that maybe (this is just a out of the top of my head proposal):

(defroute :invalid-url [] ...)

Is such a thing possible with Secretary out of the box?

In our project we needed to handle it recently and so far I worked around it with this JS snippet that redirects to the "/error-path" with a proper error message. This is far from being ideal though.

(function() {
  var wrapWithCatch = function(fnName) {
    var orig = window[fnName];
    window[fnName] = function() {
      try {
        return orig.apply(null, arguments);
      } catch (e) {
        console.error("Error in " + fnName + ", redirecting.", e);
        window.location.hash = "/error-path";
      }
    };
  };

  wrapWithCatch("decodeURIComponent");
  wrapWithCatch("encodeURIComponent");
})();

Call a javascript function after loading a new page?

I'm using Foundation 5 for my CSS template and their javascript component asks for calling their JS function at the end of each page. Foundation is not seeing the new DOMs on routing so they aren't working. Is it possible to run a function after each page dispatch in secretary?

preventing navigation on clicking anchor tag with href

I just put together a minimal example recreating something I have been experienced. I was wondering if it were possible for clicking on an href not to retrigger the router. It seems that something in the secretary library is listening to the click event before my click handler has the chance to prevent it from doing so. Is it possible to keep the route's handler from being fired?

(ns minimal-example.core
    (:require [reagent.core :as reagent :refer [atom]]
              [reagent.session :as session]
              [secretary.core :as secretary :include-macros true]
              [accountant.core :as accountant]))

;; -------------------------
;; Views

(defn first-tab []
  [:div "FIRST TAB!"])

(defn second-tab []
  [:div "SECOND TAB!"])

(def selected-tab (atom nil))

(def tabs [{:key "one"
            :name "one"
            :cmp first-tab}
           {:key "two"
            :name "two"
            :cmp second-tab}])

(defn home-page []
  (reagent/create-class
   {:component-did-mount (fn []
                           (reset! selected-tab (-> tabs
                                                    first
                                                    :key)))
    :reagent-render (fn []
                      [:div
                       [:ul
                        (doall
                         (map (fn [item]
                                [:li
                                 [:a {:href "#"
                                      :on-click (fn [e]
                                                (reset! selected-tab (:key item))
                                                (.preventDefault e)
                                                (.preventPropogation e)
                                                false)}
                                  (:name item)]]
                                ) tabs))]
                       (doall
                        (map (fn [{:keys [key cmp]}]
                              (when (= key @selected-tab)
                                [cmp]))
                             tabs))])}))

(defn current-page []
  [:div [(session/get :current-page)]])

(secretary/defroute "/" []
  (session/put! :current-page #'home-page))

(defn mount-root []
  (reagent/render [current-page] (.getElementById js/document "app")))

(defn init! []
  (accountant/configure-navigation!
    {:nav-handler
     (fn [path]
       (secretary/dispatch! path))
     :path-exists?
     (fn [path]
       (secretary/locate-route path))})
  (accountant/dispatch-current!)
  (mount-root))

pre- / post- actions for post-dispatch

...continues discussion from IRC.

I propose to add a mechanism for performing pre-/during-/post- actions, after dispatch has happened (that is, wrapping the form in defroute).

Here is a set of proposed protocols and some usage patterns:

(defprotocol IPreRoutingAction
   (pre-routing-action [this] ...))

(defprotocol IRoutingAction
   (routing-action [this] ...))

(defprotocol IPostRoutingAction
   (routing-action [this] ...))

By default, you would continue to call defroute as now:

(defroute "/myroute/:id" [id]
  ;; ...do stuff...)

But, you could then also do something like this (mimicking Om usage patterns):

(defroute "/myroute/:id" [id]
  (reify
    IPreRoutingAction
    (pre-routing-action [this]
     ;; ...)
    IRoutingAction
    (routing-action [this]
     ;; ... the stuff from before ...)))

A more concrete use-case (and why I was proposing it in the first place):

(deftype Page []
  IPreRoutingAction
  (pre-routing-action [this page-args]
    (load-page! page-args))
  IRoutingAction
  (routing-action [this page-args]
    (load-header! (:name page-args)))

(defroute "/myroute/:id" [id]
  (Page. id {:name "foo"}))

This would eliminate a lot of boilerplate and satisfy the needs of many developers building complex one-page apps in CLJS (as we are).

Also proposed by noprompt: IWillNavigate/INavigate/IDidNavigate protocols. Should pre-dispatch protocols be allowed in arguments to defroute? Etc.

Okay, this is just the first proposal--let me know what you think.

Dispatch encoding problem

When dispatch method receives a URL like

page-visits/path=%2F60645%2Fzon-en-zee-strandhotel-westende%2F/day=2015-08-19%7C2015-08-19

;; this is the route
(defroute "/:module/*" {:as params}
  (move-to params))

the route is called with the following parameters

{:module "page-visits" 
  :* "path=/60645/zon-en-zee-strandhotel-westende//day=2015-08-19|2015-08-19"

Is this an expected behaviour ? Could it be because of this https://github.com/gf3/secretary/blob/fc8b53e77604e87db82a68aec0e4e98fe3ba6fb8/src/secretary/core.cljs#L250

Cheers,
Bahadir

Secretary doesn't comply to URL syntax specification

Hiya there,

First of all, thank you very much for that piece of free software :-) I've been using happily for a long time and it works well expect for this edge case which has been rather painful for me. To the best of my knowledge there is no issue about this so I fill this one just to reference this behaviour.

According to Wikipedia (and explained in a very circumlocutory way on https://url.spec.whatwg.org/) the syntax of a URL is :

scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]

However, when secretary is used without html5 routing the in-app route is stored in the fragment and then the query part is added. Reasons for this are obvious (legibility) but resulting URL respect no longer the syntax. Instead, they use this custom syntax:

scheme:[//[user:password@]host[:port]][/]path[#fragment][?query]

Using this custom syntax will cause Secretary and Standard libraries to firmly disagree on some painful edge cases, e.g. :

https://www.example.com/path/of/app#path/inside/app?query=params&as=defined&by=secretary
  • fragment: "path/inside/app?query=params&as=defined&by=secretary" for standard libraries but should be "path/inside/app" according to Secretary
  • query: "" but should be "query=params&as=defined&by=secretary" according to Secretary

Of course, a very naive regex will get us data we need:

(def hash-according-to-secretary-syntax
  (comp second (partial re-find #"\#([^?]*)"))

I just wonder if you could add tiny functions somewhere in a utility namespace to allow Secretary's users to access fragment and query as meant by the custom syntax used. If you agree with this but don't have much time to spare on this detail, just give me general directions about where / how put new code and I'll be happy to do that.

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.