Skip to content

Commit

Permalink
One way controls (#49)
Browse files Browse the repository at this point in the history
* Make one-way-input-mask component

* Don't bind mask and options to the attribute

* Update README

* Update babel

* Make one-way-number-mask component

Fix issues with calling `update` erroneously

* Document one-way-number-mask
  • Loading branch information
brandynbennett authored Dec 6, 2017
1 parent 17823b2 commit 042b053
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 10 deletions.
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Ember Inputmask currently has two branches:
mode. Critical bugs will be fixed, but minor issues will not be fixed and
new features will not be added.

- [v0.4.x (master)](https://github.com/pzuraq/ember-inputmask/tree/master)
- [>v0.4.x](https://github.com/pzuraq/ember-inputmask/tree/master)
pulls Inputmask 3.3.x from NPM. Bower and jQuery are not required.

Versions prior to 0.4.0 automatically add `jquery.inputmask` into your
Expand All @@ -46,7 +46,26 @@ upgrade to the latest version of either v0.2.x or v0.4.x.
$ ember install ember-inputmask
```

## Usage
## One Way Input Mask

```hbs
{{one-way-input-mask value mask='999-aaa-***' update=(action (mut value))}}
```

This component extends from the [ember-one-way-controls](https://github.com/DockYard/ember-one-way-controls) addon and follows the data-down-actions-up (DDAU) pattern. You should use the "one-way" components in this addon as the "non-one-way" versions are deprecated as of `0.5.0` and will be removed in `1.0.0`.

### Usage

This component has the same interface as it's [ember-one-way-controls counterpart](https://github.com/DockYard/ember-one-way-controls/blob/master/docs/one-way-input.md), but accepts two additional arguments:

* `mask` The type of mask to put on the input
* `options` Any additional masking options from [Inputmask](https://github.com/RobinHerbots/Inputmask) you would like to add

## Other One Way Masks

* [{{one-way-number-mask}}](docs/one-way-number-mask.md)

## Input Mask Component (deprecated)

The standard `input-mask` component:

Expand Down Expand Up @@ -179,7 +198,7 @@ following:
{{input-mask mask='email' unmaskedValue=foo}}
```

### Number Inputs
### Number Inputs (deprecated)

```hbs
{{number-input unmaskedValue=foo group=false groupSize=3 separator=',''
Expand Down
179 changes: 179 additions & 0 deletions addon/components/one-way-input-mask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/* global Inputmask */
import { OneWayInput } from 'ember-one-way-controls';
import { computed, get, set } from '@ember/object';
import { schedule } from '@ember/runloop';

const DEFAULT_OPTIONS = {
rightAlign: false,
};

/**
* Displays an input with the specified mask applied to it
* using Inputmask library. Follows Data-down actions up pattern
*
* @param {string} value The unmasked value to display in the input
* @param {action} update The function to perform when the value changes. Will be passed the
* unmasked value and the masked values
* @param {string} mask The mask to use on the input
* @param {object} options The options to pass into the Inputmask library
*/
export default OneWayInput.extend({
/**
* Set the `_value` to be whatever the `element.value` is
*/
attributeBindings: [
'type',
'_value:value'
],

// In ember-one-way-controls all attributes are bound dynamically via a mixin, except for
// the ones specified in this property. We need to include 'mask', and 'options' to the list
NON_ATTRIBUTE_BOUND_PROPS: [
'keyEvents',
'classNames',
'positionalParamValue',
'update',
'mask',
'options',
],

/**
* mask - Pass in the `mask` string to set it on the element
*
* @public
*/
mask: '',

/**
* options - Options accepted by the Inputmask library
*/
options: null,

/**
* Setup _value to be a positional param or the passed param if that is not defined
*
* @private
*/
_value: computed('positionalParamValue', 'value', {
get() {
let value = get(this, 'positionalParamValue');
if (value === undefined) {
value = get(this, 'value');
}

return value;
}
}),

init() {
this._super(...arguments);

// Give the mask some default options that can be overridden
let options = get(this, 'options');
set(this, 'options', Object.assign({}, DEFAULT_OPTIONS, options));
},

/**
* update - This action will be called when the value changes and will be passed the unmasked value
* and the masked value
*
* @public
*/
update() {},

didInsertElement() {
this._setupMask();
},

willDestroyElement() {
this._destroyMask();
this.element.removeEventListener('input', this._changeEventListener);
},

/**
* Disabling this so we don't have conflicts with manual addEventListener in case something
* changes one day
*
* @override
*/
change(){},

/**
* Disabling thi so we don't have conflicts with manual addEventListener in case something
* changes one day
*
* @override
*/
input(){},

/**
* _changeEventListener - A place to store the event listener we setup to listen to the 'input'
* events, because the Inputmask library events don't play nice with the Ember components event
*
* @private
*/
_changeEventListener() {},

/**
* _processNewValue - Handle when a new value changes
*
* @private
* @param {string} value - The masked value visible in the element
*/
_processNewValue(value) {
let cursorStart = this.element.selectionStart;
let cursorEnd = this.element.selectionEnd;
let unmaskedValue = this._getUnmaskedValue();
let oldUnmaskedValue = get(this, '_value');
let options = get(this, 'options');

// We only want to make changes if something is different so we don't cause infinite loops or
// double renders.
// We want to make sure that that values we compare are going to come out the same through
// the masking algorithm, to ensure that we only call `update` if the values are actually different
// (e.g. '1234.' will be masked as '1234' and so when `update` is called and passed back
// into the component the decimal will be removed, we don't want this)
if (Inputmask.format(String(oldUnmaskedValue), options) !== Inputmask.format(unmaskedValue, options)) {
get(this, 'update')(unmaskedValue, value);

// When the value is updated, and then sent back down the cursor moves to the end of the field.
// We therefore need to put it back to where the user was typing so they don't get janked around
schedule('afterRender', () => {
this.element.setSelectionRange(cursorStart, cursorEnd);
});
}
},

/**
* _setupMask - Connect the 3rd party input masking library to the element
*
* @private
*/
_setupMask() {
let mask = get(this, 'mask'), options = get(this, 'options');
let inputmask = new Inputmask(mask, options);
inputmask.mask(this.element);

// We need to setup a manual event listener for the change event instead of using the Ember
// Component event methods, because the Inputmask events don't play nice with the Component
// ones. Similar issue happens in React.js as well
// https://github.com/RobinHerbots/Inputmask/issues/1377
let eventListener = event => this._processNewValue(event.target.value);
set(this, '_changeEventListener', eventListener);
this.element.addEventListener('input', eventListener);
},

/**
* _getUnmaskedValue - Get the value of the element without the mask
*
* @private
* @return {string} The unmasked value
*/
_getUnmaskedValue() {
return this.element.inputmask.unmaskedvalue();
},

_destroyMask() {
this.element.inputmask.remove();
},
});
37 changes: 37 additions & 0 deletions addon/components/one-way-number-mask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import OneWayInputMask from 'ember-inputmask/components/one-way-input-mask';
import { get, set } from '@ember/object';
import { isBlank } from '@ember/utils';

const DEFAULT_OPTIONS = {
groupSeparator: ',',
radixPoint: '.',
groupSize: '3',
autoGroup: true,
};

export default OneWayInputMask.extend({
/**
* @override
*/
mask: 'integer',

/**
* Set this to true to include decimals
*/
decimal: false,

init() {
this._super(...arguments);

set(this, 'options', Object.assign({}, get(this, 'options'), DEFAULT_OPTIONS));

if (get(this, 'decimal')) {
set(this, 'mask', 'decimal');

// Give default digits if we don't have them aleady
if (isBlank(get(this, 'options.digits'))) {
set(this, 'options.digits', 2);
}
}
},
});
2 changes: 2 additions & 0 deletions app/components/one-way-input-mask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import OneWayInputMaskComponent from 'ember-inputmask/components/one-way-input-mask';
export default OneWayInputMaskComponent;
1 change: 1 addition & 0 deletions app/components/one-way-number-mask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-inputmask/components/one-way-number-mask';
18 changes: 18 additions & 0 deletions docs/one-way-number-mask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
One Way Number Mask
===================

```
{{one-way-number-mask value update=(action (mut value))}}
```

This component defaults to masking as an `integer`. You can pass other number based options supported by [Inputmask](https://github.com/RobinHerbots/Inputmask) in the `options` hash.

## Arguments

### decimal

```
{{one-way-number-mask value decimal=true update=(action (mut value))}}
```

Pass in `decimal` and it will mask as a decimal with 2 digits. If you'd like more or less digits then you can pass in `options.digits`.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"test": "ember try:each"
},
"dependencies": {
"ember-cli-babel": "^6.6.0",
"ember-cli-babel": "^6.10.0",
"ember-cli-node-assets": "^0.2.2",
"ember-one-way-controls": "^3.0.1",
"fastboot-transform": "^0.1.1",
"inputmask": "3.3.6"
},
Expand Down
42 changes: 42 additions & 0 deletions tests/integration/components/one-way-number-mask-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { find, fillIn } from 'ember-native-dom-helpers';

moduleForComponent('one-way-number-mask', 'Integration | Component | one way number mask', {
integration: true
});

test('It defaults to an integer mask', function(assert) {
this.set('value', 1234.56)
this.render(hbs`{{one-way-number-mask value}}`);

assert.equal(find('input').value, '1,234');
});

test('It can be a decimal mask with 2 digits with one argument', function(assert) {
this.set('value', 1234.567)
this.render(hbs`{{one-way-number-mask value decimal=true}}`);

assert.equal(find('input').value, '1,234.57');
});

test('Can change default digits with options', function(assert) {
this.set('value', 1234.567)
this.render(hbs`{{one-way-number-mask value decimal=true options=(hash digits=3)}}`);

assert.equal(find('input').value, '1,234.567');
});

test('Can change default digits with options', function(assert) {
this.set('value', 1234.567)
this.render(hbs`{{one-way-number-mask value decimal=true options=(hash digits=3)}}`);

assert.equal(find('input').value, '1,234.567');
});

test('The parent can receive the updated value via the `update` action', async function(assert) {
this.set('value', 123)
this.render(hbs`{{one-way-number-mask value update=(action (mut value))}}`);
await fillIn('input', 456);
assert.equal(this.get('value'), '456');
});
Loading

0 comments on commit 042b053

Please sign in to comment.