Custom modal edit views on Saleslogix Mobile
Posted By: nicocrm on November 15th, 2013 in Saleslogix, SLX Mobile
No Gravatar

From time to time we have the need to prompt the user with more detailed information than what can be collected in a single field. For these cases we often resort to a modal dialog in the web client. However if you attempt to create a standard dojo dialog (dijit.dialog) in the mobile client, you will find many things don’t work quite right, especially when it is used on a mobile device (as opposed to testing in the browser). For the mobile there is a different paradigm which is to use a view to collect the information. This edit view takes the whole screen, but keeps context of the original view it was called from so clicking the “Accept” or “Cancel” button will send the user back to it. It’s similar to a modal dialog in concept but is better adapted to the small screen size. A good example is the address control in the default mobile client.

If you want to create your own custom “dialog” or edit view, you will need to create a custom view that represents the layout of your dialog, and a custom field subclassing EditorField (Sage/Platform/Mobile/Fields/EditorField) in which you will specify the custom view as the target view for the edit action. An EditorField is a Field that can be used in your custom view layouts (you know, the views where you use “createLayout” to define what is shown), as well as in customized layout for standard views. But it is also a dijit which means it can be used in “from scratch” views as well, if you define a view from the ground up in Javascript. Either way the process is the same:

  • The EditorField will specify a “view” property as the id of a view that was previously registered with the application (using App.registerView), OR override navigateToEditView to obtain a custom view instance (though do not that in that case the view must still be registered using App.registerView, and you must use App.getView at least once to retrieve it, or the navigation will not work properly – some of the initialization code takes place in those 2 methods)
  • In the EditorField, override formatValue to specify how the values are to be displayed on a single line
  • Add CSS rules for your field (the original rules in base.css are hard-coded for specific subclasses of EditorField
  • Optionally, use FieldManager.register to make the custom field available to standard views
  • If you need some custom handling on the values after they are returned from the edit view, you can override getValuesFromView and have it work with this.currentValue (representing the “dirty” values from the edit view) or this.validationValue (which represents values from all the fields from the view – but do note, this won’t include data for properties that are not bound to a field – this is important to remember if you are using a field to edit a complex object that has many hidden properties)
  • The edit view is a normal view, subclassing Edit (Sage/Platform/Mobile/Edit) – you use the createLayout method to specify the fields to be displayed

For a practical example, I created a custom field to be used in place of the “Account Name” edit field – it prompts the user for an edit reason and stores the name of the person doing the edit and the date it was made (this is a bit redundant since this part is tracked in the history table but it makes it more convenient to see on one screen, especially on mobile):

Detail View

Corresponding edit view:

Edit View

There is a little bit of logic – it won’t let the user change the name without entering a reason, and it won’t let them enter a reason unless they are changing the name. It also sets the edited by name / date automatically when the user edits the name. Almost everything is in the AccountNameEdit control:

define('demo/Fields/AccountNameEdit',
    ['dojo/_base/declare', 'dojo/_base/lang', 'dojo/on', 'Sage/Platform/Mobile/Fields/EditorField', 'Sage/Platform/Mobile/Edit', 'Sage/Platform/Mobile/FieldManager',
        'Mobile/Saleslogix/Views/Account/Detail'],
function (declare, lang, on, EditorField, Edit, FieldManager, AccountDetail) {
    var View = declare([Edit], {
        titleText: "Edit Account Name",

        // keep track of whether we have reset the reason field yet or not
        _reasonReset: false,

        createLayout: function () {
            return this.layout || (this.layout = [{
                name: 'AccountName',
                property: 'AccountName',
                label: 'account',
                type: 'text',
                notificationTrigger: 'keyup',
                validator: { fn: this._validateAccountName, scope: this }
            }, {
                name: 'AccountNameEditReason',
                property: 'AccountNameEditReason',
                label: 'edit reason',
                type: 'text'
            }, {
                name: 'AccountNameEditedBy',
                label: 'edited by',
                readonly: true,
                type: 'text'
            }, {
                name: 'AccountNameEditedDate',
                label: 'edited date',
                readonly: true,
                // setting the view to false prevent the user from going to the calendar
                // (even when setting the date control to readonly, it still responds to click events)
                view: false,
                type: 'date'
            }]);
        },

        startup: function () {
            this.inherited(arguments);

            this.fields.AccountName.onChange = lang.hitch(this, "_onAccountChange");
        },

        onShow: function () {
            this.inherited(arguments);
            // disable the reason until they make an edit
            this.fields.AccountNameEditReason.disable();
            this._reasonReset = false;
        },

        // when the account name is edited, reset the reason, date and edited by fields, and enable reason field
        _onAccountChange: function () {
            if (!this._reasonReset) {
                this.fields.AccountNameEditedBy.setValue(App.context.user.UserInfo.UserName);
                this.fields.AccountNameEditedDate.setValue(new Date());
                this.fields.AccountNameEditReason.setValue('');
                this.fields.AccountNameEditReason.enable();                
                this._reasonReset = true;
            }
        },

        _validateAccountName: function (val) {
            if (val != this.entry.AccountName) {
                if (!this.fields.AccountNameEditReason.getValue()) {
                    return "Please provide reason for edit";
                }
            }
            return false;
        }
    });

    var Control = declare([EditorField], {
        view: "demo_accountnameedit_editview",

        // don't forget this part - it will enable the properties set in this control to merge with the current object
        applyTo: ".",

        formatValue: function (val) {
            return val.AccountName;
        },

        navigateToEditView: function () {
            if (!App.getView(this.view))
                App.registerView(new View({ id: this.view }));
            this.inherited(arguments);
        }
    });

    // We must extend the account detail view to include the fields used by our custom field
    lang.extend(AccountDetail, {
        querySelect: AccountDetail.prototype.querySelect.concat([
            "AccountNameEditReason", "AccountNameEditedBy", "AccountNameEditedDate"
        ])
    });


    return FieldManager.register('demo_accountnameedit', Control);
});

The only thing left is registering this onto the account view, in ApplicationModule, similar to other customizations – note the ApplicationModule is requiring the AccountNameEdit file, which will in turn call FieldManager.register and make our custom field available to be used in layouts:

define('demo/ApplicationModule', 
    ['dojo/_base/declare', 'Sage/Platform/Mobile/ApplicationModule', './Fields/AccountNameEdit'],
function (declare, ApplicationModule, AccountNameEdit) {
    return declare("demo.ApplicationModule", ApplicationModule, {
        loadCustomizations: function () {
            this.inherited(arguments);

            this.registerAccountCustomizations();
        },

        registerAccountCustomizations: function () {
            this.registerCustomization('edit', 'account_edit', {
                at: function (row) { return row.name == 'AccountName' },
                type: 'replace',
                value: {
                    name: 'AccountName',
                    // pass the whole object - we'll need it to be able to populate the extra fields from the edit view
                    property: ".",  
                    label: "acct",
                    type: "demo_accountnameedit"                    
                }
            });
        }
    });
});

And finally the CSS for it:

/* this stylesheet rule is hardcoded according to the control type in base.css.  
  Here we add one for our field so that the button may be displayed properly.
*/
.row-edit[data-field-type="demo_accountnameedit"] > div >.button
{
    margin: 0;
    padding: 0 8px;
    position: absolute;
    right: 4px;
}

/* Match the padding on the "input-text" class, defined in base.css
  For some reason that class is not automatically applied to the input field for EditorField fields.
*/
.row-edit[data-field-type="demo_accountnameedit"] > div >input[type=text]
{
    padding: 8px 34px 8px 110px !important;
}

This is the bundle for this sample, if you want to take a closer look.


Leave a Reply