Forms & Dialogs
The next generation
Old Style
- Big
- Not easy to develop with
- Non-standard
Old Style
postCreate: function() {
aspect.before(this, '_setValues', this.__setValues.bind(this));
...
/**
* Runs before _setValues using aspect.before, set up in postCreate
*/
__setValues: function(values) {
this._removeAddedElements();
if (values !== undefined) {
values = this.parseDefinitionValues(values);
if ('seed_provider' in values) {
var seeds;
try {
seeds = values.seed_provider[0].parameters[0].seeds;
} catch (e) {}
values.seeds = seeds;
delete values.seed_provider;
}
}
return [values];
}
Old Style
/**
* Populate form based on cluster conf
*/
_populate: function() {
var values = {},
config = this.cluster_config || {};
if (config.cassandra) {
['seed_hosts', 'ssl_validate', 'ssl_ca_certs'].forEach(function(prop) {
values[prop] = config.cassandra[prop] || '';
});
lang.mixin(values, {
'thrift_port': config.cassandra.api_port || undefined, // want field's default value
'thrift_username': config.cassandra.username || '',
'thrift_password': config.cassandra.password || ''});
}
if (config.jmx) {
lang.mixin(values, {
'jmx_port': config.jmx.port || undefined, // want field's default value
'jmx_username': config.jmx.username || '',
'jmx_password': config.jmx.password || ''});
}
if (config.agents) {
lang.mixin(values, {
'ssl_keystore': config.agents.ssl_keystore || '',
'ssl_keystore_password': config.agents.ssl_keystore_password || ''});
}
if (config.kerberos) {
values.kerberos_service_name = config.kerberos.default_service || '';
}
this._setValues(values);
this._toggleCreds(values.jmx_username || values.thrift_username);
this.kerberosCheckboxNode.set('checked', !!values.kerberos_service_name);
this.sslCheckboxNode.set('checked', !!values.ssl_ca_certs);
}
So how do we fix it?
Separation Of Concerns
- Do One Thing, and Do It Well
- Clean APIs bring clean code
- Provide a path to follow
- Make it easy to test and
people will test
Promises
- Process Flow
- Easy to reason about
var prompt = new Prompt({
title: 'Flush?',
message: phrases.flushWarning,
buttons: ['Flush']
});
prompt.show().then(function() {
this.executeNodeOp(
this.cluster_ds.flushNodeKeyspace(
this.getNodeSelection(),
ksname,
column_families,
true),
phrases.flushSuccess,
phrases.flushError,
phrases.flushStarted
);
}.bind(this));
var prompt = new Simple({
title: 'Flush?',
message: phrases.flushWarning,
button_label: 'Flush'
});
prompt.on('confirm', function() {
this.executeNodeOp(
this.cluster_ds.flushNodeKeyspace(
this.getNodeSelection(),
ksname,
column_families,
true),
phrases.flushSuccess,
phrases.flushError,
phrases.flushStarted
);
}.bind(this));
prompt.show();
Trivial Example
More Involved
var ChangePasswordDialog = declare([
ChangePassword,
_FormDialogMixin,
_SendValidationErrorsToPrompt
], {
title: phrases.changePwdTitle
});
...
new ChangePasswordDialog({
resolutionAction: function(value_object) {
return this.opsc_datasource.updateUser(this._meta.username, value_object.values)
.then(function() {
return new NotifyPrompt({
title: phrases.changePwdSuccessTitle,
message: phrases.changePwdSuccessMessage
}).show();
});
}.bind(this),
resolutionActionFailure: function(err) {
ui.error({prefix: phrases.changePwdUpdateError, error: err});
}
}).show();
More Involved
new ChangePasswordDialog({
opsc_ds: this.opsc_datasource,
destroy_on_hide: true,
username: this._meta.username
}).show();
More Involved
postCreate: function() {
this.setContent(this.domNode);
this.inherited(arguments);
this.own(
this.on('submit', this._updatePassword.bind(this)),
aspect.after(this.passwordNode, 'validator', function(is_valid) {
var values = this.getValues();
return is_valid && values.password === values.password_2;
}.bind(this))
);
},
_updatePassword: function() {
var values = this.getValues();
return this.opsc_ds.updateUser(this.username, values)
.then(function() {
new Notify({
title: phrases.changePwdSuccessTitle,
message: phrases.changePwdSuccessMessage
}).show();
this.hide();
}.bind(this)).otherwise(function(err) {
ui.error({prefix: phrases.changePwdUpdateError, error: err});
}.bind(this));
}
More Involved
// vim:ts=4:sw=4:et:tw=100:
//>> pure-amd
define([
'dojo/_base/declare',
'dojo/text!./changepassword.html',
'ripcord/forms/_form',
'ripcord/phrases',
'ripcord/widgets/_phrasesmixin',
// template
'dijit/form/ValidationTextBox'
], function(declare, template, _Form, phrases, _PhrasesMixin) {
/**
* Widget that shows the change password dialog
*/
return declare([_Form, _PhrasesMixin], {
/** @inheritDoc */
templateString: template,
getValidation: function(values) {
if (values.password !== values.password_2) {
return phrases.changePwdPasswordsDoNotMatch;
}
}
});});
Old Way
ChangePasswordDialog.js
77 lines of code
Main.js
5 lines of code
_Form.js
413 lines of code
_TemplatedDialogMixin.js
95 lines of code
_FormDialogMixin.js
58 lines of code
New Way
ChangePassword.js
28 lines of code
Main.js
14 lines of code
_Form.js
255 lines of code
_TemplatedDialogMixin.js
274 lines of code
_FormDialogMixin.js
34 lines of code
API
_templateddialogmixin.js
- buttons
- resolutionAction(s)
- resolutionActionFailure(s)
- whenResolve
- whenReject
buttons
Array of labels for buttons!
new DialogedClass({
buttons: ['Foo', 'Bar']
})
Can also be an array of Objects with more granular control of attributes -- see tests
resolutionAction(s)
A method (returning Promise|Anything) defining what to do when a resolving button is pressed.
new DialogedClass({
buttons: ["Save"],
resolutionAction: function() {
return asynchronousTask();
}
});
Dialog will remain open until promise returned by fn() or when(fn()) succeeds
resolutionActions is an object which gives you action-level control over your API calls
new DialogedClass({
buttons: ['Save', 'Clone'],
resolutionActions: {
'Save' : function() {
return asynchronousSaveTask();
},
'Clone' : function() {
return asynchronousCloneTask();
}
}
});
whenResolve\whenReject
You generally won't need to use this unless you're making a non-buttoned Dialog
buttons: [],
// public:
/** @inheritDoc */
startup: function() {
this.inherited(arguments);
this.own(
this.provNode.on('click', this.whenResolve.bind(this, 'createnew')),
this.addNode.on('click', this.whenResolve.bind(this, 'manageexisting'))
);
}
This allows you to hook your own template buttons into the resolve/rejection flow (and resolutionActions)
Overriding whenRe*
Unless you're writing a mixin for use with _TemplatedDialogMixin, it's not a good idea.
You should almost certainly call
somewhere in your override if you do.
this.inherited(arguments);
whenResolve: function(action) {
var isInvalid = this.isInvalid();
if (isInvalid) {
this.handleValidationError(isInvalid);
this._progress({
isValidationError: true,
values: isInvalid
});
return;
}
var values = this.getValues();
values.action = action;
this.inherited(arguments, [action, values]);
},
API
_Form
- transformDataIn/Out
- getValues
- setValues
- isDirty
- initializeFields
- getValidation
- isInvalid
transformData*
Handle transformation of data for your form here.
Forms like flat objects of name/value pairs
APIs like nested objects.
Meld the two together so each side gets what they want.
transformDataOut: function(form_values) {
var values = lang.mixin({}, form_values);
if (form_values.exclude_foo) {
delete values.foo;
}
return values;
},
transformDataIn: function(values) {
var form_values = lang.mixin({}, values);
if (!values.hasOwnProperty('foo')) {
form_values.exclude_foo = true;
}
return form_values;
}
Try to make these complementary functions, it will make your life a lot easier.
Also, don't put validation in here. Just convert values.
get/setValues
Odds are, you shouldn't really need these, but they exist. get and setValues both go through their respective transformation functions
form.getValues() === { "my" : "transformed values" }
form.setValues({"my": "incoming values"});
// You REALLY shouldn't need these
form._getValues() === { "my_textbox": "incoming values" }
form._setValues({"my_textbox": "transformed values"})
isDirty
Not much changed from before -- works off of untransformed values
form._getValues() // { 'my_textbox' : 'some value' }
form.isDirty() // false
form.findChildByName('my_textbox').set('value', 'Something else');
form.isDirty() // true
form.findChildByName('my_textbox').set('value', 'some value');
form.isDirty() // false
form._setValues({'my_textbox':'Something else'});
form.isDirty() // false -- _setValues resets clean values
initializeFields
Lets you initialize values with defaults if you like
new DialogedClass({
initializeFields: function(form_values) {
form_values.my_textbox = 'Some other default for whatever reason';
return form_values;
}
});
getValidation
Throw some custom validation logic in here
getValidation: function(values) {
if (values.password !== values.password_2) {
return phrases.changePwdPasswordsDoNotMatch;
}
}
Returns either a string or an array of strings which represent the error messages associated with the form
isInvalid
Returns false if the form is valid
Returns an array of error messages associated with the form, including custom and standard messages if the form is invalid
form = new DialogedClass({
getValidation: function(form_values) {
if (form_values.my_required_textbox !== 'foo') {
return "'my_required_textbox' must be 'foo'";
}
}
});
form.isInvalid(); // ["my_required_textbox is required", "'my_required_textbox' must be 'foo'"]
form.findChildByName('my_required_textbox').set('value', 'bar');
form.isInvalid(); // ["'my_required_textbox' must be 'foo'"]
form.findChildByName('my_required_textbox').set('value', 'foo');
form.isInvalid(); // false
Added Bonus
Nested Forms!
(see tests)
API
_FormDialogMixin
- handleValidationError
- some other juicy tidbits
handleValidationError
What do you do in a form when the user tries to submit, but the _Form isInvalid()?
new DialogedClass({
handleValidationError: function(errors) {
new NotifyPrompt({
title: phrases.validationMessageTitle,
escapeHtml: false,
message: phrases.validationMessageBlurb + '<ul><li>'+errors.join("</li><li>")+'</li></ul>'
}).show();
}
});
Neatly abstracted away to _SendValidationErrorsToPrompt mixin
(OPSC-3510)
...some extras
action-value object
Sometimes it's important to know some metadata about the form being submitted
{
action: 'Save',
values: {
"these": "are the",
"fully": ["transformed", "and", "validated", "values"]
}
}
This gets returned from the show() promise so the consumer knows what action was taken
...some extras
buttons can be specified as objects, we already know:
new DialogedClass({
buttons: [{label: 'Save This Information', action: 'Save'}]
});
But with _FormDialogMixin, we can specify if the validation should be processed
new DialogedClass({
buttons: [{label: 'Save This Information', action: 'Save', validate: false}]
});
EDIT I could have sworn I did this, but it doesn't look like it now. It should happen.
Happy Dialoging!
New Forms & Dialogs
By Mike McElroy
New Forms & Dialogs
- 1,116