###*
Enhancing elements
==================
Unpoly keeps a persistent Javascript environment during page transitions.
If you wire Javascript to run on `ready` or `onload` events, those scripts will
only run during the initial page load. Subsequently [inserted](/up.replace)
page fragments will not be compiled.
Let's say your Javascript plugin wants you to call `lightboxify()`
on links that should open a lightbox. You decide to
do this for all links with an `lightbox` class:
River
Ocean
You should **avoid** doing this on page load:
$(document).on('ready', function() {
$('a.lightbox').lightboxify();
});
Instead you should register a [`compiler`](/up.compiler) for the `a.lightbox` selector:
up.compiler('a.lightbox', function($element) {
$element.lightboxify();
});
The compiler function will be called on matching elements when
the page loads, or whenever a matching fragment is [updated through Unpoly](/up.replace)
later.
@class up.syntax
###
up.syntax = (($) ->
u = up.util
DESTRUCTABLE_CLASS = 'up-destructable'
DESTRUCTORS_KEY = 'up-destructors'
compilers = []
macros = []
###*
Registers a function to be called whenever an element with
the given selector is inserted into the DOM.
up.compiler('.action', function($element) {
// your code here
});
The functions will be called on elements matching `.action` when
the page loads, or whenever a matching fragment is [updated through Unpoly](/up.replace)
later.
If you have used Angular.js before, this resembles
[Angular directives](https://docs.angularjs.org/guide/directive).
\#\#\#\# Integrating jQuery plugins
`up.compiler` is a great way to integrate jQuery plugins.
Let's say your Javascript plugin wants you to call `lightboxify()`
on links that should open a lightbox. You decide to
do this for all links with an `lightbox` class:
River
Ocean
This Javascript will do exactly that:
up.compiler('a.lightbox', function($element) {
$element.lightboxify();
});
\#\#\#\# Custom elements
You can use `up.compiler` to implement custom elements like this:
Here is the Javascript that inserts the current time into to these elements:
up.compiler('clock', function($element) {
var now = new Date();
$element.text(now.toString()));
});
\#\#\#\# Cleaning up after yourself
If your compiler returns a function, Unpoly will use this as a *destructor* to
clean up if the element leaves the DOM. Note that in Unpoly the same DOM ad Javascript environment
will persist through many page loads, so it's important to not create
[memory leaks](https://makandracards.com/makandra/31325-how-to-create-memory-leaks-in-jquery).
You should clean up after yourself whenever your compilers have global
side effects, like a [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval)
or event handlers bound to the document root.
Here is a version of `` that updates
the time every second, and cleans up once it's done:
up.compiler('clock', function($element) {
function update() {
var now = new Date();
$element.text(now.toString()));
}
setInterval(update, 1000);
return function() {
clearInterval(update);
};
});
If we didn't clean up after ourselves, we would have many ticking intervals
operating on detached DOM elements after we have created and removed a couple
of `` elements.
\#\#\#\# Attaching structured data
In case you want to attach structured data to the event you're observing,
you can serialize the data to JSON and put it into an `[up-data]` attribute.
For instance, a container for a [Google Map](https://developers.google.com/maps/documentation/javascript/tutorial)
might attach the location and names of its marker pins:
The JSON will parsed and handed to your compiler as a second argument:
up.compiler('.google-map', function($element, pins) {
var map = new google.maps.Map($element);
pins.forEach(function(pin) {
var position = new google.maps.LatLng(pin.lat, pin.lng);
new google.maps.Marker({
position: position,
map: map,
title: pin.title
});
});
});
\#\#\#\# Migrating jQuery event handlers to `up.compiler`
Within the compiler, Unpoly will bind `this` to the
native DOM element to help you migrate your existing jQuery code to
this new syntax.
So if you had this before:
$(function() {
$('.action').on('click', function() {
$(this).something();
});
});
... you can reuse the event handler like this:
$('.action').compiler(function($element) {
$element.on('click', function() {
$(this).something();
});
});
@function up.compiler
@param {String} selector
The selector to match.
@param {Number} [options.priority=0]
The priority of this compilers.
Compilers with a higher priority are run first.
Two compilers with the same priority are run in the order they were registered.
@param {Boolean} [options.batch=false]
If set to `true` and a fragment insertion contains multiple
elements matching the selector, `compiler` is only called once
with a jQuery collection containing all matching elements.
@param {Boolean} [options.keep=false]
If set to `true` compiled fragment will be [persisted](/up-keep) during
[page updates](/a-up-target).
This has the same effect as setting an `up-keep` attribute on the element.
@param {Function($element, data)} compiler
The function to call when a matching element is inserted.
The function takes the new element as the first argument (as a jQuery object).
If the element has an [`up-data`](/up-data) attribute, its value is parsed as JSON
and passed as a second argument.
The function may return a destructor function that destroys the compiled
object before it is removed from the DOM. The destructor is supposed to
clear global state such as time-outs and event handlers bound to the document.
The destructor is *not* expected to remove the element from the DOM, which
is already handled by [`up.destroy`](/up.destroy).
@stable
###
compiler = (args...) ->
insertCompiler(compilers, args...)
###*
Registers a [compiler](/up.compiler) that is run before all other compilers.
You can use `up.macro` to register a compiler that sets other UJS attributes.
\#\#\#\# Example
You will sometimes find yourself setting the same combination of UJS attributes again and again:
Page 1
Page 2
Page 3
We would much rather define a new `content-link` attribute that let's us
write the same links like this:
Page 1
Page 2
Page 3
We can define the `content-link` attribute by registering a macro that
sets the `up-target`, `up-transition` and `up-duration` attributes for us:
up.macro('[content-link]', function($link) {
$link.attr('up-target', '.content');
$link.attr('up-transition', 'cross-fade');
$link.attr('up-duration', '300');
});
Examples for built-in macros are [`up-dash`](/up-dash) and [`up-expand`](/up-expand).
@function up.macro
@param {String} selector
The selector to match.
@param {Object} options
See options for [`up.compiler`](/up.compiler).
@param {Function($element, data)} compiler
The function to call when a matching element is inserted.
See [`up.compiler`](/up.compiler) for details.
@stable
###
macro = (args...) ->
insertCompiler(macros, args...)
buildCompiler = (selector, args...) ->
callback = args.pop()
options = u.options(args[0], priority: 0)
if options.priority == 'first'
options.priority = Number.POSITIVE_INFINITY
else if options.priority == 'last'
options.priority = Number.NEGATIVE_INFINITY
selector: selector
callback: callback
priority: options.priority
batch: options.batch
keep: options.keep
insertCompiler = (queue, args...) ->
# Silently discard any compilers that are registered on unsupported browsers
return unless up.browser.isSupported()
newCompiler = buildCompiler(args...)
index = 0
while (oldCompiler = queue[index]) && (oldCompiler.priority >= newCompiler.priority)
index += 1
queue.splice(index, 0, newCompiler)
applyCompiler = (compiler, $jqueryElement, nativeElement) ->
up.puts ("Compiling '%s' on %o" unless compiler.isDefault), compiler.selector, nativeElement
if compiler.keep
value = if u.isString(compiler.keep) then compiler.keep else ''
$jqueryElement.attr('up-keep', value)
destructor = compiler.callback.apply(nativeElement, [$jqueryElement, data($jqueryElement)])
if u.isFunction(destructor)
addDestructor($jqueryElement, destructor)
addDestructor = ($jqueryElement, destructor) ->
$jqueryElement.addClass(DESTRUCTABLE_CLASS)
destructors = $jqueryElement.data(DESTRUCTORS_KEY) || []
destructors.push(destructor)
$jqueryElement.data(DESTRUCTORS_KEY, destructors)
###*
Applies all compilers on the given element and its descendants.
Unlike [`up.hello`](/up.hello), this doesn't emit any events.
@function up.syntax.compile
@param {Array} [options.skip]
A list of elements whose subtrees should not be compiled.
@internal
###
compile = ($fragment, options) ->
options = u.options(options)
$skipSubtrees = $(options.skip)
up.log.group "Compiling fragment %o", $fragment.get(0), ->
for queue in [macros, compilers]
for compiler in queue
$matches = u.findWithSelf($fragment, compiler.selector)
$matches = $matches.filter ->
$match = $(this)
u.all $skipSubtrees, (element) ->
$match.closest(element).length == 0
if $matches.length
up.log.group ("Compiling '%s' on %d element(s)" unless compiler.isDefault), compiler.selector, $matches.length, ->
if compiler.batch
applyCompiler(compiler, $matches, $matches.get())
else
$matches.each -> applyCompiler(compiler, $(this), this)
###*
Runs any destroyers on the given fragment and its descendants.
Unlike [`up.destroy`](/up.destroy), this doesn't emit any events
and does not remove the element from the DOM.
@function up.syntax.clean
@internal
###
clean = ($fragment) ->
u.findWithSelf($fragment, ".#{DESTRUCTABLE_CLASS}").each ->
$element = $(this)
destructors = $element.data(DESTRUCTORS_KEY)
destructor() for destructor in destructors
$element.removeData(DESTRUCTORS_KEY)
$element.removeClass(DESTRUCTABLE_CLASS)
###*
Checks if the given element has an [`up-data`](/up-data) attribute.
If yes, parses the attribute value as JSON and returns the parsed object.
Returns an empty object if the element has no `up-data` attribute.
\#\#\#\# Example
You have an element with JSON data serialized into an `up-data` attribute:
Bob
Calling `up.syntax.data` will deserialize the JSON string into a Javascript object:
up.syntax.data('.person') // returns { age: 18, name: 'Bob' }
@function up.syntax.data
@param {String|Element|jQuery} elementOrSelector
@return
The JSON-decoded value of the `up-data` attribute.
Returns an empty object (`{}`) if the element has no (or an empty) `up-data` attribute.
@experimental
###
###*
If an element annotated with [`up-data`] is inserted into the DOM,
Up will parse the JSON and pass the resulting object to any matching
[`up.compiler`](/up.compiler) handlers.
For instance, a container for a [Google Map](https://developers.google.com/maps/documentation/javascript/tutorial)
might attach the location and names of its marker pins:
The JSON will parsed and handed to your compiler as a second argument:
up.compiler('.google-map', function($element, pins) {
var map = new google.maps.Map($element);
pins.forEach(function(pin) {
var position = new google.maps.LatLng(pin.lat, pin.lng);
new google.maps.Marker({
position: position,
map: map,
title: pin.title
});
});
});
Similarly, when an event is triggered on an element annotated with
[`up-data`], the parsed object will be passed to any matching
[`up.on`](/up.on) handlers.
up.on('click', '.google-map', function(event, $element, pins) {
console.log("There are %d pins on the clicked map", pins.length);
});
@selector [up-data]
@param {JSON} up-data
A serialized JSON string
@stable
###
data = (elementOrSelector) ->
$element = $(elementOrSelector)
json = $element.attr('up-data')
if u.isString(json) && u.trim(json) != ''
JSON.parse(json)
else
{}
###*
Makes a snapshot of the currently registered event listeners,
to later be restored through `reset`.
@internal
###
snapshot = ->
setDefault = (compiler) -> compiler.isDefault = true
u.each(compilers, setDefault)
u.each(macros, setDefault)
###*
Resets the list of registered compiler directives to the
moment when the framework was booted.
@internal
###
reset = ->
isDefault = (compiler) -> compiler.isDefault
compilers = u.select(compilers, isDefault)
macros = u.select(macros, isDefault)
up.on 'up:framework:booted', snapshot
up.on 'up:framework:reset', reset
compiler: compiler
macro: macro
compile: compile
clean: clean
data: data
)(jQuery)
up.compiler = up.syntax.compiler
up.macro = up.syntax.macro
up.ready = -> up.util.error('up.ready no longer exists. Please use up.hello instead.')
up.awaken = -> up.util.error('up.awaken no longer exists. Please use up.compiler instead.')