###* Registering behavior and custom elements ======================================== Up.js keeps a persistent Javascript environment during page transitions. To prevent memory leaks it is important to cleanly set up and tear down event handlers and custom elements. \#\#\# Incomplete documentation! We need to work on this page: - Better class-level introduction for this module @class up.magic ### up.magic = (-> u = up.util DESTROYABLE_CLASS = 'up-destroyable' DESTROYER_KEY = 'up-destroyer' ###* Binds an event handler to the document, which will be executed whenever the given event is triggered on the given selector: up.on('click', '.button', function(event, $element) { console.log("Someone clicked the button %o", $element); }); This is roughly equivalent to binding a jQuery element to `document`. \#\#\#\# 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: Bob Jim The JSON will parsed and handed to your event handler as a third argument: up.on('click', '.person', function(event, $element, data) { console.log("This is %o who is %o years old", data.name, data.age); }); \#\#\#\# Migrating jQuery event handlers to `up.on` Within the event handler, Up.js 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: $(document).on('click', '.button', function() { $(this).something(); }); ... you can simply copy the event handler to `up.on`: up.on('click', '.button', function() { $(this).something(); }); @method up.on @param {String} events A space-separated list of event names to bind. @param {String} selector The selector an on which the event must be triggered. @param {Function(event, $element, data)} behavior The handler that should be called. The function takes the affected element as the first argument (as a jQuery object). If the element has an `up-data` attribute, its value is parsed as JSON and passed as a second argument. ### liveDescriptions = [] defaultLiveDescriptions = null live = (events, selector, behavior) -> # Silently discard any awakeners that are registered on unsupported browsers return unless up.browser.isSupported() description = [ events, selector, (event) -> behavior.apply(this, [event, $(this), data(this)]) ] liveDescriptions.push(description) $(document).on(description...) ###* Registers a function to be called whenever an element with the given selector is inserted into the DOM through Up.js. This 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 `[rel=lightbox]` attribute: River Ocean This Javascript will do exactly that: up.awaken('a[rel=lightbox]', function($element) { $element.lightboxify(); }); Note that within the awakener, Up.js will bind `this` to the native DOM element to help you migrate your existing jQuery code to this new syntax. \#\#\#\# Custom elements You can also use `up.awaken` to implement custom elements like this: Here is the Javascript that inserts the current time into to these elements: up.awaken('current-time', function($element) { var now = new Date(); $element.text(now.toString())); }); \#\#\#\# Cleaning up after yourself If your awakener returns a function, Up.js will use this as a *destructor* to clean up if the element leaves the DOM. Note that in Up.js 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 awakeners 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.awaken('current-time', 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 event handler as a second argument: up.awaken('.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.on` Within the awakener, Up.js will bind `this` to the native DOM element to help you migrate your existing jQuery code to this new syntax. @method up.awaken @param {String} selector The selector to match. @param {Boolean} [options.batch=false] If set to `true` and a fragment insertion contains multiple elements matching the selector, `awakener` is only called once with a jQuery collection containing all matching elements. @param {Function($element, data)} awakener 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` attribute, its value is parsed as JSON and passed as a second argument. The function may return another function that destroys the awakened object when it is removed from the DOM, by clearing global state such as time-outs and event handlers bound to the document. ### awakeners = [] defaultAwakeners = null awaken = (selector, args...) -> # Silently discard any awakeners that are registered on unsupported browsers return unless up.browser.isSupported() awakener = args.pop() options = u.options(args[0], batch: false) awakeners.push selector: selector callback: awakener batch: options.batch applyAwakener = (awakener, $jqueryElement, nativeElement) -> u.debug "Applying awakener %o on %o", awakener.selector, nativeElement destroyer = awakener.callback.apply(nativeElement, [$jqueryElement, data($jqueryElement)]) if u.isFunction(destroyer) $jqueryElement.addClass(DESTROYABLE_CLASS) $jqueryElement.data(DESTROYER_KEY, destroyer) compile = ($fragment) -> u.debug "Compiling fragment %o", $fragment for awakener in awakeners $matches = u.findWithSelf($fragment, awakener.selector) if $matches.length if awakener.batch applyAwakener(awakener, $matches, $matches.get()) else $matches.each -> applyAwakener(awakener, $(this), this) destroy = ($fragment) -> u.findWithSelf($fragment, ".#{DESTROYABLE_CLASS}").each -> $element = $(this) destroyer = $element.data(DESTROYER_KEY) destroyer() ###* Checks if the given element has an `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. The API of this method is likely to change in the future, so we can support getting or setting individual keys. @protected @method up.magic.data @param {String|Element|jQuery} elementOrSelector ### ### Stores a JSON-string with the element. 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.awaken`](/up.magic#up.magic.awaken) handlers. 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.magic#up.on) handlers. @ujs @method [up-data] @param {JSON} [up-data] ### 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 [`up.bus.reset`](/up.bus#up.bus.reset). @private @method up.magic.snapshot ### snapshot = -> defaultLiveDescriptions = u.copy(liveDescriptions) defaultAwakeners = u.copy(awakeners) ###* Resets the list of registered event listeners to the moment when the framework was booted. @private @method up.magic.reset ### reset = -> for description in liveDescriptions unless u.contains(defaultLiveDescriptions, description) $(document).off(description...) liveDescriptions = u.copy(defaultLiveDescriptions) awakeners = u.copy(defaultAwakeners) ###* Sends a notification that the given element has been inserted into the DOM. This causes Up.js to compile the fragment (apply event listeners, etc.). This method is called automatically if you change elements through other Up.js methods. You will only need to call this if you manipulate the DOM without going through Up.js. @method up.ready @param {String|Element|jQuery} selectorOrFragment ### ready = (selectorOrFragment) -> $fragment = $(selectorOrFragment) up.bus.emit('fragment:ready', $fragment) $fragment onEscape = (handler) -> live('keydown', 'body', (event) -> if u.escapePressed(event) handler(event) ) up.bus.on 'app:ready', (-> ready(document.body)) up.bus.on 'fragment:ready', compile up.bus.on 'fragment:destroy', destroy up.bus.on 'framework:ready', snapshot up.bus.on 'framework:reset', reset awaken: awaken on: live ready: ready onEscape: onEscape data: data )() up.awaken = up.magic.awaken up.on = up.magic.on up.ready = up.magic.ready