krivaten
3/14/2017 - 3:43 PM

ui all the things

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;
        });
    });
});