Skip to content
This repository has been archived by the owner on Jul 28, 2018. It is now read-only.

Commit

Permalink
Merge pull request #621 from anarchocurious/append_and_prepend_support
Browse files Browse the repository at this point in the history
Adds support for appending and prepending on partial page replacement
  • Loading branch information
Thibaut committed Nov 10, 2015
2 parents 90d4132 + f1aca2f commit 75115a3
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 36 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,23 @@
## Turbolinks (master)

* Turbolinks supports `append` and `prepend` options.

```coffeescript
# Specifying the `append` or `prepend` option to `Turbolinks.replace` and `Turbolinks.visit` causes the children of the matching nodes in the new document to be appended/prepended to the corresponding node in the existing document.

Turbolinks.visit(url, prepend: ['change'])
Turbolinks.replace(url, append: ['change'])
```

```ruby
# In Ruby, the target nodes' ids are passed via `append` or `prepend`.

render render_options, append: 'comment-list'
redirect_to path, prepend: 'comment-list'
```

*Alec Larsen*

* `Turbolinks.visit` and `Turbolinks.replace` accept a `title` option.

*Guillaume Malette*
Expand Down
11 changes: 10 additions & 1 deletion README.md
Expand Up @@ -327,10 +327,17 @@ Turbolinks.replace(html, options);

**Server-side partial replacement**

Partial replacement decisions can also be made server-side by using `redirect_to` or `render` with `change`, `keep`, or `flush` options.
Partial replacement decisions can also be made server-side by using `redirect_to` or `render` with `change`, `append`, `prepend`, `keep`, or `flush` options.

