How do you use the on-change on sp-editable-field

edmo1001
Kilo Explorer

I'm updating the User Profile page in Service Portal. I'm using the sp-editable-field, so users can update additional fields on the sys_user table. When company is changed, I need to clear the department field and refresh the department list, based on what company was chosen. Currently the field stays populated with the old value the list doesn't update unless the page is refreshed. There is an on-change variable on the sp-editable-field but I can't find any documentation or examples about it.

1 ACCEPTED SOLUTION

You are right, the main problem is not implementing on-change handler, but to force department and location fields to save modified (empty) data. I suggest to add both on-change and on-submit handler

<sp-editable-field ng-if="::displayField('sys_user', 'company', true)"
                   editable-by-user="data.isLoggedInUsersProfile"
                   table="sys_user"
                   table-id="data.sysUserID"
                   field-model="data.sysUserModel.company"
                   on-change="onChangeCompany"
                   on-submit="onSubmitCompany"></sp-editable-field>

and to add the following code in the client controller code:

var gformDepartment, gformLocation, scopeDepartment, scopeLocation;
$scope.$on("spEditableField.gForm.initialized", function (e, gFormInstance, shadowModel) {
	if (shadowModel.name === "department") {
		gformDepartment = gFormInstance;
		scopeDepartment = e.targetScope;
	} else if (shadowModel.name === "location") {
		gformLocation = gFormInstance;
		scopeLocation = e.targetScope;
	}
});
	
$scope.onChangeCompany = function (gform, shadowModel, oldValue, newValue) {
	if (oldValue !== newValue) {
		$scope.data.sysUserModel.department.value = "";
		$scope.data.sysUserModel.department.displayValue = "";
		$scope.data.sysUserModel.location.value = "";
		$scope.data.sysUserModel.location.displayValue = "";
		scopeDepartment.shadowModel.value = "";
		scopeLocation.shadowModel.value = "";
	}
};
	
$scope.onSubmitCompany = function (gform, shadowModel, oldValue, newValue) {
	scopeDepartment.saveForm();
	scopeLocation.saveForm();
};

Probably the above code is too complex and there are exist more easy way, but I didn't found it. The above code works in my tests.

View solution in original post

20 REPLIES 20

Hi Annie,

sorry, but I can try to help you only if you will post more details (HTML and JavaScript code fragments of your code) about your solution.

You can open User Profile in Service Portal, then open Developer Tools of Chrome for example. After that you can press Ctrl-Shift-F to open search field and search in page Sources for "spEditableField", click on the line 

;/*! RESOURCE: /scripts/app.$sp/directive.spEditableField.js */

in searching results and finally on "{}" to format the code:

find_real_file.png

you will see the source code of sp-editable-field directive:

