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,024