dist/up.js in upjs-rails-0.12.5 vs dist/up.js in upjs-rails-0.13.0

- old
+ new

@@ -23,11 +23,11 @@ (function() { var slice = [].slice; up.util = (function($) { - var $createElementFromSelector, ANIMATION_PROMISE_KEY, CONSOLE_PLACEHOLDERS, ajax, any, cache, castedAttr, clientSize, compact, config, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, debug, detect, each, emptyJQuery, endsWith, error, escapePressed, evalConsoleTemplate, extend, findWithSelf, finishCssAnimate, fixedToAbsolute, forceCompositing, identity, ifGiven, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isNumber, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, last, locationFromXhr, map, measure, memoize, merge, methodFromXhr, multiSelector, nextFrame, normalizeMethod, normalizeUrl, nullJquery, offsetParent, once, only, option, options, parseUrl, presence, presentAttr, remove, resolvableWhen, resolvedDeferred, resolvedPromise, scrollbarWidth, select, setMissingAttrs, startsWith, temporaryCss, times, toArray, trim, unJquery, uniq, unresolvablePromise, unwrapElement, warn; + var $createElementFromSelector, ANIMATION_PROMISE_KEY, CONSOLE_PLACEHOLDERS, ajax, any, cache, castedAttr, clientSize, compact, config, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, debug, detect, each, emptyJQuery, endsWith, error, escapePressed, evalConsoleTemplate, extend, findWithSelf, finishCssAnimate, fixedToAbsolute, forceCompositing, identity, ifGiven, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isNumber, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, last, locationFromXhr, map, measure, memoize, merge, methodFromXhr, multiSelector, nextFrame, normalizeMethod, normalizeUrl, nullJquery, offsetParent, once, only, option, options, parseUrl, presence, presentAttr, remove, resolvableWhen, resolvedDeferred, resolvedPromise, scrollbarWidth, select, setMissingAttrs, startsWith, temporaryCss, times, titleFromXhr, toArray, trim, unJquery, uniq, unresolvablePromise, unwrapElement, warn; memoize = function(func) { var cache, cached; cache = void 0; cached = false; return function() { @@ -42,13 +42,12 @@ }; }; ajax = function(request) { request = copy(request); if (request.selector) { - request.headers = { - "X-Up-Selector": request.selector - }; + request.headers || (request.headers = {}); + request.headers['X-Up-Selector'] = request.selector; } return $.ajax(request); }; /** @@ -259,23 +258,31 @@ } } return arg; }); }; - createSelectorFromElement = function($element) { - var classString, classes, id, j, klass, len, selector; - debug("Creating selector from element %o", $element); - classes = (classString = $element.attr("class")) ? classString.split(" ") : []; - id = $element.attr("id"); - selector = $element.prop("tagName").toLowerCase(); - if (id) { - selector += "#" + id; + createSelectorFromElement = function(element) { + var $element, classString, classes, id, j, klass, len, name, selector, upId; + $element = $(element); + selector = void 0; + debug("Creating selector from element %o", $element.get(0)); + if (upId = presence($element.attr("up-id"))) { + selector = "[up-id='" + upId + "']"; + } else if (id = presence($element.attr("id"))) { + selector = "#" + id; + } else if (name = presence($element.attr("name"))) { + selector = "[name='" + name + "']"; + } else if (classString = presence($element.attr("class"))) { + classes = classString.split(' '); + selector = ''; + for (j = 0, len = classes.length; j < len; j++) { + klass = classes[j]; + selector += "." + klass; + } + } else { + selector = $element.prop('tagName').toLowerCase(); } - for (j = 0, len = classes.length; j < len; j++) { - klass = classes[j]; - selector += "." + klass; - } return selector; }; createElementFromHtml = function(html) { var anything, bodyElement, bodyMatch, bodyPattern, capture, closeTag, headElement, htmlElement, openTag, titleElement, titleMatch, titlePattern; openTag = function(tag) { @@ -779,10 +786,13 @@ } }; locationFromXhr = function(xhr) { return xhr.getResponseHeader('X-Up-Location'); }; + titleFromXhr = function(xhr) { + return xhr.getResponseHeader('X-Up-Title'); + }; methodFromXhr = function(xhr) { return xhr.getResponseHeader('X-Up-Method'); }; only = function() { var filtered, j, key, keys, len, object; @@ -934,10 +944,13 @@ @param {Number|Function} [config.expiry] The number of milliseconds after which a cache entry will be discarded. @param {String} [config.log] A prefix for log entries printed by this cache object. + @param {Function<Object>} [config.key] + A function that takes an argument and returns a `String` key + for storage. If omitted, `toString()` is called on the argument. */ cache = function(config) { var alias, clear, expiryMilis, get, isFresh, keys, log, maxSize, normalizeStoreKey, set, store, timestamp; if (config == null) { config = {}; @@ -1184,10 +1197,11 @@ endsWith: endsWith, isArray: isArray, toArray: toArray, castedAttr: castedAttr, locationFromXhr: locationFromXhr, + titleFromXhr: titleFromXhr, methodFromXhr: methodFromXhr, clientSize: clientSize, only: only, trim: trim, unresolvablePromise: unresolvablePromise, @@ -1323,12 +1337,13 @@ /** Returns whether Up.js supports the current browser. Currently Up.js supports IE9 with jQuery 1.9+. - On older browsers Up.js will prevent itself from [booting](/up.boot), - leaving you with a classic server-side application. + On older browsers Up.js will prevent itself from [booting](/up.boot) + and ignores all registered [event handlers](/up.on) and [compilers](/up.compiler). + This leaves you with a classic server-side application. @function up.browser.isSupported */ isSupported = function() { return (!isIE8OrWorse()) && isRecentJQuery(); @@ -1659,23 +1674,41 @@ up.boot = up.bus.boot; }).call(this); /** -Custom elements -=============== - +Enhancing 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. +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. -\#\#\# Incomplete documentation! +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: -We need to work on this page: + <a href="river.png" class="lightbox">River</a> + <a href="ocean.png" class="lightbox">Ocean</a> -- Better class-level introduction for this module +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 Up.js](/up.replace) +later. + @class up.syntax */ (function() { var slice = [].slice; @@ -1696,26 +1729,27 @@ Compiler functions will be called on matching elements when the page loads, or whenever a matching fragment is [updated through Up.js](/up.replace) later. - If you have used Angular.js before, this resembles [Angular directives](https://docs.angularjs.org/guide/directive). + 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 `[rel=lightbox]` attribute: + do this for all links with an `lightbox` class: - <a href="river.png" rel="lightbox">River</a> - <a href="ocean.png" rel="lightbox">Ocean</a> + <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[rel=lightbox]', function($element) { + up.compiler('a.lightbox', function($element) { $element.lightboxify(); }); \#\#\#\# Custom elements @@ -1956,21 +1990,21 @@ reset = function() { return compilers = u.copy(defaultCompilers); }; /** - 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.). + Compiles a page fragment that has been inserted into the DOM + without Up.js. **As long as you manipulate the DOM using Up.js, you will never need to call this method.** You only need to use `up.hello` if the - DOM is manipulated without Up.js' involvement, e.g. by plugin code that - is not aware of Up.js: + DOM is manipulated without Up.js' involvement, e.g. by setting + the `innerHTML` property or calling jQuery methods like + `html`, `insertAfter` or `appendTo`: - // Add an element with naked jQuery, without going through Upjs: - $element = $('<div>...</div>').appendTo(document.body); + $element = $('.element'); + $element.html('<div>...</div>'); up.hello($element); This function emits the [`up:fragment:inserted`](/up:fragment:inserted) event. @@ -2052,11 +2086,11 @@ var buildState, config, currentUrl, isCurrentUrl, manipulate, nextPreviousUrl, normalizeUrl, observeNewUrl, pop, previousUrl, push, register, replace, reset, restoreStateOnPop, u; u = up.util; /** @property up.history.config - @param {Array<String>} [config.popTargets=['body']] + @param {Array} [config.popTargets=['body']] An array of CSS selectors to replace when the user goes back in history. @param {Boolean} [config.restoreScroll=true] Whether to restore the known scroll positions when the user goes back or forward in history. @@ -2258,20 +2292,20 @@ /** Configures the application layout. @property up.layout.config - @param {Array<String>} [config.viewports] + @param {Array} [config.viewports] An array of CSS selectors that find viewports (containers that scroll their contents). - @param {Array<String>} [config.fixedTop] + @param {Array} [config.fixedTop] An array of CSS selectors that find elements fixed to the top edge of the screen (using `position: fixed`). - @param {Array<String>} [config.fixedBottom] + @param {Array} [config.fixedBottom] An array of CSS selectors that find elements fixed to the bottom edge of the screen (using `position: fixed`). - @param {Array<String>} [config.anchoredRight] + @param {Array} [config.anchoredRight] An array of CSS selectors that find elements anchored to the right edge of the screen (using `position: fixed` or `position: absolute`). @param {Number} [config.duration] The duration of the scrolling animation in milliseconds. Setting this to `0` will disable scrolling animations. @@ -2836,11 +2870,11 @@ @class up.flow */ (function() { up.flow = (function($) { - var autofocus, destroy, elementsInserted, findOldFragment, first, fragmentNotFound, implant, isRealElement, parseImplantSteps, parseResponse, reload, replace, setSource, source, swapElements, u; + var autofocus, destroy, elementsInserted, findOldFragment, first, fragmentNotFound, implant, isRealElement, parseImplantSteps, parseResponse, reload, replace, resolveSelector, setSource, source, swapElements, u; u = up.util; setSource = function(element, sourceUrl) { var $element; $element = $(element); if (u.isPresent(sourceUrl)) { @@ -2853,10 +2887,32 @@ $element = $(selectorOrElement).closest('[up-source]'); return u.presence($element.attr("up-source")) || up.browser.url(); }; /** + @function up.flow.resolveSelector + @private + */ + resolveSelector = function(selectorOrElement, options) { + var origin, originSelector, selector; + if (u.isString(selectorOrElement)) { + selector = selectorOrElement; + if (u.contains(selector, '&')) { + if (origin = u.presence(options.origin)) { + originSelector = u.createSelectorFromElement(origin); + selector = selector.replace(/\&/, originSelector); + } else { + u.error("Found origin reference %o in selector %o, but options.origin is missing", '&', selector); + } + } + } else { + selector = u.createSelectorFromElement(selectorOrElement); + } + return selector; + }; + + /** Replaces elements on the current page with corresponding elements from a new page fetched from the server. The current and new elements must have the same CSS selector. @@ -2883,10 +2939,45 @@ <div class="two">new two</div> Note how only `.two` has changed. The update for `.one` was discarded, since it didn't match the selector. + \#\#\#\# Appending or prepending instead of replacing + + By default Up.js 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: + + <ul class="tasks"> + <li>Wash car</li> + <li>Purchase supplies</li> + <li>Fix tent</li> + </ul> + + In order to append more items from a URL, replace into + the `.tasks:after` selector: + + up.replace('.tasks:after', '/page/2') + + \#\#\#\# Setting the window title from the server + + If the `replace` call changes history, the document title will be set + to the contents of a `<title>` tag in the response. + + The server can also change the document title by setting + an `X-Up-Title` header in the response. + + \#\#\#\# Optimizing response rendering + + The server is free to optimize Up.js requests by only rendering the HTML fragment + that is being updated. The request's `X-Up-Selector` header will contain + the CSS selector for the updating fragment. + \#\#\#\# Events Up.js will emit [`up:fragment:destroyed`](/up:fragment:destroyed) on the element that was replaced and [`up:fragment:inserted`](/up:fragment:inserted) on the new element that replaces it. @@ -2913,19 +3004,25 @@ of all the viewports around or below the updated element. The position will be reset to the last known top position before a previous history change for the current URL. @param {Boolean} [options.cache] Whether to use a [cached response](/up.proxy) if available. + @param {Element|jQuery} [options.origin] + The element that triggered the replacement. The element's selector will + be substituted for the `&` shorthand in the target selector. @param {String} [options.historyMethod='push'] + @param {Object} [options.headers={}] + An object of additional header key/value pairs to send along + with the request. @return {Promise} A promise that will be resolved when the page has been updated. */ replace = function(selectorOrElement, url, options) { var promise, request, selector; u.debug("Replace %o with %o (options %o)", selectorOrElement, url, options); options = u.options(options); - selector = u.presence(selectorOrElement) ? selectorOrElement : u.createSelectorFromElement($(selectorOrElement)); + selector = resolveSelector(selectorOrElement, options); if (!up.browser.canPushState() && options.history !== false) { if (!options.preload) { up.browser.loadPage(url, u.only(options, 'method')); } return u.unresolvablePromise(); @@ -2933,11 +3030,12 @@ request = { url: url, method: options.method, selector: selector, cache: options.cache, - preload: options.preload + preload: options.preload, + headers: options.headers }; promise = up.proxy.ajax(request); promise.done(function(html, textStatus, xhr) { var currentLocation, newRequest; if (currentLocation = u.locationFromXhr(xhr)) { @@ -2954,10 +3052,11 @@ options.history = url; } if (options.source !== false) { options.source = url; } + options.title || (options.title = u.titleFromXhr(xhr)); if (!options.preload) { return implant(selector, html, options); } }); promise.fail(u.error); @@ -2992,17 +3091,18 @@ Note how only `.two` has changed. The update for `.one` was discarded, since it didn't match the selector. @function up.flow.implant @protected - @param {String} selector + @param {String|Element|jQuery} selectorOrElement @param {String} html @param {Object} [options] See options for [`up.replace`](/up.replace). */ - implant = function(selector, html, options) { - var $new, $old, j, len, ref, response, results, step; + implant = function(selectorOrElement, html, options) { + var $new, $old, j, len, ref, response, results, selector, step; + selector = resolveSelector(selectorOrElement, options); options = u.options(options, { historyMethod: 'push' }); options.source = u.option(options.source, options.history); response = parseResponse(html); @@ -3039,11 +3139,11 @@ var ref; return (ref = htmlElement.querySelector("title")) != null ? ref.textContent : void 0; }, find: function(selector) { var child; - if (child = htmlElement.querySelector(selector)) { + if (child = $.find(selector, htmlElement)[0]) { return $(child); } else { return u.error("Could not find selector %o in response %o", selector, html); } } @@ -3092,25 +3192,30 @@ } }); } }; parseImplantSteps = function(selector, options) { - var comma, disjunction, i, j, len, results, selectorAtom, selectorParts, transition, transitionString, transitions; + var comma, disjunction, i, j, len, pseudoClass, results, selectorAtom, selectorParts, transition, transitionString, transitions; transitionString = options.transition || options.animation || 'none'; comma = /\ *,\ */; disjunction = selector.split(comma); if (u.isPresent(transitionString)) { transitions = transitionString.split(comma); } results = []; for (i = j = 0, len = disjunction.length; j < len; i = ++j) { selectorAtom = disjunction[i]; selectorParts = selectorAtom.match(/^(.+?)(?:\:(before|after))?$/); + selector = selectorParts[1]; + if (selector === 'html') { + selector = 'body'; + } + pseudoClass = selectorParts[2]; transition = transitions[i] || u.last(transitions); results.push({ - selector: selectorParts[1], - pseudoClass: selectorParts[2], + selector: selector, + pseudoClass: pseudoClass, transition: transition }); } return results; }; @@ -3132,19 +3237,31 @@ Returns the first element matching the given selector. Excludes elements that also match `.up-ghost` or `.up-destroying` or that are children of elements with these selectors. + If the given argument is already a jQuery collection (or an array + of DOM elements), the first element matching these conditions + is returned. + Returns `undefined` if no element matches these conditions. @protected @function up.first - @param {String} selector + @param {String|Element|jQuery} selectorOrElement + @return {jQuery} + The first element that is neither a ghost or being destroyed, + or `undefined` if no such element was given. */ - first = function(selector) { + first = function(selectorOrElement) { var $element, $match, element, elements, j, len; - elements = $(selector).get(); + elements = void 0; + if (u.isString(selectorOrElement)) { + elements = $(selectorOrElement).get(); + } else { + elements = selectorOrElement; + } $match = void 0; for (j = 0, len = elements.length; j < len; j++) { element = elements[j]; $element = $(element); if (isRealElement($element)) { @@ -3271,11 +3388,12 @@ return { replace: replace, reload: reload, destroy: destroy, implant: implant, - first: first + first: first, + resolveSelector: resolveSelector }; })(jQuery); up.replace = up.flow.replace; @@ -4022,11 +4140,11 @@ If the size is exceeded, the oldest items will be dropped from the cache. @param {Number} [config.cacheExpiry=300000] The number of milliseconds until a cache entry expires. Defaults to 5 minutes. @param {Number} [config.busyDelay=300] - How long the proxy waits until emitting the `proxy:busy` [event](/up.bus). + How long the proxy waits until emitting the [`up:proxy:busy` event](/up:proxy:busy). Use this to prevent flickering of spinners. */ config = u.config({ busyDelay: 300, preloadDelay: 75, @@ -4050,11 +4168,33 @@ /** @protected @function up.proxy.get */ - get = cache.get; + get = function(request) { + var candidate, candidates, i, len, requestForBody, requestForHtml, response; + request = normalizeRequest(request); + candidates = [request]; + if (request.selector !== 'html') { + requestForHtml = u.merge(request, { + selector: 'html' + }); + candidates.push(requestForHtml); + if (request.selector !== 'body') { + requestForBody = u.merge(request, { + selector: 'body' + }); + candidates.push(requestForBody); + } + } + for (i = 0, len = candidates.length; i < len; i++) { + candidate = candidates[i]; + if (response = cache.get(candidate)) { + return response; + } + } + }; /** @protected @function up.proxy.set */ @@ -4111,27 +4251,30 @@ not be cached and the entire cache will be cleared. Only requests with a method of `GET`, `OPTIONS` and `HEAD` are considered to be read-only. If a network connection is attempted, the proxy will emit - a `proxy:load` event with the `request` as its argument. - Once the response is received, a `proxy:receive` event will + a `up:proxy:load` event with the `request` as its argument. + Once the response is received, a `up:proxy:receive` event will be emitted. @function up.proxy.ajax @param {String} request.url @param {String} [request.method='GET'] @param {String} [request.selector] @param {Boolean} [request.cache] Whether to use a cached response, if available. If set to `false` a network connection will always be attempted. + @param {Object} [request.headers={}] + An object of additional header key/value pairs to send along + with the request. */ ajax = function(options) { var forceCache, ignoreCache, pending, promise, request; forceCache = options.cache === true; ignoreCache = options.cache === false; - request = u.only(options, 'url', 'method', 'data', 'selector', '_normalized'); + request = u.only(options, 'url', 'method', 'data', 'selector', 'headers', '_normalized'); pending = true; if (!isIdempotent(request) && !forceCache) { clear(); promise = load(request); } else if ((promise = get(request)) && !ignoreCache) { @@ -4153,11 +4296,11 @@ /** Returns `true` if the proxy is not currently waiting for a request to finish. Returns `false` otherwise. - The proxy will also emit an `proxy:idle` [event](/up.bus) if it + The proxy will also emit an [`up:proxy:idle` event](/up:proxy:idle) if it used to busy, but is now idle. @function up.proxy.idle @return {Boolean} Whether the proxy is idle */ @@ -4167,12 +4310,12 @@ /** Returns `true` if the proxy is currently waiting for a request to finish. Returns `false` otherwise. - The proxy will also emit an `proxy:busy` [event](/up.bus) if it - used to idle, but is now busy. + The proxy will also emit an [`up:proxy:busy` event](/up:proxy:busy) if it + used to be idle, but is now busy. @function up.proxy.busy @return {Boolean} Whether the proxy is busy */ busy = function() { @@ -4224,11 +4367,11 @@ /** This event is [emitted]/(up.emit) when [AJAX requests](/up.proxy.ajax) have [taken long to finish](/up:proxy:busy), but have finished now. - @event up:proxy:busy + @event up:proxy:idle */ load = function(request) { var promise; u.debug('Loading URL %o', request.url); up.emit('up:proxy:load', request); @@ -4486,16 +4629,19 @@ @param {String} [options.easing] The timing function that controls the transition's acceleration. [`up.morph`](/up.morph). @param {Element|jQuery|String} [options.reveal] Whether to reveal the target element within its viewport before updating. @param {Boolean} [options.restoreScroll] - If set to `true`, this will attempt to [`restore scroll positions`](/up.restoreScroll) + If set to `true`, this will attempt to [restore scroll positions](/up.restoreScroll) previously seen on the destination URL. @param {Boolean} [options.cache] Whether to force the use of a cached response (`true`) or never use the cache (`false`) or make an educated guess (`undefined`). + @param {Object} [options.headers={}] + An object of additional header key/value pairs to send along + with the request. */ follow = function(linkOrSelector, options) { var $link, selector, url; $link = $(linkOrSelector); options = u.options(options); @@ -4505,10 +4651,11 @@ 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 = u.merge(options, up.motion.animateOptions(options, $link)); return up.replace(selector, url, options); }; /** @@ -4784,45 +4931,58 @@ up.follow = up.link.follow; }).call(this); /** -Forms and controls -================== +Forms +===== -Up.js comes with functionality to submit forms without -leaving the current page. This means you can replace page fragments, +Up.js comes with functionality to [submit](/form-up-target) and [validate](/up-validate) +forms without leaving the current page. This means you can replace page fragments, open dialogs with sub-forms, etc. all without losing form state. - -\#\#\# Incomplete documentation! - -We need to work on this page: - -- Explain how to display form errors -- Explain that the server needs to send 2xx or 5xx status codes so - Up.js can decide whether the form submission was successful -- Explain that the server needs to send `X-Up-Location` and `X-Up-Method` headers - if an successful form submission resulted in a redirect -- Examples @class up.form */ (function() { up.form = (function($) { - var observe, submit, u; + var config, observe, reset, resolveValidateTarget, submit, u, validate; u = up.util; /** - Submits a form using the Up.js flow: + Sets default options for form submission and validation. - up.submit('form.new_user') + @property up.form.config + @param {Array} [config.validateTargets=['[up-fieldset]:has(&)', 'fieldset:has(&)', 'label:has(&)', 'form:has(&)']] + An array of CSS selectors that are searched around a form field + that wants to [validate](/up.validate). The first matching selector + will be updated with the validation messages from the server. + By default this looks for a `<fieldset>`, `<label>` or `<form>` + around the validating input field, or any element with an + `up-fieldset` attribute. + */ + config = u.config({ + validateTargets: ['[up-fieldset]:has(&)', 'fieldset:has(&)', 'label:has(&)', 'form:has(&)'] + }); + reset = function() { + return config.reset(); + }; + + /** + Submits a form via AJAX and updates a page fragment with the response. + + up.submit('form.new-user', { target: '.main' }) + Instead of loading a new page, the form is submitted via AJAX. The response is parsed for a CSS selector and the matching elements will replace corresponding elements on the current page. + The UJS variant of this is the [`form[up-target]`](/form-up-target) selector. + See the documentation for [`form[up-target]`](/form-up-target) for more + information on how AJAX form submissions work in Up.js. + @function up.submit @param {Element|jQuery|String} formOrSelector A reference or selector for the form to submit. If the argument points to an element that is not a form, Up.js will search its ancestors for the closest form. @@ -4868,43 +5028,56 @@ or never use the cache (`false`) or make an educated guess (`undefined`). By default only responses to `GET` requests are cached for a few minutes. + @param {Object} [options.headers={}] + An object of additional header key/value pairs to send along + with the request. @return {Promise} A promise for the successful form submission. */ submit = function(formOrSelector, options) { - var $form, failureSelector, failureTransition, historyOption, httpMethod, implantOptions, request, successSelector, successTransition, successUrl, url, useCache; + var $form, failureSelector, failureTransition, hasFileInputs, headers, historyOption, httpMethod, implantOptions, request, successSelector, successTransition, successUrl, url, useCache; $form = $(formOrSelector).closest('form'); options = u.options(options); - successSelector = u.option(options.target, $form.attr('up-target'), 'body'); - failureSelector = u.option(options.failTarget, $form.attr('up-fail-target'), function() { + successSelector = up.flow.resolveSelector(u.option(options.target, $form.attr('up-target'), 'body'), options); + failureSelector = up.flow.resolveSelector(u.option(options.failTarget, $form.attr('up-fail-target'), function() { return u.createSelectorFromElement($form); - }); + }), options); historyOption = u.option(options.history, u.castedAttr($form, 'up-history'), true); successTransition = u.option(options.transition, u.castedAttr($form, 'up-transition')); failureTransition = u.option(options.failTransition, u.castedAttr($form, 'up-fail-transition'), successTransition); httpMethod = u.option(options.method, $form.attr('up-method'), $form.attr('data-method'), $form.attr('method'), 'post').toUpperCase(); + headers = u.option(options.headers, {}); implantOptions = {}; implantOptions.reveal = u.option(options.reveal, u.castedAttr($form, 'up-reveal'), true); implantOptions.cache = u.option(options.cache, u.castedAttr($form, 'up-cache')); implantOptions.restoreScroll = u.option(options.restoreScroll, u.castedAttr($form, 'up-restore-scroll')); + implantOptions.origin = u.option(options.origin, $form); implantOptions = u.extend(implantOptions, up.motion.animateOptions(options, $form)); useCache = u.option(options.cache, u.castedAttr($form, 'up-cache')); url = u.option(options.url, $form.attr('action'), up.browser.url()); + hasFileInputs = $form.find('input[type=file]').length; + if (options.validate) { + headers['X-Up-Validate'] = options.validate; + if (hasFileInputs) { + return u.unresolvablePromise(); + } + } $form.addClass('up-active'); - if (!up.browser.canPushState() && historyOption !== false) { + if (hasFileInputs || (!up.browser.canPushState() && historyOption !== false)) { $form.get(0).submit(); - return; + return u.unresolvablePromise(); } request = { url: url, method: httpMethod, data: $form.serialize(), selector: successSelector, - cache: useCache + cache: useCache, + headers: headers }; successUrl = function(xhr) { var currentLocation; url = void 0; if (u.isGiven(historyOption)) { @@ -4939,11 +5112,15 @@ /** Observes a form field and runs a callback when its value changes. This is useful for observing text fields while the user is typing. - For instance, the following would submit the form whenever the + The UJS variant of this is the [`up-observe`](/up-observe) attribute. + + \#\#\#\# Example + + The following would submit the form whenever the text field value changes: up.observe('input[name=query]', { change: function(value, $input) { up.submit($input) } }); @@ -4967,11 +5144,10 @@ up.observe('input', { delay: 100, change: function(value, $input) { up.submit($input) } }); - @function up.observe @param {Element|jQuery|String} fieldOrSelector @param {Function(value, $field)|String} options.change The callback to execute when the field's value changes. If given as a function, it must take two arguments (`value`, `$field`). @@ -5041,19 +5217,135 @@ changeEvents = up.browser.canInputEvent() ? 'input change' : 'input change keypress paste cut click propertychange'; $field.on(changeEvents, check); check(); return clearTimer; }; + resolveValidateTarget = function($field, options) { + var target; + target = u.option(options.target, $field.attr('up-validate')); + if (u.isBlank(target)) { + target || (target = u.detect(config.validateTargets, function(defaultTarget) { + var resolvedDefault; + resolvedDefault = up.flow.resolveSelector(defaultTarget, options); + return $field.closest(resolvedDefault).length; + })); + } + if (u.isBlank(target)) { + error('Could not find default validation target for %o (tried ancestors %o)', $field, config.validateTargets); + } + if (!u.isString(target)) { + target = u.createSelectorFromElement(target); + } + return target; + }; /** - Submits the form through AJAX, searches the response for the selector - given in `up-target` and [replaces](/up.replace) the selector content in the current page: + Performs a server-side validation of a form and update the form + with validation messages. + `up.validate` submits the given field's form with an additional `X-Up-Validate` + HTTP header. Upon seeing this header, the server is expected to validate (but not save) + the form submission and render a new copy of the form with validation errors. + + The UJS variant of this is the [`[up-validate]`](/up-validate) selector. + See the documentation for [`[up-validate]`](/up-validate) for more information + on how server-side validation works in Up.js. + + \#\#\#\# Example + + up.validate('input[name=email]', { target: '.email-errors' }) + + @function up.validate + @param {String|Element|jQuery} fieldOrSelector + @param {String|Element|jQuery} [options.target] + @return {Promise} + A promise that is resolved when the server-side + validation is received and the form was updated. + */ + validate = function(fieldOrSelector, options) { + var $field, $form, promise; + $field = $(fieldOrSelector); + options = u.options(options); + options.origin = $field; + options.target = resolveValidateTarget($field, options); + options.failTarget = options.target; + options.history = false; + options.headers = u.option(options.headers, {}); + options.validate = $field.attr('name') || '__none__'; + options = u.merge(options, up.motion.animateOptions(options, $field)); + $form = $field.closest('form'); + promise = up.submit($form, options); + return promise; + }; + + /** + Forms with an `up-target` attribute are [submitted via AJAX](/up.submit) + instead of triggering a full page reload. + <form method="post" action="/users" up-target=".main"> ... </form> + The server response is searched for the selector given in `up-target`. + The selector content is then [replaced](/up.replace) in the current page. + + The programmatic variant of this is the [`up.submit`](/up.submit) function. + + \#\#\#\# Validation errors + + When the server was unable to save the form due to invalid data, + it will usually re-render an updated copy of the form with + validation messages. + + For Up.js to be able to pick up a validation failure, + the form must be re-rendered with a non-200 HTTP status code. + We recommend to use either 400 (bad request) or + 422 (unprocessable entity). + + In Ruby on Rails, you can pass a + [`:status` option to `render`](http://guides.rubyonrails.org/layouts_and_rendering.html#the-status-option) + for this: + + class UsersController < ApplicationController + + def create + user_params = params[:user].permit(:email, :password) + @user = User.new(user_params) + if @user.save? + sign_in @user + else + render 'form', status: :bad_request + end + end + + end + + Note that you can also use the + [`up-validate`](/up-validate) attribute to perform server-side + validations while the user is completing fields. + + \#\#\#\# Redirects + + Up.js requires two additional response headers to detect redirects, + which are otherwise undetectable for an AJAX client. + + When the form's action performs a redirect, the server should echo + the new request's URL as a response header `X-Up-Location` + and the request's HTTP method as `X-Up-Method`. + + If you are using Up.js via the `upjs-rails` gem, these headers + are set automatically for every request. + + \#\#\#\# Giving feedback while the form is processing + + The `<form>` element will be assigned a CSS class `up-active` while + the submission is loading. + + You can also [implement a spinner](/up.proxy/#spinners) + by [listening](/up.on) to the [`up:proxy:busy`](/up:proxy:busy) + and [`up:proxy:idle`](/up:proxy:idle) events. + @selector form[up-target] @param {String} up-target The selector to [replace](/up.replace) if the form submission is successful (200 status code). @param {String} [up-fail-target] The selector to [replace](/up.replace) if the form submission is not successful (non-200 status code). @@ -5085,48 +5377,200 @@ event.preventDefault(); return submit($form); }); /** + When a form field with this attribute is changed, + the form is validated on the server and is updated with + validation messages. + + The programmatic variant of this is the [`up.validate`](/up.validate) function. + + \#\#\#\# Example + + Let's look at a standard registration form that asks for an e-mail and password: + + <form action="/users"> + + <label> + E-mail: <input type="text" name="email" /> + </label> + + <label> + Password: <input type="password" name="password" /> + </label> + + <button type="submit">Register</button> + + </form> + + When the user changes the `email` field, we want to validate that + the e-mail address is valid and still available. Also we want to + change the `password` field for the minimum required password length. + We can do this by giving both fields an `up-validate` attribute: + + <form action="/users"> + + <label> + E-mail: <input type="text" name="email" up-validate /> + </label> + + <label> + Password: <input type="password" name="password" up-validate /> + </label> + + <button type="submit">Register</button> + + </form> + + Whenever a field with `up-validate` changes, the form is POSTed to + `/users` with an additional `X-Up-Validate` HTTP header. + Upon seeing this header, the server is expected to validate (but not save) + the form submission and render a new copy of the form with validation errors. + + In Ruby on Rails the processing action should behave like this: + + class UsersController < ApplicationController + + * This action handles POST /users + def create + user_params = params[:user].permit(:email, :password) + @user = User.new(user_params) + if request.headers['X-Up-Validate'] + @user.valid? # run validations, but don't save to the database + render 'form' # render form with error messages + elsif @user.save? + sign_in @user + else + render 'form', status: :bad_request + end + end + + end + + Note that if you're using the `upjs-rails` gem you can simply say `up.validate?` + instead of manually checking for `request.headers['X-Up-Validate']`. + + The server now renders an updated copy of the form with eventual validation errors: + + <form action="/users"> + + <label class="has-error"> + E-mail: <input type="text" name="email" value="foo@bar.com" /> + Has already been taken! + </label> + + <button type="submit">Register</button> + + </form> + + The `<label>` around the e-mail field is now updated to have the `has-error` + class and display the validation message. + + \#\#\#\# How validation results are displayed + + Although the server will usually respond to a validation with a complete, + fresh copy of the form, Up.js will by default not update the entire form. + This is done in order to preserve volatile state such as the scroll position + of `<textarea>` elements. + + By default Up.js looks for a `<fieldset>`, `<label>` or `<form>` + around the validating input field, or any element with an + `up-fieldset` attribute. + With the Bootstrap bindings, Up.js will also look + for a container with the `form-group` class. + + You can change this default behavior by setting `up.config.validateTargets`: + + // Always update the entire form containing the current field ("&") + up.config.validateTargets = ['form &'] + + You can also individually override what to update by setting the `up-validate` + attribute to a CSS selector: + + <input type="text" name="email" up-validate=".email-errors"> + <span class="email-errors"></span> + + + \#\#\#\# Updating dependent fields + + The `[up-validate]` behavior is also a great way to partially update a form + when one fields depends on the value of another field. + + Let's say you have a form with one `<select>` to pick a department (sales, engineering, ...) + and another `<select>` to pick an employeee from the selected department: + + <form action="/contracts"> + <select name="department">...</select> <!-- options for all departments --> + <select name="employeed">...</select> <!-- options for employees of selected department --> + </form> + + The list of employees needs to be updated as the appartment changes: + + <form action="/contracts"> + <select name="department" up-validate="[name=employee]">...</select> + <select name="employee">...</select> + </form> + + In order to update the `department` field in addition to the `employee` field, you could say + `up-validate="&, [name=employee]"`, or simply `up-validate="form"` to update the entire form. + + @selector [up-validate] + @param {String} up-validate + The CSS selector to update with the server response. + + This defaults to a fieldset or form group around the validating field. + */ + up.on('change', '[up-validate]', function(event, $field) { + return validate($field); + }); + + /** Observes this form field and runs the given script when its value changes. This is useful for observing text fields while the user is typing. + The programmatic variant of this is the [`up.observe`](/up.observe) function. + + \#\#\#\# Example + For instance, the following would submit the form whenever the text field value changes: <form method="GET" action="/search"> <input type="query" up-observe="up.form.submit(this)"> </form> - The script given with `up-observe` runs with the following context: + The script given to `up-observe` runs with the following context: | Name | Type | Description | | -------- | --------- | ------------------------------------- | | `value` | `String` | The current value of the field | | `this` | `Element` | The form field | | `$field` | `jQuery` | The form field as a jQuery collection | - See up.observe. - - @selector input[up-observe] - The code to run when the field's value changes. + @selector [up-observe] @param {String} up-observe + The code to run when the field's value changes. */ up.compiler('[up-observe]', function($field) { return observe($field); }); + up.on('up:framework:reset', reset); return { submit: submit, - observe: observe + observe: observe, + validate: validate }; })(jQuery); up.submit = up.form.submit; up.observe = up.form.observe; + up.validate = up.form.validate; + }).call(this); /** Pop-up overlays =============== @@ -5967,9 +6411,10 @@ return event.preventDefault(); } }); up.on('up:framework:reset', reset); return { + knife: eval(typeof Knife !== "undefined" && Knife !== null ? Knife.point : void 0), visit: visit, follow: follow, open: function() { return up.error('up.modal.open no longer exists. Please use either up.modal.follow or up.modal.visit.'); },