turbograft 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +27 -0
- data/lib/assets/javascripts/turbograft/turbohead.coffee +88 -190
- data/lib/assets/javascripts/turbograft/turbolinks.coffee +4 -7
- data/lib/turbograft/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67edae73737f3b56016f637e17650cb6f2687fb9
|
4
|
+
data.tar.gz: 39ca6585568e5854ea8b2a1b043d0f5989d4384d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0bb4419a7514de8a92d9312b3a2389372752069ef8f214bf6ee31660aa3b084b459206cedb83a83528bdc11bf7f784b24dfb9ab7b978e3f8b4bd41e8f585a2af
|
7
|
+
data.tar.gz: 1f8b7574f1a0815f6aafd6d948219339fe033b7c89a6ae246cd19b92afba557c1f5dc805b567b2493438e1533367122020f2c0b4b91410d28b1c0fdc9fa07f9d
|
data/README.md
CHANGED
@@ -9,8 +9,12 @@ In botany, one can take parts of a tree and splice it onto another tree. The DO
|
|
9
9
|
|
10
10
|
## One render path
|
11
11
|
Turbograft gives you the ability to maintain a single, canonical render path for views. Your ERB views are the single definition of what will be rendered, without the worry of conditionally fetching snippets of HTML from elsewhere. This approach leads to clear, simplified code.
|
12
|
+
|
12
13
|
## Client-side performance
|
13
14
|
Partial page refreshes mean that CSS and JavaScript are only reloaded when you need them to be. Turbograft improves on the native, single-page application feel for the user while keeping these benefits inherited from Turbolinks.
|
15
|
+
|
16
|
+
Head asset tracking means that you can split your large CSS and Javascript bundles into smaller area bundles, decreasing your page weight and further increasing the responsiveness of your app.
|
17
|
+
|
14
18
|
## Simplicity
|
15
19
|
Turbograft was built with simplicity in mind. It intends to offer the smallest amount of overhead required on top of a traditional Rails stack to solve the problem of making a Rails app feel native to the browser.
|
16
20
|
|
@@ -74,6 +78,19 @@ The `data-tg-refresh-never` attribute will cause a node only appear once in the
|
|
74
78
|
### updatePushState
|
75
79
|
Defaults to `true`. When set to false it prevents `Page.refresh()` from updating the url in the browser.
|
76
80
|
|
81
|
+
## Head Asset Tracking
|
82
|
+
The Turbohead module allows you to track css and javascript assets in the head of the document and change them intelligently. This can be useful in large applications which want to lighten their
|
83
|
+
asset weight by splitting their script and style bundles by area.
|
84
|
+
|
85
|
+
### Link Tracking
|
86
|
+
When navigating, Turbograft will perform a full diff of `<link>` tags with `data-turbolinks-track` between the upstream and currently active `<head>`. It will attempt to perform the minimum number of DOM manipulations
|
87
|
+
to move from the current set to the upstream one. `<link>` tags will always be removed if they are not present in the upstream document, and order will be maintained.
|
88
|
+
|
89
|
+
### Script Tracking
|
90
|
+
When a `<script>` tag with `data-turbolinks-track` and a unique `src` is encountered in a response document Turbograft will insert it into the active DOM and force it to execute. Unlike links, scripts from previous pages are *not* removed once added.
|
91
|
+
|
92
|
+
If marked with a `data-turbolinks-track-as` attribute, scripts will additionally have their `track-as` values compared. If a script with a different `src` but the same `data-turbolinks-track-as` value is found upstream, turbograft will force a full page refresh. This prevents potential multiple executions of a script bundle when a new version of your app is shipped.
|
93
|
+
|
77
94
|
## data-tg-remote
|
78
95
|
|
79
96
|
The `data-tg-remote` option allows you to query methods on or submit forms to different endpoints, and gives partial page replacement on specified refresh keys depending on the response status.
|
@@ -160,6 +177,16 @@ and
|
|
160
177
|
|
161
178
|
The `data-tg-remote-noserialize` is useful in scenarios where a whole section of the page should be editable, i.e. not `disabled`, but should only conditionally be submitted to the server.
|
162
179
|
|
180
|
+
## Head Asset Tracking
|
181
|
+
### NOTE: This functionality is experimental, has changed significantly since 0.3.0, and may change again in the future before 1.0
|
182
|
+
The Turbohead module allows you to track css and javascript assets in the head of the document and change them intelligently. This can be useful in large applications which want to lighten their asset weight by splitting their script and style bundles by area.
|
183
|
+
|
184
|
+
When a `<script>` or `<link>` tag with a unique name in it's `data-turbolinks-track` is encountered in a response document Turbograft will insert it into the active DOM and, if it's a script, force it to execute.
|
185
|
+
|
186
|
+
If an asset with a different `src`/`href` but the same `data-turbolinks-track` value is found upstream, turbograft will force a full page refresh. This prevents potential multiple executions of a script bundle when a new version of your app is shipped.
|
187
|
+
|
188
|
+
As of version `0.4.0`, this functionality has been made backwards compatible. If `data-turbolinks-track="true"` head assets are present, turbograft will cause a full page refresh when the set is changed in any way.
|
189
|
+
|
163
190
|
## Example App
|
164
191
|
|
165
192
|
There is an example app that you can boot to play with TurboGraft. Open the console and network inspector and see it in action! This same app is also used in the TurboGraft browser testing suite.
|
@@ -1,42 +1,88 @@
|
|
1
|
+
TRACKED_ASSET_SELECTOR = '[data-turbolinks-track]'
|
2
|
+
TRACKED_ATTRIBUTE_NAME = 'turbolinksTrack'
|
3
|
+
ANONYMOUS_TRACK_VALUE = 'true'
|
4
|
+
|
1
5
|
class window.TurboHead
|
2
6
|
constructor: (@activeDocument, @upstreamDocument) ->
|
7
|
+
@activeAssets = extractTrackedAssets(@activeDocument)
|
8
|
+
@upstreamAssets = extractTrackedAssets(@upstreamDocument)
|
9
|
+
@newScripts = @upstreamAssets
|
10
|
+
.filter(attributeMatches('nodeName', 'SCRIPT'))
|
11
|
+
.filter(noAttributeMatchesIn('src', @activeAssets))
|
3
12
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
{activeScripts, newScripts} = processScripts(activeAssets, upstreamAssets)
|
13
|
+
@newLinks = @upstreamAssets
|
14
|
+
.filter(attributeMatches('nodeName', 'LINK'))
|
15
|
+
.filter(noAttributeMatchesIn('href', @activeAssets))
|
8
16
|
|
9
|
-
|
10
|
-
|
17
|
+
hasChangedAnonymousAssets: () ->
|
18
|
+
anonymousUpstreamAssets = @upstreamAssets
|
19
|
+
.filter(datasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE))
|
20
|
+
anonymousActiveAssets = @activeAssets
|
21
|
+
.filter(datasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE))
|
11
22
|
|
12
|
-
|
13
|
-
|
23
|
+
if anonymousActiveAssets.length != anonymousUpstreamAssets.length
|
24
|
+
return true
|
14
25
|
|
15
|
-
|
16
|
-
|
17
|
-
upstreamLinks = upstreamAssets.filter(filterForNodeType('LINK'))
|
18
|
-
remainingActiveLinks = removeStaleLinks(activeLinks, upstreamLinks)
|
19
|
-
reorderedActiveLinks = reorderActiveLinks(remainingActiveLinks, upstreamLinks)
|
20
|
-
insertNewLinks(reorderedActiveLinks, upstreamLinks)
|
26
|
+
noMatchingSrc = noAttributeMatchesIn('src', anonymousUpstreamAssets)
|
27
|
+
noMatchingHref = noAttributeMatchesIn('href', anonymousUpstreamAssets)
|
21
28
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
)
|
29
|
+
anonymousActiveAssets.some((node) ->
|
30
|
+
noMatchingSrc(node) || noMatchingHref(node)
|
31
|
+
)
|
32
|
+
|
33
|
+
hasNamedAssetConflicts: () ->
|
34
|
+
@newScripts
|
35
|
+
.concat(@newLinks)
|
36
|
+
.filter(noDatasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE))
|
37
|
+
.some(datasetMatchesIn(TRACKED_ATTRIBUTE_NAME, @activeAssets))
|
38
|
+
|
39
|
+
hasAssetConflicts: () ->
|
40
|
+
@hasNamedAssetConflicts() || @hasChangedAnonymousAssets()
|
41
|
+
|
42
|
+
insertNewAssets: (callback) ->
|
43
|
+
updateLinkTags(@activeDocument, @newLinks)
|
44
|
+
updateScriptTags(@activeDocument, @newScripts, callback)
|
27
45
|
|
28
46
|
extractTrackedAssets = (doc) ->
|
29
|
-
[].slice.call(doc.querySelectorAll(
|
47
|
+
[].slice.call(doc.querySelectorAll(TRACKED_ASSET_SELECTOR))
|
48
|
+
|
49
|
+
attributeMatches = (attribute, value) ->
|
50
|
+
(node) -> node[attribute] == value
|
51
|
+
|
52
|
+
attributeMatchesIn = (attribute, collection) ->
|
53
|
+
(node) ->
|
54
|
+
collection.some((nodeFromCollection) -> node[attribute] == nodeFromCollection[attribute])
|
30
55
|
|
31
|
-
|
32
|
-
(node) ->
|
56
|
+
noAttributeMatchesIn = (attribute, collection) ->
|
57
|
+
(node) ->
|
58
|
+
!collection.some((nodeFromCollection) -> node[attribute] == nodeFromCollection[attribute])
|
33
59
|
|
34
|
-
|
35
|
-
|
36
|
-
activeScripts.some (activeNode) ->
|
37
|
-
upstreamNode.dataset.turbolinksTrackScriptAs == activeNode.dataset.turbolinksTrackScriptAs
|
60
|
+
datasetMatches = (attribute, value) ->
|
61
|
+
(node) -> node.dataset[attribute] == value
|
38
62
|
|
39
|
-
|
63
|
+
noDatasetMatches = (attribute, value) ->
|
64
|
+
(node) -> node.dataset[attribute] != value
|
65
|
+
|
66
|
+
datasetMatchesIn = (attribute, collection) ->
|
67
|
+
(node) ->
|
68
|
+
value = node.dataset[attribute]
|
69
|
+
collection.some(datasetMatches(attribute, value))
|
70
|
+
|
71
|
+
noDatasetMatchesIn = (attribute, collection) ->
|
72
|
+
(node) ->
|
73
|
+
value = node.dataset[attribute]
|
74
|
+
!collection.some(datasetMatches(attribute, value))
|
75
|
+
|
76
|
+
updateLinkTags = (activeDocument, newLinks) ->
|
77
|
+
# style tag load events don't work in all browsers
|
78
|
+
# as such we just hope they load ¯\_(ツ)_/¯
|
79
|
+
newLinks.forEach((linkNode) -> insertLinkTask(activeDocument, linkNode)())
|
80
|
+
|
81
|
+
updateScriptTags = (activeDocument, newScripts, callback) ->
|
82
|
+
asyncSeries(
|
83
|
+
newScripts.map((scriptNode) -> insertScriptTask(activeDocument, scriptNode)),
|
84
|
+
callback
|
85
|
+
)
|
40
86
|
|
41
87
|
asyncSeries = (tasks, callback) ->
|
42
88
|
return callback() if tasks.length == 0
|
@@ -48,167 +94,19 @@ insertScriptTask = (activeDocument, scriptNode) ->
|
|
48
94
|
newNode = activeDocument.createElement('SCRIPT')
|
49
95
|
newNode.setAttribute(attr.name, attr.value) for attr in scriptNode.attributes
|
50
96
|
newNode.appendChild(activeDocument.createTextNode(scriptNode.innerHTML))
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
97
|
+
insertAssetTask(activeDocument, newNode, 'script')
|
98
|
+
|
99
|
+
insertLinkTask = (activeDocument, node) ->
|
100
|
+
insertAssetTask(activeDocument, node.cloneNode(), 'link')
|
101
|
+
|
102
|
+
insertAssetTask = (activeDocument, newNode, name) ->
|
103
|
+
(done) ->
|
104
|
+
onAssetEvent = (event) ->
|
105
|
+
triggerEvent("page:#{name}-error", event) if event.type == 'error'
|
106
|
+
newNode.removeEventListener('load', onAssetEvent)
|
107
|
+
newNode.removeEventListener('error', onAssetEvent)
|
108
|
+
done() if typeof done == 'function'
|
109
|
+
newNode.addEventListener('load', onAssetEvent)
|
110
|
+
newNode.addEventListener('error', onAssetEvent)
|
60
111
|
activeDocument.head.appendChild(newNode)
|
61
|
-
triggerEvent(
|
62
|
-
|
63
|
-
processScripts = (activeAssets, upstreamAssets) ->
|
64
|
-
activeScripts = activeAssets.filter(filterForNodeType('SCRIPT'))
|
65
|
-
upstreamScripts = upstreamAssets.filter(filterForNodeType('SCRIPT'))
|
66
|
-
hasNewSrc = (upstreamNode) ->
|
67
|
-
activeScripts.every (activeNode) ->
|
68
|
-
upstreamNode.src != activeNode.src
|
69
|
-
|
70
|
-
newScripts = upstreamScripts.filter(hasNewSrc)
|
71
|
-
|
72
|
-
{activeScripts, newScripts}
|
73
|
-
|
74
|
-
removeStaleLinks = (activeLinks, upstreamLinks) ->
|
75
|
-
isStaleLink = (link) ->
|
76
|
-
upstreamLinks.every (upstreamLink) ->
|
77
|
-
upstreamLink.href != link.href
|
78
|
-
|
79
|
-
staleLinks = activeLinks.filter(isStaleLink)
|
80
|
-
|
81
|
-
for staleLink in staleLinks
|
82
|
-
removedLink = document.head.removeChild(staleLink)
|
83
|
-
triggerEvent('page:after-link-removed', removedLink)
|
84
|
-
|
85
|
-
activeLinks.filter((link) -> !isStaleLink(link))
|
86
|
-
|
87
|
-
reorderAlreadyExists = (link1, link2, reorders) ->
|
88
|
-
reorders.some (reorderPair) ->
|
89
|
-
link1 in reorderPair && link2 in reorderPair
|
90
|
-
|
91
|
-
generateReorderGraph = (activeLinks, upstreamLinks) ->
|
92
|
-
reorders = []
|
93
|
-
for activeLink1 in activeLinks
|
94
|
-
for activeLink2 in activeLinks
|
95
|
-
continue if activeLink1.href == activeLink2.href
|
96
|
-
continue if reorderAlreadyExists(activeLink1, activeLink2, reorders)
|
97
|
-
|
98
|
-
upstreamLink1 = upstreamLinks.filter((link) -> link.href == activeLink1.href)[0]
|
99
|
-
upstreamLink2 = upstreamLinks.filter((link) -> link.href == activeLink2.href)[0]
|
100
|
-
|
101
|
-
orderHasChanged =
|
102
|
-
(activeLinks.indexOf(activeLink1) < activeLinks.indexOf(activeLink2)) !=
|
103
|
-
(upstreamLinks.indexOf(upstreamLink1) < upstreamLinks.indexOf(upstreamLink2))
|
104
|
-
|
105
|
-
reorders.push([activeLink1, activeLink2]) if orderHasChanged
|
106
|
-
reorders
|
107
|
-
|
108
|
-
nextMove = (activeLinks, reorders) ->
|
109
|
-
changesAssociatedTo = (link) ->
|
110
|
-
reorders.filter (reorderPair) ->
|
111
|
-
link in reorderPair
|
112
|
-
|
113
|
-
linksSortedByMovePriority = activeLinks
|
114
|
-
.slice()
|
115
|
-
.sort (link1, link2) ->
|
116
|
-
changesAssociatedTo(link2).length - changesAssociatedTo(link1).length
|
117
|
-
|
118
|
-
linkToMove = linksSortedByMovePriority[0]
|
119
|
-
|
120
|
-
linksToPassBy = changesAssociatedTo(linkToMove).map (reorderPair) ->
|
121
|
-
(reorderPair.filter (link) -> link.href != linkToMove.href)[0]
|
122
|
-
|
123
|
-
{linkToMove, linksToPassBy}
|
124
|
-
|
125
|
-
reorderActiveLinks = (activeLinks, upstreamLinks) ->
|
126
|
-
activeLinksCopy = activeLinks.slice()
|
127
|
-
pendingReorders = generateReorderGraph(activeLinksCopy, upstreamLinks)
|
128
|
-
|
129
|
-
removeReorder = (link1, link2) ->
|
130
|
-
reorderToRemove = (pendingReorders.filter (reorderPair) ->
|
131
|
-
link1 in reorderPair && link2 in reorderPair)[0]
|
132
|
-
indexToRemove = pendingReorders.indexOf(reorderToRemove)
|
133
|
-
pendingReorders.splice(indexToRemove, 1)
|
134
|
-
|
135
|
-
addNewReorder = (link1, link2) ->
|
136
|
-
pendingReorders.push [link1, link2]
|
137
|
-
|
138
|
-
markReorderAsFinished = (linkToMove, linkToPass, remainingLinksToPass) ->
|
139
|
-
removeReorder(linkToMove, linkToPass)
|
140
|
-
removalIndex = remainingLinksToPass.indexOf(linkToPass)
|
141
|
-
remainingLinksToPass.splice(removalIndex, 1)
|
142
|
-
|
143
|
-
removeLink = (linkToRemove, indexOfLink) ->
|
144
|
-
removedLink = document.head.removeChild(linkToRemove)
|
145
|
-
triggerEvent('page:after-link-removed', removedLink)
|
146
|
-
activeLinksCopy.splice(indexOfLink, 1)
|
147
|
-
|
148
|
-
performMove = (linkToMove, linksToPassBy) ->
|
149
|
-
moveDirection = if activeLinksCopy.indexOf(linkToMove) > activeLinksCopy.indexOf(linksToPassBy[0]) then 'UP' else 'DOWN'
|
150
|
-
startIndex = activeLinksCopy.indexOf(linkToMove)
|
151
|
-
|
152
|
-
switch moveDirection
|
153
|
-
when 'UP'
|
154
|
-
for i in [(startIndex - 1)..0]
|
155
|
-
currentLink = activeLinksCopy[i]
|
156
|
-
if currentLink in linksToPassBy
|
157
|
-
markReorderAsFinished(linkToMove, currentLink, linksToPassBy)
|
158
|
-
|
159
|
-
if linksToPassBy.length == 0
|
160
|
-
removeLink(linkToMove, startIndex)
|
161
|
-
|
162
|
-
document.head.insertBefore(linkToMove, activeLinksCopy[i])
|
163
|
-
activeLinksCopy.splice(i, 0, linkToMove)
|
164
|
-
triggerEvent('page:after-link-inserted', linkToMove)
|
165
|
-
return
|
166
|
-
else
|
167
|
-
addNewReorder(linkToMove, currentLink, pendingReorders)
|
168
|
-
when 'DOWN'
|
169
|
-
for i in [(startIndex + 1)...activeLinksCopy.length]
|
170
|
-
currentLink = activeLinksCopy[i]
|
171
|
-
if currentLink in linksToPassBy
|
172
|
-
markReorderAsFinished(linkToMove, currentLink, linksToPassBy)
|
173
|
-
|
174
|
-
if linksToPassBy.length == 0
|
175
|
-
removeLink(linkToMove, startIndex)
|
176
|
-
|
177
|
-
targetIndex = i - 1
|
178
|
-
if targetIndex == activeLinksCopy.length - 1
|
179
|
-
document.head.appendChild(linkToMove)
|
180
|
-
activeLinksCopy.push(linkToMove)
|
181
|
-
else
|
182
|
-
document.head.insertBefore(linkToMove, activeLinksCopy[targetIndex + 1])
|
183
|
-
activeLinksCopy.splice(targetIndex + 1, 0, linkToMove)
|
184
|
-
triggerEvent('page:after-link-inserted', linkToMove)
|
185
|
-
return
|
186
|
-
else
|
187
|
-
addNewReorder(linkToMove, currentLink, pendingReorders)
|
188
|
-
|
189
|
-
while pendingReorders.length > 0
|
190
|
-
{linkToMove, linksToPassBy} = nextMove(activeLinksCopy, pendingReorders)
|
191
|
-
performMove(linkToMove, linksToPassBy)
|
192
|
-
|
193
|
-
activeLinksCopy
|
194
|
-
|
195
|
-
insertNewLinks = (activeLinks, upstreamLinks) ->
|
196
|
-
isNewLink = (link) ->
|
197
|
-
activeLinks.every (activeLink) ->
|
198
|
-
activeLink.href != link.href
|
199
|
-
|
200
|
-
upstreamLinks
|
201
|
-
.filter(isNewLink)
|
202
|
-
.reverse() # This is because we can't insert before a sibling that hasn't been inserted yet.
|
203
|
-
.forEach (newUpstreamLink) ->
|
204
|
-
index = upstreamLinks.indexOf(newUpstreamLink)
|
205
|
-
newActiveLink = newUpstreamLink.cloneNode()
|
206
|
-
if index == upstreamLinks.length - 1
|
207
|
-
document.head.appendChild(newActiveLink)
|
208
|
-
activeLinks.push(newActiveLink)
|
209
|
-
else
|
210
|
-
targetIndex = activeLinks.indexOf((activeLinks.filter (link) ->
|
211
|
-
link.href == upstreamLinks[index + 1].href)[0])
|
212
|
-
document.head.insertBefore(newActiveLink, activeLinks[targetIndex])
|
213
|
-
activeLinks.splice(targetIndex, 0, newActiveLink)
|
214
|
-
triggerEvent('page:after-link-inserted', newActiveLink)
|
112
|
+
triggerEvent("page:after-#{name}-inserted", newNode)
|
@@ -135,13 +135,10 @@ class window.Turbolinks
|
|
135
135
|
if options.partialReplace
|
136
136
|
updateBody(upstreamDocument, xhr, options)
|
137
137
|
else
|
138
|
-
new TurboHead(document, upstreamDocument)
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
onHeadUpdateError = ->
|
143
|
-
Turbolinks.fullPageNavigate(url.absolute)
|
144
|
-
)
|
138
|
+
turbohead = new TurboHead(document, upstreamDocument)
|
139
|
+
if turbohead.hasAssetConflicts()
|
140
|
+
return Turbolinks.fullPageNavigate(url.absolute)
|
141
|
+
turbohead.insertNewAssets(-> updateBody(upstreamDocument, xhr, options))
|
145
142
|
else
|
146
143
|
triggerEvent 'page:error', xhr
|
147
144
|
Turbolinks.fullPageNavigate(url.absolute) if url?
|
data/lib/turbograft/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbograft
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kristian Plettenberg-Dussault
|
@@ -13,7 +13,7 @@ authors:
|
|
13
13
|
autorequire:
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
|
-
date: 2016-
|
16
|
+
date: 2016-09-02 00:00:00.000000000 Z
|
17
17
|
dependencies:
|
18
18
|
- !ruby/object:Gem::Dependency
|
19
19
|
name: coffee-rails
|