diff --git a/.editorconfig b/.editorconfig index 2fe4874..47c5438 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,6 +18,7 @@ indent_style = space indent_size = 2 [*.hbs] +insert_final_newline = false indent_style = space indent_size = 2 diff --git a/.ember-cli b/.ember-cli index 26cfcf3..ee64cfe 100644 --- a/.ember-cli +++ b/.ember-cli @@ -5,6 +5,5 @@ Setting `disableAnalytics` to true will prevent any data from being sent. */ - "disableAnalytics": false, - "liveReload": false /* Set to false for non web-socket browser debugging */ + "disableAnalytics": false } diff --git a/.npmignore b/.npmignore index a28e4ab..49996f5 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,7 @@ bower_components/ tests/ tmp/ +dist/ .bowerrc .editorconfig diff --git a/.travis.yml b/.travis.yml index cf23938..b2f7d35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ --- language: node_js +node_js: + - "0.12" sudo: false @@ -7,7 +9,24 @@ cache: directories: - node_modules +env: + - EMBER_TRY_SCENARIO=default + - EMBER_TRY_SCENARIO=ember-release + - EMBER_TRY_SCENARIO=ember-beta + - EMBER_TRY_SCENARIO=ember-canary + +matrix: + fast_finish: true + allow_failures: + - env: EMBER_TRY_SCENARIO=ember-release # https://github.com/rwjblue/ember-qunit/issues/178 + - env: EMBER_TRY_SCENARIO=ember-beta + - env: EMBER_TRY_SCENARIO=ember-canary + before_install: + - mkdir travis-phantomjs + - wget https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 + - tar -xvf $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -C $PWD/travis-phantomjs + - export PATH=$PWD/travis-phantomjs:$PATH - "npm config set spin false" - "npm install -g npm@^2" @@ -17,4 +36,4 @@ install: - bower install script: - - npm test + - ember try $EMBER_TRY_SCENARIO test diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 0000000..5e9462c --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["tmp"] +} diff --git a/Brocfile.js b/Brocfile.js index fdce144..2a682c0 100644 --- a/Brocfile.js +++ b/Brocfile.js @@ -3,23 +3,14 @@ var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); -var app = new EmberAddon({ - vendorFiles: { - 'handlebars.js': null - } -}); +/* + This Brocfile specifes the options for the dummy test app of this + addon, located in `/tests/dummy` -// Use `app.import` to add additional libraries to the generated -// output files. -// -// If you need to use different assets in different -// environments, specify an object as the first parameter. That -// object's keys should be the environment name and the values -// should be the asset to use in that environment. -// -// If the library that you are including contains AMD or ES6 -// modules that you would like to import into your application -// please specify an object with the list of modules as keys -// along with the exports of each module as its value. + This Brocfile does *not* influence how the addon or the app using it + behave. You most likely want to be modifying `./index.js` or app's Brocfile +*/ + +var app = new EmberAddon(); module.exports = app.toTree(); diff --git a/README.md b/README.md index ea8730c..609d7c8 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,17 @@ -# Ember Easy Form Extensions +Ember Easy Form Extensions [![Build Status](https://travis-ci.org/sir-dunxalot/ember-easy-form-extensions.svg?branch=master)](https://travis-ci.org/sir-dunxalot/ember-easy-form-extensions) +====== -This addon extends Ember EasyForm into the view and controller layers of your Ember CLI app to provide easy event and action handling using mixins and components. The newly accessible developer-friendly layer includes form submission handlers, components, and integration with ember-validations. +This Ember addon enhances Ember EasyForm by providing easy action handling, validations, and Ember 1.13 support for your forms -**This is also the easiest known way to use Easy Form with Ember 1.10 and HTMLBars.** +**To support Ember 1.13 Easy Form has been temporarily rewritten for Ember CLI. When EasyForm is updated by Dockyard this addon will support that instead of our own form components.** -## Ember 1.11+ Users - -Whilst this addon's master branch currently supports Ember 1.10, there are even more known issues with Ember Easy Forms and Ember 1.11. As such, work has begun on a completely Ember CLIified branch [here](https://github.com/sir-dunxalot/ember-easy-form-extensions/tree/ember-1.11) - -The `ember-1.11` branch is an evolved easy form API. It is very early stage and undocumented but no longer requires vendor files either. A release will come soon but feel free to dig into the addon tree's source in the meantime. - -```json -devDepencencies: { - "ember-easy-form-extensions": "sir-dunxalot/ember-easy-form-extensions#ember-1.11", -} -``` +Ember apps running 1.12 or below may behave unexpectedly. ## Installation -Uninstall any references to `ember-easy-form` and `ember-validations`. Then: +Uninstall any references to `ember-easy-form` and `ember-validations`and then: -``` +```sh ember install ember-easy-form-extensions ``` @@ -28,151 +19,51 @@ ember install ember-easy-form-extensions `ember-easy-form-extensions` comes prepackaged with `ember-easy-form` and `ember-validations` so you can now build awesome forms and handle the subsequent submission events just as easily as Easy Form makes writing your templates. -The below code works out of the box but is also very customizable and extendible. +Here's an example: ```hbs {{!--app-name/templates/posts/new.hbs--}} {{#form-wrapper}} {{#form-controls legend='Write a new post'}} - {{input title}} - {{input description as='text'}} - {{/form-controls}} - {{form-submission}} -{{/form-wrapper}} -``` - -```js -// app-name/views/posts/new.js + {{!--model.title--}} + {{input-group property='title'}} -import Ember from 'ember'; -import Submitting from 'ember-easy-form-extensions/mixins/views/submitting'; + {{!--model.description--}} + {{input-group property='description' type='textarea'}} -export default Ember.View.extend( - Submitting, { + {{/form-controls}} -}); + {{!--Submit and cancel buttons--}} + {{form-submission}} +{{/form-wrapper}} ``` ```js // app-name/controllers/posts/new.js import Ember from 'ember'; -import Saving from 'ember-easy-form-extensions/mixins/controllers/saving'; +import FormMixin from 'ember-easy-form-extensions/mixins/controllers/form'; -export default Ember.ObjectController.extend( - Saving, { +export default Ember.Controller.extend( + FormMixin, { - // Validations run out of the box validations: { - title: { + 'model.title': { presence: true } } - cancel: function() { - this.transitionTo('posts'); - }, - - save: function() { - // Validations have already been run by the Saving mixin - this.get('content').save().then(function(post) { - this.transitionTo('post', post); - }); - }, - -}); -``` - -# Documentation - -- [Mixins](#mixins) -- [Components](#components) -- [Templating](#templating) - -## Mixins - -The core functionality added by `ember-easy-form-extensions` lies in it's mixins. The mixins handle form submission events and work with the included components to make validating, saving, deleting, and cancelling, a breeze. - -In most situations you will add the `Saving` mixin to your controller, the `Submitting` mixin to your view, and either `Rollback` or `DeleteRecord` to your route. - -### Form Submitting (for views) - -The form submitting mixin is intended to be mixed into **views** to handle `cancel`, `save`, and `destroy` (aka delete) events. This mixin works in parallel with the included [components](#components). - -If you don't specify custom event handlers, the events will be routed directly to the controller. - -This mixin also takes care of hiding buttons after submission to enhance the usability of your forms. - -```js -// app-name/views/posts/new.js - -import Ember from 'ember'; -import Submitting from 'ember-easy-form-extensions/mixins/views/submitting'; - -export default Ember.View.extend( - Submitting, { - -}); -``` - -If you want to add custom handling for the events, you can add `submitHandler()`, `cancelHandler()`, and `destroyHandler()` to your view to handle the respective events. **Each handler should return a promise.** - -```js -// app-name/views/posts/new.js - -import Ember from 'ember'; -import Submitting from 'ember-easy-form-extensions/mixins/views/submitting'; - -export default Ember.View.extend( - Submitting, { - - submitHandler: function() { - return new Ember.RSVP.Promise(function(resolve, reject) { - // Do something unusual here, then resolve. - - this.showCoolAnimation(); - - if (!this.get('someViewProperty')) { - reject(); // Resets the form submission state - } else { - resolve(); // Will call the controller method - } - }); - } - -}); -``` - -### Form Saving (for controllers) - -The form saving mixin is intended to be mixed into **controllers**. Once added, the events handled in your view will automatically validate your model against the controller's validations object (powered by [ember-validations](https://github.com/dockyard/ember-validations)). - -In most use cases, you should add a `save()` and `cancel()` method to the controller: - -```js -// app-name/controllers/posts/new.js - -import Ember from 'ember'; -import Saving from 'ember-easy-form-extensions/mixins/controllers/saving'; - -export default Ember.ObjectController.extend( - Saving, { - - // Validations run out of the box - validations: { - title: { - presence: true - } - }, + /* Runs if cancel button in {{form-submission}} is clicked */ cancel: function() { this.transitionTo('posts'); }, + /* Runs after validations pass and submit button in {{form-submission}} is clicked */ + save: function() { - // Validations have already been run by the Saving mixin this.get('content').save().then(function(post) { this.transitionTo('post', post); }); @@ -181,276 +72,6 @@ export default Ember.ObjectController.extend( }); ``` -The saving mixin will handle all internals including the state of the form (submitted or not submitted) and running the validations - you just have to specify what you want to happen when validations have been run. - -You can also add a destroy method: - -```js -// app-name/controllers/post/edit.js - -import Ember from 'ember'; -import Saving from 'ember-easy-form-extensions/mixins/controllers/saving'; - -export default Ember.ObjectController.extend( - Saving, { - - destroy: function() { - var _this = this; - - // Runs when the user clicks on the destroy submission component after the view has handled the destroy action and checked to see if you've specified a destroyHandler in the view - _this.get('content').destroyRecord().then(function() { - _this.transitionToRoute('posts'); - }); - } - -}); -``` - -Please note, this is best used when you're deleting a persisted model, not a new one - in the latter situation consider using [the delete record mixin](#delete-record). - -The `Saving` mixin also provides support for automatically revalidating your model when you're using a computed property with `ember-validations`. Just add the computed property names to the `revalidateFor` array: - -```js -// app-name/controllers/posts/new.js - -import Ember from 'ember'; -import Saving from 'ember-easy-form-extensions/mixins/controllers/saving'; - -export default Ember.ObjectController.extend( - Saving, { - - revalidateFor: ['required'], - - // Validations run out of the box - validations: { - title: { - presence: { - if: 'required' - } - } - }, - - required: function() { - return this.get('somethingElse') && !this.get('anotherThing'); - }.property('somethingElse', 'anotherThing'), - - cancel: function() {}, - save: function() {}, - -}); -``` - -If your routes follow a RESTful naming convention, you can take advantage of two new **boolean** properties on the controller: -- `newModel` - True if the route is for a new model (e.g. `this.route('new')`) -- `editingModel` - True if the route is for editing a model (e.g. `this.route('edit')`) - -You can use these to set the button text, for example: - -```js -// app-name/controllers/posts/new.js - -import Ember from 'ember'; -import Saving from 'ember-easy-form-extensions/mixins/controllers/saving'; - -export default Ember.ObjectController.extend( - Saving, { - - saveButtonText: function() { - return this.get('editingModel') ? 'Save' : 'Add post'; - }.property('editing'), -}); -``` - -The `saveButtonText` could then be used in your [`{{form-submission}}` component](#form-submission). - -### Rollback (for routes) - -The rollback mixin is intended for use in routes where you are **editing** a model. This mixin will check to see if the model is dirty and will automatically rollback it's changes if it is. The most common reason for this to happen is the user navigates to the edit route of a resource and then clicks cancel. - -```js -// app-name/routes/post/edit.js - -import Ember from 'ember'; -import Rollback from 'ember-easy-form-extensions/mixins/routes/rollback'; - -export default Ember.Route.extend( - Rollback, { - - model: function(params) { - return this.modelFor('post'); - } - -}); -``` - -### Delete Record (for routes) - -The delete record is intended for use in routes where you are creating a **new** record. This mixin will check to see if the model is dirty and will automatically rollback it's changes if it is. The most common reason for this to happen is the user navigates to the new route of a resource and then clicks cancel. - -```js -// app-name/routes/posts/new.js - -import Ember from 'ember'; -import DeleteRecord from 'ember-easy-form-extensions/mixins/routes/delete-record'; - -export default Ember.Route.extend( - DeleteRecord, { - - model: function() { - return this.store.createRecord('post'); - } - -}); -``` - -## Components - -To extend the class of any components just import them from this addon and then export them in your app. For example: - -```js -// app-name/components/form-submissison.js - -import FormSubmissionComponent from 'ember-easy-form-extensions/components/form-submission'; - -export default FormSubmissionComponent.extend({ - // Your functionality here - className: ['buttons-group'] - classNames: ['form_submission'] -}); -``` - -### Form Wrapper - -The `{{#form-wrapper}}` component wraps your code in a `
` tag to enable HTML5 form events. It also disables HTMl5 validations. It's pretty simple: - -```hbs -{{!--app-name/templates/posts/new.hbs--}} - -{{#form-wrapper}} - {{!--Your inputs here--}} - - {{form-submission}} -{{/form-wrapper}} -``` - -You can use custom the base classname by passing a `className` attribute: - -```hbs -{{!--app-name/templates/posts/new.hbs--}} - -{{#form-wrapper className='form-static'}} - {{!--Your inputs here--}} - - {{form-submission}} -{{/form-wrapper}} -``` - -Otherwise, this component work just like any other Ember component. - -### Form Controls - -The `{{#form-controls}}` component adds more sementicism to your templates. Use it **inside** your `{{#form-wrapper}}`: - -```hbs -{{!--app-name/templates/posts/new.hbs--}} - -{{#form-wrapper}} - {{#form-controls legend='Write a new post'}} - {{!--Your inputs here--}} - {{/form-controls}} - - {{form-submission}} -{{/form-wrapper}} -``` - -Note two important things: -- `{{form-submission}}` goes **outside** the `{{#form-controls}}` -- `{{#form-controls}}` requires a `legend` attribute for accessibility - - -### Form Submission - -The `{{form-submission}}` component adds buttons for submit/save and cancel to your form. - -You can customize the text of the buttons and which buttons show by passing in options. The default values are shown below: - -```hbs -{{!--app-name/templates/posts/new.hbs--}} - -{{form-submission - cancel=true {{!--Whether to show cancel button--}} - cancelText='Cancel' {{!--Cancel button text--}} - submit=true {{!--Whether to show submit button--}} - submitText='Save' {{!--Submit button text--}] -}} -``` - -The argument can be bound easily: - -```hbs -{{!--app-name/templates/posts/new.hbs--}} - -{{!--saveButtonText is a property on the controller--}} -{{form-submission submitText=saveButtonText}} -``` - -The buttons will automatically be replaced by a [loading spinner](#loading-spinner) when the form is submitted. The form will return to it's original state if there are validation errors, etc, so the user can resubmit the form. - -### Destroy Submission - -The `{{destroy-submission}}` component can be used in the same template as the `{{form-submission}}` component. It is built to handle events that delete a record. - -An example would be the user is on the route to edit an item but is also given to option to delete the item entirely. Whether the user clicks delete, save, or cancel, all submission buttons are disabled until the event is resolved or rejected. - -You can customize the text of the button by passing in the `destroytext` option. The default value is shown below: - -```hbs -{{!--app-name/templates/post/edit.hbs--}} - -{{!--Slightly detached from your form UI...--}} -{{destroy-submission destroyText='Delete'}} - -{{!--And somewhere later in the template...--}} -{{form-submission}} -``` - -#### Template customization - -To customize the template, just override the path at `app-name/templates/components/destroy-submission.hbs` - easy! - -### Loading Spinner - -The loading spinner component replaces the buttons in your submission components when the form has been submitted. - -If you already have a spinner component simply export it at the correct path to immediately use your component: - -```js -// app-name/components/loading-spinner.js - -import CustomSpinner from 'app-name/components/custom-spinner'; - -export default CustomSpinner; -``` - -Alternatively, just add your spinner to the template: - -```hbs -{{!--app-name/templates/components/loading-spinner.hbs--}} - - -``` - -If you really don't want to use the `{{loading-spinner}}` component anywhere in your app, edit the submission component templates as described in [template customization](#template-customization). - - -## Templating - -To customize the template of any components just override the path in your app. For example, `app-name/templates/components/form-submission.hbs` - easy! - -To override the template of any Easy Form view, just override the easyform path: +## Documentation -- Error: `app-name/templates/easy-form/error.hbs` -- Hint: `app-name/templates/easy-form/hint.hbs` -- Control: `app-name/templates/easy-form/input-controls.hbs` -- Input: `app-name/templates/easy-form/input.hbs` -- Label: `app-name/templates/easy-form/label.hbs` +A walkthrough and documentation can be found in the [wiki](https://github.com/sir-dunxalot/ember-easy-form-extensions/wiki). diff --git a/addon/components/destroy-submission.js b/addon/components/destroy-submission.js deleted file mode 100644 index 48e4a80..0000000 --- a/addon/components/destroy-submission.js +++ /dev/null @@ -1,17 +0,0 @@ -import Ember from 'ember'; -import WalkViews from 'ember-easy-form-extensions/mixins/views/walk-views'; - -export default Ember.Component.extend( - WalkViews, { - - classNames: ['buttons'], - destroyText: 'Delete', - formSubmitted: Ember.computed.readOnly('formView.formSubmitted'), - iconClass: 'icon-delete', - - actions: { - destroy: function() { - this.get('formView').send('destroy'); - } - }, -}); diff --git a/addon/components/error-field.js b/addon/components/error-field.js new file mode 100644 index 0000000..2ab18d5 --- /dev/null +++ b/addon/components/error-field.js @@ -0,0 +1,73 @@ +import Ember from 'ember'; +import layout from '../templates/components/error-field'; + +const { computed, observer, on } = Ember; + +export default Ember.Component.extend({ + + /* Options */ + + className: 'error', + label: computed.oneWay('property'), + property: null, + + /* Properties */ + + bindingForErrors: null, + classNameBindings: ['className', 'showError'], + errors: null, + invalidAction: 'setGroupAsInvalid', + layout: layout, + tagName: 'span', + validAction: 'setGroupAsValid', + showError: false, + + text: computed('errors.firstObject', 'label', function() { + const error = this.get('errors.firstObject'); + const label = this.get('label'); + + return `${label} ${error}`; + }), + + formController: computed(function() { + const hasFormController = this.nearestWithProperty('formController'); + + return hasFormController.get('formController'); + }), + + /* Methods */ + + // TODO - update from observer to event when possible + + notifyChangeInValidity: observer('showError', function() { + const actionProperty = this.get('showError') ? 'invalidAction' : 'validAction'; + + this.sendAction(actionProperty); + }), + + addBindingForErrors: on('didInitAttrs', function() { + const property = this.get('property'); + + Ember.assert('You must set a property attribute on the {{error-field}} component', property); + + const formController = this.get('formController'); + const validations = formController.get('validations'); + const validationsForProperty = !!validations[property]; + + if (validationsForProperty && !this.get('bindingForErrors')) { + const errorPath = `formController.errors.${property}`; + const binding = Ember.oneWay(this, 'errors', errorPath); + + this.set('bindingForErrors', binding); + } + }), + + removeBindingForErrors: on('willDestroyElement', function() { + const property = 'bindingForErrors'; + + if (this.get(property)) { + this.get(property).disconnect(this); + this.set(property, null); + } + }), +}); diff --git a/addon/components/form-controls.js b/addon/components/form-controls.js index 2f32d9a..424fd83 100644 --- a/addon/components/form-controls.js +++ b/addon/components/form-controls.js @@ -1,16 +1,21 @@ import Ember from 'ember'; +import layout from '../templates/components/form-controls'; +import softAssert from '../utils/observers/soft-assert'; + +const { computed } = Ember; export default Ember.Component.extend({ attributeBindings: ['legend'], - classNameBindings: ['className'], className: 'controls', + classNameBindings: ['className'], + layout: layout, legend: null, + modelPath: 'model', tagName: 'fieldset', + checkForLegend: softAssert('legend'), + + isFormControls: computed(function() { + return true; + }).readOnly(), - checkForLegend: function() { - Ember.assert( - 'You must pass a legend (description) to the form-controls component like {{#form-controls legend=\'Write a new blog post\'}}', - this.get('legend') - ); - }.on('didInsertElement') }); diff --git a/addon/components/form-submission-button.js b/addon/components/form-submission-button.js new file mode 100644 index 0000000..4f45387 --- /dev/null +++ b/addon/components/form-submission-button.js @@ -0,0 +1,38 @@ +import Ember from 'ember'; +import layout from '../templates/components/form-submission-button'; + +const { computed } = Ember; + +export default Ember.Component.extend({ + + /* Options */ + + action: null, + className: 'button', + disabled: false, + text: null, + type: 'button', + + /* Properties */ + + attributeBindings: ['dataTest:data-test', 'disabled', 'type'], + classNameBindings: ['className'], + layout: layout, + tagName: 'button', + + dataTest: computed('action', function() { + const action = this.get('action') || ''; + const dasherizedAction = Ember.String.dasherize(action); + + return `button-for-${dasherizedAction}`; + }), + + /* Methods */ + + click: function(event) { + event.preventDefault(); + event.stopPropagation(); + + this.sendAction(); + }, +}); diff --git a/addon/components/form-submission.js b/addon/components/form-submission.js index 8af5a5f..6f70542 100644 --- a/addon/components/form-submission.js +++ b/addon/components/form-submission.js @@ -1,27 +1,57 @@ import Ember from 'ember'; -import WalkViews from 'ember-easy-form-extensions/mixins/views/walk-views'; +import FormSubmissionClassNameMixin from 'ember-easy-form-extensions/mixins/components/form-submission-class-name'; +import layout from '../templates/components/form-submission'; + +const { computed } = Ember; export default Ember.Component.extend( - WalkViews, { + FormSubmissionClassNameMixin, { + + /* Options */ + + className: 'form-submission', cancel: true, + cancelAction: 'cancel', cancelText: 'Cancel', - classNames: ['buttons', 'submission'], - formSubmitted: Ember.computed.readOnly('formView.formSubmitted'), - submit: true, - submitText: 'Save', + + delete: false, + deleteAction: 'delete', + deleteText: 'Delete', + + save: true, + saveAction: 'save', + saveText: 'Save', + + /* Properties */ + + classNameBindings: ['className'], + formIsSubmitted: computed.oneWay('formController.formIsSubmitted'), + layout: layout, + + formController: computed(function() { + const hasFormController = this.nearestWithProperty('formController'); + + return hasFormController.get('formController'); + }), + + /* Actions */ actions: { - cancel: function() { - this.get('formView').send('cancel'); - } + + cancel() { + this.sendAction('cancelAction'); + }, + + delete() { + this.sendAction('delete'); + }, + + save() { + this.sendAction('saveAction'); + }, + }, - _watchForEmptyComponent: function() { - Ember.warn( - 'The {{form-submission}} component is not showing the submit or the cancel button.', - this.get('cancel') || this.get('submit') - ); - }.observes('cancel', 'submit'), }); diff --git a/addon/components/form-wrapper.js b/addon/components/form-wrapper.js index b6758a7..95352d0 100644 --- a/addon/components/form-wrapper.js +++ b/addon/components/form-wrapper.js @@ -1,9 +1,55 @@ import Ember from 'ember'; +import FormSubmissionClassNameMixin from 'ember-easy-form-extensions/mixins/components/form-submission-class-name'; +import layout from '../templates/components/form-wrapper'; + +const { computed, on } = Ember; + +export default Ember.Component.extend( + FormSubmissionClassNameMixin, { + + /* Options */ -export default Ember.Component.extend({ - attributeBindings: ['novalidate'], - classNameBindings: ['className'], className: 'form', novalidate: true, + + /* Properties */ + + attributeBindings: ['novalidate'], + classNameBindings: ['className'], + formIsSubmitted: computed.oneWay('formController.formIsSubmitted'), + layout: layout, tagName: 'form', + + /* Properties */ + + /* A shim to enabled use with controller and components + moving forward */ + + formController: Ember.computed(function() { + const routeController = this.get('targetObject'); + + if (this.get('hasFormMixin')) { + return this; + } else if (routeController.get('hasFormMixin')) { + return routeController; + } else { + return null; + } + }), + + /* Methods */ + + /* Autofocus on the first input. + + TODO - move to form component + mixin when routeable components land */ + + autofocus: on('didInsertElement', function() { + var input = this.$().find('input').first(); + + if (!Ember.$(input).hasClass('datepicker')) { + input.focus(); + } + }), + }); diff --git a/addon/components/hint-field.js b/addon/components/hint-field.js new file mode 100644 index 0000000..e33ec39 --- /dev/null +++ b/addon/components/hint-field.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; +import layout from '../templates/components/hint-field'; + +export default Ember.Component.extend({ + className: ['hint'], + classNameBindings: ['className'], + layout: layout, + tagName: 'span', + text: null, +}); diff --git a/addon/components/input-group.js b/addon/components/input-group.js new file mode 100644 index 0000000..25d853a --- /dev/null +++ b/addon/components/input-group.js @@ -0,0 +1,222 @@ +import defaultFor from '../utils/default-for'; +import Ember from 'ember'; +import humanize from '../utils/humanize'; +import layout from '../templates/components/input-group'; + +const { computed, observer, on, run, typeOf } = Ember; + +export default Ember.Component.extend({ + + /* Options */ + + className: 'input-wrapper', + hint: null, + pathToInputPartials: 'form-inputs', + property: null, + newlyValidDuration: 3000, + + /* Input attributes */ + + collection: null, + content: null, + optionValuePath: 'content', + optionLabelPath: 'content', + selection: null, + multiple: null, + name: computed.oneWay('property'), + placeholder: null, + prompt: null, + disabled: false, + + /* Properties */ + + attributeBindings: ['dataTest:data-test'], + bindingForValue: null, // Avoid xBinding naming convention + classNameBindings: ['className', 'validityClass'], + formControls: null, + isInvalid: computed.not('isValid'), + isNewlyValid: false, + isValid: true, + layout: layout, + modelPath: computed.oneWay('formControls.modelPath'), + registerAction: 'registerInputGroup', + showError: false, + unregisterAction: 'unregisterInputGroup', + value: null, + + dataTest: computed('property', function() { + const property = this.get('property'); + const dasherizedProperty = Ember.String.dasherize(property); + + return `input-wrapper-for-${dasherizedProperty}`; + }), + + formController: computed(function() { + const hasFormController = this.nearestWithProperty('formController'); + + return hasFormController.get('formController'); + }), + + inputId: computed(function() { + return this.get('elementId') + '-input'; + }), + + inputPartial: computed('type', function() { + const { container, pathToInputPartials, type } = this.getProperties( + [ 'container', 'pathToInputPartials', 'type' ] + ); + + /* Remove leading and trailing slashes for consistency */ + + const dir = pathToInputPartials.replace(/^\/|\/$/g, ''); + + if (!!container.lookup(`template:${dir}/${type}`)) { + return `${dir}/${type}`; + } else { + return `${dir}/default`; + } + }), + + isInputWrapper: computed(function() { + return true; + }).readOnly(), + + label: computed('property', function() { + const property = defaultFor(this.get('property'), ''); + + return humanize(property); + }), + + propertyWithModel: computed('property', 'modelPath', function() { + const { modelPath, property } = this.getProperties( + [ 'modelPath', 'property' ] + ); + + if (modelPath) { + return `${modelPath}.${property}`; + } else { + return property; + } + }), + + type: computed('content', 'property', 'value', function() { + const property = this.get('property'); + + let type; + + if (this.get('content')) { + type = 'select'; + } else if (property.match(/password/)) { + type = 'password'; + } else if (property.match(/email/)) { + type = 'email'; + } else if (property.match(/url/)) { + type = 'url'; + } else if (property.match(/color/)) { + type = 'color'; + } else if (property.match(/^tel/) || property.match(/^phone/)) { + type = 'tel'; + } else if (property.match(/search/)) { + type = 'search'; + } else { + const value = this.get('value'); + + if (typeOf(value) === 'number') { + type = 'number'; + } else if (typeOf(value) === 'date') { + type = 'date'; + } else if (typeOf(value) === 'boolean') { + type = 'checkbox'; + } + } + + return type; + }), + + validityClass: computed('className', 'isNewlyValid', 'isValid', + function() { + const className = this.get('className'); + + let modifier; + + if (this.get('isNewlyValid')) { + modifier = 'newly-valid'; + } else if (this.get('isValid')) { + modifier = 'valid'; + } else { + modifier = 'error'; + } + + if (modifier) { + return `${className}-${modifier}`; + } else { + return className; + } + } + ), + + /* Actions */ + + actions: { + + showError() { + this.set('showError', true); + }, + + setGroupAsInvalid() { + this.set('isValid', false); + }, + + setGroupAsValid() { + this.set('isValid', true); + }, + + }, + + /* Public methods - avoid xBinding syntax */ + + listenForNewlyValid: observer('isValid', function() { + if (this.get('isValid')) { + this.set('isNewlyValid', true); + } + + run.later(this, function() { + if (!this.get('isDestroying')) { + this.set('isNewlyValid', false); + } + }, this.get('newlyValidDuration')); + }), + + removeBindingForValue: on('willDestroyElement', function() { + const property = 'bindingForValue'; + + if (this.get(property)) { + this.get(property).disconnect(this); + this.set(property, null); + } + }), + + setBindingForValue: on('didInitAttrs', function() { + Ember.assert('You must set a property attribute on the {{input-group}} component', this.get('property')); + + const propertyWithModel = this.get('propertyWithModel'); + const binding = Ember.bind(this, 'value', `formController.${propertyWithModel}`); + + this.set('bindingForValue', binding); + }), + + setFormControls: on('init', function() { + this.set('formControls', this.nearestWithProperty('isFormControls')); + }), + + /* Private methods */ + + _registerWithFormController: on('init', function() { + this.sendAction('registerAction', this); + }), + + _unregisterWithFormController: on('willDestroyElement', function() { + this.sendAction('unregisterAction', this); + }), + +}); diff --git a/addon/components/label-field.js b/addon/components/label-field.js new file mode 100644 index 0000000..05dcbc4 --- /dev/null +++ b/addon/components/label-field.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; +import layout from '../templates/components/label-field'; + +export default Ember.Component.extend({ + attributeBindings: ['for'], + className: 'label', + classNameBindings: ['className'], + for: null, + text: null, + layout: layout, + tagName: 'label', +}); diff --git a/addon/components/loading-spinner.js b/addon/components/loading-spinner.js deleted file mode 100644 index 70afc9c..0000000 --- a/addon/components/loading-spinner.js +++ /dev/null @@ -1,5 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Component.extend({ - classNames: ['spinner'] -}); diff --git a/addon/helpers/capitalize-string.js b/addon/helpers/capitalize-string.js new file mode 100644 index 0000000..6fcd40e --- /dev/null +++ b/addon/helpers/capitalize-string.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; + +export function capitalizeString(params) { + const string = params[0] || ''; + + return Ember.String.capitalize(string); +} + +export default Ember.HTMLBars.makeBoundHelper(capitalizeString); diff --git a/addon/initializers/easy-form-extensions.js b/addon/initializers/easy-form-extensions.js index e179338..fa80c08 100644 --- a/addon/initializers/easy-form-extensions.js +++ b/addon/initializers/easy-form-extensions.js @@ -1,114 +1,33 @@ import Ember from 'ember'; -export function initialize(/* container, app */) { - var run = Ember.run; +export function initialize() { - /** - Default option overrides - */ - - Ember.EasyForm.Config.registerWrapper('default', { - errorClass: 'error', - errorTemplate: 'easy-form/error', - - formClass: 'form', - fieldErrorClass: 'control-error', - - hintClass: 'hint', - hintTemplate: 'easy-form/hint', - - inputClass: 'control', - inputTemplate: 'easy-form/input', - - labelClass: 'label', - labelTemplate: 'easy-form/label' + Ember.Checkbox.reopen({ + attributeBindings: [ + 'checked:aria-checked', + ], }); - Ember.EasyForm.Checkbox.reopen({ - attributeBindings: ['dataTest:data-test'], - classNames: ['input-checkbox'], - dataTest: Ember.computed.alias('parentView.dataTest'), + Ember.TextArea.reopen({ + attributeBindings: [ + 'invalid:aria-invalid', + 'required:aria-required', + ], }); - Ember.EasyForm.TextField.reopen({ - attributeBindings: ['dataTest:data-test'], - classNames: ['input'], - dataTest: Ember.computed.alias('parentView.dataTest'), + Ember.TextField.reopen({ + attributeBindings: [ + 'invalid:aria-invalid', + 'required:aria-required', + ], }); - Ember.EasyForm.TextArea.reopen({ - attributeBindings: ['dataTest:data-test'], - classNames: ['input-textarea'], - dataTest: Ember.computed.alias('parentView.dataTest'), + Ember.Select.reopen({ + attributeBindings: [ + 'invalid:aria-invalid', + 'required:aria-required', + ], }); - - /** - Overrides the original `errorText` property to add the property name to the error message. For example: - - can't be blank --> Name can't be blank - must be a number --> Age must be a number - - If a label is specified on the input, this will be used in place of the property name. - */ - - Ember.EasyForm.Error.reopen({ - errorText: function() { - var propertyName = this.get('parentView.label') || this.get('property') || ''; - - return Ember.EasyForm.humanize(propertyName) + ' ' + this.get('errors.firstObject'); - }.property('errors.[]', 'value'), - }); - - /** - Temporarily binds a success class the the control when the input goes from invalid to valid. - */ - - Ember.EasyForm.Input.reopen({ - classNameBindings: ['showValidity:control-valid'], - showValidity: false, - - setInvalidToValid: function() { - // If we go from error to no error - if (!this.get('showError') && this.get('canShowValidationError')) { - run.debounce(this, function() { - var hasAnError = this.get('formForModel.errors.' + this.get('property') + '.length'); - - if (!hasAnError && !this.get('isDestroying')) { - this.set('showValidity', true); - - run.later(this, function() { - if (!this.get('isDestroying')) { - this.set('showValidity', false); - } - }, 2000); - } - }, 50); - } - }.observes('showError'), - - /** - An override of easyForm's default `focusOut` method to ensure validations are not shown when the user clicks cancel. - - @method focusOut - */ - - focusOut: function() { - - /* Hacky - delay check so focusOut runs after the cancel action */ - - run.later(this, function() { - var cancelClicked = this.get('parentView.cancelClicked'); - var isDestroying = this.get('isDestroying'); - - if (!cancelClicked && !isDestroying) { - this.set('hasFocusedOut', true); - this.showValidationError(); - } - }, 100); - }, - - }); - } export default { diff --git a/addon/initializers/routing-events.js b/addon/initializers/routing-events.js index 6d82589..0ddf15c 100644 --- a/addon/initializers/routing-events.js +++ b/addon/initializers/routing-events.js @@ -2,12 +2,14 @@ import Ember from 'ember'; export function initialize(/* container, app */) { + /* TODO - Deprecate with routable components */ + /** - @class Ember.ControllerMixin + @class Ember.Controller @submodule controllers */ - Ember.ControllerMixin.reopen( + Ember.Controller.reopen( Ember.Evented, { }); @@ -17,8 +19,7 @@ export function initialize(/* container, app */) { @submodule routes */ - Ember.Route.reopen( - Ember.Evented, { + Ember.Route.reopen({ /** @ISSUE https://github.com/emberjs/ember.js/issues/5394 @@ -47,7 +48,6 @@ export function initialize(/* container, app */) { didTransition: function() { this.get('controller').trigger('routeDidTransition'); - this.trigger('didTransition'); return true; // So action bubbles }, @@ -63,7 +63,6 @@ export function initialize(/* container, app */) { willTransition: function() { this.get('controller').trigger('routeWillTransition'); - this.trigger('willTransition'); return true; // So action bubbles } diff --git a/addon/mixins/components/form-submission-class-name.js b/addon/mixins/components/form-submission-class-name.js new file mode 100644 index 0000000..7fce389 --- /dev/null +++ b/addon/mixins/components/form-submission-class-name.js @@ -0,0 +1,21 @@ +import Ember from 'ember'; + +const { computed } = Ember; + +export default Ember.Mixin.create({ + className: null, + classNameBindings: ['submissionClassName'], + + submissionClassName: computed('className', 'formIsSubmitted', + function() { + const className = this.get('className'); + const formIsSubmitted = this.get('formIsSubmitted'); + + if (formIsSubmitted) { + return `${className}-submitted`; + } else { + return null; + } + } + ), +}); diff --git a/addon/mixins/controllers/conditional-validations.js b/addon/mixins/controllers/conditional-validations.js new file mode 100644 index 0000000..421788b --- /dev/null +++ b/addon/mixins/controllers/conditional-validations.js @@ -0,0 +1,41 @@ +import Ember from 'ember'; + +const { on, typeOf } = Ember; + +export default Ember.Mixin.create({ + revalidateFor: [], + + forEachRevalidator(callback) { + const revalidateFor = this.get('revalidateFor'); + + if (revalidateFor.length) { + revalidateFor.forEach(function(property) { + callback(property); + }); + } + }, + + /* TODO - update to new controller hooks with routable + components release. At that time, deprecate the routing-events + initializer */ + + _revalidate: on('routeDidTransition', + function() { + const validateExists = typeOf(this.validate) === 'function'; + + Ember.assert('No validate() method detected. You must use the conditional validations mixin with the form mixin.', validateExists); + + this.forEachRevalidator(function(property) { + this.addObserver(property, this.validate); + }.bind(this)); + } + ), + + _removeRevalidationObservers: on('routeWillTransition', + function() { + this.forEachRevalidator(function(property) { + this.removeObserver(property, this.validate); + }.bind(this)); + } + ), +}); diff --git a/addon/mixins/controllers/form.js b/addon/mixins/controllers/form.js new file mode 100644 index 0000000..6bab03a --- /dev/null +++ b/addon/mixins/controllers/form.js @@ -0,0 +1,168 @@ +import Ember from 'ember'; +import EmberValidations from 'ember-validations'; + +const { computed, on } = Ember; + +export default Ember.Mixin.create( + EmberValidations.Mixin, { + + /* Properties */ + + formIsSubmitted: false, + + editing: computed(function() { + return this.toString().indexOf('/edit:') > -1; + }), + + hasFormMixin: computed(function() { + return true; + }).readOnly(), + + new: computed(function() { + return this.toString().indexOf('/new:') > -1; + }), + + /* Actions */ + + actions: { + + /* Actions for child input groups */ + + registerInputGroup(inputGroupComponent) { + this.on('submission', function() { + inputGroupComponent.send('showError'); + }); + }, + + unregisterInputGroup(inputGroupComponent) { + this.off('submission', function() { + inputGroupComponent.send('showError'); + }); + }, + + /* Form submission actions */ + + cancel() { + this.set('formIsSubmitted', true); + this._eventHandler('cancel'); + }, + + delete() { + this.set('formIsSubmitted', true); + this._eventHandler('delete'); + }, + + /* Show validation errors on submit click */ + + save() { + this.set('formIsSubmitted', true); + this.trigger('submission'); + this._eventHandler('save'); + }, + + }, + + /* Methods */ + + resetForm: on('routeDidTransition', function() { + this.resetSubmission(); + + /* Add logic for resetting anything to do with + input here */ + }), + + resetSubmission() { + this.set('formIsSubmitted', false); + }, + + showServerError(/* xhr */) { + this.resetSubmission(); + }, + + validateThenSave() { + const _this = this; + + function resolve() { + Ember.assert( + 'You need to specify a save method on this controller', + typeof _this.save === 'function' + ); + + _this.save(); + } + + function reject() { + _this.set('formIsSubmitted', false); + } + + /* If there is a custom validations method, resolve it */ + + if (this.runCustomValidations) { + const customValidationsPromise = this.runCustomValidations(); + + if (!customValidationsPromise || !customValidationsPromise.then) { + Ember.assert( + 'runCustomValidations() must return a promise (e.g. return new Ember.RSVP.Promise(...)).' + ); + } + + customValidationsPromise.then(resolve, reject); + } else { + + /* Else save with normal ember-validations checks */ + + if (!this.get('isValid')) { + reject(); + } else { + resolve(); + } + + } + }, + + /* Private methods */ + + _eventHandler(type) { + const capitalizedType = Ember.String.capitalize(type); + const handlerMethodName = `before${capitalizedType}`; + const handler = this[handlerMethodName]; + + function isFunction(key) { + return Ember.typeOf(key) === 'function'; + } + + /* If event is save, method is renamed */ + + if (type === 'save') { + type = 'validateThenSave'; + } + + const method = this[type]; + + Ember.assert(`You need to specify a ${type} method on this controller`, method && isFunction(method)); + + /* If handler exists, resolve the promise then call + the method... */ + + if (handler) { + Ember.assert(`${handlerMethodName}() must be a function`, isFunction(handler)); + + const handlerPromise = handler(); + + if (!handlerPromise.then) { + Ember.assert('handler() must return a promise (e.g. return new Ember.RSVP.Promise(...))'); + } + + handlerPromise.then(function() { + this[type](); + }.bind(this)); + + /* ...Else, just call the method */ + + } else { + this[type](); + } + + }, + +}); diff --git a/addon/mixins/controllers/saving.js b/addon/mixins/controllers/saving.js deleted file mode 100644 index 56ae886..0000000 --- a/addon/mixins/controllers/saving.js +++ /dev/null @@ -1,90 +0,0 @@ -import Ember from 'ember'; -import EmberValidations from 'ember-validations'; - -export default Ember.Mixin.create( - EmberValidations.Mixin, { - - formSubmitted: false, - revalidateFor: [], - - forEachRevalidator: function(callback) { - var revalidateFor = this.get('revalidateFor'); - - if (revalidateFor.get('length')) { - revalidateFor.forEach(function(property) { - callback(property); - }); - } - }, - - editingModel: function() { - return this.toString().indexOf('/edit:') > -1; - }.property().readOnly(), - - newModel: function() { - return this.toString().indexOf('/new:') > -1; - }.property().readOnly(), - - showServerError: function(/* xhr */) { - this.set('formSubmitted', false); - }, - - validateAndSave: function() { - var _this = this; - var customValidationsPromise; - - var resolve = function() { - Ember.assert( - 'You need to specify a save method on this controller', - typeof _this.save === 'function' - ); - - _this.save(); - }; - - var reject = function() { - _this.set('formSubmitted', false); - }; - - /* If there is a custom validations method, resolve it */ - - if (this.runCustomValidations) { - customValidationsPromise = this.runCustomValidations(); - - if (!customValidationsPromise.then) { - Ember.assert( - 'runCustomValidations() must return a promise (e.g. return new Ember.RSVP.Promise(...)).' - ); - } - - customValidationsPromise.then(resolve, reject); - } else { - - /* Else save with normal ember-validations checks */ - - if (!_this.get('isValid')) { - reject(); - } else { - resolve(); - } - - } - }, - - _revalidate: function() { - var _this = this; - - _this.forEachRevalidator(function(property) { - _this.addObserver(property, _this.validate); - }); - }.observes('revalidateFor.[]').on('routeDidTransition'), - - _removeRevalidationObservers: function() { - var _this = this; - - _this.forEachRevalidator(function(property) { - _this.removeObserver(property, _this.validate); - }); - }.on('routeWillTransition'), - -}); diff --git a/addon/mixins/routes/delete-record.js b/addon/mixins/routes/delete-record.js deleted file mode 100644 index 88c4e6f..0000000 --- a/addon/mixins/routes/delete-record.js +++ /dev/null @@ -1,13 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Mixin.create({ - - deleteRecord: function() { - var model = this.get('controller.content'); - - if (model.get('isDirty')) { - model.deleteRecord(); - } - }.on('willTransition'), - -}); diff --git a/addon/mixins/routes/dirty-record-handler.js b/addon/mixins/routes/dirty-record-handler.js new file mode 100644 index 0000000..c722693 --- /dev/null +++ b/addon/mixins/routes/dirty-record-handler.js @@ -0,0 +1,41 @@ +/** +Undo changes in the store made to an existing but not-saved +model. This should be mixed into 'edit' routes like +`CampaignEditRoute` and `BusinessEditRoute`. All non-persisted +changes to the model are undone. + +@class DireyRecordHandler +@submodule mixins +*/ + +import Ember from 'ember'; +import defaultFor from 'ember-easy-form-extensions/utils/default-for'; + +const { on } = Ember; + +export default Ember.Mixin.create({ + + /** + If the model `isDirty` (i.e. some data has been temporarily + changed) rollback the record to the most recent clean version + in the store. If there is no clean version in the store, + delete the record. + + TODO - add ability to stop transition and ask for transition confirm + + @method rollbackifDirty + */ + + rollbackIfDirty: on('willTransition', function(model) { + model = defaultFor(model, this.get('controller.model')); + + if (model.get('isDirty')) { + if (model.get('id')) { + model.rollback(); + } else { + model.deleteRecord(); + } + } + }), + +}); diff --git a/addon/mixins/routes/rollback.js b/addon/mixins/routes/rollback.js deleted file mode 100644 index 4df51de..0000000 --- a/addon/mixins/routes/rollback.js +++ /dev/null @@ -1,13 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Mixin.create({ - - rollback: function() { - var model = this.get('controller.content'); - - if (model.get('isDirty')) { - model.rollback(); - } - }.on('willTransition'), - -}); diff --git a/addon/mixins/views/submitting.js b/addon/mixins/views/submitting.js deleted file mode 100644 index f953a2a..0000000 --- a/addon/mixins/views/submitting.js +++ /dev/null @@ -1,111 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Mixin.create({ - cancelClicked: false, - formSubmitted: Ember.computed.alias('controller.formSubmitted'), - - actions: { - - cancel: function() { - this.setProperties({ - cancelClicked: true, - formSubmitted: true - }); - - this._eventHandler('cancel'); - }, - - destroy: function() { - this.set('formSubmitted', true); - - this._eventHandler('destroy'); - }, - - }, - - /* Autofocus on the first input */ - - autofocus: function() { - var input = this.$().find('input').first(); - - if (!Ember.$(input).hasClass('datepicker')) { - input.focus(); - } - }.on('didInsertElement'), - - /* Show validation errors on submit click */ - - submit: function(event) { - event.preventDefault(); - event.stopPropagation(); - - this.set('formSubmitted', true); - - this.get('childViews').forEach(function(view) { - var viewConstructor = view.get('constructor').toString(); - - /* If the view is an Easy Form input, manually call focus - out to show the validation error */ - - if (viewConstructor === 'Ember.EasyForm.Input') { - view.focusOut(); - } - }); - - this._eventHandler('submit'); - }, - - resetForm: function() { - this.set('formSubmitted', false); - }.on('willInsertElement'), - - /* Private methods */ - - _eventHandler: function(type) { - var controller = this.get('controller'); - var methodName = type + 'Handler'; - var handler = this[methodName]; - var controllerMethod, handlerPromise; - - /* If event is submit, controller method is renamed */ - - type = type === 'submit' ? 'validateAndSave' : type; - controllerMethod = controller[type]; - - Ember.assert( - 'You need to specify a ' + type + ' method on this view\'s controller', - controllerMethod && Ember.typeOf(controllerMethod) === 'function' - ); - - /* Don't use controller[type] variable so we keep scope */ - - /* Else, if handler exists, resolve the promise then call - the method on the controller */ - - if (handler) { - Ember.assert( - methodName + '() must be a function', - Ember.typeOf(handler) === 'function' - ); - - handlerPromise = handler(); - - if (!handlerPromise.then) { - Ember.assert( - 'handler() must return a promise (e.g. return new Ember.RSVP.Promise(...))' - ); - } - - handlerPromise.then(function() { - controller[type](); - }); - - /* Else, just call the method on the controller */ - - } else { - controller[type](); - } - - }, - -}); diff --git a/addon/mixins/views/walk-views.js b/addon/mixins/views/walk-views.js deleted file mode 100644 index 4d4d0e7..0000000 --- a/addon/mixins/views/walk-views.js +++ /dev/null @@ -1,25 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Mixin.create({ - - formView: function() { - return this.walkViews(this.get('parentView')); - }.property(), - - walkViews: function(view) { - var parentView; - - if (view.submit) { - return view; - } else { - parentView = view.get('parentView'); - - if (parentView) { - return this.walkViews(parentView); - } else { - Ember.warn('No view found with the Submitting mixin.'); - } - } - }, - -}); diff --git a/addon/templates/components/error-field.hbs b/addon/templates/components/error-field.hbs new file mode 100644 index 0000000..f14cee9 --- /dev/null +++ b/addon/templates/components/error-field.hbs @@ -0,0 +1,5 @@ +{{#if errors.length}} + {{#if showError}} + {{capitalize-string text}} + {{/if}} +{{/if}} diff --git a/app/templates/components/form-controls.hbs b/addon/templates/components/form-controls.hbs similarity index 100% rename from app/templates/components/form-controls.hbs rename to addon/templates/components/form-controls.hbs diff --git a/addon/templates/components/form-submission-button.hbs b/addon/templates/components/form-submission-button.hbs new file mode 100644 index 0000000..e347e48 --- /dev/null +++ b/addon/templates/components/form-submission-button.hbs @@ -0,0 +1,5 @@ +{{#if hasBlock}} + {{yield}} +{{else}} + {{text}} +{{/if}} diff --git a/addon/templates/components/form-submission.hbs b/addon/templates/components/form-submission.hbs new file mode 100644 index 0000000..21a5ef7 --- /dev/null +++ b/addon/templates/components/form-submission.hbs @@ -0,0 +1,29 @@ +{{#if cancel}} + {{form-submission-button + action=cancelAction + disabled=formIsSubmitted + text=cancelText + type='reset' + }} +{{/if}} + +{{#if delete}} + {{form-submission-button + action=deleteAction + disabled=formIsSubmitted + text=deleteText + }} +{{/if}} + +{{#if save}} + {{form-submission-button + action=saveAction + disabled=formIsSubmitted + text=saveText + type='submit' + }} +{{/if}} + +{{#if formIsSubmitted}} + {{partial 'form-is-submitted'}} +{{/if}} diff --git a/app/templates/components/form-wrapper.hbs b/addon/templates/components/form-wrapper.hbs similarity index 100% rename from app/templates/components/form-wrapper.hbs rename to addon/templates/components/form-wrapper.hbs diff --git a/addon/templates/components/hint-field.hbs b/addon/templates/components/hint-field.hbs new file mode 100644 index 0000000..bea1f81 --- /dev/null +++ b/addon/templates/components/hint-field.hbs @@ -0,0 +1 @@ +{{capitalize-string text}} diff --git a/addon/templates/components/input-group.hbs b/addon/templates/components/input-group.hbs new file mode 100644 index 0000000..d8b0228 --- /dev/null +++ b/addon/templates/components/input-group.hbs @@ -0,0 +1,24 @@ +{{#if label}} + {{label-field + for=inputId + text=label + }} +{{/if}} + +{{#if hasBlock}} + {{!-- If block form... --}} + {{yield}} +{{else}} + {{!-- ...else show input --}} + {{partial inputPartial}} +{{/if}} + +{{error-field + label=label + property=propertyWithModel + showError=showError +}} + +{{#if hint}} + {{hint-field text=hint}} +{{/if}} diff --git a/addon/templates/components/label-field.hbs b/addon/templates/components/label-field.hbs new file mode 100644 index 0000000..bea1f81 --- /dev/null +++ b/addon/templates/components/label-field.hbs @@ -0,0 +1 @@ +{{capitalize-string text}} diff --git a/addon/utils/computed/insert.js b/addon/utils/computed/insert.js deleted file mode 100644 index 5e5012e..0000000 --- a/addon/utils/computed/insert.js +++ /dev/null @@ -1,28 +0,0 @@ -import Em from 'ember'; - -/** -Example usage: - -``` -App.SomeController = Em.Controller.extend({ - type: 'MultipleChoice', - questionController: Utils.computed.insert('type', 'App.{{value}}QuestionController') -}); -``` - -`this.get('questionController')` will now return `App.MultipleChoiceQuestionController`. - -@method Utils.insert -@param {String} dependentKey The name of the Ember property to observe -@param {String} string The string to insert the value of `dependentKey` into -@return A string equal to `string` but with `{{value}}` replaced by the value of `dependentKey` -*/ - -export default function(dependentKey, string) { - return function() { - var inCorrectFormat = string.indexOf('{{value}}') > -1; - - Em.assert('You must pass a string in the format "Some stuff {{value}}" as the second argument of Utils.computed.insert', inCorrectFormat); - return string.replace('{{value}}', this.get(dependentKey)); - }.property(dependentKey); -} diff --git a/addon/utils/humanize.js b/addon/utils/humanize.js new file mode 100644 index 0000000..0fa6954 --- /dev/null +++ b/addon/utils/humanize.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +export default function(string) { + const underscored = Ember.String.underscore(string); + const spaced = underscored.split('_').join(' '); + + /* Replace dots then remove double spaces */ + + return spaced.replace(/\./g, ' ').replace(/ +(?= )/g,''); +} diff --git a/addon/utils/observers/soft-assert.js b/addon/utils/observers/soft-assert.js new file mode 100644 index 0000000..fb07581 --- /dev/null +++ b/addon/utils/observers/soft-assert.js @@ -0,0 +1,46 @@ +/** +Checks whether a property is present on a class and shows a warning to the developer when it is not. + +Options can be pased as a second parameter: + +```js +Ember.Component.extend({ + checkForDescription: softAssert('descriptions', { + eventName: 'didInsertElement', // Defaults to 'init' + onTrue: function() { + this.set('hasCorrectProperties', true); + }, + onFalse: function() { + this.set('hasCorrectProperties', false); + } + }); +}); +``` + +@method Utils.computed.softAssert +@param {String} dependentKey The name of the Ember property to observe +@param {Object} options An object containing options for your assertion +*/ + +import Ember from 'ember'; +import defaultFor from '../default-for'; + +export default function softAssert(dependentKey, options = {}) { + const eventName = defaultFor(options.eventName, 'init'); + + return Ember.on(eventName, function() { + const value = defaultFor(this.get(dependentKey), ''); + + if (!value) { + const name = this.get('className'); // TODO - Find a better way to identify component constructor + + Ember.warn(`You failed to pass a ${dependentKey} property to ${name}`); + + if (options.onTrue) { + options.callbacks.onTrue().bind(this); + } + } else if (options.onFalse) { + options.callbacks.onFalse().bind(this); + } + }); +} diff --git a/app/components/destroy-submission.js b/app/components/destroy-submission.js index f05485b..94e4f92 100644 --- a/app/components/destroy-submission.js +++ b/app/components/destroy-submission.js @@ -1,3 +1 @@ -import Component from 'ember-easy-form-extensions/components/destroy-submission'; - -export default Component; +export { default } from 'ember-easy-form-extensions/components/destroy-submission'; diff --git a/app/components/error-field.js b/app/components/error-field.js new file mode 100644 index 0000000..914bf7b --- /dev/null +++ b/app/components/error-field.js @@ -0,0 +1 @@ +export { default } from 'ember-easy-form-extensions/components/error-field'; diff --git a/app/components/form-controls.js b/app/components/form-controls.js index 8a00b28..249214e 100644 --- a/app/components/form-controls.js +++ b/app/components/form-controls.js @@ -1,3 +1 @@ -import Component from 'ember-easy-form-extensions/components/form-controls'; - -export default Component; +export { default } from 'ember-easy-form-extensions/components/form-controls'; diff --git a/app/components/form-submission-button.js b/app/components/form-submission-button.js new file mode 100644 index 0000000..a6503b6 --- /dev/null +++ b/app/components/form-submission-button.js @@ -0,0 +1 @@ +export { default } from 'ember-easy-form-extensions/components/form-submission-button'; \ No newline at end of file diff --git a/app/components/form-submission.js b/app/components/form-submission.js index fd457ea..50b238f 100644 --- a/app/components/form-submission.js +++ b/app/components/form-submission.js @@ -1,3 +1 @@ -import Component from 'ember-easy-form-extensions/components/form-submission'; - -export default Component; +export { default } from 'ember-easy-form-extensions/components/form-submission'; diff --git a/app/components/form-wrapper.js b/app/components/form-wrapper.js index b635a84..4e7d460 100644 --- a/app/components/form-wrapper.js +++ b/app/components/form-wrapper.js @@ -1,3 +1 @@ -import Component from 'ember-easy-form-extensions/components/form-wrapper'; - -export default Component; +export { default } from 'ember-easy-form-extensions/components/form-wrapper'; diff --git a/app/components/hint-field.js b/app/components/hint-field.js new file mode 100644 index 0000000..79d5c89 --- /dev/null +++ b/app/components/hint-field.js @@ -0,0 +1 @@ +export { default } from 'ember-easy-form-extensions/components/hint-field'; diff --git a/app/components/input-group.js b/app/components/input-group.js new file mode 100644 index 0000000..886dbeb --- /dev/null +++ b/app/components/input-group.js @@ -0,0 +1 @@ +export { default } from 'ember-easy-form-extensions/components/input-group'; diff --git a/app/components/label-field.js b/app/components/label-field.js new file mode 100644 index 0000000..b433f01 --- /dev/null +++ b/app/components/label-field.js @@ -0,0 +1 @@ +export { default } from 'ember-easy-form-extensions/components/label-field'; diff --git a/app/components/loading-spinner.js b/app/components/loading-spinner.js index 73d82c5..c30a1d7 100644 --- a/app/components/loading-spinner.js +++ b/app/components/loading-spinner.js @@ -1,3 +1 @@ -import Component from 'ember-easy-form-extensions/components/loading-spinner'; - -export default Component; +export { default } from 'ember-easy-form-extensions/components/loading-spinner'; diff --git a/app/helpers/capitalize-string.js b/app/helpers/capitalize-string.js new file mode 100644 index 0000000..cbe68ac --- /dev/null +++ b/app/helpers/capitalize-string.js @@ -0,0 +1 @@ +export { default } from 'ember-easy-form-extensions/helpers/capitalize-string'; diff --git a/app/initializers/easy-form-extensions.js b/app/initializers/easy-form-extensions.js index b5ea7f8..4e101ea 100644 --- a/app/initializers/easy-form-extensions.js +++ b/app/initializers/easy-form-extensions.js @@ -1,3 +1 @@ -import Initializer from 'ember-easy-form-extensions/initializers/easy-form-extensions'; - -export default Initializer; +export { default } from 'ember-easy-form-extensions/initializers/easy-form-extensions'; diff --git a/app/initializers/routing-events.js b/app/initializers/routing-events.js index 9072cd2..a122df5 100644 --- a/app/initializers/routing-events.js +++ b/app/initializers/routing-events.js @@ -1,3 +1 @@ -import Initializer from 'ember-easy-form-extensions/initializers/routing-events'; - -export default Initializer; +export { default } from 'ember-easy-form-extensions/initializers/routing-events'; diff --git a/app/templates/components/destroy-submission.hbs b/app/templates/components/destroy-submission.hbs deleted file mode 100644 index 27f4fca..0000000 --- a/app/templates/components/destroy-submission.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{#if formSubmitted}} - {{loading-spinner}} -{{else}} - -{{/if}} diff --git a/app/templates/components/form-submission.hbs b/app/templates/components/form-submission.hbs deleted file mode 100644 index 96e7af3..0000000 --- a/app/templates/components/form-submission.hbs +++ /dev/null @@ -1,22 +0,0 @@ -{{#if formSubmitted}} - {{loading-spinner}} -{{else}} - - {{#if cancel}} - - {{/if}} - - {{#if submit}} - - {{/if}} - -{{/if}} diff --git a/app/templates/components/loading-spinner.hbs b/app/templates/components/loading-spinner.hbs deleted file mode 100644 index 40f3548..0000000 --- a/app/templates/components/loading-spinner.hbs +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/templates/easy-form/error.hbs b/app/templates/easy-form/error.hbs deleted file mode 100644 index d5d3082..0000000 --- a/app/templates/easy-form/error.hbs +++ /dev/null @@ -1 +0,0 @@ -{{view.errorText}} diff --git a/app/templates/easy-form/hint.hbs b/app/templates/easy-form/hint.hbs deleted file mode 100644 index ba2337b..0000000 --- a/app/templates/easy-form/hint.hbs +++ /dev/null @@ -1 +0,0 @@ -{{view.hintText}} diff --git a/app/templates/easy-form/input-controls.hbs b/app/templates/easy-form/input-controls.hbs deleted file mode 100644 index 7b7d540..0000000 --- a/app/templates/easy-form/input-controls.hbs +++ /dev/null @@ -1,15 +0,0 @@ -{{input-field - propertyBinding='view.property' - inputOptionsBinding='view.inputOptionsValues' -}} - -{{#if view.showError}} - {{error-field propertyBinding='view.property'}} -{{/if}} - -{{#if view.hint}} - {{hint-field - propertyBinding='view.property' - textBinding='view.hint' - }} -{{/if}} diff --git a/app/templates/easy-form/input.hbs b/app/templates/easy-form/input.hbs deleted file mode 100644 index d222120..0000000 --- a/app/templates/easy-form/input.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{label-field propertyBinding='view.property' textBinding='view.label'}} - -
- {{partial 'easy-form/input-controls'}} -
diff --git a/app/templates/easy-form/label.hbs b/app/templates/easy-form/label.hbs deleted file mode 100644 index f87d1de..0000000 --- a/app/templates/easy-form/label.hbs +++ /dev/null @@ -1 +0,0 @@ -{{view.labelText}} diff --git a/app/templates/form-inputs/checkbox.hbs b/app/templates/form-inputs/checkbox.hbs new file mode 100644 index 0000000..e4535f9 --- /dev/null +++ b/app/templates/form-inputs/checkbox.hbs @@ -0,0 +1,12 @@ +{{input + id=inputId + + placeholder=placeholder + name=name + type=type + checked=value + disabled=disabled + tabindex=tabindex + indeterminate=indeterminate + form=form +}} diff --git a/app/templates/form-inputs/default.hbs b/app/templates/form-inputs/default.hbs new file mode 100644 index 0000000..ca88747 --- /dev/null +++ b/app/templates/form-inputs/default.hbs @@ -0,0 +1,36 @@ +{{input + id=inputId + invalid=isInvalid + focus-out='showError' + + placeholder=placeholder + name=name + type=type + value=value + readonly=readonly + required=required + autofocus=autofocus + disabled=disabled + size=size + tabindex=tabindex + maxlegth=maxlength + min=min + max=max + pattern=patterm + accept=accept + autocomplete=autocomplete + autosave=autosave + formaction=formaction + formenctype=formenctype + formmethod=formmethod + formnovalidate=formnovalidate + formtarget=formtarget + height=height + inputmode=inputmode + multiple=multiple + step=step + width=width + form=form + selectionDirection=selectionDirection + spellcheck=spellcheck +}} diff --git a/app/templates/form-inputs/select.hbs b/app/templates/form-inputs/select.hbs new file mode 100644 index 0000000..c9aaac7 --- /dev/null +++ b/app/templates/form-inputs/select.hbs @@ -0,0 +1,14 @@ +{{view 'select' + id=inputId + invalid=isInvalid + + content=content + name=name + value=value + selection=selection + prompt=prompt + disabled=disabled + required=required + optionValuePath=optionValuePath + optionLabelPath=optionLabelPath +}} diff --git a/app/templates/form-inputs/textarea.hbs b/app/templates/form-inputs/textarea.hbs new file mode 100644 index 0000000..47a5e96 --- /dev/null +++ b/app/templates/form-inputs/textarea.hbs @@ -0,0 +1,24 @@ +{{textarea + id=inputId + invalid=isInvalid + focus-out='showError' + + placeholder=placeholder + name=name + type=type + value=value + rows=rows + cols=cols + disabled=disabled + maxlength=maxLength + tabindex=tabIndex + selectionEnd=selectionEnd + selectionStart=selectionStart + selectionDirection=selectionDirection + wrap=wrap + readonly=readonly + autofocus=autofocus + form=form + spellcheck=spellcheck + required=required +}} diff --git a/app/templates/form-is-submitted.hbs b/app/templates/form-is-submitted.hbs new file mode 100644 index 0000000..1a0b3dc --- /dev/null +++ b/app/templates/form-is-submitted.hbs @@ -0,0 +1 @@ +Submitting... diff --git a/app/test-helpers/sync/fill-in-input.js b/app/test-helpers/sync/fill-in-input.js new file mode 100644 index 0000000..95d118d --- /dev/null +++ b/app/test-helpers/sync/fill-in-input.js @@ -0,0 +1 @@ +export { default } from 'ember-easy-form-extensions/test-helpers/sync/fill-in-input'; \ No newline at end of file diff --git a/bower.json b/bower.json index b26ae82..a70a2fd 100644 --- a/bower.json +++ b/bower.json @@ -1,16 +1,17 @@ { "name": "ember-easy-form-extensions", "dependencies": { - "jquery": "^1.11.1", - "ember": "1.10.0", - "ember-data": "1.0.0-beta.15", - "ember-resolver": "~0.1.12", - "loader.js": "ember-cli/loader.js#3.2.0", + "ember": "1.13.2", "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3", "ember-cli-test-loader": "ember-cli-test-loader#0.1.3", - "ember-load-initializers": "ember-cli/ember-load-initializers#0.0.2", - "ember-qunit": "0.2.8", + "ember-data": "1.13.4", + "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.4", + "ember-qunit": "0.3.3", "ember-qunit-notifications": "0.0.7", - "qunit": "~1.17.1" + "ember-resolver": "~0.1.15", + "jquery": "^1.11.1", + "loader.js": "ember-cli/loader.js#3.2.0", + "qunit": "^1.18.0", + "ember-cli-moment-shim": "~0.2.0" } -} \ No newline at end of file +} diff --git a/config/ember-try.js b/config/ember-try.js new file mode 100644 index 0000000..83dab0f --- /dev/null +++ b/config/ember-try.js @@ -0,0 +1,35 @@ +module.exports = { + scenarios: [ + { + name: 'default', + dependencies: { } + }, + { + name: 'ember-release', + dependencies: { + 'ember': 'components/ember#release' + }, + resolutions: { + 'ember': 'release' + } + }, + { + name: 'ember-beta', + dependencies: { + 'ember': 'components/ember#beta' + }, + resolutions: { + 'ember': 'beta' + } + }, + { + name: 'ember-canary', + dependencies: { + 'ember': 'components/ember#canary' + }, + resolutions: { + 'ember': 'canary' + } + } + ] +}; diff --git a/index.js b/index.js index 9a97b38..88ae01a 100644 --- a/index.js +++ b/index.js @@ -2,14 +2,5 @@ 'use strict'; module.exports = { - name: 'ember-easy-form-extensions', - - included: function(app) { - - /* Because ember-easy-form isn't Ember CLI-ified */ - - app.import('vendor/ember-easy-form.js', { - type: 'vendor' - }); - } + name: 'ember-easy-form-extensions' }; diff --git a/package.json b/package.json index a1e1d5f..3b09fdd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ember-easy-form-extensions", - "version": "0.2.11", - "description": "Extends Ember EasyForm into the view and controller layers of your Ember CLI app to provide easy event and action handling using mixins and components.", + "version": "1.0.0", + "description": "Enhances Ember EasyForm by providing easy action handling, validations, and Ember 1.13 support for your forms", "directories": { "doc": "doc", "test": "tests" @@ -9,34 +9,31 @@ "scripts": { "start": "ember server", "build": "ember build", - "test": "ember test" - }, - "repository": { - "type": "git", - "url": "git://github.com/sir-dunxalot/ember-easy-form-extensions.git" + "test": "ember try:testall" }, + "repository": "https://github.com/sir-dunxalot/ember-easy-form-extensions", "engines": { "node": ">= 0.10.0" }, - "author": "Duncan Walker ", + "author": "", "license": "MIT", - "dependencies": { - "ember-validations": "2.0.0-alpha.3" - }, "devDependencies": { - "broccoli-asset-rev": "^2.0.0", - "ember-cli": "0.2.0", - "ember-cli-app-version": "0.3.2", - "ember-cli-babel": "^4.0.0", - "ember-cli-content-security-policy": "0.3.0", - "ember-cli-dependency-checker": "0.0.8", - "ember-cli-htmlbars": "0.7.4", + "broccoli-asset-rev": "^2.0.2", + "ember-cli": "0.2.7", + "ember-cli-app-version": "0.3.3", + "ember-cli-content-security-policy": "0.4.0", + "ember-cli-dependency-checker": "^1.0.0", "ember-cli-ic-ajax": "0.1.1", "ember-cli-inject-live-reload": "^1.3.0", - "ember-cli-qunit": "0.3.9", - "ember-cli-uglify": "1.0.1", - "ember-data": "1.0.0-beta.15", - "ember-export-application-global": "^1.0.2" + "ember-cli-qunit": "0.3.13", + "ember-cli-uglify": "^1.0.1", + "ember-data": "1.13.4", + "ember-data-fixture-adapter": "1.0.0", + "ember-disable-prototype-extensions": "^1.0.0", + "ember-disable-proxy-controllers": "^1.0.0", + "ember-export-application-global": "^1.0.2", + "ember-moment": "1.2.1", + "ember-try": "0.0.6" }, "keywords": [ "ember-addon", @@ -52,6 +49,11 @@ "control", "controls" ], + "dependencies": { + "ember-cli-babel": "^5.0.0", + "ember-cli-htmlbars": "0.7.6", + "ember-validations": "2.0.0-alpha.3" + }, "ember-addon": { "configPath": "tests/dummy/config" } diff --git a/testem.json b/testem.json index 42a4ddb..0f35392 100644 --- a/testem.json +++ b/testem.json @@ -1,6 +1,7 @@ { "framework": "qunit", "test_page": "tests/index.html?hidepassed", + "disable_watching": true, "launch_in_ci": [ "PhantomJS" ], diff --git a/tests/.jshintrc b/tests/.jshintrc index ea8b88f..f06bd1b 100644 --- a/tests/.jshintrc +++ b/tests/.jshintrc @@ -21,7 +21,14 @@ "andThen", "currentURL", "currentPath", - "currentRouteName" + "currentRouteName", + "moment", + "asyncClick", + "clickInputFor", + "fillInInputFor", + "inspect", + "inspectInputFor", + "inspectModelValueFor" ], "node": false, "browser": false, diff --git a/tests/acceptance/fruit-form-test.js b/tests/acceptance/fruit-form-test.js new file mode 100644 index 0000000..e764695 --- /dev/null +++ b/tests/acceptance/fruit-form-test.js @@ -0,0 +1,128 @@ +import Ember from 'ember'; +import Fruit from '../fixtures/fruit'; +import { module, test } from 'qunit'; +import startApp from '../helpers/start-app'; + +let application; + +const { + color, + description, + isTropical, + name, + numberOfSeeds, + pickedOn, +} = Fruit; + +module('Acceptance | fruit/form', { + beforeEach: function() { + application = startApp(); + }, + + afterEach: function() { + Ember.run(application, 'destroy'); + } +}); + +test('Binding text values to inputs and the model', function(assert) { + + assert.expect(6); + + visit('/fruit/form'); + + fillInInputFor('name', name); + + fillInInputFor('description', description, 'textarea'); + + fillInInputFor('numberOfSeeds', numberOfSeeds); + + andThen(function() { + + assert.equal(inspectInputFor('name').val(), name, + `The title value should be updated on the input element`); + + assert.equal(inspectInputFor('description', 'textarea').val(), description, + `The description value should be updated on the textarea element`); + + assert.equal(inspectInputFor('numberOfSeeds').val(), numberOfSeeds, + `The numberOfSeeds value should be updated on the input element`); + + [ + 'description', + 'name', + 'numberOfSeeds' + ].forEach(function(property) { + + assert.equal(inspectModelValueFor(property), Fruit.get(property), + `${property} should be bound to the model`); + + }); + + }); +}); + +test('Binding select values and options', function(assert) { + const defaultValue = 'orange'; + + visit('/fruit/form'); + + /* Check the default value */ + + andThen(function() { + + assert.equal(inspectInputFor('color', 'select').val(), defaultValue, + `The default value of the selected option should be set by default on the model`); + + assert.equal(inspectModelValueFor('color'), defaultValue, + `Default select values should be bound to the model`); + + }); + + fillInInputFor('color', color, 'select'); + + /* Then check changing the select works */ + + andThen(function() { + + assert.equal(inspectInputFor('color', 'select').val(), color, + `The value of the selected option should be updated on the select`); + + assert.equal(inspectModelValueFor('color'), color, + `Select values should be bound to the model`); + + }); + +}); + +test('Binding checkbox values', function(assert) { + + visit('/fruit/form'); + + clickInputFor('isTropical'); + + andThen(function() { + + assert.equal(inspectInputFor('isTropical').prop('checked'), isTropical, + `The value of isTropical should be updated on the checkbox`); + + assert.equal(inspectModelValueFor('isTropical'), isTropical.toString(), + `Checkbox values should be bound to the model`); + + }); + +}); + +// TODO - Dates - coming soon + +// test('Binding dates', function(assert) { + +// visit('/fruit/form'); + +// fillInInputFor('pickedOn', pickedOn); + +// andThen(function() { + + +// }); + +// }); diff --git a/tests/acceptance/model-path-test.js b/tests/acceptance/model-path-test.js new file mode 100644 index 0000000..6c94e9d --- /dev/null +++ b/tests/acceptance/model-path-test.js @@ -0,0 +1,45 @@ +import Ember from 'ember'; +import Fruit from '../fixtures/fruit'; +import { module, test } from 'qunit'; +import startApp from '../helpers/start-app'; + +let application; + +const { + color, + description, + isTropical, + name, + numberOfSeeds, + pickedOn, +} = Fruit; + +module('Acceptance | model path', { + beforeEach: function() { + application = startApp(); + }, + + afterEach: function() { + Ember.run(application, 'destroy'); + } +}); + +test('Binding text values to inputs and the model', function(assert) { + const name = 'Banana'; + + assert.expect(2); + + visit('/fruit/model-path'); + + fillInInputFor('name', name); + + andThen(function() { + + assert.equal(inspectInputFor('name').val(), name, + `The name value should be updated on the input element`); + + assert.equal(inspectModelValueFor('fruit-name'), name, + `The name value should be updated on model`); + + }); +}); diff --git a/tests/dummy/app/adapters/application.js b/tests/dummy/app/adapters/application.js index b0a4c1e..e0e672a 100644 --- a/tests/dummy/app/adapters/application.js +++ b/tests/dummy/app/adapters/application.js @@ -1,6 +1,7 @@ import DS from 'ember-data'; +import FixtureAdapter from 'ember-data-fixture-adapter'; -export default DS.FixtureAdapter.extend({ +export default FixtureAdapter.extend({ simulateRemoteResponse: false, /** diff --git a/tests/dummy/app/app.js b/tests/dummy/app/app.js index 757df38..8d66b95 100644 --- a/tests/dummy/app/app.js +++ b/tests/dummy/app/app.js @@ -3,9 +3,11 @@ import Resolver from 'ember/resolver'; import loadInitializers from 'ember/load-initializers'; import config from './config/environment'; +var App; + Ember.MODEL_FACTORY_INJECTIONS = true; -var App = Ember.Application.extend({ +App = Ember.Application.extend({ modulePrefix: config.modulePrefix, podModulePrefix: config.podModulePrefix, Resolver: Resolver diff --git a/tests/dummy/app/components/.gitkeep b/tests/dummy/app/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/controllers/.gitkeep b/tests/dummy/app/controllers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/initializers/test-attributes.js b/tests/dummy/app/initializers/test-attributes.js new file mode 100644 index 0000000..6cb8834 --- /dev/null +++ b/tests/dummy/app/initializers/test-attributes.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; + +export function initialize(/* container, application */) { + + ['Checkbox', 'Component', 'TextArea', 'TextField', 'Select'].forEach(function(instanceName) { + Ember[instanceName].reopen({ + attributeBindings: ['dataTest:data-test'], + dataTest: null, + }); + }); + +} + +export default { + name: 'test-attributes', + initialize: initialize +}; diff --git a/tests/dummy/app/models/.gitkeep b/tests/dummy/app/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/resources/application/template.hbs b/tests/dummy/app/resources/application/template.hbs new file mode 100644 index 0000000..612fbf4 --- /dev/null +++ b/tests/dummy/app/resources/application/template.hbs @@ -0,0 +1,12 @@ +

ember-easy-form-extensions

+
+
Current path
+
{{currentPath}}
+
+ + + +{{outlet}} diff --git a/tests/dummy/app/resources/fruit/form/controller.js b/tests/dummy/app/resources/fruit/form/controller.js new file mode 100644 index 0000000..fb82ec5 --- /dev/null +++ b/tests/dummy/app/resources/fruit/form/controller.js @@ -0,0 +1,38 @@ +import Ember from 'ember'; +import FormMixin from 'ember-easy-form-extensions/mixins/controllers/form'; + +export default Ember.Controller.extend( + FormMixin, { + + colors: Ember.A(['orange', 'yellow', 'green']), + + validations: { + 'model.name': { + presence: true + }, + 'model.description': { + presence: true + }, + 'model.color': { + presence: true + } + }, + + save: function() { + Ember.debug('Saving'); + }, + + cancel: function() { + this.transitionToRoute('index'); + }, + + submitHandler: function() { + return new Ember.RSVP.Promise(function(resolve, reject) { + Ember.debug('Submit handler running'); + + resolve(); + }); + } + + +}); diff --git a/tests/dummy/app/resources/fruit/form/route.js b/tests/dummy/app/resources/fruit/form/route.js new file mode 100644 index 0000000..6adbdcf --- /dev/null +++ b/tests/dummy/app/resources/fruit/form/route.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import DirtyRecordHandler from 'ember-easy-form-extensions/mixins/routes/dirty-record-handler'; + +export default Ember.Route.extend( + DirtyRecordHandler, { + + model: function() { + return this.store.createRecord('fruit'); + } + +}); diff --git a/tests/dummy/app/resources/fruit/form/template.hbs b/tests/dummy/app/resources/fruit/form/template.hbs new file mode 100644 index 0000000..6e6b0b0 --- /dev/null +++ b/tests/dummy/app/resources/fruit/form/template.hbs @@ -0,0 +1,11 @@ +{{#form-wrapper}} + + {{#form-controls legend='Write a new post'}} + {{partial 'input-groups'}} + {{/form-controls}} + + {{form-submission}} + +{{/form-wrapper}} + +{{partial 'input-values'}} diff --git a/tests/dummy/app/resources/fruit/model-path/controller.js b/tests/dummy/app/resources/fruit/model-path/controller.js new file mode 100644 index 0000000..d9e70ca --- /dev/null +++ b/tests/dummy/app/resources/fruit/model-path/controller.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; +import FormMixin from 'ember-easy-form-extensions/mixins/controllers/form'; + +export default Ember.Controller.extend( + FormMixin, { + + model: { + fruit: { + name: null + } + }, + +}); diff --git a/tests/dummy/app/resources/fruit/model-path/template.hbs b/tests/dummy/app/resources/fruit/model-path/template.hbs new file mode 100644 index 0000000..83a32d7 --- /dev/null +++ b/tests/dummy/app/resources/fruit/model-path/template.hbs @@ -0,0 +1,16 @@ +{{#form-wrapper}} + + {{#form-controls legend='Write a new post' modelPath='model.fruit'}} + + {{input-group property='name'}} + + {{/form-controls}} + + {{form-submission}} + +{{/form-wrapper}} + +
+
Fruit Name
+
{{model.fruit.name}}
+
diff --git a/tests/dummy/app/resources/fruit/model.js b/tests/dummy/app/resources/fruit/model.js new file mode 100644 index 0000000..a8cae1a --- /dev/null +++ b/tests/dummy/app/resources/fruit/model.js @@ -0,0 +1,28 @@ +import DS from 'ember-data'; + +const { attr } = DS; + +export default DS.Model.extend({ + color: attr('string', { + defaultValue() { + return 'orange'; + } + }), // Select + numberOfSeeds: attr('number', { + defaultValue() { + return 10; + } + }), + description: attr('string'), // Textarea + isTropical: attr('boolean', { + defaultValue() { + return false; + } + }), + name: attr('string'), + pickedOn: attr('date', { + defaultValue() { + return moment('2008-06-24').toDate(); + } + }), +}); diff --git a/tests/dummy/app/resources/post/edit/controller.js b/tests/dummy/app/resources/post/edit/controller.js deleted file mode 100644 index 3d4ec50..0000000 --- a/tests/dummy/app/resources/post/edit/controller.js +++ /dev/null @@ -1,26 +0,0 @@ -import Ember from 'ember'; -import Saving from 'ember-easy-form-extensions/mixins/controllers/saving'; - -export default Ember.ObjectController.extend( - Saving, { - - validations: { - title: { - presence: true - } - }, - - save: function() { - console.log('Saving'); - }, - - cancel: function() { - this.transitionToRoute('index'); - }, - - destroy: function() { - console.log('Destroying'); - } - -}); - diff --git a/tests/dummy/app/resources/post/edit/route.js b/tests/dummy/app/resources/post/edit/route.js deleted file mode 100644 index da12612..0000000 --- a/tests/dummy/app/resources/post/edit/route.js +++ /dev/null @@ -1,11 +0,0 @@ -import Ember from 'ember'; -import Rollback from 'ember-easy-form-extensions/mixins/routes/rollback'; - -export default Ember.Route.extend( - Rollback, { - - model: function() { - return this.modelFor('post'); - } - -}); diff --git a/tests/dummy/app/resources/post/edit/view.js b/tests/dummy/app/resources/post/edit/view.js deleted file mode 100644 index f325373..0000000 --- a/tests/dummy/app/resources/post/edit/view.js +++ /dev/null @@ -1,5 +0,0 @@ -import PostsNewView from 'dummy/resources/posts/new/view'; - -export default PostsNewView.extend({ - templateName: 'posts/new' -}); diff --git a/tests/dummy/app/resources/post/index/controller.js b/tests/dummy/app/resources/post/index/controller.js deleted file mode 100644 index e2b8e36..0000000 --- a/tests/dummy/app/resources/post/index/controller.js +++ /dev/null @@ -1,5 +0,0 @@ -import Ember from 'ember'; - -export default Ember.ObjectController.extend({ - -}); diff --git a/tests/dummy/app/resources/post/index/route.js b/tests/dummy/app/resources/post/index/route.js deleted file mode 100644 index b5e667e..0000000 --- a/tests/dummy/app/resources/post/index/route.js +++ /dev/null @@ -1,9 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Route.extend({ - - model: function(params) { - return this.store.find('post', params.id); - } - -}); diff --git a/tests/dummy/app/resources/post/index/template.hbs b/tests/dummy/app/resources/post/index/template.hbs deleted file mode 100644 index 99d09d4..0000000 --- a/tests/dummy/app/resources/post/index/template.hbs +++ /dev/null @@ -1,6 +0,0 @@ -
-
Title
-
{{title}}
-
Description
-
{{description}}
-
diff --git a/tests/dummy/app/resources/post/model.js b/tests/dummy/app/resources/post/model.js deleted file mode 100644 index 671dcb7..0000000 --- a/tests/dummy/app/resources/post/model.js +++ /dev/null @@ -1,21 +0,0 @@ -import DS from 'ember-data'; -import Ember from 'ember'; - -var attr = DS.attr; - -var PostModel = DS.Model.extend({ - title: attr('string'), - description: attr('string') -}); - -PostModel.reopenClass({ - FIXTURES: [ - { - id: 1, - title: 'How to Ember', - description: 'This is an introduction on how to Ember. Wow.' - } - ] -}); - -export default PostModel; diff --git a/tests/dummy/app/resources/posts/index/controller.js b/tests/dummy/app/resources/posts/index/controller.js deleted file mode 100644 index 602325d..0000000 --- a/tests/dummy/app/resources/posts/index/controller.js +++ /dev/null @@ -1,5 +0,0 @@ -import Ember from 'ember'; - -export default Ember.ArrayController.extend({ - -}); diff --git a/tests/dummy/app/resources/posts/index/route.js b/tests/dummy/app/resources/posts/index/route.js deleted file mode 100644 index 2afafb6..0000000 --- a/tests/dummy/app/resources/posts/index/route.js +++ /dev/null @@ -1,9 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Route.extend({ - - model: function() { - return this.store.find('post'); - } - -}); diff --git a/tests/dummy/app/resources/posts/index/template.hbs b/tests/dummy/app/resources/posts/index/template.hbs deleted file mode 100644 index 97fa19f..0000000 --- a/tests/dummy/app/resources/posts/index/template.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#each post in content}} - {{#link-to 'post.edit' post}} - {{post.title}} - {{/link-to}} -{{/each}} diff --git a/tests/dummy/app/resources/posts/new/controller.js b/tests/dummy/app/resources/posts/new/controller.js deleted file mode 100644 index c6054bc..0000000 --- a/tests/dummy/app/resources/posts/new/controller.js +++ /dev/null @@ -1,22 +0,0 @@ -import Ember from 'ember'; -import Saving from 'ember-easy-form-extensions/mixins/controllers/saving'; - -export default Ember.ObjectController.extend( - Saving, { - - validations: { - title: { - presence: true - } - }, - - save: function() { - console.log('Saving'); - }, - - cancel: function() { - this.transitionToRoute('index'); - } - -}); - diff --git a/tests/dummy/app/resources/posts/new/route.js b/tests/dummy/app/resources/posts/new/route.js deleted file mode 100644 index 9f1f5da..0000000 --- a/tests/dummy/app/resources/posts/new/route.js +++ /dev/null @@ -1,11 +0,0 @@ -import Ember from 'ember'; -import DeleteRecord from 'ember-easy-form-extensions/mixins/routes/delete-record'; - -export default Ember.Route.extend( - DeleteRecord, { - - model: function() { - return this.store.createRecord('post'); - } - -}); diff --git a/tests/dummy/app/resources/posts/new/template.hbs b/tests/dummy/app/resources/posts/new/template.hbs deleted file mode 100644 index 2097ecc..0000000 --- a/tests/dummy/app/resources/posts/new/template.hbs +++ /dev/null @@ -1,22 +0,0 @@ -{{#form-wrapper}} - - {{#form-controls legend='Write a new post'}} - {{input title}} - {{input description}} - {{input valueBinding=title}} - {{/form-controls}} - - {{form-submission}} - -{{/form-wrapper}} - -
-
Title
-
{{title}}
-
Description
-
{{description}}
-
- -{{#if editing}} - {{destroy-submission}} -{{/if}} diff --git a/tests/dummy/app/resources/posts/new/view.js b/tests/dummy/app/resources/posts/new/view.js deleted file mode 100644 index 4f2a881..0000000 --- a/tests/dummy/app/resources/posts/new/view.js +++ /dev/null @@ -1,15 +0,0 @@ -import Ember from 'ember'; -import Submitting from 'ember-easy-form-extensions/mixins/views/submitting'; - -export default Ember.View.extend( - Submitting, { - - submitHandler: function() { - return new Ember.RSVP.Promise(function(resolve, reject) { - console.log('submitting'); - - resolve(); - }); - } - -}); diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 7cfd538..ff2e89c 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -7,13 +7,9 @@ var Router = Ember.Router.extend({ Router.map(function() { - this.resource('posts', function() { - this.route('new'); - - this.resource('post', { path: '/:post_id' }, function() { - this.route('edit'); - }); - + this.route('fruit', function() { + this.route('form'); + this.route('model-path'); }); }); diff --git a/tests/dummy/app/routes/.gitkeep b/tests/dummy/app/routes/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/serializers/application.js b/tests/dummy/app/serializers/application.js index aec8f42..6257894 100644 --- a/tests/dummy/app/serializers/application.js +++ b/tests/dummy/app/serializers/application.js @@ -1,5 +1,3 @@ import DS from 'ember-data'; -export default DS.RESTSerializer.extend({ - -}); +export default DS.RESTSerializer.extend(); diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs deleted file mode 100644 index e8a9e3c..0000000 --- a/tests/dummy/app/templates/application.hbs +++ /dev/null @@ -1,13 +0,0 @@ -

ember-easy-form-extensions

-
-
Current path
-
{{currentPath}}
-
- - - -{{outlet}} diff --git a/tests/dummy/app/templates/components/.gitkeep b/tests/dummy/app/templates/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/templates/input-groups.hbs b/tests/dummy/app/templates/input-groups.hbs new file mode 100644 index 0000000..4a078d5 --- /dev/null +++ b/tests/dummy/app/templates/input-groups.hbs @@ -0,0 +1,40 @@ +{{!-- Default --}} + +{{input-group + property='name' +}} + +{{!-- Textarea --}} + +{{input-group + property='description' + type='textarea' +}} + +{{!-- Select --}} + +{{input-group + property='color' + content=colors +}} + +{{!-- Number --}} + +{{input-group + property='numberOfSeeds' + type='number' +}} + +{{!-- Checkbox --}} + +{{input-group + property='isTropical' + type='checkbox' +}} + +{{!-- Date --}} + +{{input-group + property='pickedOn' + type='date' +}} diff --git a/tests/dummy/app/templates/input-values.hbs b/tests/dummy/app/templates/input-values.hbs new file mode 100644 index 0000000..0d6ddde --- /dev/null +++ b/tests/dummy/app/templates/input-values.hbs @@ -0,0 +1,14 @@ +
+
Name
+
{{model.name}}
+
Description
+
{{model.description}}
+
Category
+
{{model.color}}
+
Number of Seeds
+
{{model.numberOfSeeds}}
+
Is Tropical
+
{{model.isTropical}}
+
Picked On
+
{{model.pickedOn}}
+
diff --git a/tests/dummy/app/views/.gitkeep b/tests/dummy/app/views/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/public/crossdomain.xml b/tests/dummy/public/crossdomain.xml index 29a035d..0c16a7a 100644 --- a/tests/dummy/public/crossdomain.xml +++ b/tests/dummy/public/crossdomain.xml @@ -1,15 +1,15 @@ - + - - + + - - + + diff --git a/tests/dummy/public/robots.txt b/tests/dummy/public/robots.txt index 5debfa4..f591645 100644 --- a/tests/dummy/public/robots.txt +++ b/tests/dummy/public/robots.txt @@ -1,2 +1,3 @@ # http://www.robotstxt.org User-agent: * +Disallow: diff --git a/tests/fixtures/fruit.js b/tests/fixtures/fruit.js new file mode 100644 index 0000000..7ce6264 --- /dev/null +++ b/tests/fixtures/fruit.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +export default Ember.Object.create({ + color: 'yellow', + description: 'A big yellow fruit with leaves', + isTropical: true, + name: 'Pinapple', + numberOfSeeds: 70, + writtenOn: moment('1995-12-25'), +}); diff --git a/tests/helpers/async/async-click.js b/tests/helpers/async/async-click.js new file mode 100644 index 0000000..12853c5 --- /dev/null +++ b/tests/helpers/async/async-click.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; + +/* like click() but runs asyncrously allowing you to +use it outside of an andThen function with the same +stuff in the DOM */ + +export default Ember.Test.registerAsyncHelper('asyncClick', + function(app, name) { + click(inspect(name)); + } +); diff --git a/tests/helpers/async/click-input-for.js b/tests/helpers/async/click-input-for.js new file mode 100644 index 0000000..85b36c7 --- /dev/null +++ b/tests/helpers/async/click-input-for.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import selectorFor from '../selector-for'; + +export default Ember.Test.registerAsyncHelper('clickInputFor', + function(app, property, value, tagName = 'input') { + const dasherizedProperty = Ember.String.dasherize(property); + const dataTest = `input-wrapper-for-${dasherizedProperty}`; + + click(inspect(dataTest).find(tagName)); + } +); diff --git a/tests/helpers/async/fill-in-input-for.js b/tests/helpers/async/fill-in-input-for.js new file mode 100644 index 0000000..06740f2 --- /dev/null +++ b/tests/helpers/async/fill-in-input-for.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import selectorFor from '../selector-for'; + +export default Ember.Test.registerAsyncHelper('fillInInputFor', + function(app, property, value, tagName = 'input') { + const dasherizedProperty = Ember.String.dasherize(property); + const selector = selectorFor(`input-wrapper-for-${dasherizedProperty}`); + + fillIn(`${selector} ${tagName}`, value); + } +); diff --git a/tests/helpers/selector-for.js b/tests/helpers/selector-for.js new file mode 100644 index 0000000..3b09b2b --- /dev/null +++ b/tests/helpers/selector-for.js @@ -0,0 +1,3 @@ +export default function(name) { + return `[data-test="${name}"]`; +} diff --git a/tests/helpers/start-app.js b/tests/helpers/start-app.js index 16cc7c3..4355198 100644 --- a/tests/helpers/start-app.js +++ b/tests/helpers/start-app.js @@ -3,6 +3,13 @@ import Application from '../../app'; import Router from '../../router'; import config from '../../config/environment'; +import asyncClick from './async/async-click'; +import clickInputFir from './async/click-input-for'; +import fillInInputFor from './async/fill-in-input-for'; +import inspectInputFor from './sync/inspect-input-for'; +import inspectModelValueFor from './sync/inspect-model-value-for'; +import inspect from './sync/inspect'; + export default function startApp(attrs) { var application; diff --git a/tests/helpers/sync/inspect-input-for.js b/tests/helpers/sync/inspect-input-for.js new file mode 100644 index 0000000..87931f7 --- /dev/null +++ b/tests/helpers/sync/inspect-input-for.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import selectorFor from '../selector-for'; + +export default Ember.Test.registerHelper('inspectInputFor', + function(app, property, tagName = 'input') { + const dasherizedProperty = Ember.String.dasherize(property); + + return inspect(`input-wrapper-for-${dasherizedProperty}`).find(tagName); + } +); + diff --git a/tests/helpers/sync/inspect-model-value-for.js b/tests/helpers/sync/inspect-model-value-for.js new file mode 100644 index 0000000..58b76c1 --- /dev/null +++ b/tests/helpers/sync/inspect-model-value-for.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import selectorFor from '../selector-for'; + +export default Ember.Test.registerHelper('inspectModelValueFor', + function(app, property) { + const dasherizedProperty = Ember.String.dasherize(property); + + return inspect(`model-${dasherizedProperty}`).html(); + } +); + diff --git a/tests/helpers/sync/inspect.js b/tests/helpers/sync/inspect.js new file mode 100644 index 0000000..3194688 --- /dev/null +++ b/tests/helpers/sync/inspect.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import selectorFor from '../selector-for'; + +export default Ember.Test.registerHelper('inspect', + function(app, name, context) { + const selector = selectorFor(name); + + let element; + + if (context) { + element = find(selector, context); + } else { + element = find(selector); + } + + return element; + } +); diff --git a/tests/helpers/unit/component.js b/tests/helpers/unit/component.js new file mode 100644 index 0000000..90ba5e7 --- /dev/null +++ b/tests/helpers/unit/component.js @@ -0,0 +1,98 @@ +import Ember from 'ember'; +import FormMixin from 'ember-easy-form-extensions/mixins/controllers/form'; + +const { run } = Ember; + +export function testClassNameBinding(assert, component) { + + Ember.assert( + 'No element was found for the component. You must call this.render() before testClassNameBinding. Perhaps you forgot to pass assert as the first param?', + component && component.$() + ); + + assertClass(assert, component, component.get('className')); + +} + +export function destroy(component) { + run(function() { + component.destroy(); + }); +} + +export function initAttrs(component) { + run(function() { + component.trigger('didInitAttrs'); + }); +} + +export function renderingTests(assert, context, component) { + + assert.equal(component._state, 'preRender', + 'The component instance should be created'); + + context.render(); + + assert.equal(component._state, 'inDOM', + 'The component should be inserted into the DOM after render'); + +} + +export function sendAction(component, action) { + run(function() { + component.send(action); + }); +} + +export function setOnComponent(component, key, value) { + run(function() { + component.set(key, value); + }); +} + +export function setOnController(component, key, value) { + const formController = component.get('formController'); + + Ember.assert('No formController was found on the component. You must use setupComponent() in the beforeEach hook', formController); + + run(function() { + component.set(`formController.${key}`, value); + }); +} + +export function setPropertiesOnComponent(component, properties) { + run(function() { + component.setProperties(properties); + }); +} + +export function setPropertiesOnController(component, properties) { + const formController = component.get('formController'); + + Ember.assert('No formController was found on the component. You must use setupComponent() in the beforeEach hook', formController); + + run(function() { + component.get('formController').setProperties(properties); + }); +} + +export function setupComponent(context, options) { + let componentProperties = { + formController: Ember.Controller.createWithMixins(FormMixin), + }; + + if (options) { + componentProperties = Ember.merge(componentProperties, options); + } + + return context.subject(componentProperties); +} + +export function assertClass(assert, component, expectedClassName) { + + Ember.assert('No element was found for the component. You must call this.render() before testClassNameBinding', component.$()); + + assert.ok(component.$().hasClass(expectedClassName), + `The ${expectedClassName} class should be bound on the element`); + +} diff --git a/tests/unit/components/error-field-test.js b/tests/unit/components/error-field-test.js new file mode 100644 index 0000000..6427780 --- /dev/null +++ b/tests/unit/components/error-field-test.js @@ -0,0 +1,129 @@ +import Ember from 'ember'; +import { moduleForComponent, test } from 'ember-qunit'; +import { + destroy, + initAttrs, + renderingTests, + setOnComponent, + setPropertiesOnComponent, + setPropertiesOnController, + setupComponent +} from '../../helpers/unit/component'; + +const { run, typeOf } = Ember; + +let component; + +moduleForComponent('error-field', 'Unit | Component | error field', { + needs: ['helper:capitalize-string'], + unit: true, + + beforeEach: function() { + component = setupComponent(this); + }, +}); + +test('Rendering', function(assert) { + + assert.expect(2); + + renderingTests(assert, this, component); +}); + +test('Properties', function(assert) { + const property = 'bananas'; + + assert.expect(8); + + assert.ok(!!component.get('className'), + 'The component should have a default class name'); + + assert.notOk(component.get('property'), + 'The component should not have a default property'); + + assert.ok(typeOf(component.get('formController')) === 'instance', + 'The component should have a formController binding'); + + assert.notOk(component.get('visible'), + 'Visible should be false by default'); + + setPropertiesOnComponent(component, { property }); + + assert.equal(component.get('property'), property, + 'The component should have a value for property'); + + assert.equal(component.get('label'), property, + 'The label should default to the value of property'); + + assert.ok(component.get('text').indexOf(property) > -1, + 'The error text should contain the new property name'); + + this.render(); + + const element = this.$(); + + assert.ok(element.hasClass(component.get('className')), + 'The class name should be bound to the element'); + +}); + +test('Error binding', function(assert) { + const property = 'apples'; + + assert.expect(3); + + assert.notOk(component.get('bindingForErrors'), + 'The component should not have created a binding for validation errors'); + + this.render(); + + setPropertiesOnController(component, { + 'validations.apples': { + presence: true + } + }); + + setPropertiesOnComponent(component, { property }); + + initAttrs(component); + + assert.ok(!!component.get('bindingForErrors'), + 'The component should have created a binding for validation errors'); + + destroy(component); + + assert.notOk(!!component.get('bindingForErrors'), + 'The component should remove the binding for validation errors when the component is being destroyed'); + +}); + +test('The DOM', function(assert) { + const property = 'apples'; + const error = 'should be present'; + + assert.expect(1); + + setPropertiesOnController(component, { + 'validations.apples': { + presence: true + }, + 'errors.apples': Ember.A([ + error + ]), + }); + + setPropertiesOnComponent(component, { + property, + showError: true, + }); + + initAttrs(component); + + this.render(); + + const layout = component.$().html().trim().toLowerCase(); + + assert.equal(layout, `${property} ${error}`, + 'The component should show a user-friendly error message'); + +}); diff --git a/tests/unit/components/form-controls-test.js b/tests/unit/components/form-controls-test.js new file mode 100644 index 0000000..31d564c --- /dev/null +++ b/tests/unit/components/form-controls-test.js @@ -0,0 +1,51 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import { + renderingTests, + setPropertiesOnComponent +} from '../../helpers/unit/component'; + +const legend = 'This is a form'; + +let component; + +moduleForComponent('form-controls', 'Unit | Component | form controls', { + unit: true, + + beforeEach: function() { + component = this.subject({ + legend + }); + }, +}); + +test('Rendering', function(assert) { + + assert.expect(2); + + renderingTests(assert, this, component); +}); + +test('Properties', function(assert) { + + assert.ok(component.get('isFormControls'), + 'The component should have an isFormControls helper property'); + + assert.equal(component.get('modelPath'), 'model', + 'The component should have a default modelPath property'); + + assert.ok(!!component.get('className'), + 'The component should have a default class name'); + + setPropertiesOnComponent(component, { + legend + }); + + const $element = this.$(); // Calls render + + assert.equal($element.attr('legend'), legend, + 'The legend attribute should be bound'); + + assert.equal($element.prop('tagName').toLowerCase(), 'fieldset', + 'The component should be a fieldset'); + +}); diff --git a/tests/unit/components/form-submission-button-test.js b/tests/unit/components/form-submission-button-test.js new file mode 100644 index 0000000..b81bcaa --- /dev/null +++ b/tests/unit/components/form-submission-button-test.js @@ -0,0 +1,101 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import { + initAttrs, + renderingTests, + setPropertiesOnComponent, +} from '../../helpers/unit/component'; + +let component; + +moduleForComponent('form-submission-button', 'Unit | Component | form submission button', { + unit: true, + + beforeEach: function() { + component = this.subject(); + }, +}); + +test('Rendering', function(assert) { + + assert.expect(2); + + renderingTests(assert, this, component); +}); + +test('Properties', function(assert) { + const $element = this.$(); // Render + const className = 'button-primary'; + const type = 'reset'; + + assert.expect(7); + + assert.strictEqual(component.get('disabled'), false, + 'The component should not be disabled by default'); + + assert.equal($element.prop('tagName').toLowerCase(), 'button', + 'The component should be a button'); + + assert.equal($element.attr('type'), 'button', + 'The component should have a default type attribute'); + + assert.ok($element.hasClass('button'), + 'The component should have a default button class name'); + + /* Update the properties to check bindings */ + + setPropertiesOnComponent(component, { + className, + disabled: true, + type + }); + + assert.equal($element.attr('type'), type, + 'The component should have the new type attribute'); + + assert.ok($element.attr('disabled'), + 'The component should have the disabled attribute bound'); + + assert.ok($element.hasClass(className), + 'The component should have the new button class name bound'); + +}); + +test('The DOM', function(assert) { + const text = 'Submit me!'; + + assert.expect(1); + + setPropertiesOnComponent(component, { + text, + }); + + initAttrs(component); + + this.render(); + + const layout = component.$().html().trim(); + + assert.equal(layout, text, + 'The component should show text on the button'); + +}); + +test('Actions', function(assert) { + + assert.expect(1); + + setPropertiesOnComponent(component, { + action: 'orderBigMacs', + + sendAction() { + + assert.ok(true, + 'The action should call the sendAction method'); + + } + }); + + this.render(); + + component.$().click(); +}); diff --git a/tests/unit/components/form-submission-test.js b/tests/unit/components/form-submission-test.js new file mode 100644 index 0000000..e6e4d98 --- /dev/null +++ b/tests/unit/components/form-submission-test.js @@ -0,0 +1,122 @@ +import Ember from 'ember'; +import { moduleForComponent, test } from 'ember-qunit'; +import { + renderingTests, + setupComponent, + setOnComponent +} from '../../helpers/unit/component'; +import selectorFor from '../../helpers/selector-for'; + +const supportedButtons = ['cancel', 'delete', 'save']; +const { run, typeOf } = Ember; + +let component; + +moduleForComponent('form-submission', 'Unit | Component | form submission', { + needs: [ + 'component:form-submission-button', + 'template:form-is-submitted', + ], + unit: true, + + beforeEach: function() { + component = setupComponent(this); + }, +}); + +test('Rendering', function(assert) { + + assert.expect(2); + + renderingTests(assert, this, component); +}); + +test('Properties', function(assert) { + + assert.expect(15); + + /* Check default properties */ + + assert.strictEqual(component.get('formIsSubmitted'), false, + 'Form is submitted should be false by default'); + + assert.ok(typeOf(component.get('formController')) === 'instance', + 'The component should have a formController binding'); + + /* Check cancel, cancelAction, and cancelText, etc */ + + supportedButtons.forEach(function(buttonType) { + const actionProperty = `${buttonType}Action`; + const existsByDefault = buttonType !== 'delete'; + const expectedButtonText = Ember.String.capitalize(buttonType); + const textProperty = `${buttonType}Text`; + + assert.equal(component.get(buttonType), existsByDefault, + `The ${buttonType} property should be ${existsByDefault} by default`); + + assert.equal(component.get(actionProperty), buttonType, + `The ${actionProperty} property should refer to the ${buttonType} action`); + + assert.equal(component.get(textProperty), expectedButtonText, + `The ${textProperty} property should be ${expectedButtonText}`); + + assert.ok(typeOf(component.get(`_actions.${buttonType}`)) === 'function', + `The ${buttonType} action should exist on the component`); + + }); + + this.render(); + + assert.ok(component.$().hasClass(component.get('className')), + 'The class name should be bound to the element'); + +}); + +test('The DOM', function(assert) { + const submittingText = 'Submitting...'; + + assert.expect(11); + + this.render(); + + /* Check the template before any submitting */ + + assert.ok(component.$().html().indexOf(submittingText) === -1, + 'The template should reflect that the form has not been submitted'); + + /* Check in button in turn */ + + supportedButtons.forEach(function(action, i) { + const selector = selectorFor(`button-for-${action}`); + + setOnComponent(component, action, true); + + setOnComponent(component, `_actions.${action}`, function() { + + assert.ok(true, + `The ${action} action should be called after clicking the button`); + + }); + + const $button = component.$(selector); + + assert.ok(!!$button, + 'The ${action} button should exist in the template'); + + $button.click(); + + setOnComponent(component, action, false); + + assert.notOk(component.$(selector).length, + 'The ${action} button should no longer exist in the template'); + + }, this); + + /* Fake submission and check the template */ + + setOnComponent(component, 'formIsSubmitted', true); + + assert.ok(component.$().html().indexOf(submittingText) > -1, + 'The template should reflect that the form is submitting'); + +}); diff --git a/tests/unit/components/form-wrapper-test.js b/tests/unit/components/form-wrapper-test.js new file mode 100644 index 0000000..a81d068 --- /dev/null +++ b/tests/unit/components/form-wrapper-test.js @@ -0,0 +1,55 @@ +import Ember from 'ember'; +import { moduleForComponent, test } from 'ember-qunit'; +import { + renderingTests, + setupComponent +} from '../../helpers/unit/component'; + +const { typeOf } = Ember; + +let component; + +moduleForComponent('form-wrapper', 'Unit | Component | form wrapper', { + unit: true, + + beforeEach: function() { + component = setupComponent(this); + }, +}); + +test('Rendering', function(assert) { + + assert.expect(2); + + renderingTests(assert, this, component); +}); + +test('Properties', function(assert) { + + assert.expect(5); + + assert.ok(component.get('novalidate'), + 'The novalidate property should be false by default because Ember Validation provides client-side validations'); + + assert.ok(!!component.get('className'), + 'The component should have a default class name'); + + assert.ok(typeOf(component.get('formIsSubmitted')) === 'boolean', + 'The component should have a boolean formIsSubmitted property bound for ease of use'); + + assert.ok(typeOf(component.get('formController')) === 'instance', + 'The component should have a formController instance bound'); + + this.render(); + + assert.ok(component.$().hasClass(component.get('className')), + 'The className property should be bound'); + +}); + +test('Methods', function(assert) { + + assert.expect(0); + + /* TODO - use inline template compiler to check autofocus functionality */ +}); diff --git a/tests/unit/components/hint-field-test.js b/tests/unit/components/hint-field-test.js new file mode 100644 index 0000000..8cb5854 --- /dev/null +++ b/tests/unit/components/hint-field-test.js @@ -0,0 +1,40 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import { + renderingTests, + setOnComponent, + testClassNameBinding, +} from '../../helpers/unit/component'; + +let component; + +moduleForComponent('hint-field', 'Unit | Component | hint field', { + needs: ['helper:capitalize-string'], + unit: true, + + beforeEach: function() { + component = this.subject(); + }, +}); + +test('Rendering', function(assert) { + + assert.expect(2); + + renderingTests(assert, this, component); +}); + +test('Properties', function(assert) { + const hintText = 'This field is required'; + + assert.expect(2); + + this.render(); + + testClassNameBinding(assert, component); + + setOnComponent(component, 'text', hintText); + + assert.equal(component.$().html().trim(), hintText, + 'The hint text property should appear in the template'); + +}); diff --git a/tests/unit/components/input-group-test.js b/tests/unit/components/input-group-test.js new file mode 100644 index 0000000..e1394fd --- /dev/null +++ b/tests/unit/components/input-group-test.js @@ -0,0 +1,303 @@ +import Ember from 'ember'; +import { moduleForComponent, test } from 'ember-qunit'; +import { + assertClass, + destroy, + initAttrs, + renderingTests, + sendAction, + setOnController, + setOnComponent, + setPropertiesOnComponent, + setupComponent, + testClassNameBinding +} from '../../helpers/unit/component'; + +const { typeOf, run } = Ember; + +let component; + +moduleForComponent('input-group', 'Unit | Component | input group', { + needs: [ + 'component:hint-field', + 'component:error-field', + 'component:label-field', + 'helper:capitalize-string', + 'template:form-inputs/checkbox', + 'template:form-inputs/default', + 'template:form-inputs/select', + 'template:form-inputs/textarea', + ], + unit: true, + + beforeEach: function() { + component = setupComponent(this); + + component.set('property', 'fake-property'); + }, +}); + +test('Rendering', function(assert) { + + assert.expect(2); + + renderingTests(assert, this, component); +}); + +test('Basic properties', function(assert) { + const modelPath = 'gazumbo'; + const newClassName = 'control-group'; + const propertyName = 'height'; + const property = `${modelPath}.${propertyName}`; + + function checkType(key, type) { + return typeOf(component.get(key) === type); + } + + assert.expect(5); + + assert.ok(checkType('formController', 'instance'), + 'The component should have a formController binding'); + + assert.ok(checkType('newlyValidDuration', 'number'), + 'The component should have a newlyValidDuration binding in milliseconds'); + + setPropertiesOnComponent(component, { + modelPath, + property, + }); + + assert.equal(component.get('property'), property, + "The 'cleaned' property should be a relative reference from the model path"); + + this.render(); + + testClassNameBinding(assert, component); + + setOnComponent(component, 'className', newClassName); + + assert.ok(component.$().hasClass(newClassName), + 'The component should have the new class name bound'); + +}); + +test('Class name bindings', function(assert) { + const className = component.get('className'); + const done = assert.async(); // Enable async test + const newlyValidDuration = 100; + + assert.expect(4); + + this.render(); + + testClassNameBinding(assert, component); + + setPropertiesOnComponent(component, { newlyValidDuration }); + + /* Set as invalid then look at class */ + + sendAction(component, 'setGroupAsInvalid'); + assertClass(assert, component, `${className}-error`); + + /* Set as invalid then look at class */ + + sendAction(component, 'setGroupAsValid'); + assertClass(assert, component, `${className}-newly-valid`); + + /* Look at class after validity period is over */ + + run.later(component, function() { + + sendAction(component, 'setGroupAsValid'); + assertClass(assert, component, `${className}-valid`); + + done(); + }, newlyValidDuration + 100); + +}); + +test('Input attribute properties', function(assert) { + + const properties = [ + 'collection', + 'content', + 'optionValuePath', + 'optionLabelPath', + 'selection', + 'multiple', + 'name', + 'placeholder', + 'prompt', + 'disabled', + ]; + + assert.expect(properties.length); + + properties.forEach(function(property) { + const value = component.get(property); + + assert.ok(value !== undefined, + `The ipnut group should have a binding for the input attribute ${property}`); + + }); + +}); + +test('Type property and partial', function(assert) { + const inputId = component.get('inputId'); + const done = assert.async(); + + assert.expect(33); + + /* Render early so we can check the type attribute */ + + this.render(); + + /* Check the type attributes we expect to detect based on the + property (e.g. password, email, etc) for HTML5 inputs */ + + const expectedTypeDetects = { + password: 'password', + email: 'email', + url: 'url', + color: 'color', + tel: 'tel', + phone: 'tel', + search: 'search' + }; + + for (let type in expectedTypeDetects) { + const expectedType = expectedTypeDetects[type]; + + setOnComponent(component, 'property', type); + + assert.equal(component.get('type'), expectedType, + 'Type should be autodetected by string matches when no type is set'); + + assert.equal(component.get('inputPartial'), 'form-inputs/default', + 'The form input partial should still be the default'); + + assert.equal(component.$(`#${inputId}`).attr('type'), expectedType, + 'The type attribute should be bound to the input element'); + } + + /* Now check for selects */ + + setOnComponent(component, 'content', Ember.A([1, 2, 3])); + + assert.equal(component.get('type'), 'select', + 'Type should be set to select when the content property is present (regardless of the property name)'); + + assert.equal(component.$(`#${inputId}`).prop('tagName').toLowerCase(), 'select', + "The type attribute, 'select', should be bound to the input element"); + + /* Reset the component for the next set of tests */ + + setPropertiesOnComponent(component, { + content: null, + property: 'bananas', + }); + + /* Now, for fallbacks, check the type of the value */ + + const expectedValueDetects = { + number: 123, + date: new Date(), + checkbox: true, + }; + + this.render(); + + for (let type in expectedValueDetects) { + const partialName = type === 'checkbox' ? type : 'default'; + const value = expectedValueDetects[type]; + + setPropertiesOnComponent(component, { + value, + }); + + assert.equal(component.get('type'), type, + 'Type should be autodetected by matching the type of value when no type is set'); + + assert.equal(component.get('inputPartial'), `form-inputs/${partialName}`, + 'The form input partial should reflect the value detect'); + + assert.equal(component.$(`#${inputId}`).attr('type'), type, + 'The type attribute should be bound to the input element'); + } + + /* Finally, check setting the type manually using textarea */ + + setOnComponent(component, 'type', 'textarea'); + + assert.equal(component.$(`#${inputId}`).prop('tagName').toLowerCase(), 'textarea', + "The type attribute, 'select', should be bound to the input element and setabble manually"); + + done(); + +}); + +test('Value and property binding', function(assert) { + const property = 'fruit'; + const value = 'apples'; + + assert.expect(6); + + assert.notOk(component.get('bindingForValue'), + 'The component should not have created a binding for the value'); + + this.render(); + + setOnController(component, property, value); + + setOnComponent(component, 'property', property); + + assert.equal(component.get('label'), property, + 'The label should update once the property is bound'); + + initAttrs(component); + + assert.ok(!!component.get('bindingForValue'), + 'The component should have created a binding for the value'); + + assert.equal(component.get('value'), value, + 'The property value on the controller should be bound to the input group'); + + assert.equal(component.$().data('test'), `input-wrapper-for-${property}`, + 'The component should have the correct testig attribute bound'); + + destroy(component); + + assert.notOk(!!component.get('bindingForValue'), + 'The component should remove the binding for the value'); + +}); + +test('Validity action routing', function(assert) { + + assert.expect(3); + + /* Ensure properties begin as we want */ + + setPropertiesOnComponent(component, { + isValid: false, + newlyValidDuration: 0, + showError: false, + }); + + sendAction(component, 'showError'); + + assert.ok(component.get('showError'), + 'Calling the showError() action should set showError to true'); + + sendAction(component, 'setGroupAsValid'); + + assert.ok(component.get('isValid'), + 'Calling the setGroupAsValid() action should set isValid to true'); + + sendAction(component, 'setGroupAsInvalid'); + + assert.notOk(component.get('isValid'), + 'Calling the setGroupAsInvalid() action should set isValid to false'); + +}); diff --git a/tests/unit/components/label-field-test.js b/tests/unit/components/label-field-test.js new file mode 100644 index 0000000..014e8c8 --- /dev/null +++ b/tests/unit/components/label-field-test.js @@ -0,0 +1,59 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import { + assertClass, + destroy, + initAttrs, + renderingTests, + sendAction, + setOnController, + setOnComponent, + setPropertiesOnComponent, + setupComponent, + testClassNameBinding +} from '../../helpers/unit/component'; + +let component; + +moduleForComponent('label-field', 'Unit | Component | label field', { + needs: ['helper:capitalize-string'], + unit: true, + + beforeEach: function() { + component = this.subject(); + }, +}); + +test('Rendering', function(assert) { + + assert.expect(2); + + renderingTests(assert, this, component); +}); + +test('Properties', function(assert) { + const forId = '#ember-fake-123'; + const text = 'Group of bananas?'; + + assert.expect(4); + + this.render(); + + testClassNameBinding(assert, component); + + setPropertiesOnComponent(component, { + for: forId, + text + }); + + const $component = component.$(); + + assert.equal($component.html().trim(), text, + 'The label text property should appear in the template'); + + assert.equal($component.attr('for'), forId, + 'The for attribute should be bound to the element'); + + assert.equal($component.prop('tagName').toLowerCase(), 'label', + 'The element should be a