###* Forms
¶ ↑
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.
@class up.form ### up.form = (($) ->
u = up.util ###* Sets default options for form submission and validation. @property up.form.config @param {Number} [config.observeDelay=0] The number of miliseconds to wait before [`up.observe`](/up.observe) runs the callback after the input value changes. Use this to limit how often the callback will be invoked for a fast typist. @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. @param {String} [config.fields] An array of CSS selectors that represent form fields, such as `input` or `select`. @stable ### config = u.config validateTargets: ['[up-fieldset]:has(&)', 'fieldset:has(&)', 'label:has(&)', 'form:has(&)'] fields: [':input'] observeDelay: 0 reset = -> 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. @param {String} [options.url] The URL where to submit the form. Defaults to the form's `action` attribute, or to the current URL of the browser window. @param {String} [options.method] The HTTP method used for the form submission. Defaults to the form's `up-method`, `data-method` or `method` attribute, or to `'post'` if none of these attributes are given. @param {String} [options.target] The selector to update when the form submission succeeds (server responds with status 200). Defaults to the form's `up-target` attribute, or to `'body'`. @param {String} [options.failTarget] The selector to update when the form submission fails (server responds with non-200 status). Defaults to the form's `up-fail-target` attribute, or to an auto-generated selector that matches the form itself. @param {Boolean|String} [options.history=true] Successful form submissions will add a history entry and change the browser's location bar if the form either uses the `GET` method or the response redirected to another page (this requires the `upjs-rails` gem). If want to prevent history changes in any case, set this to `false`. If you pass a `String`, it is used as the URL for the browser history. @param {String} [options.transition='none'] The transition to use when a successful form submission updates the `options.target` selector. Defaults to the form's `up-transition` attribute, or to `'none'`. @param {String} [options.failTransition='none'] The transition to use when a failed form submission updates the `options.failTarget` selector. Defaults to the form's `up-fail-transition` attribute, or to `options.transition`, or to `'none'`. @param {Number} [options.duration] The duration of the transition. See [`up.morph`](/up.morph). @param {Number} [options.delay] The delay before the transition starts. See [`up.morph`](/up.morph). @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. @param {Boolean} [options.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`). 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. @stable ### submit = (formOrSelector, options) -> $form = $(formOrSelector).closest('form') options = u.options(options) target = u.option(options.target, $form.attr('up-target'), 'body') url = u.option(options.url, $form.attr('action'), up.browser.url()) options.failTarget = u.option(options.failTarget, $form.attr('up-fail-target')) || u.selectorForElement($form) options.history = u.option(options.history, u.castedAttr($form, 'up-history'), true) options.transition = u.option(options.transition, u.castedAttr($form, 'up-transition'), 'none') options.failTransition = u.option(options.failTransition, u.castedAttr($form, 'up-fail-transition'), 'none') options.method = u.option(options.method, $form.attr('up-method'), $form.attr('data-method'), $form.attr('method'), 'post').toUpperCase() options.headers = u.option(options.headers, {}) options.reveal = u.option(options.reveal, u.castedAttr($form, 'up-reveal'), true) options.cache = u.option(options.cache, u.castedAttr($form, 'up-cache')) options.restoreScroll = u.option(options.restoreScroll, u.castedAttr($form, 'up-restore-scroll')) options.origin = u.option(options.origin, $form) options.data = $form.serializeArray() options = u.merge(options, up.motion.animateOptions(options, $form)) hasFileInputs = $form.find('input[type=file]').length if options.validate options.headers ||= {} options.headers['X-Up-Validate'] = options.validate # Since we cannot (yet) submit file inputs via AJAX, we cannot # offer inline validation for such forms. if hasFileInputs return u.unresolvablePromise() $form.addClass('up-active') if hasFileInputs || (!up.browser.canPushState() && options.history != false) $form.get(0).submit() return u.unresolvablePromise() promise = up.replace(target, url, options) promise.always -> $form.removeClass('up-active') return promise ###* Observes a field or form and runs a callback when a value changes. This is useful for observing text fields while the user is typing. 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]', function(value, $input) { up.submit($input) }); \#\#\#\# Preventing concurrency Firing asynchronous code after a form field can cause [concurrency issues](https://makandracards.com/makandra/961-concurrency-issues-with-find-as-you-type-boxes). To mitigate this, `up.observe` will try to never run a callback before the previous callback has completed. To take advantage of this, your callback code must return a promise. Note that all asynchronous Up.js functions return promises. \#\#\#\# Throttling If you are concerned about fast typists causing too much load on your server, you can use a `delay` option to wait a few miliseconds before executing the callback: up.observe('input', { delay: 100 }, function(value, $input) { up.submit($input) }); @function up.observe @param {Element|jQuery|String} fieldOrSelector @param {Number} [options.delay=up.form.config.observeDelay] The number of miliseconds to wait before executing the callback after the input value changes. Use this to limit how often the callback will be invoked for a fast typist. @param {Function(value, $field)|String} onChange The callback to execute when the field's value changes. If given as a function, it must take two arguments (`value`, `$field`). If given as a string, it will be evaled as Javascript code in a context where (`value`, `$field`) are set. @return {Function} A destructor function that removes the observe watch when called. @stable ### observe = (selectorOrElement, args...) -> options = {} callbackArg = undefined if args.length == 1 callbackArg = args[0] if args.length > 1 options = u.options(args[0]) callbackArg = args[1] $element = $(selectorOrElement) options = u.options(options) delay = u.option($element.attr('up-delay'), options.delay, config.observeDelay) delay = parseInt(delay) callback = null if u.isGiven(options.change) u.error('up.observe now takes the change callback as the last argument') rawCallback = u.option(u.presentAttr($element, 'op-observe'), callbackArg) if u.isString(rawCallback) callback = (value, $field) -> eval(rawCallback) else callback = rawCallback or u.error('up.observe: No change callback given') if $element.is('form') return observeForm($element, options, callback) knownValue = null callbackTimer = null callbackPromise = u.resolvedPromise() # This holds the next callback function, curried with `value` and `$field`. # Since we're waiting for callback promises to resolve before running # another callback, this might be overwritten while we're waiting for a # previous callback to finish. nextCallback = null runNextCallback = -> if nextCallback returnValue = nextCallback() nextCallback = null returnValue check = -> value = $element.val() # don't run the callback for the check during initialization skipCallback = u.isNull(knownValue) if knownValue != value knownValue = value unless skipCallback clearTimer() nextCallback = -> callback.apply($element.get(0), [value, $element]) runAndChain = -> # Only run the callback once the previous callback's # promise resolves. callbackPromise.then -> returnValue = runNextCallback() # If the callback returns a promise, we will remember it # and chain additional callback invocations to it. if u.isPromise(returnValue) callbackPromise = returnValue else callbackPromise = u.resolvedPromise() if delay == 0 runAndChain() else setTimeout(runAndChain, delay) clearTimer = -> clearTimeout(callbackTimer) changeEvents = if up.browser.canInputEvent() # Actually we only need `input`, but we want to notice # if another script manually triggers `change` on the element. 'input change' else # Actually we won't ever get `input` from the user in this browser, # but we want to notice if another script manually triggers `input` # on the element. 'input change keypress paste cut click propertychange' $element.on(changeEvents, check) check() # return destructor return -> $element.off(changeEvents, check) clearTimer() ###* @function observeForm @internal ### observeForm = ($form, options, callback) -> $fields = u.multiSelector(config.fields).find($form) destructors = u.map $fields, ($field) -> observe($field, callback) -> destructor() for destructor in destructors ###* [Observes](/up.observe) a field or form and submits the form when a value changes. The changed form field will be assigned a CSS class [`up-active`](/up-active) while the autosubmitted form is processing. The UJS variant of this is the [`up-autosubmit`](/up-autosubmit) attribute. @function up.autosubmit @param {String|Element|jQuery} selectorOrElement The form field to observe. @param {Object} [options] See options for [`up.observe`](/up.observe) @return {Function} A destructor function that removes the observe watch when called. @stable ### autosubmit = (selectorOrElement, options) -> observe(selectorOrElement, options, (value, $field) -> $form = $field.closest('form') $field.addClass('up-active') submit($form).always -> $field.removeClass('up-active') ) resolveValidateTarget = ($field, options) -> target = u.option(options.target, $field.attr('up-validate')) if u.isBlank(target) target ||= u.detect(config.validateTargets, (defaultTarget) -> resolvedDefault = up.flow.resolveSelector(defaultTarget, options.origin) $field.closest(resolvedDefault).length ) if u.isBlank(target) u.error('Could not find default validation target for %o (tried ancestors %o)', $field.get(0), config.validateTargets) unless u.isString(target) target = u.selectorForElement(target) target ###* 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. @stable ### validate = (fieldOrSelector, options) -> $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, {}) # Make sure the X-Up-Validate header is present, so the server-side # knows that it should not persist the form submission options.validate = ($field.attr('name') || '__none__') options = u.merge(options, up.motion.animateOptions(options, $field)) $form = $field.closest('form') promise = up.submit($form, options) promise currentValuesForToggle = ($field) -> values = undefined if $field.is('input[type=checkbox]') if $field.is(':checked') values = [':checked', ':present', $field.val()] else values = [':unchecked', ':blank'] else if $field.is('input[type=radio]') $checkedButton = $field.closest('form, body').find("input[type='radio'][name='#{$field.attr('name')}']:checked") if $checkedButton.length values = [':checked', ':present', $checkedButton.val()] else values = [':unchecked', ':blank'] else value = $field.val() if u.isPresent(value) values = [':present', value] else values = [':blank'] values currentValuesForToggle = ($field) -> if $field.is('input[type=checkbox]') if $field.is(':checked') value = $field.val() meta = ':checked' else meta = ':unchecked' else if $field.is('input[type=radio]') $checkedButton = $field.closest('form, body').find("input[type='radio'][name='#{$field.attr('name')}']:checked") if $checkedButton.length meta = ':checked' value = $checkedButton.val() else meta = ':unchecked' else value = $field.val() values = [] if u.isPresent(value) values.push(value) values.push(':present') else values.push(':blank') if u.isPresent(meta) values.push(meta) values ###* Shows or hides a target selector depending on the value. See [`[up-toggle]`](/up-toggle) for more documentation and examples. This function does not currently have a very useful API outside of our use for `up-toggle`'s UJS behavior, that's why it's currently still marked `@internal`. @function up.form.toggle @param {String|Element|jQuery} fieldOrSelector @param {String} [options.target] The target selectors to toggle. Defaults to an `up-toggle` attribute on the given field. @internal ### toggleTargets = (fieldOrSelector, options) -> $field = $(fieldOrSelector) options = u.options(options) targets = u.option(options.target, $field.attr('up-toggle')) u.isPresent(targets) or u.error("No toggle target given for %o", $field.get(0)) fieldValues = currentValuesForToggle($field) $(targets).each -> $target = $(this) if hideValues = $target.attr('up-hide-for') hideValues = hideValues.split(' ') show = u.intersect(fieldValues, hideValues).length == 0 else if showValues = $target.attr('up-show-for') showValues = showValues.split(' ') else # If the target has neither up-show-for or up-hide-for attributes, # assume the user wants the target to be visible whenever anything # is checked or entered. showValues = [':present', ':checked'] show = u.intersect(fieldValues, showValues).length > 0 $target.toggle(show) ###* 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. \#\#\#\# Failed submission 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 detect a failed form submission,, 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: GET`. 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). If omitted, Up.js will replace the `<form>` tag itself, assuming that the server has echoed the form with validation errors. @param {String} [up-transition] The animation to use when the form is replaced after a successful submission. @param {String} [up-fail-transition] The animation to use when the form is replaced after a failed submission. @param [up-history] Set this to `'false'` to prevent the current URL from being updated. @param {String} [up-method] The HTTP method to be used to submit the form (`get`, `post`, `put`, `delete`, `patch`). Alternately you can use an attribute `data-method` ([Rails UJS](https://github.com/rails/jquery-ujs/wiki/Unobtrusive-scripting-support-for-jQuery)) or `method` (vanilla HTML) for the same purpose. @param {String} [up-reveal='true'] Whether to reveal the target element within its viewport before updating. @param {String} [up-restore-scroll='false'] Whether to restore previously known scroll position of all viewports within the target selector. @param {String} [up-cache] Whether to force the use of a cached response (`true`) 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. @stable ### up.on 'submit', 'form[up-target]', (event, $form) -> event.preventDefault() 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.form.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. @stable ### up.on 'change', '[up-validate]', (event, $field) -> validate($field) ###* Show or hide part of a form if certain options are selected or boxes are checked. \#\#\#\# Example The triggering input gets an `up-toggle` attribute with a selector for the elements to show or hide: <select name="advancedness" up-toggle=".target"> <option value="basic">Basic parts</option> <option value="advanced">Advanced parts</option> <option value="very-advanced">Very advanced parts</option> </select> The target elements get a space-separated list of select values for which they are shown or hidden: <div class="target" up-show-for="basic"> only shown for advancedness = basic </div> <div class="target" up-hide-for="basic"> hidden for advancedness = basic </div> <div class="target" up-show-for="advanced very-advanced"> shown for advancedness = advanced or very-advanced </div> For checkboxes you can also use the pseudo-values `:checked` or `:unchecked` like so: <input type="checkbox" name="flag" up-toggle=".target"> <div class="target" up-show-for=":checked"> only shown when checkbox is checked </div> You can also use the pseudo-values `:blank` to match an empty input value, or `:present` to match a non-empty input value: <input type="text" name="email" up-toggle=".target"> <div class="target" up-show-for=":blank"> please enter an email address </div> @selector [up-toggle] @stable ### ###* Show this element only if a form field has a given value. See [`[up-toggle]`](/up-toggle) for more documentation and examples. @selector [up-show-for] @param up-show-for A space-separated list of values for which to show this element. @stable ### ###* Hide this element if a form field has a given value. See [`[up-toggle]`](/up-toggle) for more documentation and examples. @selector [up-hide-for] @param up-hide-for A space-separated list of values for which to hide this element. @stable ### up.on 'change', '[up-toggle]', (event, $field) -> console.log("CHANGE EVENT") toggleTargets($field) up.compiler '[up-toggle]', ($field) -> toggleTargets($field) ###* Observes this field or form and runs a callback when a 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 The following would run a global `showSuggestions(value)` function whenever the `<input>` changes: <form> <input type="query" up-observe="showSuggestions(value)"> </form> \#\#\#\# Callback 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 | @selector [up-observe] @param {String} up-observe The code to run when the field's value changes. @param {String} up-delay The number of miliseconds to wait after a change before the code is run. @stable ### up.compiler '[up-observe]', ($formOrField) -> observe($formOrField) ###* [Observes](/up.observe) this field or form and submits the form when a value changes. The form field will be assigned a CSS class [`up-active`](/up-active) while the autosubmitted form is processing. The programmatic variant of this is the [`up.autosubmit`](/up.autosubmit) function. \#\#\#\# Example The following would submit the form whenever the text field value changes: <form method="GET" action="/search" up-autosubmit> <input type="query"> </form> @selector [up-autosubmit] @param {String} up-delay The number of miliseconds to wait after the change before the form is submitted. @stable ### up.compiler '[up-autosubmit]', ($formOrField) -> autosubmit($formOrField) up.on 'up:framework:reset', reset knife: eval(Knife?.point) config: config submit: submit observe: observe validate: validate toggleTargets: toggleTargets
)(jQuery)
up.submit = up.form.submit up.observe = up.form.observe up.autosubmit = up.form.autosubmit up.validate = up.form.validate