lib/assets/javascripts/unpoly/form.js.coffee in unpoly-rails-0.27.3 vs lib/assets/javascripts/unpoly/form.js.coffee in unpoly-rails-0.28.0

- old
+ new

@@ -26,11 +26,11 @@ 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] + @param {String} [config.fields=[':input']] 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(&)'] @@ -155,11 +155,11 @@ promise = up.replace(target, url, options) promise.always -> up.navigation.unmarkActive($form) return promise ###* - Observes a field or form and runs a callback when a value changes. + Observes form fields 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. @@ -167,13 +167,17 @@ The following would submit the form whenever the text field value changes: up.observe('input[name=query]', function(value, $input) { - up.submit($input) + console.log('Query is now ' + value); }); + Instead of a single form field, you can also + pass, a `<form>` or any container that contains form fields. + The callback will be run if any of the given fields change. + \#\#\#\# 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). @@ -191,53 +195,56 @@ up.observe('input', { delay: 100 }, function(value, $input) { up.submit($input) }); @function up.observe - @param {Element|jQuery|String} fieldOrSelector + @param {Element|jQuery|String} selectorOrElement + The form fields that wiill be observed. + + You can pass one or more fields, a `<form>` or any container that contains form fields. + The callback will be run if any of the given fields change. @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. + The callback to run 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...) -> - + observe = (selectorOrElement, extraArgs...) -> options = {} callbackArg = undefined - if args.length == 1 - callbackArg = args[0] - if args.length > 1 - options = u.options(args[0]) - callbackArg = args[1] + if extraArgs.length == 1 + callbackArg = extraArgs[0] + else if extraArgs.length > 1 + options = u.options(extraArgs[0]) + callbackArg = extraArgs[1] $element = $(selectorOrElement) - options = u.options(options) - delay = u.option($element.attr('up-delay'), options.delay, config.observeDelay) - delay = parseInt(delay) + $fields = u.multiSelector(config.fields).findWithSelf($element) 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, 'up-observe'), callbackArg) + rawCallback = u.option(callbackArg, u.presentAttr($fields, 'up-observe')) if u.isString(rawCallback) callback = (value, $field) -> eval(rawCallback) else - callback = rawCallback or u.error('up.observe: No change callback given') + callback = rawCallback or up.fail('up.observe: No change callback given') - if $element.is('form') - return observeForm($element, options, callback) + delay = u.option($fields.attr('up-delay'), options.delay, config.observeDelay) + delay = parseInt(delay) + destructors = u.map $fields, (field) -> + observeField($(field), options, callback) + + u.sequence(destructors...) + + observeField = ($field, delay, callback) -> knownValue = null callbackTimer = null callbackPromise = u.resolvedPromise() # This holds the next callback function, curried with `value` and `$field`. @@ -251,18 +258,18 @@ returnValue = nextCallback() nextCallback = null returnValue check = -> - value = $element.val() + value = u.submittedValue($field) # 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]) + nextCallback = -> callback.apply($field.get(0), [value, $field]) runAndChain = -> # Only run the callback once the previous callback's # promise resolves. callbackPromise.then -> returnValue = runNextCallback() @@ -275,40 +282,29 @@ u.setTimer(delay, runAndChain) 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) + # Although (depending on the browser) we only need/receive either input or change, + # we always bind to both events in case another script manually triggers it. + changeEvents = 'input change' + unless up.browser.canInputEvent() + # In case this browser doesn't support input, we listen + # to all sorts of events that might change form controls. + changeEvents += ' keypress paste cut click propertychange' + + $field.on(changeEvents, check) + check() # return destructor return -> - $element.off(changeEvents, check) + $field.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. @@ -335,11 +331,11 @@ 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) + up.fail('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 ###* @@ -447,11 +443,11 @@ ### switchTargets = (fieldOrSelector, options) -> $field = $(fieldOrSelector) options = u.options(options) targets = u.option(options.target, $field.attr('up-switch')) - u.isPresent(targets) or u.error("No switch target given for %o", $field.get(0)) + u.isPresent(targets) or up.fail("No switch target given for %o", $field.get(0)) fieldValues = currentValuesForSwitch($field) $(targets).each -> $target = $(this) if hideValues = $target.attr('up-hide-for') hideValues = hideValues.split(' ') @@ -869,9 +865,10 @@ config: config submit: submit observe: observe validate: validate switchTargets: switchTargets + autosubmit: autosubmit )(jQuery) up.submit = up.form.submit up.observe = up.form.observe