;/*! RESOURCE: /scripts/app.$sp/directive.spEditableField.js */
angular.module('sn.$sp').directive('spEditableField', function(glideFormFactory, $http, spUtil, spModelUtil, $timeout) {
    return {
        restrict: 'E',
        templateUrl: 'sp_editable_field.xml',
        scope: {
            fieldModel: "=",
            table: "@",
            tableId: "=",
            block: "=?",
            editableByUser: "=",
            onChange: "=?",
            onSubmit: "=?",
            asyncSubmitValidation: "=?"
        },
        transclude: true,
        replace: true,
        controller: function($scope) {
            var REST_API_PATH = "/api/now/v2/table/";
            var g_form;
            this.createShadowModel = function() {
                spModelUtil.extendField($scope.fieldModel);
                $scope.shadowModel = angular.copy($scope.fieldModel);
                $scope.shadowModel.table = $scope.table;
                $scope.shadowModel.sys_id = $scope.tableId;
                $scope.blockDisplay = $scope.block ? {
                    display: 'block'
                } : {};
                $scope.editable = !$scope.shadowModel.readonly && $scope.editableByUser;
                $scope.fieldID = $scope.table + "-" + $scope.shadowModel.name.replace('.', '_dot_') + "-" + $scope.tableId;
                initGlideForm();
            }
            ;
            this.createShadowModel();
            $scope.getGlideForm = function() {
                return g_form;
            }
            ;
            $scope.saveForm = function() {
                if (g_form)
                    g_form.submit();
                if (angular.isDefined($scope.asyncSubmitValidation)) {
                    $scope.asyncSubmitValidation(g_form, $scope.shadowModel).then(function(result) {
                        if (result)
                            completeSave();
                    });
                }
            }
            ;
            function completeSave() {
                var url = REST_API_PATH + $scope.table + "/" + $scope.tableId + "?sysparm_display_value=all&sysparm_fields=" + $scope.shadowModel.name;
                if ($scope.shadowModel.type === "password" || $scope.shadowModel.type === "password2")
                    url += "&sysparm_input_display_value=true";
                var data = {};
                data[$scope.shadowModel.name] = $scope.shadowModel.value;
                $http.put(url, data).success(function(data) {
                    if (data.result)
                        updateFieldModel(data.result);
                    $scope.closePopover();
                }).error(function(reason) {
                    console.log("Field update failure", reason);
                    spUtil.retrieveSessionMessages();
                });
            }
            $scope.checkNullChoiceOverride = function() {
                if ($scope.fieldModel.type != "choice")
                    return;
                if ($scope.fieldModel.value || $scope.fieldModel.displayValue)
                    return;
                var choices = $scope.fieldModel.choices || [];
                for (var i = 0; i < choices.length; i++) {
                    if (choices[i].value == "") {
                        $scope.fieldModel.displayValue = choices[i].label;
                        return;
                    }
                }
            }
            function updateFieldModel(record) {
                if (record && $scope.fieldModel.name in record) {
                    var updated = record[$scope.fieldModel.name];
                    $scope.fieldModel.value = updated.value;
                    $scope.fieldModel.displayValue = updated.display_value;
                    $scope.checkNullChoiceOverride();
                }
            }
            function initGlideForm() {
                if (g_form)
                    g_form.$private.events.cleanup();
                var uiMessageHandler = function(g_form, type, message) {
                    switch (type) {
                    case 'infoMessage':
                        spUtil.addInfoMessage(message);
                        break;
                    case 'errorMessage':
                        spUtil.addErrorMessage(message);
                        break;
                    case 'clearMessages':
                        spUtil.clearMessages();
                        break;
                    default:
                        return false;
                    }
                };
                g_form = glideFormFactory.create($scope, $scope.table, $scope.tableId, [$scope.shadowModel], null, {
                    uiMessageHandler: uiMessageHandler
                });
                $scope.$emit("spEditableField.gForm.initialized", g_form, $scope.shadowModel);
                if (angular.isDefined($scope.onChange))
                    g_form.$private.events.on("change", function(fieldName, oldValue, newValue) {
                        return $scope.onChange.call($scope.onChange, g_form, $scope.shadowModel, oldValue, newValue);
                    });
                if (angular.isDefined($scope.onSubmit))
                    g_form.$private.events.on("submit", function() {
                        return $scope.onSubmit.call($scope.onSubmit, g_form, $scope.shadowModel);
                    });
                if (!angular.isDefined($scope.asyncSubmitValidation)) {
                    g_form.$private.events.on('submitted', function() {
                        completeSave();
                    });
                }
            }
        },
        link: function(scope, el, attrs, ctrl) {
            var returnFocus = true;
            scope.checkNullChoiceOverride();
            scope.closePopover = function() {
                if (scope.shadowModel.popoverIsOpen)
                    ctrl.createShadowModel();
                scope.shadowModel.popoverIsOpen = false;
                if (returnFocus) {
                    var trigger = el[0].querySelector('#field-' + scope.fieldID);
                    trigger.focus();
                }
                $('body').off('keydown', executeEventHandlers);
                $('body').off('click', closePopoverOnOutsideClick);
            }
            scope.toggleClick = function($event) {
                if ($event.type === "click") {
                    scope.togglePopover($event);
                }
            }
            scope.toggleKeydown = function($event) {
                if (($event.which === 13 || $event.which === 32)) {
                    scope.togglePopover($event);
                }
            }
            scope.togglePopover = function(evt) {
                scope.shadowModel.popoverIsOpen = !scope.shadowModel.popoverIsOpen;
                scope.shadowModel.value = scope.shadowModel.stagedValue = scope.fieldModel.value;
                scope.shadowModel.displayValue = scope.fieldModel.displayValue;
                if (scope.shadowModel.popoverIsOpen) {
                    returnFocus = true;
                    $('body').on('keydown', executeEventHandlers);
                    $('body').on('click', closePopoverOnOutsideClick);
                }
            }
            function executeEventHandlers(event) {
                trapKeyboardFocus(event);
                closePopoverOnEscape(event);
            }
            function trapKeyboardFocus(event) {
                if (!scope.shadowModel.popoverIsOpen)
                    return;
                if (event.which === 9 && !event.shiftKey) {
                    if (($(event.target).is("button[ng-click='saveForm()']"))) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
                if (event.which === 9 && event.shiftKey) {
                    if (!isTargetedElementSPFormField(event))
                        return;
                    if ($('button[ng-click="openReference(field, formModel.view)"]').length === 1) {
                        if ($(event.target).is("button")) {
                            event.preventDefault();
                            event.stopPropagation();
                        }
                    } else if ($("sp-email-element").length === 1) {
                        if ($("sp-email-element").length === 1) {
                            if ($(event.target).is("input")) {
                                event.preventDefault();
                                event.stopPropagation();
                            }
                        }
                    } else {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
            }
            function isTargetedElementSPFormField(event) {
                return $(event.target).parents("#editableSaveForm").length === 1;
            }
            function isModalLaunchedFromPopOver() {
                var modal = $('.modal.in');
                return (modal.length && !modal.find('.sp-editable-field .popover').length);
            }
            function closePopoverOnEscape(event) {
                if (event.which === 27 && !isModalLaunchedFromPopOver())
                    closePopover();
            }
            function closePopover() {
                scope.$evalAsync('closePopover()');
            }
            function closePopoverOnOutsideClick(event) {
                var $et = $(event.target);
                var closeButton = $et.attr('ng-click') && $et.attr('ng-click') === 'closePopover()';
                var saveButton = $et.attr('ng-click') && $et.attr('ng-click') === 'saveForm()';
                if (closeButton || saveButton)
                    return;
                if (!($et.closest(".popover-" + scope.fieldID).length || $et.closest(".popover-trigger-" + scope.fieldID).length) && !$et.closest("[uib-popover-template-popup]").length && $et.attr("uib-popover-template-popup") !== "" && !isModalLaunchedFromPopOver()) {
                    returnFocus = false;
                    scope.$evalAsync('closePopover()');
                }
            }
            scope.$on("$destroy", function() {
                $('body').off('keydown', executeEventHandlers);
                $('body').off('click', closePopoverOnOutsideClick);
            });
            scope.$on('sp.spFormField.rendered', function(e, element, input) {
                var parent = input.parent();
                var select2Input = parent[0].querySelector('.select2-container input');
                $timeout(function() {
                    if (select2Input)
                        select2Input.focus();
                    else
                        input.focus();
                }, 0, false);
            });
        }
    }
});

Another directive sp-editable-field2, which is very close to sp-editable-field, will be used in the widget Portal config (id=widget-portal-config). The code contains the usage of on-change callback. You can debug it if you open /sp_config?id=branding_editor URL and modify Portal Title. If you set breakpoint on c.onTitleChange before changes and changing the focus on another field, you will see that c.onTitleChange will be called.

Anne21
Kilo Contributor

Thank you for your quick response Oleg - this is all very helpful and I will be sure to look into that.

 

But, I have replaced Line 13 in the HTML of the "My Team" widget with the following code:

<div class="col-xs-7"> <sp-editable-field editable-by-user="data.isLoggedInUsersProfile" table="sys_user" table-id="data.sysUserID" field-model="data.sysUserModel.manager"></sp-editable-field></div>

 

I also added the following code to the Server Script:

data.sysUserID = $sp.getParameter("sys_id");
if (!data.sysUserID)
data.sysUserID = gs.getUser().getID();
var sysUserGR = new GlideRecord("sys_user");
data.userExists = sysUserGR.get(data.sysUserID) && sysUserGR.canRead();

if (data.userExists) {
sysUserGR = GlideScriptRecordUtil.get(sysUserGR).getRealRecord();
data.table = sysUserGR.getRecordClassName();
data.name = sysUserGR.getValue("name");
var loggedInSysUserID = gs.getUser().getID();
}

data.isLoggedInUsersProfile = loggedInSysUserID.equals(data.sysUserID);
var sysUserForm = $sp.getForm(data.table, data.sysUserID);
data.sysUserView = sysUserForm._view;
data.sysUserModel = sysUserForm._fields;
data.sysUserModelList = [];

for (var i = 0; i < data.sysUserView.length; i++) {
data.sysUserModelList.push(data.sysUserModel[data.sysUserView[i].name]);
}
function buildUser(userGR) {
return {
email: userGR.getValue("email") || "",
first_name: userGR.getValue("first_name"),
last_name: userGR.getValue("last_name"),
name: userGR.getValue("name"),
phone: userGR.getValue("phone") || "",
sys_id: userGR.getValue("sys_id")
}
}

 

 

Her is the entire HTML Template:

<sp-panel rect="rect" widget-title="'${Team}'" ng-if="showTeamWidget()">

<div id="manager" ng-if="myManager && myManager.sys_id">
<h3 ng-if="myManager.name && !showMyProfile" class="team_header m-t-none">${My Manager}</h3>
<h3 ng-if="myManager.name && showMyProfile" class="team_header m-t-none">${Manager}</h3>

<div class="row info-row">
<div class="col-xs-2" ng-click="openProfile(myManager)">
<span class="navbar-avatar">
<sn-avatar class="avatar-small-medium" primary="myManager.sys_id" show-presence="false" />
</span>
</div>
<!-- <div class="col-xs-7" ng-click="openProfile(myManager)" aria-label="Open Profile: {{myManager.name}}" tabindex="0">{{myManager.name}}</div> -->
<div class="col-xs-7"> <sp-editable-field editable-by-user="data.isLoggedInUsersProfile" table="sys_user" table-id="data.sysUserID" field-model="data.sysUserModel.manager"></sp-editable-field></div>
<div class="col-xs-1" ng-class="{'hide-element': !myManager.phone}">
<a href="tel:{{myManager.phone}}" aria-label="{{myManager.phone}}">
<fa name="phone" size="2"></fa>
</a>
</div>
<div class="col-xs-1" ng-class="{'hide-element': !myManager.email}">
<a href="mailto:{{myManager.email}}" aria-label="{{myManager.email}}">
<fa name="envelope" size="2"></fa>
</a>
</div>
</div>
</div>

<div id="colleagues" ng-if="teamMembers && teamMembers.length > 0">
<h3 ng-if="!showMyProfile" class="team_header">${My Coworkers}</h3>
<h3 ng-if="showMyProfile" class="team_header">${Coworkers}</h3>

<div class="row info-row" ng-repeat="member in teamMembers | filter:{sys_id: '!' + data.user_id} | limitTo: showFullInfoCollegues ? teamMembers.length : data.limitTo">
<div class="col-xs-2" ng-click="openProfile(member)">
<span class="navbar-avatar">
<sn-avatar class="avatar-small-medium" primary="member.sys_id" show-presence="false" />
</span>
</div>
<div class="col-xs-7" ng-click="openProfile(member)" aria-label="Open Profile: {{member['name']}}" tabindex="0">{{member['name']}}</div>
<div class="col-xs-1" ng-class="{'hide-element': !member.phone}">
<a href="tel:{{member.phone}}" aria-label="{{member.phone}}">
<fa name="phone" size="2"></fa>
</a>
</div>
<div class="col-xs-1" ng-class="{'hide-element': !member.email}" style="text-align: right;">
<a href="mailto:{{member.email}}" aria-label="{{member.email}}">
<fa name="envelope" size="2"></fa>
</a>
</div>
</div>

<div ng-if="teamMembers.length > data.limitTo" class="pull-right">
<a href="javascript:void(0)" class="more_link" ng-if="!showFullInfoCollegues" ng-click="updateList('showFullInfoCollegues',true)">${show all ({{teamMembers.length}})}
</a>
<a href="javascript:void(0)" class="more_link" ng-if="showFullInfoCollegues" ng-click="updateList('showFullInfoCollegues',false)">${show less}</a>
</div>

</div>

<div id="direct_reports" ng-if="directReports && directReports.length > 0">
<h3 ng-if="directReports.length > 0 && !showMyProfile" class="team_header">${My Direct Reports}</h3>
<h3 ng-if="directReports.length > 0 && showMyProfile" class="team_header">${Direct Reports}</h3>

<div class="row info-row" ng-repeat="direct_report in directReports | filter:{sys_id: '!' + data.user_id} | limitTo: showFullInfoReports ? directReports.length : data.limitTo">
<div class="col-xs-2" ng-click="openProfile(direct_report)">
<span class="navbar-avatar">
<sn-avatar class="avatar-small-medium" primary="direct_report.sys_id" show-presence="false" />
</span>
</div>
<div class="col-xs-7" ng-click="openProfile(direct_report)" aria-label="Open Profile: {{direct_report['name']}}" tabindex="0">{{direct_report['name']}}</div>
<div class="col-xs-1" ng-class="{'hide-element': !direct_report.phone}">
<a href="tel:{{direct_report.phone}}" aria-label="{{direct_report.phone}}">
<fa name="phone" size="2"></fa>
</a>
</div>
<div class="col-xs-1" ng-class="{'hide-element': !direct_report.email}" style="text-align: right;">
<a href="mailto:{{direct_report.email}}" aria-label="{{direct_report.email}}">
<fa name="envelope" size="2"></fa>
</a>
</div>
</div>

<div ng-if="directReports.length > data.limitTo" class="pull-right">
<a href="javascript:void(0)" class="more_link" ng-if="!showFullInfoReports" ng-click="updateList('showFullInfoReports',true)">${show all ({{directReports.length}})}</a>
<a href="javascript:void(0)" class="more_link" ng-if="showFullInfoReports" ng-click="updateList('showFullInfoReports',false)">${show less}</a>
</div>

</div>

<a id="my_org_chart" class="widget-button" ng-if="!showMyProfile" href="?id=my_org_chart">
<i class="fa fa-sitemap"></i>
${my org chart}
</a>
<a id="my_org_chart" class="widget-button" ng-if="showMyProfile" ng-href="?id=my_org_chart&p={{userID}}">
<i class="fa fa-sitemap"></i>
${org chart}
</a>

</sp-panel>

 

Here is the entire Server Script:

data.limitTo = 5;
data.user_id = gs.getUserID();

data.sysUserID = $sp.getParameter("sys_id");
if (!data.sysUserID)
data.sysUserID = gs.getUser().getID();
var sysUserGR = new GlideRecord("sys_user");
data.userExists = sysUserGR.get(data.sysUserID) && sysUserGR.canRead();

if (data.userExists) {
sysUserGR = GlideScriptRecordUtil.get(sysUserGR).getRealRecord();
data.table = sysUserGR.getRecordClassName();
data.name = sysUserGR.getValue("name");
var loggedInSysUserID = gs.getUser().getID();
}

data.isLoggedInUsersProfile = loggedInSysUserID.equals(data.sysUserID);
var sysUserForm = $sp.getForm(data.table, data.sysUserID);
data.sysUserView = sysUserForm._view;
data.sysUserModel = sysUserForm._fields;
data.sysUserModelList = [];

for (var i = 0; i < data.sysUserView.length; i++) {
data.sysUserModelList.push(data.sysUserModel[data.sysUserView[i].name]);
}
function buildUser(userGR) {
return {
email: userGR.getValue("email") || "",
first_name: userGR.getValue("first_name"),
last_name: userGR.getValue("last_name"),
name: userGR.getValue("name"),
phone: userGR.getValue("phone") || "",
sys_id: userGR.getValue("sys_id")
}
}

 

Here is the entire Client Controller:

function ($scope, $location) {

$scope.showFullInfoCollegues = false;
$scope.showFullInfoReports = false;
$scope.showMyProfile = false;

$scope.$on('finishedChanged', function(event, data) {
$scope.myManager = data.profile.manager;
$scope.teamMembers = data.profile.members;
$scope.directReports = data.profile.direct_reports;
if(data.profile.user && data.profile.user.sys_id) { //if user exists, then this isn't 'my' profile. It's someone elses.
$scope.showMyProfile = true;
$scope.userID = data.profile.user.sys_id;
}
});

$scope.openProfile = function(user) {
$location.search("id=user_profile&sys_id=" + user.sys_id);
};

$scope.showTeamWidget = function() {
return ($scope.myManager && $scope.myManager.sys_id) ||
($scope.teamMembers && $scope.teamMembers.length > 1) ||
($scope.directReports && $scope.directReports.length > 0);
};

$scope.updateList = function(variable, value) {
if (variable == "showFullInfoReports")
$scope.showFullInfoReports = value;
else if (variable == "showFullInfoCollegues")
$scope.showFullInfoCollegues = value;
};

}

Sorry, but I still can't follow you. You wrote in your initial post the following

When company is changed, I need to clear the department field and refresh the department list, based on what company was chosen.

The HTML code, which you posted, contains only one sp-editable-field and it's without on-change attribute:

<sp-editable-field editable-by-user="data.isLoggedInUsersProfile"
   table="sys_user" table-id="data.sysUserID" 
   field-model="data.sysUserModel.manager"></sp-editable-field>

Usage of on-change attribute is the main part of my answer. (Search for on-change="onChangeCompany" in HTML part and for "$scope.onChangeCompany = function (gform, shadowModel, oldValue, newValue) {" in my answer).

The part of your code where you use $scope.$on('finishedChanged', function(event, data) {... will probably never work, because event finishedChanged is unknown and will be never fired.

Anne21
Kilo Contributor

I apologize, I copied and pasted the wrong thing but there is what I meant to say:

I have customized the User Profile widget so that it calls a customized "My Team" widget, where I made the Manager field editable. When the user changes the Manager and clicks Save, the Manager changes BUT the Avatar, the Phone, the Email, and My Coworkers aren't updated to those of the new Manager (not until after I refresh the page). Is anyone able to help with implementing the code to automatically update those items as soon as the user clicks Save?

Anne21
Kilo Contributor

Please let me know if you have any questions on that requirement.

Thanks in advance,

Annie