lib/assets/javascripts/up/flow.js.coffee in upjs-rails-0.12.5 vs lib/assets/javascripts/up/flow.js.coffee in upjs-rails-0.13.0
- old
+ new
@@ -22,10 +22,27 @@
source = (selectorOrElement) ->
$element = $(selectorOrElement).closest('[up-source]')
u.presence($element.attr("up-source")) || up.browser.url()
###*
+ @function up.flow.resolveSelector
+ @private
+ ###
+ resolveSelector = (selectorOrElement, options) ->
+ if u.isString(selectorOrElement)
+ selector = selectorOrElement
+ if u.contains(selector, '&')
+ if origin = u.presence(options.origin)
+ originSelector = u.createSelectorFromElement(origin)
+ selector = selector.replace(/\&/, originSelector)
+ else
+ u.error("Found origin reference %o in selector %o, but options.origin is missing", '&', selector)
+ else
+ selector = u.createSelectorFromElement(selectorOrElement)
+ selector
+
+ ###*
Replaces elements on the current page with corresponding elements
from a new page fetched from the server.
The current and new elements must have the same CSS selector.
@@ -52,10 +69,45 @@
<div class="two">new two</div>
Note how only `.two` has changed. The update for `.one` was
discarded, since it didn't match the selector.
+ \#\#\#\# Appending or prepending instead of replacing
+
+ By default Up.js will replace the given selector with the same
+ selector from a freshly fetched page. Instead of replacing you
+ can *append* the loaded content to the existing content by using the
+ `:after` pseudo selector. In the same fashion, you can use `:before`
+ to indicate that you would like the *prepend* the loaded content.
+
+ A practical example would be a paginated list of items:
+
+ <ul class="tasks">
+ <li>Wash car</li>
+ <li>Purchase supplies</li>
+ <li>Fix tent</li>
+ </ul>
+
+ In order to append more items from a URL, replace into
+ the `.tasks:after` selector:
+
+ up.replace('.tasks:after', '/page/2')
+
+ \#\#\#\# Setting the window title from the server
+
+ If the `replace` call changes history, the document title will be set
+ to the contents of a `<title>` tag in the response.
+
+ The server can also change the document title by setting
+ an `X-Up-Title` header in the response.
+
+ \#\#\#\# Optimizing response rendering
+
+ The server is free to optimize Up.js requests by only rendering the HTML fragment
+ that is being updated. The request's `X-Up-Selector` header will contain
+ the CSS selector for the updating fragment.
+
\#\#\#\# Events
Up.js will emit [`up:fragment:destroyed`](/up:fragment:destroyed) on the element
that was replaced and [`up:fragment:inserted`](/up:fragment:inserted) on the new
element that replaces it.
@@ -82,24 +134,27 @@
of all the viewports around or below the updated element. The position
will be reset to the last known top position before a previous
history change for the current URL.
@param {Boolean} [options.cache]
Whether to use a [cached response](/up.proxy) if available.
+ @param {Element|jQuery} [options.origin]
+ The element that triggered the replacement. The element's selector will
+ be substituted for the `&` shorthand in the target selector.
@param {String} [options.historyMethod='push']
+ @param {Object} [options.headers={}]
+ An object of additional header key/value pairs to send along
+ with the request.
@return {Promise}
A promise that will be resolved when the page has been updated.
###
replace = (selectorOrElement, url, options) ->
u.debug("Replace %o with %o (options %o)", selectorOrElement, url, options)
options = u.options(options)
- selector = if u.presence(selectorOrElement)
- selectorOrElement
- else
- u.createSelectorFromElement($(selectorOrElement))
+ selector = resolveSelector(selectorOrElement, options)
if !up.browser.canPushState() && options.history != false
up.browser.loadPage(url, u.only(options, 'method')) unless options.preload
return u.unresolvablePromise()
@@ -107,10 +162,11 @@
url: url
method: options.method
selector: selector
cache: options.cache
preload: options.preload
+ headers: options.headers
promise = up.proxy.ajax(request)
promise.done (html, textStatus, xhr) ->
# The server can send us the current path using a header value.
@@ -125,10 +181,11 @@
url = currentLocation
unless options.history is false
options.history = url
unless options.source is false
options.source = url
+ options.title ||= u.titleFromXhr(xhr)
implant(selector, html, options) unless options.preload
promise.fail(u.error)
promise
@@ -161,21 +218,20 @@
Note how only `.two` has changed. The update for `.one` was
discarded, since it didn't match the selector.
@function up.flow.implant
@protected
- @param {String} selector
+ @param {String|Element|jQuery} selectorOrElement
@param {String} html
@param {Object} [options]
See options for [`up.replace`](/up.replace).
###
- implant = (selector, html, options) ->
-
- options = u.options(options,
+ implant = (selectorOrElement, html, options) ->
+ selector = resolveSelector(selectorOrElement, options)
+ options = u.options(options,
historyMethod: 'push'
)
-
options.source = u.option(options.source, options.history)
response = parseResponse(html)
options.title ||= response.title()
up.layout.saveScroll() unless options.saveScroll == false
@@ -197,16 +253,20 @@
if message[0] == '#'
message += ' (avoid using IDs)'
u.error(message, selector)
parseResponse = (html) ->
- # jQuery cannot construct transient elements that contain <html> or <body> tags,
- # so we're using the native browser API to grep through the HTML
+ # jQuery cannot construct transient elements that contain <html> or <body> tags
htmlElement = u.createElementFromHtml(html)
title: -> htmlElement.querySelector("title")?.textContent
find: (selector) ->
- if child = htmlElement.querySelector(selector)
+ # Although we cannot have a jQuery collection from an entire HTML document,
+ # we can use jQuery's Sizzle engine to grep through a DOM tree.
+ # jQuery.find is the Sizzle function (https://github.com/jquery/sizzle/wiki#public-api)
+ # which gives us non-standard CSS selectors such as `:has`.
+ # It returns an array of DOM elements, NOT a jQuery collection.
+ if child = $.find(selector, htmlElement)[0]
$(child)
else
u.error("Could not find selector %o in response %o", selector, html)
elementsInserted = ($new, options) ->
@@ -273,13 +333,19 @@
disjunction = selector.split(comma)
transitions = transitionString.split(comma) if u.isPresent(transitionString)
for selectorAtom, i in disjunction
# Splitting the atom
selectorParts = selectorAtom.match(/^(.+?)(?:\:(before|after))?$/)
+ selector = selectorParts[1]
+ if selector == 'html'
+ # If someone really asked us to replace the <html> root, the best
+ # we can do is replace the <body>.
+ selector = 'body'
+ pseudoClass = selectorParts[2]
transition = transitions[i] || u.last(transitions)
- selector: selectorParts[1]
- pseudoClass: selectorParts[2]
+ selector: selector
+ pseudoClass: pseudoClass
transition: transition
autofocus = ($element) ->
selector = '[autofocus]:last'
$control = u.findWithSelf($element, selector)
@@ -296,18 +362,29 @@
Returns the first element matching the given selector.
Excludes elements that also match `.up-ghost` or `.up-destroying`
or that are children of elements with these selectors.
+ If the given argument is already a jQuery collection (or an array
+ of DOM elements), the first element matching these conditions
+ is returned.
+
Returns `undefined` if no element matches these conditions.
@protected
@function up.first
- @param {String} selector
+ @param {String|Element|jQuery} selectorOrElement
+ @return {jQuery}
+ The first element that is neither a ghost or being destroyed,
+ or `undefined` if no such element was given.
###
- first = (selector) ->
- elements = $(selector).get()
+ first = (selectorOrElement) ->
+ elements = undefined
+ if u.isString(selectorOrElement)
+ elements = $(selectorOrElement).get()
+ else
+ elements = selectorOrElement
$match = undefined
for element in elements
$element = $(element)
if isRealElement($element)
$match = $element
@@ -422,9 +499,10 @@
replace: replace
reload: reload
destroy: destroy
implant: implant
first: first
+ resolveSelector: resolveSelector
)(jQuery)
up.replace = up.flow.replace
up.reload = up.flow.reload