###** 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/) library for every element with a `grid` class, use this compiler: up.compiler('.grid', function(element) { new Masonry(element, { itemSelector: '.grid--item' }) }) 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. @module up.syntax ### up.syntax = do -> u = up.util e = up.element SYSTEM_MACRO_PRIORITIES = { '[up-back]': -100 # sets [up-href] to previous URL '[up-drawer]': -200 # sets [up-modal] and makes link followable '[up-dash]': -200 # sets [up-href] unless already set, also other [up-*] attributes '[up-expand]': -300 # distributes [up-*] attributes to parents '[data-method]': -400, # converts [data-method] to [up-method] only if link has followable [up-*] attributes '[data-confirm]': -400, # converts [data-conform] to [up-confirm] only if link has followable [up-*] attributes } compilers = [] macros = [] ###** Registers a function to be called when an element with the given selector is inserted into the DOM. Use compilers to activate your custom Javascript behavior on matching elements. You should migrate your [`DOMContentLoaded`](https://api.jquery.com/ready/) callbacks to compilers. This will make sure they run both at page load and when [a new fragment is inserted later](/a-up-target). It will also organize your JavaScript snippets by selector of affected elements. \#\#\# Example This jQuery compiler will insert the current time into a `
`: up.compiler('.current-time', function(element) { var now = new Date() element.textContent = now.toString() }) The compiler function will be called once for each matching element when the page loads, or when a matching fragment is [inserted](/up.replace) later. \#\#\# Integrating JavaScript libraries `up.compiler()` is a great way to integrate JavaScript libraries. 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) { lightboxify(element) }) \#\#\# 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 and 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 `.current-time` that updates the time every second, and cleans up once it's done. Note how it returns a function that calls `clearInterval`: up.compiler('.current-time', function(element) { function update() { var now = new Date() element.textContent = 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 be 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 }) }) }) @function up.compiler @param {string} selector The selector to match. @param {number} [options.priority=0] The priority of this compiler. 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. 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 cleans 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). @stable ### registerCompiler = (args...) -> compiler = buildCompiler(args) return insertCompiler(compilers, compiler) ###** Registers a function to be called when an element with the given selector is inserted into the DOM. The function is called with each matching element as a [jQuery object](https://learn.jquery.com/using-jquery-core/jquery-object/). If you're not using jQuery, use `up.compiler()` instead, which calls the compiler function with a native element. \#\#\# Example This jQuery compiler will insert the current time into a `
`: up.$compiler('.current-time', function($element) { var now = new Date() $element.text(now.toString()) }) @function up.$compiler @param {string} selector The selector to match. @param {Object} [options] See [`options` argument for `up.compiler()`](/up.compiler#parameters). @param {Function($element, data)} compiler The function to call when a matching element is inserted. See [`compiler` argument for `up.compiler()`](/up.compiler#parameters). @stable ### registerJQueryCompiler = (args...) -> compiler = registerCompiler(args...) compiler.jQuery = true compiler ###** Registers a [compiler](/up.compiler) that is run before all other compilers. Use `up.macro()` to register a compiler that sets multiple Unpoly 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.setAttribute('up-target', '.content') link.setAttribute('up-transition', 'cross-fade') link.setAttribute('up-duration', '300') }) Examples for built-in macros are [`a[up-dash]`](/a-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)} macro The function to call when a matching element is inserted. See [`up.compiler()`](/up.compiler#parameters) for details. @stable ### registerMacro = (args...) -> macro = buildCompiler(args) if up.framework.isBooting() macro.priority = detectSystemMacroPriority(macro.selector) || up.fail('Unregistered priority for system macro %o', macro.selector) return insertCompiler(macros, macro) ###** Registers a [compiler](/up.compiler) that is run before all other compilers. The compiler function is called with each matching element as a [jQuery object](https://learn.jquery.com/using-jquery-core/jquery-object/). If you're not using jQuery, use `up.macro()` instead, which calls the macro function with a native element. \#\#\# Example up.$macro('[content-link]', function($link) { $link.attr( 'up-target': '.content', 'up-transition': 'cross-fade', 'up-duration':'300' ) }) @function up.$macro @param {string} selector The selector to match. @param {Object} options See [`options` argument for `up.compiler()`](/up.compiler#parameters). @param {Function(element, data)} macro The function to call when a matching element is inserted. See [`compiler` argument for `up.compiler()`](/up.compiler#parameters). @stable ### registerJQueryMacro = (args...) -> macro = registerMacro(args...) macro.jQuery = true macro detectSystemMacroPriority = (macroSelector) -> for substr, priority of SYSTEM_MACRO_PRIORITIES if macroSelector.indexOf(substr) >= 0 return priority parseCompilerArgs = (args) -> selector = args.shift() callback = args.pop() options = u.extractOptions(args) return [selector, options, callback] buildCompiler = (args) -> [selector, options, callback] = parseCompilerArgs(args) options = u.options(options, selector: selector, isDefault: up.framework.isBooting(), priority: 0, batch: false keep: false, jQuery: false ) return u.assign(callback, options) insertCompiler = (queue, newCompiler) -> index = 0 while (existingCompiler = queue[index]) && (existingCompiler.priority >= newCompiler.priority) index += 1 queue.splice(index, 0, newCompiler) return newCompiler ###** 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) -> orderedCompilers = macros.concat(compilers) compileRun = new up.CompilePass(fragment, orderedCompilers, options) compileRun.compile() ###** Registers a function to be called when the given element is [destroyed](/up.destroy). The preferred way to register a destructor function is to `return` it from a [compiler function](/up.compiler). @function up.destructor @param {Element} element @param {Function|Array} destructor One or more destructor functions @internal ### registerDestructor = (element, destructor) -> unless destructors = element.upDestructors destructors = [] element.upDestructors = destructors element.classList.add('up-can-clean') if u.isArray(destructor) destructors.push(destructor...) else destructors.push(destructor) ###** Runs any destructor 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) -> cleanables = e.subtree(fragment, '.up-can-clean') u.each cleanables, (cleanable) -> if destructors = u.pluckKey(cleanable, 'upDestructors') destructor() for destructor in destructors cleanable.classList.remove('up-can-clean') ###** 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 `undefined` 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 `undefined` if the element has no (or an empty) `up-data` attribute. @experimental ### ###** If an element with an `up-data` attribute enters the DOM, Unpoly 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 be 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 ### readData = (elementOrSelector) -> element = e.get(elementOrSelector) e.jsonAttr(element, 'up-data') || {} ###** Resets the list of registered compiler directives to the moment when the framework was booted. @internal ### reset = -> compilers = u.filter(compilers, 'isDefault') macros = u.filter(macros, 'isDefault') up.on 'up:framework:reset', reset <% if ENV['JS_KNIFE'] %>knife: eval(Knife.point)<% end %> compiler: registerCompiler macro: registerMacro $compiler: registerJQueryCompiler $macro: registerJQueryMacro destructor: registerDestructor compile: compile clean: clean data: readData up.compiler = up.syntax.compiler up.$compiler = up.syntax.$compiler up.destructor = up.syntax.destructor up.macro = up.syntax.macro up.$macro = up.syntax.$macro