/
BadassAppRouter.coffee
170 lines (127 loc) · 5.48 KB
/
BadassAppRouter.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""
BadassAppRouter takes the philosophical position that only one router
can be used for one html page / single page app and it is the top most
app container object other than window.
A bad ass router knows how to construct all the pieces of a page using
@appConstructor. This includes all models, collections and views.
It also assumes that each route has an associated top level views
which becomes visible when you hit that route and all other become hidden
"""
module.exports = class BadassAppRouter extends Backbone.Router
# Set logging on /off - Very dandy during dev to see flow of routes
logging: off
# Set off if you don't want pushSate - great during testing!
pushState: on
# Almost always a good idea to override pushStateRoot in child router
pushStateRoot: '/'
# Good to turn off during testing & admin pages
enableExternalProviders: on
# takes pageData to pre-load data into the SPA without ajax calls
constructor: (pageData, callback) ->
if @logging
history = "pushState: #{@pushState}, root: #{@pushStateRoot}"
$log 'BadassRouter.ctor', history, @pageData, callback
@pageData = pageData if pageData?
@defaultFragment = @getDefaultFragment()
# $log 'preConstructorHook', @preConstructorHook
if @preConstructorHook? then @preConstructorHook()
app = @appConstructor pageData, callback
@app = if @app? then _.extend @app, app else app
if callback? then callback()
if @logging
$log 'BadassRouter.app', @app
@initialize = _.wrap @initialize, (fn, args) =>
Backbone.history.start pushState: @pushState, root: @pushStateRoot
if @pushState
@enablePushStateNavigate()
if @logging then $log "Router.init", args, @app
fn.call @, args
# $log 'defaultFragment', @defaultFragment
if @defaultFragment != currentFragment = Backbone.history.getFragment()
@navTo @defaultFragment
# wire up our 3rd party provider scripts to load only after our spa
# had been initialized and constructed
if @enableExternalProviders
@loadExternalProviders()
@wrapRoutes()
# Call backbone to correctly wire up & call Router.initialize
Backbone.Router::constructor.apply @, arguments
getDefaultFragment: ->
if @pushState
fragment = window.location.pathname
root = @pushStateRoot.replace(/\/$/, '')
if !fragment.indexOf(@pushStateRoot)
fragment = fragment.substr(root.length)
else
fragment = Backbone.history.getHash()
fragment = '' if fragment is '/'
fragment
# Construct all models/ collections & views for our SPA, the will be
# assigned onto window.router.app (in the constructor) having them all
# accessible means easy testing, stubbing of all objects that make up
# our SPA
appConstructor: (pageData, callback) ->
throw new Error 'override appConstructor in child router & build all models, collections & views then return single objects'
# automatically wrap route handlers
# adding div hide/show + console logging (if enabled)
wrapRoutes: ->
for route of @routes
# routes may contain /:id etc. - we only use
# the first fragment to map to the fn name
routeName = route.replace(/:id/g,'').split('/')[0]
# can map empty routes twice to do default route
if routeName != ''
@[routeName] = (->) if !@[routeName]? # default function allows us to be lazy in child routers
@[routeName].routeName = routeName
@[routeName] = _.wrap @[routeName], (fn, args) =>
if @logging then $log "Router.#{fn.routeName}"
$(".route").hide()
$("##{fn.routeName}").show()
window.scrollTo 0, 0
@routeMiddleware fn
fn.call @, args
# override routeMiddleware to execute custom code on every route
routeMiddleware: (routeFn) ->
# load external providers like google analytics, user-voice etc.
loadExternalProviders: ->
# prefer trigger to always be true
# pass false as second arg for false
navTo: (routeUrl, trigger) ->
trigger = true if !trigger?
# re-execute code
currentFragment = Backbone.history.getFragment()
if currentFragment == routeUrl && trigger
# $log 'currentFragment', currentFragment
@navigate '/#temp' # move to a temp route so we re-execute the code
@navigate routeUrl, { trigger: trigger }
# setup the ajax links for the html5 push navigation
enablePushStateNavigate: ->
$("body").on "click", "a", (e) =>
$a = $(e.currentTarget)
href = $a.attr 'href'
# $log 'href', href, href.length, $a, $a.hasClass 'routeIgnore'
if href.length && href.charAt(0) is '#'
# TODO: add ignore classes
# if ! $a.hasClass 'routeIgnore'
e.preventDefault()
@navTo href.replace('#','')
# short hand to handle injection of pageData for pre-loading models
setOrFetch: (model, data, opts) ->
if data?
model.set data
if opts? && opts.success? then opts.success(model,'set',opts)
model.trigger 'sync'
else
opts = {} if !opts?
opts.reset = true # backbone 1.0 so slow without this set
model.fetch opts
# short hand to handle injection of pageData for pre-loading collections
resetOrFetch: (collection, data, opts) ->
if data?
collection.reset data
if opts? && opts.success? then opts.success(collection,'reset',opts)
collection.trigger 'sync'
else
opts = {} if !opts?
opts.reset = true # backbone 1.0 so slow without this set
collection.fetch opts