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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c51f5ed8a155f29814d99f83a20387c3397a29a9
4
- data.tar.gz: 92a13dac1c2aadc0526a40c78f5d67097e3a833e
3
+ metadata.gz: 67edae73737f3b56016f637e17650cb6f2687fb9
4
+ data.tar.gz: 39ca6585568e5854ea8b2a1b043d0f5989d4384d
5
5
  SHA512:
6
- metadata.gz: 85f28b6d5b0851b2c7d6684418eeccb1baf690d1a71eddc1498f8ce99acea81c7864834f7b7861d6e55888a5b399ae59d19dcdd714b5edd29a5eb9c2122948f4
7
- data.tar.gz: 0d8cd7e44378440bffc0883520c0d35408cd9e52262d6f3284324172db8b0a457b27dbb41a8534b0787103f479b0413664133004878a3a9be045c7a802dd19fb
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
- update: (successCallback, failureCallback) ->
5
- activeAssets = extractTrackedAssets(@activeDocument)
6
- upstreamAssets = extractTrackedAssets(@upstreamDocument)
7
- {activeScripts, newScripts} = processScripts(activeAssets, upstreamAssets)
13
+ @newLinks = @upstreamAssets
14
+ .filter(attributeMatches('nodeName', 'LINK'))
15
+ .filter(noAttributeMatchesIn('href', @activeAssets))
8
16
 
9
- if hasScriptConflict(activeScripts, newScripts)
10
- return failureCallback()
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
- updateLinkTags(activeAssets, upstreamAssets)
13
- updateScriptTags(@activeDocument, newScripts, successCallback)
23
+ if anonymousActiveAssets.length != anonymousUpstreamAssets.length
24
+ return true
14
25
 
15
- updateLinkTags = (activeAssets, upstreamAssets) ->
16
- activeLinks = activeAssets.filter(filterForNodeType('LINK'))
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
- updateScriptTags = (activeDocument, newScripts, callback) ->
23
- asyncSeries(
24
- newScripts.map((scriptNode) -> insertScriptTask(activeDocument, scriptNode)),
25
- callback
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('[data-turbolinks-track]'))
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
- filterForNodeType = (nodeType) ->
32
- (node) -> node.nodeName == nodeType
56
+ noAttributeMatchesIn = (attribute, collection) ->
57
+ (node) ->
58
+ !collection.some((nodeFromCollection) -> node[attribute] == nodeFromCollection[attribute])
33
59
 
34
- hasScriptConflict = (activeScripts, newScripts) ->
35
- hasExistingScriptAssetName = (upstreamNode) ->
36
- activeScripts.some (activeNode) ->
37
- upstreamNode.dataset.turbolinksTrackScriptAs == activeNode.dataset.turbolinksTrackScriptAs
60
+ datasetMatches = (attribute, value) ->
61
+ (node) -> node.dataset[attribute] == value
38
62
 
39
- newScripts.some(hasExistingScriptAssetName)
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
- return (done) ->
53
- onScriptEvent = (event) ->
54
- triggerEvent('page:script-error', event) if event.type == 'error'
55
- newNode.removeEventListener('load', onScriptEvent)
56
- newNode.removeEventListener('error', onScriptEvent)
57
- done()
58
- newNode.addEventListener('load', onScriptEvent)
59
- newNode.addEventListener('error', onScriptEvent)
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('page:after-script-inserted', newNode)
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).update(
139
- onHeadUpdateSuccess = ->
140
- updateBody(upstreamDocument, xhr, options)
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?
@@ -1,3 +1,3 @@
1
1
  module TurboGraft
2
- VERSION = '0.3.1'
2
+ VERSION = '0.4.0'
3
3
  end
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.3.1
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-08-29 00:00:00.000000000 Z
16
+ date: 2016-09-02 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: coffee-rails