###* Custom Javascript ================= Every app needs a way to pair Javascript snippets with certain HTML elements, in order to integrate libraries or implement custom behavior. Unpoly lets you organize your Javascript snippets using [compilers](/up.compiler). For instance, to activate the [Masonry](http://masonry.desandro.com/) jQuery plugin for every element with a `grid` class, use this compiler: up.compiler('.grid', function($element) { $element.masonry(); }); The compiler function will be called on matching elements when the page loads or when a matching fragment is [inserted via AJAX](/up.link) 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: <a href="river.png" class="lightbox">River</a> <a href="ocean.png" class="lightbox">Ocean</a> 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: <clock></clock> 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](/up.on). Here is a version of `<clock>` that updates the time every second, and cleans up once it's done. Note how it returns a function that calls `clearInterval`: 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 `<clock>` 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: <div class="google-map" up-data="[ { lat: 48.36, lng: 10.99, title: 'Friedberg' }, { lat: 48.75, lng: 11.45, title: 'Ingolstadt' } ]"></div> 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](/up.compiler#cleaning-up-after-yourself) such as timeouts 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). The function may also return an array of destructor functions. @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: <a href="/page1" up-target=".content" up-transition="cross-fade" up-duration="300">Page 1</a> <a href="/page2" up-target=".content" up-transition="cross-fade" up-duration="300">Page 2</a> <a href="/page3" up-target=".content" up-transition="cross-fade" up-duration="300">Page 3</a> We would much rather define a new `content-link` attribute that let's us write the same links like this: <a href="/page1" content-link>Page 1</a> <a href="/page2" content-link>Page 2</a> <a href="/page3" content-link>Page 3</a> 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) returnValue = compiler.callback.apply(nativeElement, [$jqueryElement, data($jqueryElement)]) for destructor in discoverDestructors(returnValue) addDestructor($jqueryElement, destructor) discoverDestructors = (returnValue) -> if u.isFunction(returnValue) [returnValue] else if u.isArray(returnValue) && u.all(returnValue, u.isFunction) returnValue else [] 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<Element>} [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: <span class="person" up-data="{ age: 18, name: 'Bob' }">Bob</span> 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: <div class="google-map" up-data="[ { lat: 48.36, lng: 10.99, title: 'Friedberg' }, { lat: 48.75, lng: 11.45, title: 'Ingolstadt' } ]"></div> 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