```ruby
class CommentsController < ActionController::Base
def index
@comments = Comment.page(params[:page]).per(25)

# Turbolinks appends the nodes in `comment_list`; useful for infinate scrolling
render :index, append: ['comment_list']
end

def create
@comment = Comment.new(comment_params)

Expand Down Expand Up @@ -417,6 +424,8 @@ Function | Arguments | Notes
Option | Type | Notes
----------------- | --------------------- | -----
`change` | `Array` | Replace only the nodes with the given ids.
`append` | `Array` | Append the children of nodes with the given ids.
`prepend` | `Array` | Prepend the children of nodes with the given ids.
`keep` | `Array` | Replace the body but keep the nodes with the given ids.
`flush` | `Boolean` | Replace the body, including `data-turbolinks-permanent` nodes.
`title` | `Boolean` or `String` | If `false`, don't update the `document` title. If a string, set the value as title.
Expand Down
53 changes: 40 additions & 13 deletions lib/assets/javascripts/turbolinks.coffee
Expand Up @@ -24,6 +24,9 @@ EVENTS =
BEFORE_UNLOAD: 'page:before-unload'
AFTER_REMOVE: 'page:after-remove'

isPartialReplacement = (options) ->
options.change or options.append or options.prepend

fetch = (url, options = {}) ->
url = new ComponentUrl url

Expand All @@ -33,21 +36,21 @@ fetch = (url, options = {}) ->
document.location.href = url.absolute
return

if options.change or options.keep
if isPartialReplacement(options) or options.keep
removeCurrentPageFromCache()
else
cacheCurrentPage()

rememberReferer()
progressBar?.start(delay: progressBarDelay)

if transitionCacheEnabled and !options.change and cachedPage = transitionCacheFor(url.absolute)
if transitionCacheEnabled and !isPartialReplacement(options) and cachedPage = transitionCacheFor(url.absolute)
reflectNewUrl(url)
fetchHistory cachedPage
options.showProgressBar = false
options.scroll = false
else
options.scroll ?= false if options.change and !url.hash
options.scroll ?= false if isPartialReplacement(options) and !url.hash

fetchReplacement url, options

Expand Down Expand Up @@ -85,7 +88,7 @@ fetchReplacement = (url, options) ->
if options.showProgressBar
progressBar?.done()
updateScrollPosition(options.scroll)
triggerEvent (if options.change then EVENTS.PARTIAL_LOAD else EVENTS.LOAD), loadedNodes
triggerEvent (if isPartialReplacement(options) then EVENTS.PARTIAL_LOAD else EVENTS.LOAD), loadedNodes
constrainPageCacheTo(cacheSize)
else
progressBar?.done()
Expand Down Expand Up @@ -142,24 +145,34 @@ constrainPageCacheTo = (limit) ->

replace = (html, options = {}) ->
loadedNodes = changePage extractTitleAndBody(createDocument(html))..., options
triggerEvent (if options.change then EVENTS.PARTIAL_LOAD else EVENTS.LOAD), loadedNodes
triggerEvent (if isPartialReplacement(options) then EVENTS.PARTIAL_LOAD else EVENTS.LOAD), loadedNodes

changePage = (title, body, csrfToken, options) ->
title = options.title ? title
currentBody = document.body

if options.change
nodesToChange = findNodes(currentBody, '[data-turbolinks-temporary]')
nodesToChange.push(findNodesMatchingKeys(currentBody, options.change)...)
if isPartialReplacement(options)
nodesToAppend = findNodesMatchingKeys(currentBody, options.append) if options.append
nodesToPrepend = findNodesMatchingKeys(currentBody, options.prepend) if options.prepend

nodesToReplace = findNodes(currentBody, '[data-turbolinks-temporary]')
nodesToReplace = nodesToReplace.concat findNodesMatchingKeys(currentBody, options.change) if options.change

nodesToChange = [].concat(nodesToAppend || [], nodesToPrepend || [], nodesToReplace || [])
nodesToChange = removeDuplicates(nodesToChange)
else
nodesToChange = [currentBody]

triggerEvent EVENTS.BEFORE_UNLOAD, nodesToChange
document.title = title if title isnt false

if options.change
changedNodes = swapNodes(body, nodesToChange, keep: false)
if isPartialReplacement(options)
appendedNodes = swapNodes(body, nodesToAppend, keep: false, append: true) if nodesToAppend
prependedNodes = swapNodes(body, nodesToPrepend, keep: false, prepend: true) if nodesToPrepend
replacedNodes = swapNodes(body, nodesToReplace, keep: false) if nodesToReplace

changedNodes = [].concat(appendedNodes || [], prependedNodes || [], replacedNodes || [])
changedNodes = removeDuplicates(changedNodes)
else
unless options.flush
nodesToKeep = findNodes(currentBody, '[data-turbolinks-permanent]')
Expand Down Expand Up @@ -200,9 +213,23 @@ swapNodes = (targetBody, existingNodes, options) ->
existingNode = targetNode.ownerDocument.adoptNode(existingNode)
targetNode.parentNode.replaceChild(existingNode, targetNode)
else
existingNode.parentNode.replaceChild(targetNode, existingNode)
onNodeRemoved(existingNode)
changedNodes.push(targetNode)
if options.append or options.prepend
firstChild = existingNode.firstChild

childNodes = Array::slice.call targetNode.childNodes, 0 # a copy has to be made since the list is mutated while processing

for childNode in childNodes
if !firstChild or options.append # when the parent node is empty, there is no difference between appending and prepending
existingNode.appendChild(childNode)
else if options.prepend
existingNode.insertBefore(childNode, firstChild)

changedNodes.push(existingNode)
else
existingNode.parentNode.replaceChild(targetNode, existingNode)
onNodeRemoved(existingNode)
changedNodes.push(targetNode)

return changedNodes

onNodeRemoved = (node) ->
Expand Down
28 changes: 19 additions & 9 deletions lib/turbolinks/redirection.rb
Expand Up @@ -2,6 +2,7 @@ module Turbolinks
# Provides a means of using Turbolinks to perform renders and redirects.
# The server will respond with a JavaScript call to Turbolinks.visit/replace().
module Redirection
MUTATION_MODES = [:change, :append, :prepend].freeze

def redirect_to(url = {}, response_status = {})
turbolinks, options = _extract_turbolinks_options!(response_status)
Expand Down Expand Up @@ -49,19 +50,28 @@ def redirect_via_turbolinks_to(url = {}, response_status = {})
private
def _extract_turbolinks_options!(options)
turbolinks = options.delete(:turbolinks)
options = options.extract!(:keep, :change, :flush).delete_if { |_, value| value.nil? }
raise ArgumentError, "cannot combine :keep, :change and :flush options" if options.size > 1
options = options.extract!(:keep, :change, :append, :prepend, :flush).delete_if { |_, value| value.nil? }

raise ArgumentError, "cannot combine :keep and :flush options" if options[:keep] && options[:flush]

MUTATION_MODES.each do |mutation_mode_option|
raise ArgumentError, "cannot combine :keep and :#{mutation_mode_option} options" if options[:keep] && options[mutation_mode_option]
raise ArgumentError, "cannot combine :flush and :#{mutation_mode_option} options" if options[:flush] && options[mutation_mode_option]
end if options[:keep] || options[:flush]

[turbolinks, options]
end

def _turbolinks_js_options(options)
if options[:change]
", { change: ['#{Array(options[:change]).join("', '")}'] }"
elsif options[:keep]
", { keep: ['#{Array(options[:keep]).join("', '")}'] }"
elsif options[:flush]
", { flush: true }"
end
js_options = {}

js_options[:change] = Array(options[:change]) if options[:change]
js_options[:append] = Array(options[:append]) if options[:append]
js_options[:prepend] = Array(options[:prepend]) if options[:prepend]
js_options[:keep] = Array(options[:keep]) if options[:keep]
js_options[:flush] = true if options[:flush]

", #{js_options.to_json}" if js_options.present?
end
end
end
3 changes: 3 additions & 0 deletions test/javascript/iframe.html
Expand Up @@ -22,6 +22,9 @@
<script data-turbolinks-eval="always">window.countAlways = (window.countAlways || 0) + 1;</script>
</div>
<div id="temporary" data-turbolinks-temporary>temporary content</div>
<div id="list">
<div id="list-item">original list item</div>
</div>
<script>window.i = window.i || 0; window.i++;</script>
<script data-turbolinks-eval="always">window.k = window.k || 0; window.k++;</script>
</body>
Expand Down
38 changes: 38 additions & 0 deletions test/javascript/turbolinks_replace_test.coffee
Expand Up @@ -376,3 +376,41 @@ suite 'Turbolinks.replace()', ->
assert.equal @window.count, 1 # using importNode before swapping the nodes would double-eval scripts in Chrome/Safari
done()
@Turbolinks.replace(doc, change: ['change'])

test "appends elements on change when the append option is passed", (done) ->
doc = """
<!DOCTYPE html>
<html>
<head>
<title>title</title>
</head>
<body>
<div id="list"><div id="another-list-item">inserted list item</div></div>
</body>
</html>
"""
@Turbolinks.replace(doc, append: ['list'])
assert.equal @$('#list').children.length, 2 # children is similar to childNodes except it does not include text nodes
assert.equal @$('#list').children[0].textContent, 'original list item'
assert.equal @$('#list').children[1].textContent, 'inserted list item'

done()

test "prepends elements on change when the prepend option is passed", (done) ->
doc = """
<!DOCTYPE html>
<html>
<head>
<title>title</title>
</head>
<body>
<div id="list"><div id="another-list-item">inserted list item</div></div>
</body>
</html>
"""
@Turbolinks.replace(doc, prepend: ['list'])
assert.equal @$('#list').children.length, 2 # children is similar to childNodes except it does not include text nodes
assert.equal @$('#list').children[0].textContent, 'inserted list item'
assert.equal @$('#list').children[1].textContent, 'original list item'

done()
14 changes: 7 additions & 7 deletions test/turbolinks/redirection_test.rb
Expand Up @@ -133,22 +133,22 @@ def test_redirect_to_via_put_and_not_xhr_does_normal_redirect

def test_redirect_to_via_xhr_and_post_with_single_change_option
xhr :post, :redirect_to_path_with_single_change_option
assert_turbolinks_visit 'http://test.host/path', "{ change: ['foo'] }"
assert_turbolinks_visit 'http://test.host/path', '{"change":["foo"]}'
end

def test_redirect_to_via_xhr_and_post_with_multiple_change_option
xhr :post, :redirect_to_path_with_multiple_change_option
assert_turbolinks_visit 'http://test.host/path', "{ change: ['foo', 'bar'] }"
assert_turbolinks_visit 'http://test.host/path', '{"change":["foo","bar"]}'
end

def test_redirect_to_via_xhr_and_post_with_change_option_and_custom_status
xhr :post, :redirect_to_path_with_change_option_and_custom_status
assert_turbolinks_visit 'http://test.host/path', "{ change: ['foo', 'bar'] }"
assert_turbolinks_visit 'http://test.host/path', '{"change":["foo","bar"]}'
end

def test_redirect_to_via_xhr_and_get_with_single_change_option
xhr :get, :redirect_to_path_with_single_change_option
assert_turbolinks_visit 'http://test.host/path', "{ change: ['foo'] }"
assert_turbolinks_visit 'http://test.host/path', '{"change":["foo"]}'
end

def test_redirect_to_via_post_and_not_xhr_with_change_option_and_custom_status
Expand All @@ -159,12 +159,12 @@ def test_redirect_to_via_post_and_not_xhr_with_change_option_and_custom_status

def test_redirect_to_with_turbolinks_and_single_keep_option
get :redirect_to_path_with_turbolinks_and_single_keep_option
assert_turbolinks_visit 'http://test.host/path', "{ keep: ['foo'] }"
assert_turbolinks_visit 'http://test.host/path', '{"keep":["foo"]}'
end

def test_redirect_to_with_turbolinks_and_multiple_keep_option
get :redirect_to_path_with_turbolinks_and_multiple_keep_option
assert_turbolinks_visit 'http://test.host/path', "{ keep: ['foo', 'bar'] }"
assert_turbolinks_visit 'http://test.host/path', '{"keep":["foo","bar"]}'
end

def test_redirect_to_with_change_and_keep_raises_argument_error
Expand All @@ -175,7 +175,7 @@ def test_redirect_to_with_change_and_keep_raises_argument_error

def test_redirect_to_with_turbolinks_and_flush_true
get :redirect_to_path_with_turbolinks_and_flush_true
assert_turbolinks_visit 'http://test.host/path', "{ flush: true }"
assert_turbolinks_visit 'http://test.host/path', '{"flush":true}'
end

def test_redirect_to_with_turbolinks_and_flush_false
Expand Down
30 changes: 24 additions & 6 deletions test/turbolinks/render_test.rb
Expand Up @@ -51,6 +51,14 @@ def render_with_flush_true
def render_with_flush_false
render action: :action, flush: false
end

def render_with_append
render :action, append: ['foo', 'bar']
end

def render_with_prepend
render :action, prepend: ['foo', 'bar']
end
end

class RenderTest < ActionController::TestCase
Expand Down Expand Up @@ -100,22 +108,22 @@ def test_render_unsafe_string_with_turbolinks_false

def test_render_via_xhr_and_post_with_single_change_option_renders_via_turbolinks
xhr :post, :render_with_single_change_option
assert_turbolinks_replace 'content', "{ change: ['foo'] }"
assert_turbolinks_replace 'content', '{"change":["foo"]}'
end

def test_render_via_xhr_and_put_with_multiple_change_option_renders_via_turbolinks
xhr :put, :render_with_multiple_change_option
assert_turbolinks_replace 'content', "{ change: ['foo', 'bar'] }"
assert_turbolinks_replace 'content', '{"change":["foo","bar"]}'
end

def test_render_via_xhr_and_put_with_single_keep_option_renders_via_turbolinks
xhr :put, :render_with_single_keep_option
assert_turbolinks_replace 'content', "{ keep: ['foo'] }"
assert_turbolinks_replace 'content', '{"keep":["foo"]}'
end

def test_render_via_xhr_and_delete_with_multiple_keep_option_renders_via_turbolinks
xhr :delete, :render_with_multiple_keep_option
assert_turbolinks_replace 'content', "{ keep: ['foo', 'bar'] }"
assert_turbolinks_replace 'content', '{"keep":["foo","bar"]}'
end

def test_simple_render_via_xhr_and_get_does_normal_render
Expand All @@ -127,7 +135,7 @@ def test_simple_render_via_xhr_and_get_does_normal_render
def test_render_via_xhr_and_get_with_change_option_renders_via_turbolinks
@request.env['HTTP_ACCEPT'] = Mime[:html]
xhr :get, :render_with_single_change_option
assert_turbolinks_replace 'content', "{ change: ['foo'] }"
assert_turbolinks_replace 'content', '{"change":["foo"]}'
end

def test_render_via_post_and_not_xhr_with_keep_option_does_normal_render
Expand All @@ -147,7 +155,7 @@ def test_render_with_change_and_keep_raises_argument_error

def test_render_via_xhr_and_post_with_flush_true_renders_via_turbolinks
xhr :post, :render_with_flush_true
assert_turbolinks_replace 'content', "{ flush: true }"
assert_turbolinks_replace 'content', '{"flush":true}'
end

def test_render_via_get_and_not_xhr_with_flush_true_does_normal_render
Expand Down Expand Up @@ -192,6 +200,16 @@ def test_render_with_turbolinks_returns_response_body
assert_equal ["Turbolinks.replace('test');"], result
end

def test_render_with_append
xhr :post, :render_with_append
assert_turbolinks_replace 'content', '{"append":["foo","bar"]}'
end

def test_render_with_prepend
xhr :post, :render_with_prepend
assert_turbolinks_replace 'content', '{"prepend":["foo","bar"]}'
end

private

def assert_normal_render(content)
Expand Down

0 comments on commit 75115a3

Please sign in to comment.