ui all the things
import Ember from 'ember';
const {
Component,
computed,
get,
isNone,
assert,
run: {
schedule
}
} = Ember;
const ALLOWED_TYPES = ['text', 'search', 'tel', 'password', 'num'];
const UiInputComponent = Component.extend({
tagName: 'input',
attributeBindings: [
'type',
'value',
'placeholder',
'ariaDescribedBy:aria-describedby',
'ariaLabelledBy:aria-labelledby'
],
value: null,
ariaDescribedBy: null,
ariaLabelledBy: null,
change(event) {
this._processNewValue(event.target.value);
},
input(event) {
this._processNewValue(event.target.value);
},
_processNewValue(value) {
if (get(this, '_value') !== value) {
this.sendAction('update', value);
}
schedule('afterRender', this, '_syncValue');
},
_syncValue() {
if (this.isDestroyed || this.isDestroying) {
return;
}
let actualValue = get(this, '_value');
let renderedValue = this.readDOMAttr('value');
if (!isNone(actualValue) && !isNone(renderedValue) && actualValue.toString() !== renderedValue.toString()) {
let element = this.$();
let rawElement = element.get(0);
let start;
let end;
try {
start = rawElement.selectionStart;
end = rawElement.selectionEnd;
} catch(e) {
// no-op
}
element.val(actualValue);
try {
rawElement.setSelectionRange(start, end);
} catch(e) {
// no-op
}
}
},
type: computed({
get() {
return 'text';
},
set(key, type) {
assert(`The {{ui-input}} component does not support type="${type}".`, ALLOWED_TYPES.indexOf(type) !== -1);
return type;
}
})
});
UiInputComponent.reopenClass({
positionalParams: ['value']
});
export default UiInputComponent;
import Ember from 'ember';
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { setupComponentTest } from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
const { run } = Ember;
describe('Integration | Component | ui input', function() {
setupComponentTest('ui-input', {
integration: true
});
it('renders', function() {
});
it('defaults the type to "text"', function() {
this.render(hbs`{{ui-input}}`);
let selector = this.$('input').attr('type');
let message = 'defaults type to "text"';
expect(selector, message).to.eq('text');
});
it('allows a specific type to be provided', function() {
this.render(hbs`{{ui-input type='search'}}`);
let selector = this.$('input').attr('type');
let message = 'sets the type to "search"';
expect(selector, message).to.eq('search');
});
it('triggers an assertion when the wrong input type is provided', function(done) {
this.set('type', 'text');
let message = 'trying to render with a bad type throws an error';
this.render(hbs`{{ui-input type=type}}`);
try {
this.set('type', 'test');
} catch(error) {
expect(error.name, message).to.eq('Error');
done();
}
});
it('renders with the value as an attribute', function() {
this.set('value', 'Test');
this.render(hbs`{{ui-input value=value}}`);
let selector = this.$('input').val();
let message = 'Properly renders with value as an attribute';
expect(selector, message).to.eq('Test');
});
it('renders with the value as a posistionalParam', function() {
this.set('value', 'Test');
this.render(hbs`{{ui-input value}}`);
let selector = this.$('input').val();
let message = 'Properly renders with value as a positionalParam';
expect(selector, message).to.eq('Test');
});
it('triggers update when typing in to the input', function() {
this.render(hbs`{{ui-input value=value update=(action (mut value))}}`);
this.$('input').val('Check').trigger('input');
let message = 'Value is updated to "Check"';
expect(this.get('value'), message).to.eq('Check');
});
it('triggers update when value is changed', function() {
this.render(hbs`{{ui-input value=value update=(action (mut value))}}`);
this.$('input').val('Check').trigger('change');
let message = 'Value is updated to "Check"';
expect(this.get('value'), message).to.eq('Check');
});
it('keeps the cursor at the correct position', function() {
this.render(hbs`{{ui-input value update=(action (mut value))}}`);
['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'].forEach((_, index, letters) => {
let part = letters.slice(0, index + 1).join('');
run(() => this.$('input').val(part).trigger('input'));
let selector = this.$('input').get(0).selectionStart;
let message = 'Cursor is at correct position';
expect(selector, message).to.eq(index + 1);
});
});
it('adds the placeholder attribute', function() {
this.render(hbs`{{ui-input placeholder=placeholder}}`);
let selector = this.$('input');
let message = 'Does not add placeholder attribute';
expect(selector.attr('placeholder'), message).to.be.undefined;
this.set('placeholder', 'Test');
message = 'Adds placeholder attribute';
expect(selector.attr('placeholder'), message).to.eq('Test');
});
it('adds classes to the input', function() {
this.render(hbs`{{ui-input class=class}}`);
let selector = this.$('input');
let message = 'Does not add any classes';
expect(selector.attr('class'), message).to.eq('ember-view');
this.set('class', 'test');
message = 'Adds class to input';
expect(selector.attr('class'), message).to.eq('test ember-view');
});
it('adds aria-describedby attribute', function() {
this.render(hbs`{{ui-input ariaDescribedBy=ariaDescribedBy}}`);
let selector = this.$('input');
let message = 'Does not add aria-describedby attribute';
expect(selector.attr('aria-describedby'), message).to.be.undefined;
this.set('ariaDescribedBy', 'test');
message = 'Adds aria-describedby attribute';
expect(selector.attr('aria-describedby'), message).to.eq('test');
});
it('adds aria-labelledby attribute', function() {
this.render(hbs`{{ui-input ariaLabelledBy=ariaLabelledBy}}`);
let selector = this.$('input');
let message = 'Does not add aria-labelledby attribute';
expect(selector.attr('aria-labelledby'), message).to.be.undefined;
this.set('ariaLabelledBy', 'test');
message = 'Adds aria-labelledby attribute';
expect(selector.attr('aria-labelledby'), message).to.eq('test');
});
});
import Ember from 'ember';
import hbs from 'htmlbars-inline-precompile';
const {
computed,
get,
guidFor,
assert,
isPresent
} = Ember;
const LABEL_MSG = 'You must include a "label" attribute in all uses of "{{ui-form-group}}" for disabled users. If you want to hide the label visually, you may also provide the attribute "labelVisible=false".';
const UPDATE_MSG = 'You must pass an "update" attribute in all uses of "{{ui-form-group}}" for the value to be updated. The most common use is "update=(action (mut value))".';
export default Ember.Component.extend({
classNames: ['form-group'],
layout: hbs`
{{#if label}}
<label class="{{unless labelVisible 'sr-only'}}" for="{{inputId}}" test-id="txtLabel">{{label}}</label>
{{/if}}
{{component inputComponent
id=inputId
ariaDescribedBy=descriptionId
type=inputType
value=value
update=update
}}
{{#if description}}
<p class="help-block {{unless descriptionVisible 'sr-only'}}" id="{{descriptionId}}" test-id="txtDescription">{{description}}</p>
{{/if}}
`,
labelVisible: true,
description: null,
descriptionVisible: true,
value: null,
inputType: 'text',
verifyPresence: (value, message) => assert(message, isPresent(value)),
label: computed({
get() {
this.verifyPresence(null, LABEL_MSG);
return null;
},
set(key, label) {
this.verifyPresence(label, LABEL_MSG);
return label;
}
}),
update: computed({
get() {
this.verifyPresence(null, UPDATE_MSG);
return null;
},
set(key, update) {
this.verifyPresence(update, UPDATE_MSG);
return update;
}
}),
inputComponent: computed('inputType', function() {
return 'ui-input';
}),
uuid: computed(function() {
return guidFor(this);
}),
inputId: computed('id', function() {
const guid = get(this, 'uuid');
return `${guid}-input`;
}),
descriptionId: computed('description', function() {
const uuid = get(this, 'uuid');
const hasDescription = !!get(this, 'description');
return hasDescription ? `${uuid}-description` : undefined;
})
});
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import { setupComponentTest } from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
describe('Integration | Component | ui form group', function() {
const LABEL = '[test-id="txtLabel"]';
const DESCRIPTION = '[test-id="txtDescription"]';
setupComponentTest('ui-form-group', {
integration: true
});
beforeEach(function() {
this.set('label', 'Test');
});
it('renders base component', function() {
this.render(hbs`{{ui-form-group label=label update=(action (mut value))}}`);
let message = 'Renders with default class';
expect(this.$('.form-group'), message).to.have.lengthOf(1);
});
describe('label', function() {
it('renders label and triggers error when not present', function(done) {
this.render(hbs`{{ui-form-group label=label update=(action (mut value))}}`);
let labelSelector = this.$(LABEL);
let message = 'Renders label attribute when one is provided';
expect(labelSelector, message).to.have.lengthOf(1);
try {
this.set('label', null);
} catch(error) {
message = 'Triggers error when label is null';
expect(error.name, message).to.eq('Error');
done();
}
});
it('renders correct value in label', function() {
this.render(hbs`{{ui-form-group label=label update=(action (mut value))}}`);
let labelSelector = this.$(LABEL).text().trim();
let message = `Renders with label value of "Test"`;
expect(labelSelector, message).to.eq('Test');
});
it('ties the label to the input', function() {
this.render(hbs`{{ui-form-group label=label update=(action (mut value))}}`);
let labelFor = this.$(LABEL).attr('for');
let message = 'Renders for attribute on label element';
expect(labelFor, message).to.match(/^ember(\d{1,10})-input/);
let inputId = this.$('input').attr('id');
message = 'Renders id attribute on form input';
expect(inputId, message).to.match(/^ember(\d{1,10})-input/);
message = 'Label for attribute and input id match';
expect(inputId, message).to.eq(labelFor);
});
it('properly toggles visiblity of label', function() {
this.set('labelVisible', false);
this.render(hbs`{{ui-form-group label=label update=(action (mut value)) labelVisible=labelVisible}}`);
let labelSelector = this.$(LABEL);
let message = 'Adds sr-only class to label';
expect(labelSelector.hasClass('sr-only'), message).to.be.true;
this.set('labelVisible', true);
message = 'Does not add sr-only class to label';
expect(labelSelector.hasClass('sr-only'), message).to.be.false;
});
it('properly focuses input when label is clicked', function() {
this.render(hbs`{{ui-form-group label=label update=(action (mut value))}}`);
let activeElement = this.$(document.activeElement).get(0);
let message = 'Focuses on body by default';
expect(activeElement.tagName, message).to.eq('BODY');
this.$(LABEL).click();
activeElement = this.$(document.activeElement).get(0);
message = 'Focuses on input when label is clicked';
expect(activeElement.tagName, message).to.eq('INPUT');
});
});
describe('input', function() {
it('renders form input', function() {
this.render(hbs`{{ui-form-group label=label update=(action (mut value)) value=value}}`);
let inputSelector = this.$('input');
let message = 'Renders for attribute on label element';
expect(inputSelector, message).to.have.lengthOf(1);
});
it('triggers an error if the update attribute is null', function(done) {
this.set('update', true);
this.render(hbs`{{ui-form-group label=label update=update value=value}}`);
try {
this.set('update', null);
} catch(error) {
let message = 'Triggers error when update is null';
expect(error.name, message).to.eq('Error');
done();
}
});
it('triggers the update action on input', function() {
this.render(hbs`{{ui-form-group label=label update=(action (mut value)) value=value}}`);
let inputSelector = this.$('input');
inputSelector.val('Boom').trigger('input');
let message = 'Triggers update action on input';
expect(this.get('value'), message).to.eq('Boom');
});
it('triggers the update action on change', function() {
this.render(hbs`{{ui-form-group label=label update=(action (mut value)) value=value}}`);
let inputSelector = this.$('input');
inputSelector.val('Boom').trigger('change');
let message = 'Triggers update action on change';
expect(this.get('value'), message).to.eq('Boom');
});
it('sets a custom input type', function() {
this.render(hbs`{{ui-form-group label=label update=(action (mut value)) inputType='search'}}`);
let inputSelector = this.$('input');
let message = 'Sets a custom input type';
expect(inputSelector.attr('type'), message).to.eq('search');
});
});
describe('description', function() {
it('renders description', function() {
this.render(hbs`{{ui-form-group label=label update=(action (mut value)) description=description}}`);
let descriptionSelector = this.$(DESCRIPTION);
let message = 'Does not render description when one is not provided';
expect(descriptionSelector, message).to.have.lengthOf(0);
this.set('description', 'Test');
descriptionSelector = this.$(DESCRIPTION);
message = 'Renders description attribute when one is provided';
expect(descriptionSelector, message).to.have.lengthOf(1);
});
it('renders correct value in description', function() {
let descriptionValue = 'Test';
this.set('description', descriptionValue);
this.render(hbs`{{ui-form-group label=label update=(action (mut value)) description=description}}`);
let descriptionSelector = this.$(DESCRIPTION).text().trim();
let message = `Renders with value of "${descriptionValue}"`;
expect(descriptionSelector, message).to.eq(descriptionValue);
});
it('ties the description to the input', function() {
this.set('description', 'Test');
this.render(hbs`{{ui-form-group label=label update=(action (mut value)) description=description}}`);
let descriptionId = this.$(DESCRIPTION).attr('id');
let message = 'Renders id attribute on description element';
expect(descriptionId, message).to.match(/^ember(\d{1,10})-description/);
let inputDescribedBy = this.$('input').attr('aria-describedby');
message = 'Renders describedby on input';
expect(inputDescribedBy, message).to.match(/^ember(\d{1,10})-description/);
message = 'Description id and input describedby attribute match';
expect(inputDescribedBy, message).to.eq(descriptionId);
});
it('properly toggles visiblity of description', function() {
this.set('description', 'Test');
this.set('descriptionVisible', false);
this.render(hbs`{{ui-form-group label=label update=(action (mut value)) description=description descriptionVisible=descriptionVisible}}`);
let descriptionSelector = this.$(DESCRIPTION);
let message = 'Adds sr-only class to description';
expect(descriptionSelector.hasClass('sr-only'), message).to.be.true;
this.set('descriptionVisible', true);
message = 'Does not add sr-only class to description';
expect(descriptionSelector.hasClass('sr-only'), message).to.be.false;
});
});
});