###*
Linking to fragments
====================
In a traditional web application, the entire page is destroyed and re-created when the
user follows a link:
![Traditional page flow](/images/tutorial/fragment_flow_vanilla.svg){:width="620" class="picture has_border is_sepia has_padding"}
This makes for an unfriendly experience:
- State changes caused by AJAX updates get lost during the page transition.
- Unsaved form changes get lost during the page transition.
- The JavaScript VM is reset during the page transition.
- If the page layout is composed from multiple srollable containers
(e.g. a pane view), the scroll positions get lost during the page transition.
- The user sees a "flash" as the browser loads and renders the new page,
even if large portions of the old and new page are the same (navigation, layout, etc.).
Unpoly fixes this by letting you annotate links with an [`up-target`](/a-up-target)
attribute. The value of this attribute is a CSS selector that indicates which page
fragment to update. The server **still renders full HTML pages**, but we only use
the targeted ragments and discard the rest:
![Unpoly page flow](/images/tutorial/fragment_flow_unpoly.svg){:width="620" class="picture has_border is_sepia has_padding"}
With this model, following links feel smooth. All transient DOM changes outside the updated fragment are preserved.
Pages also load much faster since the DOM, CSS and Javascript environments do not need to be
destroyed and recreated for every request.
## Example
Let's say we are rendering three pages with a tabbed navigation to switch between screens:
Your HTML could look like this:
```
A
B
C
Page A
```
Since we only want to update the `` tag, we annotate the links
with an `up-target` attribute:
```
A
B
C
```
Note that instead of `article` you can use any other CSS selector like `#main .article`.
With these [`up-target`](/a-up-target) annotations Unpoly only updates the targeted part of the screen.
The JavaScript environment will persist and the user will not see a white flash while the
new page is loading.
@class up.link
###
up.link = (($) ->
u = up.util
###*
Visits the given URL without a full page load.
This is done by fetching `url` through an AJAX request
and [replacing](/up.replace) the current `` element with the response's `` element.
For example, this would fetch the `/users` URL:
up.visit('/users')
@function up.visit
@param {string} url
The URL to visit.
@param {string} [options.target='body']
The selector to replace.
@param {Object} [options]
See options for [`up.replace()`](/up.replace)
@stable
###
visit = (url, options) ->
options = u.options(options)
selector = u.option(options.target, 'body')
up.replace(selector, url, options)
###*
Follows the given link via AJAX and [replaces](/up.replace) the current page
with HTML from the response.
By default the page's `` element will be replaced.
If the link has an attribute like [`[up-target]`](/up-target)
or [`[up-modal]`](/a-up-modal), the corresponding UJS behavior will be activated
just as if the user had clicked on the link.
Emits the event [`up:link:follow`](/up:link:follow).
\#\#\# Examples
Let's say you have a link with an [`a[up-target]`](/a-up-target) attribute:
Users
Calling `up.follow()` with this link will replace the page's `.main` fragment
as if the user had clicked on the link:
var $link = $('a:first');
up.follow($link);
@function up.follow
@param {Element|jQuery|string} linkOrSelector
An element or selector which is either an `` tag or any element with an `up-href` attribute.
@param {string} [options.target]
The selector to replace.
Defaults to the `[up-target]`, `[up-modal]` or `[up-popup]` attribute on `link`.
If no target is given, the `` element will be replaced.
@return {Promise}
A promise that will be fulfilled when the link destination
has been loaded and rendered.
@stable
###
follow = (linkOrSelector, options) ->
$link = $(linkOrSelector)
variant = followVariantForLink($link)
variant.followLink($link, options)
###*
This event is [emitted](/up.emit) when a link is [followed](/up.follow) through Unpoly.
@event up:link:follow
@param {jQuery} event.$element
The link element that will be followed.
@param event.preventDefault()
Event listeners may call this method to prevent the link from being followed.
@stable
###
###*
@function defaultFollow
@internal
###
defaultFollow = (linkOrSelector, options) ->
$link = $(linkOrSelector)
options = u.options(options)
url = u.option($link.attr('up-href'), $link.attr('href'))
target = u.option(options.target, $link.attr('up-target'))
options.failTarget = u.option(options.failTarget, $link.attr('up-fail-target'))
options.fallback = u.option(options.fallback, $link.attr('up-fallback'))
options.transition = u.option(options.transition, u.castedAttr($link, 'up-transition'), 'none')
options.failTransition = u.option(options.failTransition, u.castedAttr($link, 'up-fail-transition'), 'none')
options.history = u.option(options.history, u.castedAttr($link, 'up-history'))
options.reveal = u.option(options.reveal, u.castedAttr($link, 'up-reveal'), true)
options.cache = u.option(options.cache, u.castedAttr($link, 'up-cache'))
options.restoreScroll = u.option(options.restoreScroll, u.castedAttr($link, 'up-restore-scroll'))
options.method = followMethod($link, options)
options.origin = u.option(options.origin, $link)
options.layer = u.option(options.layer, $link.attr('up-layer'), 'auto')
options.failLayer = u.option(options.failLayer, $link.attr('up-fail-layer'), 'auto')
options.confirm = u.option(options.confirm, $link.attr('up-confirm'))
options = u.merge(options, up.motion.animateOptions(options, $link))
up.browser.whenConfirmed(options).then ->
up.replace(target, url, options)
defaultPreload = ($link, options) ->
options = u.options(options)
options.preload = true
defaultFollow($link, options)
###*
Returns the HTTP method that should be used when following the given link.
Looks at the link's `up-method` or `data-method` attribute.
Defaults to `"get"`.
@function up.link.followMethod
@param linkOrSelector
@param options.method {string}
@internal
###
followMethod = (linkOrSelector, options) ->
$link = $(linkOrSelector)
options = u.options(options)
u.option(options.method, $link.attr('up-method'), $link.attr('data-method'), 'get').toUpperCase()
###*
No-op that is called when we allow a browser's default action to go through,
so we can spy on it in unit tests. See `link_spec.js`.
@function allowDefault
@internal
###
allowDefault = (event) ->
followVariants = []
###*
Registers the given handler for links with the given selector.
This does more than a simple `click` handler:
- It also handles `[up-instant]`
- It also handles `[up-href]`
@function up.link.addFollowVariant
@param {string} simplifiedSelector
A selector without `a` or `[up-href]`, e.g. `[up-target]`
@param {Function} options.follow
@param {Function} options.preload
@internal
###
addFollowVariant = (simplifiedSelector, options) ->
variant = new up.FollowVariant(simplifiedSelector, options)
followVariants.push(variant)
variant.registerEvents()
variant
###*
Returns whether the given link will be handled by Unpoly instead of making a full page load.
A link will be handled by Unpoly if it has an attribute
like `up-target` or `up-modal`.
@function up.link.isFollowable
@param {Element|jQuery|string} linkOrSelector
The link to check.
@experimental
###
isFollowable = (link) ->
!!followVariantForLink(link, default: false)
###*
Returns the handler function that can be used to follow the given link.
E.g. it wil return a handler calling `up.modal.follow` if the link is a `[up-modal]`,
but a handler calling `up.link.follow` if the links is `[up-target]`.
@param {Element|jQuery|string}
@return {Function(jQuery)}
@internal
###
followVariantForLink = (linkOrSelector, options) ->
options = u.options(options)
$link = $(linkOrSelector)
variant = u.detect followVariants, (variant) -> variant.matchesLink($link)
variant ||= DEFAULT_FOLLOW_VARIANT unless options.default is false
variant
###*
Makes sure that the given link will be handled by Unpoly instead of making a full page load.
This is done by giving the link an `up-follow` attribute
unless it already have it an attribute like `up-target` or `up-modal`.
@function up.link.makeFollowable
@param {Element|jQuery|string} linkOrSelector
The link to process.
@experimental
###
makeFollowable = (link) ->
$link = $(link)
unless isFollowable($link)
$link.attr('up-follow', '')
shouldProcessEvent = (event, $link) ->
$target = $(event.target)
$targetedChildLink = $target.closest('a, [up-href]').not($link)
$targetedInput = up.form.fieldSelector().seekUp($target)
$targetedChildLink.length == 0 && $targetedInput.length == 0 && u.isUnmodifiedMouseEvent(event)
###*
Returns whether the given link has a [safe](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1)
HTTP method like `GET`.
@function up.link.isSafe
@experimental
###
isSafe = (selectorOrLink, options) ->
$link = $(selectorOrLink)
method = followMethod($link, options)
up.proxy.isSafeMethod(method)
###*
Follows this link via AJAX and replaces a CSS selector in the current page
with corresponding elements from a new page fetched from the server:
Read post
\#\#\# Updating multiple fragments
You can update multiple fragments from a single request by separating
separators with a comma (like in CSS). E.g. if opening a post should
also update a bubble showing the number of unread posts, you might
do this:
Read post
\#\#\# Appending or prepending content
By default Unpoly 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. Below the list is
a button to load the next page. You can append to the existing list
by using `:after` in the `up-target` selector like this:
Wash car
Purchase supplies
Fix tent
Load more tasks
\#\#\# Following elements that are no links
You can also use `[up-target]` to turn an arbitrary element into a link.
In this case, put the link's destination into the `up-href` attribute:
Go
Note that using any element other than `` will prevent users from
opening the destination in a new tab.
@selector a[up-target]
@param {string} up-target
The CSS selector to replace
@param {string} [up-method='get']
The HTTP method to use for the request.
@param {string} [up-transition='none']
The [transition](/up.motion) to use for morphing between the old and new elements.
@param [up-fail-target='body']
The selector to replace if the server responds with a non-200 status code.
@param {string} [up-fail-transition='none']
The [transition](/up.motion) to use for morphing between the old and new elements
when the server responds with a non-200 status code.
@param {string} [up-fallback]
The selector to update when the original target was not found in the page.
@param {string} [up-href]
The destination URL to follow.
If omitted, the the link's `href` attribute will be used.
@param {string} [up-confirm]
A message that will be displayed in a cancelable confirmation dialog
before the link is followed.
@param {string} [up-reveal='true']
Whether to reveal the target element within its viewport before updating.
@param {string} [up-restore-scroll='false']
Whether to restore previously known scroll position of all viewports
within the target selector.
@param {string} [up-cache]
Whether to force the use of a cached response (`true`)
or never use the cache (`false`)
or make an educated guess (default).
@param {string} [up-layer='auto']
The name of the layer that ought to be updated. Valid values are
`auto`, `page`, `modal` and `popup`.
If set to `auto` (default), Unpoly will try to find a match in the link's layer.
If no match was found in that layer,
Unpoly will search in other layers, starting from the topmost layer.
@param {string} [up-fail-layer='auto']
The name of the layer that ought to be updated if the server sends a
non-200 status code.
@param [up-history]
Whether to push an entry to the browser history when following the link.
Set this to `'false'` to prevent the URL bar from being updated.
Set this to a URL string to update the history with the given URL.
@stable
###
DEFAULT_FOLLOW_VARIANT = addFollowVariant '[up-target], [up-follow]',
# Don't just pass the `defaultFollow` function reference so we can stub it in tests
follow: ($link, options) -> defaultFollow($link, options)
preload: ($link, options) -> defaultPreload($link, options)
###*
If applied on a link, follows this link via AJAX and replaces the
current `` element with the response's `` element.
To only update a fragment instead of the entire page, see
[`a[up-target]`](/a-up-target).
\#\#\# Example
User list
\#\#\# Turn any element into a link
You can also use `[up-follow]` to turn an arbitrary element into a link.
In this case, put the link's destination into the `up-href` attribute:
Go
Note that using any element other than `` will prevent users from
opening the destination in a new tab.
@selector a[up-follow]
@param {string} [up-method='get']
The HTTP method to use for the request.
@param [up-fail-target='body']
The selector to replace if the server responds with a non-200 status code.
@param {string} [up-fallback]
The selector to update when the original target was not found in the page.
@param {string} [up-transition='none']
The [transition](/up.motion) to use for morphing between the old and new elements.
@param {string} [up-fail-transition='none']
The [transition](/up.motion) to use for morphing between the old and new elements
when the server responds with a non-200 status code.
@param [up-href]
The destination URL to follow.
If omitted, the the link's `href` attribute will be used.
@param {string} [up-confirm]
A message that will be displayed in a cancelable confirmation dialog
before the link is followed.
@param {string} [up-history]
Whether to push an entry to the browser history when following the link.
Set this to `'false'` to prevent the URL bar from being updated.
Set this to a URL string to update the history with the given URL.
@param [up-restore-scroll='false']
Whether to restore the scroll position of all viewports
within the response.
@stable
###
###*
By adding an `up-instant` attribute to a link, the destination will be
fetched on `mousedown` instead of `click` (`mouseup`).
User list
This will save precious milliseconds that otherwise spent
on waiting for the user to release the mouse button. Since an
AJAX request will be triggered right way, the interaction will
appear faster.
Note that using `[up-instant]` will prevent a user from canceling a link
click by moving the mouse away from the interaction area. However, for
navigation actions this isn't needed. E.g. popular operation
systems switch tabs on `mousedown` instead of `click`.
`up-instant` will also work for links that open [modals](/up.modal) or [popups](/up.popup).
@selector a[up-instant]
@stable
###
###*
Marks up the current link to be followed *as fast as possible*.
This is done by:
- [Following the link through AJAX](/a-up-target) instead of a full page load
- [Preloading the link's destination URL](/a-up-preload)
- [Triggering the link on `mousedown`](/a-up-instant) instead of on `click`
Use `up-dash` like this:
User list
Note that this is shorthand for:
User list
@selector a[up-dash]
@stable
###
up.macro '[up-dash]', ($element) ->
target = u.castedAttr($element, 'up-dash')
$element.removeAttr('up-dash')
newAttrs = {
'up-preload': '',
'up-instant': ''
}
if target is true
# If it's literally `true` then we don't have a target selector.
# Just follow the link by replacing ``.
makeFollowable($element)
else
newAttrs['up-target'] = target
u.setMissingAttrs($element, newAttrs)
###*
Add an `[up-expand]` attribute to any element that contains a link
in order to enlarge the link's click area.
`[up-expand]` honors all the UJS behavior in expanded links
([`a[up-target]`](/a-up-target), [`a[up-instant]`](/a-up-instant), [`a[up-preload]`](/a-up-preload), etc.).
\#\#\# Example
In the example above, clicking anywhere within `.notification` element
would [follow](/up.follow) the *Close* link.
`up-expand` also expands links that open [modals](/up.modal) or [popups](/up.popup).
\#\#\# Elements with multiple contained links
If a container contains more than one link, you can set the value of the
`up-expand` attribute to a CSS selector to define which link should be expanded:
\#\#\# Limitations
`[up-expand]` has some limitations for advanced browser users:
- Users won't be able to right-click the expanded area to open a context menu
- Users won't be able to CTRL+click the expanded area to open a new tab
To overcome these limitations, consider nesting the entire clickable area in an actual `` tag.
[It's OK to put block elements inside an anchor tag](https://makandracards.com/makandra/43549-it-s-ok-to-put-block-elements-inside-an-a-tag).
@selector [up-expand]
@param {string} [up-expand]
A CSS selector that defines which containing link should be expanded.
If omitted, the first contained link will be expanded.
@stable
###
up.macro '[up-expand]', ($area) ->
$childLinks = $area.find('a, [up-href]')
if selector = $area.attr('up-expand')
$childLinks = $childLinks.filter(selector)
if link = $childLinks.get(0)
upAttributePattern = /^up-/
newAttrs = {}
newAttrs['up-href'] = $(link).attr('href')
for attribute in link.attributes
name = attribute.name
if name.match(upAttributePattern)
newAttrs[name] = attribute.value
u.setMissingAttrs($area, newAttrs)
$area.removeAttr('up-expand')
makeFollowable($area)
knife: eval(Knife?.point)
visit: visit
follow: follow
makeFollowable: makeFollowable
isSafe: isSafe
isFollowable: isFollowable
shouldProcessEvent: shouldProcessEvent
followMethod: followMethod
addFollowVariant: addFollowVariant
followVariantForLink: followVariantForLink
allowDefault: allowDefault
)(jQuery)
up.visit = up.link.visit
up.follow = up.link.follow