├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── addon ├── .gitkeep ├── buffer.js ├── index.js └── property.js ├── app └── .gitkeep ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── server ├── index.js └── mocks │ └── users.js ├── testem.js ├── tests ├── .eslintrc.js ├── acceptance │ └── form-interactions-test.js ├── dummy │ ├── app │ │ ├── adapters │ │ │ └── application.js │ │ ├── app.js │ │ ├── components │ │ │ ├── .gitkeep │ │ │ └── each-in.js │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── index.html │ │ ├── models │ │ │ ├── .gitkeep │ │ │ └── user.js │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ ├── application.hbs │ │ │ ├── components │ │ │ ├── .gitkeep │ │ │ └── each-in.hbs │ │ │ └── index.hbs │ ├── config │ │ ├── environment.js │ │ └── targets.js │ └── public │ │ ├── crossdomain.xml │ │ └── robots.txt ├── helpers │ ├── destroy-app.js │ ├── module-for-acceptance.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── test-helper.js └── unit │ ├── .gitkeep │ ├── buffer-test.js │ └── property-test.js ├── vendor └── .gitkeep └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'simplabs', 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log* 17 | yarn-error.log 18 | testem.log 19 | 20 | # ember-try 21 | .node_modules.ember-try/ 22 | package.json.ember-try 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .gitignore 11 | .eslintrc.js 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "4" 5 | 6 | sudo: false 7 | dist: trusty 8 | 9 | addons: 10 | chrome: stable 11 | 12 | cache: 13 | yarn: true 14 | 15 | matrix: 16 | fast_finish: true 17 | allow_failures: 18 | - env: EMBER_TRY_SCENARIO=ember-canary 19 | 20 | before_install: 21 | - curl -o- -L https://yarnpkg.com/install.sh | bash 22 | - export PATH=$HOME/.yarn/bin:$PATH 23 | - yarn global add bower 24 | 25 | install: 26 | - yarn install --no-lockfile --non-interactive 27 | 28 | script: 29 | - ember try:each 30 | 31 | deploy: 32 | provider: npm 33 | email: info@simplabs.com 34 | api_key: 35 | secure: q1I/qDa3F+XPIrI1eE0zgBgXtxaBpEM1QMLz4Vcw3ojWJjlUE8lYtulDwsKKuNJ/VDliiymvkiklIGyhQ5NPERwYxmWdMXPORHqxeAARFB3Ylv9wZ7aQv+8hH9l+xm3sKHd5uBW4tMq20ogbqSWFnNRHtvtrqkOkSIOI9cHp29b1xs75rmlbjPAaeZFZJqPsjtAq9pptA6mK83FxIAld7lqPgCGbaJiamT8VujKTXYSMZdHaWAaidzxMmWZnAxUpsUDREa5hx1/jbI0nLnnO4HLWl+MOAwRG877my4AnCDRxUXL0UVSKnN1EQFRflwBP7T8aimL2y5N0gqiABXG7kEtvcBrR2xYC9uyqqRE9PiZB05MuydijuWB77qK63lUB6KIal3uKbjG9sJr9dSCpRCb38nkoRKREd5sCwn6lg4NSoqgWAtz12DsQqd6HgzSEB5uD+IoX3FUuBaldROP2ZL1YsM2unonNF1hMLp0y5U6G714ib9syEi8ZlCFHcVJuNA9NbCx3prXsf2688PyKkFtRy+t5nMCd6jy480lgN3nQBhJo4WXsSOINMQr5GSEMENX3nvx4UdsN4Vo88kJX41P1jDUaLeiJVM2nFXm8QOqN52z3SMemlN56MVWHqfBwq8p4IpWfrFNxVo25+5wccNKFj1sjSR6Zu+dxqSUHIfk= 36 | on: 37 | tags: true 38 | repo: simplabs/ember-validated-form-buffer 39 | 40 | notifications: 41 | email: false 42 | slack: 43 | rooms: 44 | secure: OOKD4ZksqzEBW/A3WRuOToODIxnDITqx+Esu7tdmmYPuQlMYgx4SUHv8j9OM9/ScFJiseeVGSkl45vJrHLLIITX9XSjO1RgiGZgw2heVujmGpF6CZNqvT6GsQuKIvMzmwF7IxuHdfV45Csr9Ou/Fg74TszR/4S2h4SOI4zhLg7A= 45 | on_success: never 46 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.2.2 2 | 3 | * ember-validated-form-buffer now works with ember-buffered-proxy 1.0.0, see 4 | #109. 5 | 6 | # 0.2.1 7 | 8 | * ember-validated-form-buffer now works with ember-buffered-proxy 0.8.0, see 9 | #89. 10 | 11 | # 0.2.0 12 | 13 | * ember-validated-form-buffer now uses Babel 6, see #76 14 | 15 | # 0.1.0 16 | 17 | * The form `Buffer` class is now exposed by the framework as well so that it 18 | can be used in cases where a computed property macro doesn't work, see #32. 19 | 20 | # 0.0.2 21 | 22 | * The ember-cp-validations and ember-buffered-proxy dependencies have been 23 | updated to the latest versions, see #26. 24 | 25 | 26 | # 0.0.1 27 | 28 | initial release 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 simplabs GmbH and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://travis-ci.org/simplabs/ember-validated-form-buffer) 2 | 3 | # ember-validated-form-buffer 4 | 5 | `ember-validated-form-buffer` implements a __validating buffer that wraps Ember 6 | Data models and can be used in forms to buffer user inputs before applying them 7 | to the underlying model__. The buffer also handles mixing client side 8 | validation errors and errors returned from the API as well as functionality 9 | that detects which API errors may have become obsolete due to modifications to 10 | the respective properties. 11 | 12 | `ember-validated-form-buffer` helps implementing common forms functionality: 13 | 14 | * preventing modification of models until the form is submitted 15 | * implementing cancel/reset functionality 16 | * filtering irrelevant errors 17 | 18 | It leverages 19 | [ember-buffered-proxy](https://github.com/yapplabs/ember-buffered-proxy) for 20 | the buffering functionality and 21 | [ember-cp-validations](https://github.com/offirgolan/ember-cp-validations) for 22 | client side validations. 23 | 24 | ## Installation 25 | 26 | Install `ember-validated-form-buffer` with 27 | 28 | `ember install ember-validated-form-buffer` 29 | 30 | ## Example 31 | 32 | In order to define a validated form buffer on a controller or component, import 33 | the `formBufferProperty` helper and define a property that wraps the model 34 | instance. Pass in the validations mixin as returned by ember-cp-validations. 35 | When the form is submitted, apply the buffered changes and save the model or 36 | discard them to reset all user input: 37 | 38 | ```js 39 | import Ember from 'ember'; 40 | import { validator, buildValidations } from 'ember-cp-validations'; 41 | import formBufferProperty from 'ember-validated-form-buffer'; 42 | 43 | const Validations = buildValidations({ 44 | name: validator('presence', true) 45 | }); 46 | 47 | export default Ember.Controller.extend({ 48 | data: formBufferProperty('model', Validations), 49 | 50 | actions: { 51 | submit(e) { 52 | e.preventDefault(); 53 | 54 | this.get('data').applyBufferedChanges(); 55 | this.get('model').save(); 56 | }, 57 | 58 | reset() { 59 | this.get('data').discardBufferedChanges(); 60 | } 61 | } 62 | }); 63 | ``` 64 | 65 | Then instead of binding form inputs to model properties directly, bind them to 66 | the buffer instead: 67 | 68 | ```hbs 69 |
75 | ``` 76 | 77 | If you're not using 2 way data bindings for the input but Data Down/Actions Up, 78 | make sure to update the buffer property instead of the model's when the 79 | respective action is called: 80 | 81 | ```hbs 82 | 88 | ``` 89 | 90 | ## API 91 | 92 | ### The buffer 93 | 94 | The buffer has methods for applying and discarding changes as well as 95 | properties for accessing its current error state. 96 | 97 | * `applyBufferedChanges` applies the changes in the buffer to the underlying 98 | model. 99 | * `discardBufferedChanges` discards the buffered changes to that the buffer's 100 | state is reset to that of the underlying model. 101 | 102 | * `apiErrors` returns the errors as returned by the API when the model was last 103 | submitted. 104 | * `clientErrors` returns the client side validation errors as returned by 105 | ember-cp-validations. 106 | * `displayErrors` returns both the API errors as well as the client side 107 | validation errors. __This does not include any API errors on properties that 108 | have been changed after the model was submitted__ as changing a property that 109 | was previously rejected by the API potentially renders the respective error 110 | invalid. 111 | * `hasDisplayErrors` returns whether the buffer currently has any errors to 112 | display which is the case when `displayErrors` is not empty. 113 | 114 | For further info on the buffer's API, check the docs of [ember-buffered-proxy](https://github.com/yapplabs/ember-buffered-proxy) 115 | and 116 | [ember-cp-validations](https://github.com/offirgolan/ember-cp-validations) 117 | respectively. 118 | 119 | The buffer can be imported and used directly: 120 | 121 | ```js 122 | import { Buffer } from 'ember-validated-form-buffer'; 123 | 124 | const Validations = buildValidations({ 125 | name: validator('presence', true) 126 | }); 127 | 128 | export default Ember.Controller.extend({ 129 | data: computed('model', function() { 130 | let owner = Ember.getOwner(this); 131 | return Buffer.extend(Validations).create(owner.ownerInjection(), { 132 | content: this.get('model') 133 | }); 134 | }), 135 | 136 | … 137 | ``` 138 | 139 | It is generally easier to use the `formBufferProperty` macro to define a form 140 | buffer property though: 141 | 142 | ### The `formBufferProperty` helper 143 | 144 | The `formBufferProperty` macro takes the name of another property that returns 145 | the Ember Data model to wrap in the buffer as well as a list of mixins that 146 | will be applied to the buffer. These mixins usually include the validation 147 | mixin as created by ember-cp-validations's `buildValidations` method. 148 | 149 | If any of the provided mixins define an `unsetApiErrors` method, that method 150 | will be called whenever any property is changed on the buffer. The method 151 | returns a property name or an array of property names for which all API errors 152 | will be excluded from the `displayErrors` until the model is submitted to the 153 | API again. That way it's possible to hide API errors on a property when a 154 | related property changes: 155 | 156 | ```js 157 | import formBufferProperty from 'ember-validated-form-buffer'; 158 | 159 | const Validations = buildValidations({ 160 | name: validator('presence', true) 161 | }); 162 | 163 | export default Ember.Controller.extend({ 164 | data: formBufferProperty('model', Validations, { 165 | unsetApiErrors() { 166 | let changedKeys = Ember.A(Object.keys(this.get('buffer'))); 167 | if (changedKeys.includes('date') || changedKeys.includes('time')) { 168 | return 'datetime'; // whenever the "date" or "time" attributes change, also hide errors on the virtual "datetime" property 169 | } 170 | } 171 | }) 172 | 173 | … 174 | ``` 175 | 176 | ## License 177 | 178 | `ember-validated-form-buffer` is developed by and © 179 | [simplabs GmbH](http://simplabs.com) and contributors. It is released under the 180 | [MIT License](https://github.com/simplabs/ember-simple-auth/blob/master/LICENSE). 181 | 182 | `ember-validated-form-buffer` is not an official part of 183 | [Ember.js](http://emberjs.com) and is not maintained by the Ember.js Core Team. 184 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * handle hasMany correctly 4 | * make it work for non Ember Data buffer contents (plain Ember.Object) 5 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-validated-form-buffer/13da770937e382bb32032aafa8a3149a28da2d29/addon/.gitkeep -------------------------------------------------------------------------------- /addon/buffer.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import DS from 'ember-data'; 3 | import BufferedProxy from 'ember-buffered-proxy/proxy'; 4 | 5 | const { keys } = Object; 6 | const { 7 | computed, 8 | isEmpty, 9 | isNone, 10 | makeArray, 11 | isPresent, 12 | Evented, 13 | A, 14 | Object: EmberObject 15 | } = Ember; 16 | const { Model } = DS; 17 | 18 | export default BufferedProxy.extend(Evented, { 19 | unsetApiErrors() {}, 20 | 21 | init() { 22 | this._super(...arguments); 23 | 24 | let content = this.get('content'); 25 | if (content instanceof Model) { 26 | content.on('didCommit', () => this._clearApiErrorBlacklist()); 27 | content.on('becameInvalid', () => this._clearApiErrorBlacklist()); 28 | } 29 | }, 30 | 31 | apiErrors: computed('content.errors.[]', function() { 32 | let content = this.get('content'); 33 | if (content instanceof Model) { 34 | return content.get('errors'); 35 | } else { 36 | return []; 37 | } 38 | }), 39 | 40 | clientErrors: computed('validations.errors.[]', function() { 41 | let validationErrors = this.get('validations.errors'); 42 | let errorAttributes = A(validationErrors).mapBy('attribute'); 43 | let clientErrors = EmberObject.create(); 44 | errorAttributes.forEach((key) => { 45 | let errors = makeArray(this.get(`validations.attrs.${key}.errors`)); 46 | let messages = A(errors).mapBy('message'); 47 | if (isPresent(messages)) { 48 | clientErrors.set(key, messages); 49 | } 50 | }); 51 | return clientErrors; 52 | }), 53 | 54 | displayErrors: computed('clientErrors.[]', 'apiErrors.[]', '_apiErrorBlacklist.[]', function() { 55 | let { apiErrors, _apiErrorBlacklist: apiErrorBlacklist, clientErrors } = this.getProperties( 56 | 'apiErrors', '_apiErrorBlacklist', 'clientErrors' 57 | ); 58 | let displayErrors = EmberObject.create(); 59 | keys(clientErrors).forEach((key) => { 60 | let value = clientErrors.get(key); 61 | displayErrors.set(key, A(value)); 62 | }); 63 | apiErrors.forEach((apiError) => { 64 | if (!apiErrorBlacklist.includes(apiError.attribute)) { 65 | if (isNone(displayErrors.get(apiError.attribute))) { 66 | displayErrors.set(apiError.attribute, A()); 67 | } 68 | displayErrors.get(apiError.attribute).pushObject(apiError.message); 69 | } 70 | }); 71 | return displayErrors; 72 | }), 73 | 74 | hasDisplayErrors: computed('displayErrors', function() { 75 | return !isEmpty(keys(this.get('displayErrors'))); 76 | }), 77 | 78 | setUnknownProperty(key) { 79 | this._super(...arguments); 80 | 81 | if (this.get(key) !== this.get(`content.${key}`)) { 82 | this.get('_apiErrorBlacklist').pushObject(key); 83 | } 84 | let unsetApiErrors = makeArray(this.unsetApiErrors.apply(this)); 85 | this.get('_apiErrorBlacklist').pushObjects(unsetApiErrors); 86 | }, 87 | 88 | _apiErrorBlacklist: computed(function() { 89 | return A(); 90 | }), 91 | 92 | _clearApiErrorBlacklist() { 93 | this.get('_apiErrorBlacklist').clear(); 94 | } 95 | }); 96 | -------------------------------------------------------------------------------- /addon/index.js: -------------------------------------------------------------------------------- 1 | export { default as Buffer } from './buffer'; 2 | import property from './property'; 3 | 4 | export default property; 5 | -------------------------------------------------------------------------------- /addon/property.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Buffer from './buffer'; 3 | 4 | const { keys } = Object; 5 | const { getOwner, computed } = Ember; 6 | 7 | function createFormBuffer(model, owner, ...mixins) { 8 | let ownerInjection = owner.ownerInjection(); 9 | let ownerProperties = keys(ownerInjection).reduce((acc, key) => { 10 | acc[key] = null; 11 | return acc; 12 | }, {}); 13 | 14 | return Buffer.extend(...mixins, ownerProperties).create(ownerInjection, { 15 | content: model 16 | }); 17 | } 18 | 19 | export default function formBufferProperty(modelProperty, ...mixins) { 20 | return computed(modelProperty, function() { 21 | let model = this.get(modelProperty); 22 | let owner = getOwner(this); 23 | 24 | return createFormBuffer(model, owner, ...mixins); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-validated-form-buffer/13da770937e382bb32032aafa8a3149a28da2d29/app/.gitkeep -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | useVersionCompatibility: true 4 | }; 5 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node */ 4 | 5 | module.exports = function(/* environment, appConfig */) { 6 | return { }; 7 | }; 8 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node */ 4 | 5 | let EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 6 | 7 | module.exports = function(defaults) { 8 | let app = new EmberAddon(defaults, { 9 | 'ember-bootstrap': { 10 | 'bootstrapVersion': 3, 11 | 'importBootstrapFont': true, 12 | 'importBootstrapCSS': true 13 | } 14 | }); 15 | 16 | /* 17 | This build file specifies the options for the dummy test app of this 18 | addon, located in `/tests/dummy` 19 | This build file does *not* influence how the addon or the app using it 20 | behave. You most likely want to be modifying `./index.js` or app's build file 21 | */ 22 | 23 | return app.toTree(); 24 | }; 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node */ 4 | 5 | module.exports = { 6 | name: 'ember-validated-form-buffer' 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-validated-form-buffer", 3 | "version": "0.2.2", 4 | "description": "A validated form buffer that wraps Ember Data models for use in forms.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "license": "MIT", 9 | "author": "simplabs GmbH", 10 | "directories": { 11 | "doc": "doc", 12 | "test": "tests" 13 | }, 14 | "repository": "https://github.com/simplabs/ember-validated-form-buffer", 15 | "scripts": { 16 | "build": "ember build", 17 | "start": "ember server", 18 | "test": "ember try:each", 19 | "lint": "eslint app addon blueprints config server test-support tests *.js" 20 | }, 21 | "dependencies": { 22 | "ember-buffered-proxy": "^0.7.0 || ^0.8.0 || ^1.0.0", 23 | "ember-cli-babel": "^6.11.0", 24 | "ember-cp-validations": "^3.1.4", 25 | "ember-getowner-polyfill": "^1.1.0 || ^2.0.0" 26 | }, 27 | "devDependencies": { 28 | "body-parser": "^1.17.2", 29 | "bootstrap": "^3.3.7", 30 | "broccoli-asset-rev": "^2.5.0", 31 | "ember-ajax": "^3.0.0", 32 | "ember-bootstrap": "1.2.1", 33 | "ember-cli": "^2.14.1", 34 | "ember-cli-chai": "^0.5.0", 35 | "ember-cli-dependency-checker": "^2.0.1", 36 | "ember-cli-eslint": "^4.2.0", 37 | "ember-cli-htmlbars": "^2.0.2", 38 | "ember-cli-htmlbars-inline-precompile": "^1.0.0", 39 | "ember-cli-inject-live-reload": "^1.7.0", 40 | "ember-cli-mocha": "^0.14.4", 41 | "ember-cli-pretender": "^1.0.1", 42 | "ember-cli-shims": "^1.1.0", 43 | "ember-cli-sri": "^2.1.1", 44 | "ember-cli-uglify": "^2.0.0", 45 | "ember-data": "^3.0.0", 46 | "ember-disable-prototype-extensions": "^1.1.3", 47 | "ember-export-application-global": "^2.0.0", 48 | "ember-load-initializers": "^1.0.0", 49 | "ember-resolver": "^4.3.0", 50 | "ember-source": "~2.18.0", 51 | "ember-test-selectors": "^0.3.6", 52 | "eslint-config-simplabs": "^0.4.0", 53 | "loader.js": "^4.5.1" 54 | }, 55 | "engines": { 56 | "node": "^4.5 || 6.* || >= 7.*" 57 | }, 58 | "ember-addon": { 59 | "configPath": "tests/dummy/config", 60 | "versionCompatibility": { 61 | "ember": ">=1.13" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node */ 4 | 5 | module.exports = function(app) { 6 | let globSync = require('glob').sync; 7 | let bodyParser = require('body-parser'); 8 | let mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); 9 | let proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); 10 | 11 | app.use(bodyParser.json({ type: 'application/*+json' })); 12 | app.use(bodyParser.urlencoded({ 13 | extended: true 14 | })); 15 | 16 | // Log proxy requests 17 | let morgan = require('morgan'); 18 | app.use(morgan('dev')); 19 | 20 | mocks.forEach(function(route) { route(app); }); 21 | proxies.forEach(function(route) { route(app); }); 22 | }; 23 | -------------------------------------------------------------------------------- /server/mocks/users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node */ 4 | 5 | module.exports = function(app) { 6 | let express = require('express'); 7 | let usersRouter = express.Router(); 8 | 9 | usersRouter.get('/:id', function(req, res) { 10 | let result = { data: { id: 1, type: 'users', attributes: { name: 'test' } } }; 11 | res.status(200).send(result); 12 | }); 13 | 14 | usersRouter.patch('/:id', function(req, res) { 15 | let result = { 16 | errors: [ 17 | { source: { pointer: '/data/attributes/name' }, title: 'too short' } 18 | ] 19 | }; 20 | res.status(422).send(result); 21 | }); 22 | 23 | app.use('/users', usersRouter); 24 | }; 25 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | framework: 'mocha', 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: [ 7 | 'Chrome' 8 | ], 9 | launch_in_dev: [ 10 | 'Chrome' 11 | ], 12 | browser_args: { 13 | Chrome: { 14 | mode: 'ci', 15 | args: [ 16 | // --no-sandbox is needed when running Chrome inside a container 17 | process.env.TRAVIS ? '--no-sandbox' : null, 18 | '--disable-gpu', 19 | '--headless', 20 | '--remote-debugging-port=9222', 21 | '--window-size=1440,900' 22 | ].filter(Boolean) 23 | }, 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'simplabs/configs/ember-mocha', 3 | }; 4 | -------------------------------------------------------------------------------- /tests/acceptance/form-interactions-test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, afterEach } from 'mocha'; 2 | import { expect } from 'chai'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | import testSelector from '../helpers/ember-test-selectors'; 6 | import Pretender from 'pretender'; 7 | 8 | const INPUT_FIELD = `${testSelector('input')} input`; 9 | 10 | describe('Acceptance | form interactions', () => { 11 | let application; 12 | let server; 13 | 14 | beforeEach(() => { 15 | server = new Pretender(function() { 16 | this.get('/users/1', () => { 17 | let response = { 18 | data: { 19 | id: 1, 20 | type: 'users', 21 | attributes: { 22 | name: 'user name' 23 | } 24 | } 25 | }; 26 | return [200, { 'Content-Type': 'application/json' }, JSON.stringify(response)]; 27 | }); 28 | 29 | this.patch('/users/1', () => { 30 | let response = { 31 | errors: [ 32 | { 33 | source: { 34 | pointer: '/data/attributes/name' 35 | }, 36 | title: 'too short' 37 | } 38 | ] 39 | }; 40 | return [422, { 'Content-Type': 'application/json' }, JSON.stringify(response)]; 41 | }); 42 | }); 43 | 44 | application = startApp(); 45 | return visit('/'); 46 | }); 47 | 48 | afterEach(() => { 49 | server.shutdown(); 50 | destroyApp(application); 51 | }); 52 | 53 | it('can reset changes', () => { 54 | expect(find(INPUT_FIELD).val()).to.eq('user name'); 55 | 56 | fillIn(INPUT_FIELD, 'new value'); 57 | andThen(() => { 58 | expect(find(INPUT_FIELD).val()).to.eq('new value'); 59 | }); 60 | 61 | click(testSelector('reset')); 62 | return andThen(() => { 63 | expect(find(INPUT_FIELD).val()).to.eq('user name'); 64 | }); 65 | }); 66 | 67 | it('shows client validation errors', () => { 68 | fillIn(INPUT_FIELD, ''); 69 | 70 | return andThen(() => { 71 | expect(find(testSelector('error')).length).to.eq(1); 72 | }); 73 | }); 74 | 75 | it('shows server errors', () => { 76 | click('button[type="submit"]'); 77 | 78 | return andThen(() => { 79 | expect(find(testSelector('error')).length).to.eq(1); 80 | }); 81 | }); 82 | 83 | it('shows client and server errors combined', () => { 84 | fillIn(INPUT_FIELD, ''); 85 | click('button[type="submit"]'); 86 | 87 | return andThen(() => { 88 | expect(find(testSelector('error')).length).to.eq(2); 89 | }); 90 | }); 91 | 92 | it('hides server errors when the field is modified', () => { 93 | click('button[type="submit"]'); 94 | andThen(() => { 95 | expect(find(testSelector('error')).length).to.eq(1); 96 | }); 97 | 98 | fillIn(INPUT_FIELD, 'new value'); 99 | return andThen(() => { 100 | expect(find(testSelector('error')).length).to.eq(0); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { JSONAPIAdapter } = DS; 4 | 5 | export default JSONAPIAdapter.extend(); 6 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const { Application } = Ember; 7 | 8 | let App; 9 | 10 | App = Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-validated-form-buffer/13da770937e382bb32032aafa8a3149a28da2d29/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/components/each-in.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { keys } = Object; 4 | const { Component, computed, get } = Ember; 5 | 6 | const EachInComponent = Component.extend({ 7 | keyValuePairs: computed('object', function() { 8 | let object = this.get('object'); 9 | 10 | return keys(object).map((key) => { 11 | return { key, value: get(object, key) }; 12 | }); 13 | }) 14 | }); 15 | 16 | EachInComponent.reopenClass({ 17 | positionalParams: ['object'] 18 | }); 19 | 20 | export default EachInComponent; 21 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-validated-form-buffer/13da770937e382bb32032aafa8a3149a28da2d29/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | import formBufferProperty from 'ember-validated-form-buffer'; 4 | 5 | const { Controller } = Ember; 6 | 7 | const Validations = buildValidations({ 8 | name: validator('presence', true) 9 | }); 10 | 11 | export default Controller.extend({ 12 | data: formBufferProperty('model', Validations), 13 | 14 | actions: { 15 | submit(e) { 16 | e.preventDefault(); 17 | 18 | this.get('data').applyBufferedChanges(); 19 | this.get('model').save().catch(function() {}); 20 | }, 21 | 22 | reset() { 23 | this.get('data').discardBufferedChanges(); 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |