Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add inline errors #5399

Merged
merged 1 commit into from
Jul 5, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions core/client/app/components/gh-error-message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Ember from 'ember';

/**
* Renders one random error message when passed a DS.Errors object
* and a property name. The message will be one of the ones associated with
* that specific property. If there are no errors associated with the property,
* nothing will be rendered.
* @param {DS.Errors} errors The DS.Errors object
* @param {string} property The property name
*/
export default Ember.Component.extend({
tagName: 'p',
classNames: ['response'],

errors: null,
property: '',

isVisible: Ember.computed.notEmpty('errors'),

message: Ember.computed('errors.[]', 'property', function () {
var property = this.get('property'),
errors = this.get('errors'),
messages = [],
index;

if (!Ember.isEmpty(errors) && errors.get(property)) {
errors.get(property).forEach(function (error) {
messages.push(error);
});
index = Math.floor(Math.random() * messages.length);
return messages[index].message;
}
})
});
25 changes: 25 additions & 0 deletions core/client/app/components/gh-form-group.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Ember from 'ember';

/**
* Handles the CSS necessary to show a specific property state. When passed a
* DS.Errors object and a property name, if the DS.Errors object has errors for
* the specified property, it will change the CSS to reflect the error state
* @param {DS.Errors} errors The DS.Errors object
* @param {string} property Name of the property
*/
export default Ember.Component.extend({
classNames: 'form-group',
classNameBindings: ['errorClass'],

errors: null,
property: '',

errorClass: Ember.computed('errors.[]', 'property', function () {
var property = this.get('property'),
errors = this.get('errors');

if (errors) {
return errors.get(property) ? 'error' : 'success';
}
})
});
4 changes: 3 additions & 1 deletion core/client/app/components/gh-input.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Ember from 'ember';
import TextInputMixin from 'ghost/mixins/text-input';

var Input = Ember.TextField.extend(TextInputMixin);
var Input = Ember.TextField.extend(TextInputMixin, {
classNames: 'gh-input'
});

export default Input;
4 changes: 3 additions & 1 deletion core/client/app/components/gh-textarea.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Ember from 'ember';
import TextInputMixin from 'ghost/mixins/text-input';

var TextArea = Ember.TextArea.extend(TextInputMixin);
var TextArea = Ember.TextArea.extend(TextInputMixin, {
classNames: 'gh-input'
});

export default TextArea;
15 changes: 8 additions & 7 deletions core/client/app/controllers/setup/two.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,20 @@ export default Ember.Controller.extend(ValidationEngine, {
return 'background-image: url(' + this.get('userImage') + ')';
}),

invalidMessage: 'The password fairy does not approve',

// ValidationEngine settings
validationType: 'setup',

actions: {
setup: function () {
var self = this,
notifications = this.get('notifications'),
data = self.getProperties('blogTitle', 'name', 'email', 'password');

notifications.closePassive();
data = self.getProperties('blogTitle', 'name', 'email', 'password'),
notifications = this.get('notifications');

this.toggleProperty('submitting');
this.validate({format: false}).then(function () {
this.validate().then(function () {
self.set('showError', false);
ajax({
url: self.get('ghostPaths.url').api('authentication', 'setup'),
type: 'POST',
Expand All @@ -70,9 +71,9 @@ export default Ember.Controller.extend(ValidationEngine, {
self.toggleProperty('submitting');
notifications.showAPIError(resp);
});
}).catch(function (errors) {
}).catch(function () {
self.toggleProperty('submitting');
notifications.showErrors(errors);
self.set('showError', true);
});
}
}
Expand Down
4 changes: 3 additions & 1 deletion core/client/app/controllers/signin.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export default Ember.Controller.extend(ValidationEngine, {
self.get('notifications').closePassive();
self.send('authenticate');
}).catch(function (errors) {
self.get('notifications').showErrors(errors);
if (errors) {
self.get('notifications').showErrors(errors);
}
});
},

Expand Down
4 changes: 3 additions & 1 deletion core/client/app/controllers/team/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ export default Ember.Controller.extend({

return model;
}).catch(function (errors) {
self.get('notifications').showErrors(errors);
if (errors) {
self.get('notifications').showErrors(errors);
}
});

this.set('lastPromise', promise);
Expand Down
41 changes: 26 additions & 15 deletions core/client/app/mixins/validation-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import TagSettingsValidator from 'ghost/validators/tag-settings';
// our extensions to the validator library
ValidatorExtensions.init();

// This is here because it is used by some things that format errors from api responses
// This function should be removed in the notifications refactor
// format errors to be used in `notifications.showErrors`.
// result is [{message: 'concatenated error messages'}]
function formatErrors(errors, opts) {
Expand Down Expand Up @@ -79,17 +81,21 @@ export default Ember.Mixin.create({
tag: TagSettingsValidator
},

// This adds the Errors object to the validation engine, and shouldn't affect
// ember-data models because they essentially use the same thing
errors: DS.Errors.create(),

/**
* Passes the model to the validator specified by validationType.
* Returns a promise that will resolve if validation succeeds, and reject if not.
* Some options can be specified:
*
* `format: false` - doesn't use formatErrors to concatenate errors for notifications.showErrors.
* will return whatever the specified validator returns.
* since notifications are a common usecase, `format` is true by default.
*
* `model: Object` - you can specify the model to be validated, rather than pass the default value of `this`,
* the class that mixes in this mixin.
*
* `property: String` - you can specify a specific property to validate. If
* no property is specified, the entire model will be
* validated
*/
validate: function (opts) {
// jscs:disable safeContextKeyword
Expand All @@ -113,23 +119,23 @@ export default Ember.Mixin.create({
opts.validationType = type;

return new Ember.RSVP.Promise(function (resolve, reject) {
var validationErrors;
var passed;

if (!type || !validator) {
validationErrors = ['The validator specified, "' + type + '", did not exist!'];
} else {
validationErrors = validator.check(model);
return reject(['The validator specified, "' + type + '", did not exist!']);
}

if (Ember.isEmpty(validationErrors)) {
return resolve();
}
passed = validator.check(model, opts.property);

if (opts.format !== false) {
validationErrors = formatErrors(validationErrors, opts);
if (passed) {
if (opts.property) {
model.get('errors').remove(opts.property);
} else {
model.get('errors').clear();
}
return resolve();
}

return reject(validationErrors);
return reject();
});
},

Expand Down Expand Up @@ -172,5 +178,10 @@ export default Ember.Mixin.create({

return Ember.RSVP.reject(result);
});
},
actions: {
validate: function (property) {
this.validate({property: property});
}
}
});
4 changes: 3 additions & 1 deletion core/client/app/routes/signin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Ember from 'ember';
import Configuration from 'simple-auth/configuration';
import styleBody from 'ghost/mixins/style-body';
import DS from 'ember-data';

var SigninRoute = Ember.Route.extend(styleBody, {
titleToken: 'Sign In',
Expand All @@ -16,7 +17,8 @@ var SigninRoute = Ember.Route.extend(styleBody, {
model: function () {
return Ember.Object.create({
identification: '',
password: ''
password: '',
errors: DS.Errors.create()
});
},

Expand Down
1 change: 1 addition & 0 deletions core/client/app/templates/components/gh-error-message.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{message}}
31 changes: 19 additions & 12 deletions core/client/app/templates/setup/two.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,24 @@
</div>
<a class="edit-account-image" href="#"><i class="icon-photos"><span class="sr-only">Upload an image</span></i></a>
</figure>
<div class="form-group">
{{#gh-form-group errors=errors property="email"}}
<label for="email-address">Email address</label>
<span class="input-icon icon-mail">
{{input type="email" name="email" placeholder="Eg. [email protected]" class="gh-input" autofocus="autofocus" autocorrect="off" value=email}}
{{gh-input type="email" name="email" placeholder="Eg. [email protected]" class="gh-input" autofocus="autofocus" autocorrect="off" value=email focusOut=(action "validate" "email")}}
</span>
</div>
<div class="form-group">
{{gh-error-message errors=errors property="email"}}
{{/gh-form-group}}
{{#gh-form-group errors=errors property="name"}}
<label for="full-name">Full name</label>
<span class="input-icon icon-user">
{{input type="text" name="name" placeholder="Eg. John H. Watson" class="gh-input" autofocus="autofocus" autocorrect="off" value=name }}
{{gh-input type="text" name="name" placeholder="Eg. John H. Watson" class="gh-input" autofocus="autofocus" autocorrect="off" value=name focusOut=(action "validate" "name")}}
</span>
</div>
<div class="form-group">
{{gh-error-message errors=errors property="name"}}
{{/gh-form-group}}
{{#gh-form-group errors=errors property="password"}}
<label for="password">Password</label>
<span class="input-icon icon-lock">
{{input type="password" name="password" placeholder="At least 8 characters" class="gh-input" autofocus="autofocus" autocorrect="off" value=password }}
{{gh-input type="password" name="password" placeholder="At least 8 characters" class="gh-input" autofocus="autofocus" autocorrect="off" value=password focusOut=(action "validate" "password")}}
<div class="pw-strength">
<div class="pw-strength-dot"></div>
<div class="pw-strength-dot"></div>
Expand All @@ -42,14 +44,19 @@
<div class="pw-strength-dot <!--pw-strength-activedot-->"></div>
</div>
</span>
</div>
<div class="form-group">
{{gh-error-message errors=errors property="password"}}
{{/gh-form-group}}
{{#gh-form-group errors=errors property="blogTitle"}}
<label for="blog-title">Blog title</label>
<span class="input-icon icon-content">
{{input type="text" name="blog-title" placeholder="Eg. The Daily Awesome" class="gh-input" autofocus="autofocus" autocorrect="off" value=blogTitle }}
{{gh-input type="text" name="blog-title" placeholder="Eg. The Daily Awesome" class="gh-input" autofocus="autofocus" autocorrect="off" value=blogTitle focusOut=(action "validate" "blogTitle")}}
</span>
</div>
{{gh-error-message errors=errors property="blogTitle"}}
{{/gh-form-group}}
</form>

<button type="submit" class="btn btn-green btn-lg btn-block" {{action "setup"}} {{submitting}}>Last step: Invite your team <i class="icon-chevron"></i></button>
{{#if showError}}
<p class="main-error">{{invalidMessage}}</p>
{{/if}}
</section>
40 changes: 40 additions & 0 deletions core/client/app/validators/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Ember from 'ember';

/**
* Base validator that all validators should extend
* Handles checking of individual properties or the entire model
*/
var BaseValidator = Ember.Object.extend({
properties: [],
passed: false,

/**
* When passed a model and (optionally) a property name,
* checks it against a list of validation functions
* @param {Ember.Object} model Model to validate
* @param {string} prop Property name to check
* @return {boolean} True if the model passed all (or one) validation(s),
* false if not
*/
check: function (model, prop) {
var self = this;

this.set('passed', true);

if (prop && this[prop]) {
this[prop](model);
} else {
this.get('properties').forEach(function (property) {
if (self[property]) {
self[property](model);
}
});
}
return this.get('passed');
},
invalidate: function () {
this.set('passed', false);
}
});

export default BaseValidator;
41 changes: 22 additions & 19 deletions core/client/app/validators/new-user.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
import Ember from 'ember';
var NewUserValidator = Ember.Object.extend({
check: function (model) {
var data = model.getProperties('name', 'email', 'password'),
validationErrors = [];
import BaseValidator from './base';

if (!validator.isLength(data.name, 1)) {
validationErrors.push({
message: 'Please enter a name.'
});
}
var NewUserValidator = BaseValidator.extend({
properties: ['name', 'email', 'password'],

name: function (model) {
var name = model.get('name');

if (!validator.isEmail(data.email)) {
validationErrors.push({
message: 'Invalid Email.'
});
if (!validator.isLength(name, 1)) {
model.get('errors').add('name', 'Please enter a name.');
this.invalidate();
}
},
email: function (model) {
var email = model.get('email');

if (!validator.isLength(data.password, 8)) {
validationErrors.push({
message: 'Password must be at least 8 characters long.'
});
if (!validator.isEmail(email)) {
model.get('errors').add('email', 'Invalid Email.');
this.invalidate();
}
},
password: function (model) {
var password = model.get('password');

return validationErrors;
if (!validator.isLength(password, 8)) {
model.get('errors').add('password', 'Password must be at least 8 characters long');
this.invalidate();
}
}
});

Expand Down
Loading