Home | History | Annotate | Download | only in iron-input
      1 <!--
      2 @license
      3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
      4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      7 Code distributed by Google as part of the polymer project is also
      8 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
      9 -->
     10 
     11 <link rel="import" href="../polymer/polymer.html">
     12 <link rel="import" href="../iron-a11y-announcer/iron-a11y-announcer.html">
     13 <link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html">
     14 
     15 <script>
     16 
     17 /*
     18 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronValidatorBehavior`
     19 to `<input>`.
     20 
     21 ### Two-way binding
     22 
     23 By default you can only get notified of changes to an `input`'s `value` due to user input:
     24 
     25     <input value="{{myValue::input}}">
     26 
     27 `iron-input` adds the `bind-value` property that mirrors the `value` property, and can be used
     28 for two-way data binding. `bind-value` will notify if it is changed either by user input or by script.
     29 
     30     <input is="iron-input" bind-value="{{myValue}}">
     31 
     32 ### Custom validators
     33 
     34 You can use custom validators that implement `Polymer.IronValidatorBehavior` with `<iron-input>`.
     35 
     36     <input is="iron-input" validator="my-custom-validator">
     37 
     38 ### Stopping invalid input
     39 
     40 It may be desirable to only allow users to enter certain characters. You can use the
     41 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
     42 is separate from validation, and `allowed-pattern` does not affect how the input is validated.
     43 
     44     <!-- only allow characters that match [0-9] -->
     45     <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
     46 
     47 @hero hero.svg
     48 @demo demo/index.html
     49 */
     50 
     51   Polymer({
     52 
     53     is: 'iron-input',
     54 
     55     extends: 'input',
     56 
     57     behaviors: [
     58       Polymer.IronValidatableBehavior
     59     ],
     60 
     61     properties: {
     62 
     63       /**
     64        * Use this property instead of `value` for two-way data binding.
     65        */
     66       bindValue: {
     67         observer: '_bindValueChanged',
     68         type: String
     69       },
     70 
     71       /**
     72        * Set to true to prevent the user from entering invalid input. If `allowedPattern` is set,
     73        * any character typed by the user will be matched against that pattern, and rejected if it's not a match.
     74        * Pasted input will have each character checked individually; if any character
     75        * doesn't match `allowedPattern`, the entire pasted string will be rejected.
     76        * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
     77        */
     78       preventInvalidInput: {
     79         type: Boolean
     80       },
     81 
     82       /**
     83        * Regular expression that list the characters allowed as input.
     84        * This pattern represents the allowed characters for the field; as the user inputs text,
     85        * each individual character will be checked against the pattern (rather than checking
     86        * the entire value as a whole). The recommended format should be a list of allowed characters;
     87        * for example, `[a-zA-Z0-9.+-!;:]`
     88        */
     89       allowedPattern: {
     90         type: String,
     91         observer: "_allowedPatternChanged"
     92       },
     93 
     94       _previousValidInput: {
     95         type: String,
     96         value: ''
     97       },
     98 
     99       _patternAlreadyChecked: {
    100         type: Boolean,
    101         value: false
    102       }
    103 
    104     },
    105 
    106     listeners: {
    107       'input': '_onInput',
    108       'keypress': '_onKeypress'
    109     },
    110 
    111     /** @suppress {checkTypes} */
    112     registered: function() {
    113       // Feature detect whether we need to patch dispatchEvent (i.e. on FF and IE).
    114       if (!this._canDispatchEventOnDisabled()) {
    115         this._origDispatchEvent = this.dispatchEvent;
    116         this.dispatchEvent = this._dispatchEventFirefoxIE;
    117       }
    118     },
    119 
    120     created: function() {
    121       Polymer.IronA11yAnnouncer.requestAvailability();
    122     },
    123 
    124     _canDispatchEventOnDisabled: function() {
    125       var input = document.createElement('input');
    126       var canDispatch = false;
    127       input.disabled = true;
    128 
    129       input.addEventListener('feature-check-dispatch-event', function() {
    130         canDispatch = true;
    131       });
    132 
    133       try {
    134         input.dispatchEvent(new Event('feature-check-dispatch-event'));
    135       } catch(e) {}
    136 
    137       return canDispatch;
    138     },
    139 
    140     _dispatchEventFirefoxIE: function() {
    141       // Due to Firefox bug, events fired on disabled form controls can throw
    142       // errors; furthermore, neither IE nor Firefox will actually dispatch
    143       // events from disabled form controls; as such, we toggle disable around
    144       // the dispatch to allow notifying properties to notify
    145       // See issue #47 for details
    146       var disabled = this.disabled;
    147       this.disabled = false;
    148       this._origDispatchEvent.apply(this, arguments);
    149       this.disabled = disabled;
    150     },
    151 
    152     get _patternRegExp() {
    153       var pattern;
    154       if (this.allowedPattern) {
    155         pattern = new RegExp(this.allowedPattern);
    156       } else {
    157         switch (this.type) {
    158           case 'number':
    159             pattern = /[0-9.,e-]/;
    160             break;
    161         }
    162       }
    163       return pattern;
    164     },
    165 
    166     ready: function() {
    167       this.bindValue = this.value;
    168     },
    169 
    170     /**
    171      * @suppress {checkTypes}
    172      */
    173     _bindValueChanged: function() {
    174       if (this.value !== this.bindValue) {
    175         this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue;
    176       }
    177       // manually notify because we don't want to notify until after setting value
    178       this.fire('bind-value-changed', {value: this.bindValue});
    179     },
    180 
    181     _allowedPatternChanged: function() {
    182       // Force to prevent invalid input when an `allowed-pattern` is set
    183       this.preventInvalidInput = this.allowedPattern ? true : false;
    184     },
    185 
    186     _onInput: function() {
    187       // Need to validate each of the characters pasted if they haven't
    188       // been validated inside `_onKeypress` already.
    189       if (this.preventInvalidInput && !this._patternAlreadyChecked) {
    190         var valid = this._checkPatternValidity();
    191         if (!valid) {
    192           this._announceInvalidCharacter('Invalid string of characters not entered.');
    193           this.value = this._previousValidInput;
    194         }
    195       }
    196 
    197       this.bindValue = this.value;
    198       this._previousValidInput = this.value;
    199       this._patternAlreadyChecked = false;
    200     },
    201 
    202     _isPrintable: function(event) {
    203       // What a control/printable character is varies wildly based on the browser.
    204       // - most control characters (arrows, backspace) do not send a `keypress` event
    205       //   in Chrome, but the *do* on Firefox
    206       // - in Firefox, when they do send a `keypress` event, control chars have
    207       //   a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
    208       // - printable characters always send a keypress event.
    209       // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
    210       //   always matches the charCode.
    211       // None of this makes any sense.
    212 
    213       // For these keys, ASCII code == browser keycode.
    214       var anyNonPrintable =
    215         (event.keyCode == 8)   ||  // backspace
    216         (event.keyCode == 9)   ||  // tab
    217         (event.keyCode == 13)  ||  // enter
    218         (event.keyCode == 27);     // escape
    219 
    220       // For these keys, make sure it's a browser keycode and not an ASCII code.
    221       var mozNonPrintable =
    222         (event.keyCode == 19)  ||  // pause
    223         (event.keyCode == 20)  ||  // caps lock
    224         (event.keyCode == 45)  ||  // insert
    225         (event.keyCode == 46)  ||  // delete
    226         (event.keyCode == 144) ||  // num lock
    227         (event.keyCode == 145) ||  // scroll lock
    228         (event.keyCode > 32 && event.keyCode < 41)   || // page up/down, end, home, arrows
    229         (event.keyCode > 111 && event.keyCode < 124); // fn keys
    230 
    231       return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
    232     },
    233 
    234     _onKeypress: function(event) {
    235       if (!this.preventInvalidInput && this.type !== 'number') {
    236         return;
    237       }
    238       var regexp = this._patternRegExp;
    239       if (!regexp) {
    240         return;
    241       }
    242 
    243       // Handle special keys and backspace
    244       if (event.metaKey || event.ctrlKey || event.altKey)
    245         return;
    246 
    247       // Check the pattern either here or in `_onInput`, but not in both.
    248       this._patternAlreadyChecked = true;
    249 
    250       var thisChar = String.fromCharCode(event.charCode);
    251       if (this._isPrintable(event) && !regexp.test(thisChar)) {
    252         event.preventDefault();
    253         this._announceInvalidCharacter('Invalid character ' + thisChar + ' not entered.');
    254       }
    255     },
    256 
    257     _checkPatternValidity: function() {
    258       var regexp = this._patternRegExp;
    259       if (!regexp) {
    260         return true;
    261       }
    262       for (var i = 0; i < this.value.length; i++) {
    263         if (!regexp.test(this.value[i])) {
    264           return false;
    265         }
    266       }
    267       return true;
    268     },
    269 
    270     /**
    271      * Returns true if `value` is valid. The validator provided in `validator` will be used first,
    272      * then any constraints.
    273      * @return {boolean} True if the value is valid.
    274      */
    275     validate: function() {
    276       // First, check what the browser thinks. Some inputs (like type=number)
    277       // behave weirdly and will set the value to "" if something invalid is
    278       // entered, but will set the validity correctly.
    279       var valid =  this.checkValidity();
    280 
    281       // Only do extra checking if the browser thought this was valid.
    282       if (valid) {
    283         // Empty, required input is invalid
    284         if (this.required && this.value === '') {
    285           valid = false;
    286         } else if (this.hasValidator()) {
    287           valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
    288         }
    289       }
    290 
    291       this.invalid = !valid;
    292       this.fire('iron-input-validate');
    293       return valid;
    294     },
    295 
    296     _announceInvalidCharacter: function(message) {
    297       this.fire('iron-announce', { text: message });
    298     }
    299   });
    300 
    301   /*
    302   The `iron-input-validate` event is fired whenever `validate()` is called.
    303   @event iron-input-validate
    304   */
    305 
    306 </script>
    307