Skip to content

Commit 042b053

Browse files
One way controls (#49)
* 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
1 parent 17823b2 commit 042b053

11 files changed

+399
-10
lines changed

README.md

+22-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Ember Inputmask currently has two branches:
2929
mode. Critical bugs will be fixed, but minor issues will not be fixed and
3030
new features will not be added.
3131

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

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

49-
## Usage
49+
## One Way Input Mask
50+
51+
```hbs
52+
{{one-way-input-mask value mask='999-aaa-***' update=(action (mut value))}}
53+
```
54+
55+
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`.
56+
57+
### Usage
58+
59+
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:
60+
61+
* `mask` The type of mask to put on the input
62+
* `options` Any additional masking options from [Inputmask](https://github.com/RobinHerbots/Inputmask) you would like to add
63+
64+
## Other One Way Masks
65+
66+
* [{{one-way-number-mask}}](docs/one-way-number-mask.md)
67+
68+
## Input Mask Component (deprecated)
5069

5170
The standard `input-mask` component:
5271

@@ -179,7 +198,7 @@ following:
179198
{{input-mask mask='email' unmaskedValue=foo}}
180199
```
181200

182-
### Number Inputs
201+
### Number Inputs (deprecated)
183202

184203
```hbs
185204
{{number-input unmaskedValue=foo group=false groupSize=3 separator=',''
+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/* global Inputmask */
2+
import { OneWayInput } from 'ember-one-way-controls';
3+
import { computed, get, set } from '@ember/object';
4+
import { schedule } from '@ember/runloop';
5+
6+
const DEFAULT_OPTIONS = {
7+
rightAlign: false,
8+
};
9+
10+
/**
11+
* Displays an input with the specified mask applied to it
12+
* using Inputmask library. Follows Data-down actions up pattern
13+
*
14+
* @param {string} value The unmasked value to display in the input
15+
* @param {action} update The function to perform when the value changes. Will be passed the
16+
* unmasked value and the masked values
17+
* @param {string} mask The mask to use on the input
18+
* @param {object} options The options to pass into the Inputmask library
19+
*/
20+
export default OneWayInput.extend({
21+
/**
22+
* Set the `_value` to be whatever the `element.value` is
23+
*/
24+
attributeBindings: [
25+
'type',
26+
'_value:value'
27+
],
28+
29+
// In ember-one-way-controls all attributes are bound dynamically via a mixin, except for
30+
// the ones specified in this property. We need to include 'mask', and 'options' to the list
31+
NON_ATTRIBUTE_BOUND_PROPS: [
32+
'keyEvents',
33+
'classNames',
34+
'positionalParamValue',
35+
'update',
36+
'mask',
37+
'options',
38+
],
39+
40+
/**
41+
* mask - Pass in the `mask` string to set it on the element
42+
*
43+
* @public
44+
*/
45+
mask: '',
46+
47+
/**
48+
* options - Options accepted by the Inputmask library
49+
*/
50+
options: null,
51+
52+
/**
53+
* Setup _value to be a positional param or the passed param if that is not defined
54+
*
55+
* @private
56+
*/
57+
_value: computed('positionalParamValue', 'value', {
58+
get() {
59+
let value = get(this, 'positionalParamValue');
60+
if (value === undefined) {
61+
value = get(this, 'value');
62+
}
63+
64+
return value;
65+
}
66+
}),
67+
68+
init() {
69+
this._super(...arguments);
70+
71+
// Give the mask some default options that can be overridden
72+
let options = get(this, 'options');
73+
set(this, 'options', Object.assign({}, DEFAULT_OPTIONS, options));
74+
},
75+
76+
/**
77+
* update - This action will be called when the value changes and will be passed the unmasked value
78+
* and the masked value
79+
*
80+
* @public
81+
*/
82+
update() {},
83+
84+
didInsertElement() {
85+
this._setupMask();
86+
},
87+
88+
willDestroyElement() {
89+
this._destroyMask();
90+
this.element.removeEventListener('input', this._changeEventListener);
91+
},
92+
93+
/**
94+
* Disabling this so we don't have conflicts with manual addEventListener in case something
95+
* changes one day
96+
*
97+
* @override
98+
*/
99+
change(){},
100+
101+
/**
102+
* Disabling thi so we don't have conflicts with manual addEventListener in case something
103+
* changes one day
104+
*
105+
* @override
106+
*/
107+
input(){},
108+
109+
/**
110+
* _changeEventListener - A place to store the event listener we setup to listen to the 'input'
111+
* events, because the Inputmask library events don't play nice with the Ember components event
112+
*
113+
* @private
114+
*/
115+
_changeEventListener() {},
116+
117+
/**
118+
* _processNewValue - Handle when a new value changes
119+
*
120+
* @private
121+
* @param {string} value - The masked value visible in the element
122+
*/
123+
_processNewValue(value) {
124+
let cursorStart = this.element.selectionStart;
125+
let cursorEnd = this.element.selectionEnd;
126+
let unmaskedValue = this._getUnmaskedValue();
127+
let oldUnmaskedValue = get(this, '_value');
128+
let options = get(this, 'options');
129+
130+
// We only want to make changes if something is different so we don't cause infinite loops or
131+
// double renders.
132+
// We want to make sure that that values we compare are going to come out the same through
133+
// the masking algorithm, to ensure that we only call `update` if the values are actually different
134+
// (e.g. '1234.' will be masked as '1234' and so when `update` is called and passed back
135+
// into the component the decimal will be removed, we don't want this)
136+
if (Inputmask.format(String(oldUnmaskedValue), options) !== Inputmask.format(unmaskedValue, options)) {
137+
get(this, 'update')(unmaskedValue, value);
138+
139+
// When the value is updated, and then sent back down the cursor moves to the end of the field.
140+
// We therefore need to put it back to where the user was typing so they don't get janked around
141+
schedule('afterRender', () => {
142+
this.element.setSelectionRange(cursorStart, cursorEnd);
143+
});
144+
}
145+
},
146+
147+
/**
148+
* _setupMask - Connect the 3rd party input masking library to the element
149+
*
150+
* @private
151+
*/
152+
_setupMask() {
153+
let mask = get(this, 'mask'), options = get(this, 'options');
154+
let inputmask = new Inputmask(mask, options);
155+
inputmask.mask(this.element);
156+
157+
// We need to setup a manual event listener for the change event instead of using the Ember
158+
// Component event methods, because the Inputmask events don't play nice with the Component
159+
// ones. Similar issue happens in React.js as well
160+
// https://github.com/RobinHerbots/Inputmask/issues/1377
161+
let eventListener = event => this._processNewValue(event.target.value);
162+
set(this, '_changeEventListener', eventListener);
163+
this.element.addEventListener('input', eventListener);
164+
},
165+
166+
/**
167+
* _getUnmaskedValue - Get the value of the element without the mask
168+
*
169+
* @private
170+
* @return {string} The unmasked value
171+
*/
172+
_getUnmaskedValue() {
173+
return this.element.inputmask.unmaskedvalue();
174+
},
175+
176+
_destroyMask() {
177+
this.element.inputmask.remove();
178+
},
179+
});
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import OneWayInputMask from 'ember-inputmask/components/one-way-input-mask';
2+
import { get, set } from '@ember/object';
3+
import { isBlank } from '@ember/utils';
4+
5+
const DEFAULT_OPTIONS = {
6+
groupSeparator: ',',
7+
radixPoint: '.',
8+
groupSize: '3',
9+
autoGroup: true,
10+
};
11+
12+
export default OneWayInputMask.extend({
13+
/**
14+
* @override
15+
*/
16+
mask: 'integer',
17+
18+
/**
19+
* Set this to true to include decimals
20+
*/
21+
decimal: false,
22+
23+
init() {
24+
this._super(...arguments);
25+
26+
set(this, 'options', Object.assign({}, get(this, 'options'), DEFAULT_OPTIONS));
27+
28+
if (get(this, 'decimal')) {
29+
set(this, 'mask', 'decimal');
30+
31+
// Give default digits if we don't have them aleady
32+
if (isBlank(get(this, 'options.digits'))) {
33+
set(this, 'options.digits', 2);
34+
}
35+
}
36+
},
37+
});

app/components/one-way-input-mask.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import OneWayInputMaskComponent from 'ember-inputmask/components/one-way-input-mask';
2+
export default OneWayInputMaskComponent;

app/components/one-way-number-mask.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from 'ember-inputmask/components/one-way-number-mask';

docs/one-way-number-mask.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
One Way Number Mask
2+
===================
3+
4+
```
5+
{{one-way-number-mask value update=(action (mut value))}}
6+
```
7+
8+
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.
9+
10+
## Arguments
11+
12+
### decimal
13+
14+
```
15+
{{one-way-number-mask value decimal=true update=(action (mut value))}}
16+
```
17+
18+
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`.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
"test": "ember try:each"
1919
},
2020
"dependencies": {
21-
"ember-cli-babel": "^6.6.0",
21+
"ember-cli-babel": "^6.10.0",
2222
"ember-cli-node-assets": "^0.2.2",
23+
"ember-one-way-controls": "^3.0.1",
2324
"fastboot-transform": "^0.1.1",
2425
"inputmask": "3.3.6"
2526
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { moduleForComponent, test } from 'ember-qunit';
2+
import hbs from 'htmlbars-inline-precompile';
3+
import { find, fillIn } from 'ember-native-dom-helpers';
4+
5+
moduleForComponent('one-way-number-mask', 'Integration | Component | one way number mask', {
6+
integration: true
7+
});
8+
9+
test('It defaults to an integer mask', function(assert) {
10+
this.set('value', 1234.56)
11+
this.render(hbs`{{one-way-number-mask value}}`);
12+
13+
assert.equal(find('input').value, '1,234');
14+
});
15+
16+
test('It can be a decimal mask with 2 digits with one argument', function(assert) {
17+
this.set('value', 1234.567)
18+
this.render(hbs`{{one-way-number-mask value decimal=true}}`);
19+
20+
assert.equal(find('input').value, '1,234.57');
21+
});
22+
23+
test('Can change default digits with options', function(assert) {
24+
this.set('value', 1234.567)
25+
this.render(hbs`{{one-way-number-mask value decimal=true options=(hash digits=3)}}`);
26+
27+
assert.equal(find('input').value, '1,234.567');
28+
});
29+
30+
test('Can change default digits with options', function(assert) {
31+
this.set('value', 1234.567)
32+
this.render(hbs`{{one-way-number-mask value decimal=true options=(hash digits=3)}}`);
33+
34+
assert.equal(find('input').value, '1,234.567');
35+
});
36+
37+
test('The parent can receive the updated value via the `update` action', async function(assert) {
38+
this.set('value', 123)
39+
this.render(hbs`{{one-way-number-mask value update=(action (mut value))}}`);
40+
await fillIn('input', 456);
41+
assert.equal(this.get('value'), '456');
42+
});

0 commit comments

Comments
 (0)