“Have a bias toward action – let’s see something happen now. You can break that big plan into small steps and take the first step right away.”
— Indira Gandhi
After working out all of the little issues in my @mentions example, it occurred to me that it might be even better to just make an @mentions textarea another supported field type like I just did for feedback. This way, you wouldn’t have to bother with all of those mentio- prefixed attributes at all — they would just become part of the internal process of rendering the form field. Some things would have to be standardized, which might limit your flexibility in certain areas, but that still wouldn’t prevent you from mentionizing your own textarea or textarea-type form field. This would just be yet another option.
The first thing to, then, would be to add mention to the list of supported field types:
var SUPPORTED_TYPE = ['choicelist', 'date', 'datetime-local', 'email', 'feedback', 'inlineradio', 'mention', 'month', 'number', 'password', 'radio', 'rating', 'reference', 'select', 'tel', 'text', 'textarea', 'time', 'url', 'week'];
That’s getting to be quite a list, but that’s not necessarily a bad thing. Next, we have to add the logic to render the input element:
} else if (type == 'mention') {
htmlText += " <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" mentio=\"\" mentio-macros=\"macros\" mentio-trigger-char=\"'@'\" mentio-items=\"members\" mentio-search=\"searchMembersAsync(term)\" mentio-template-url=\"/at-mentions.tpl\" mentio-select=\"selectAtMention('" + name + "', item)\" mentio-typed-term=\"typedTerm\" mentio-id=\"'" + name + "'\" ng-trim=\"false\" autocomplete=\"off\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";
So far, so good. Now, where to stuff the template? In my mind, the best place to locate the template would be in the Service Portal itself, as that would ensure that it would only appear on the page once and only once. You could just make it part of your portal page header and be done with it. However, if you didn’t know to do that, or if you moved your widget from a portal that had it to another portal that did not, things would not work and would not be that intuitive as to why that was. Placing it in the widget is another option, but again, you would have to know to do that. Rendering it as part of the form field means that you could have many copies of the template ending up on a single page, but then everything would be self-contained and you wouldn’t have to know to add anything else to include this feature other than the appropriate form field type. Right now, I am thinking that this latter approach is the best option, although I may end up going the other way one day after I’ve had more time to contemplate all of the ramifications of that strategy. For now, I’ve added this line just under the lines above:
htmlText += getMentionTemplate();
… and this function to provide the template:
function getMentionTemplate() {
var htmlText = " <script type=\"text/ng-template\" id=\"/at-mentions.tpl\">\n";
htmlText += " <div class=\"dropdown-menu sn-widget sn-mention\">\n";
htmlText += " <ul class=\"sn-widget-list_v2\">\n";
htmlText += " <li ng-if=\"items.length > 0 && !items[0].termLengthIsZero\" mentio-menu-item=\"person\" ng-repeat=\"person in items\">\n";
htmlText += " <div class=\"sn-widget-list-content sn-widget-list-content_static\">\n";
htmlText += " <sn-avatar primary=\"person\" class=\"avatar-small\" show-presence=\"true\"></sn-avatar>\n";
htmlText += " </div>\n";
htmlText += " <div class=\"sn-widget-list-content\">\n";
htmlText += " <span class=\"sn-widget-list-title\" ng-bind-html=\"person.name\"></span>\n";
htmlText += " <span class=\"sn-widget-list-subtitle\" ng-if=\"!person.record_is_visible\">Cannot see record</span>\n";
htmlText += " </div>\n";
htmlText += " </li>\n";
htmlText += " <li ng-if=\"items.length === 1 && items[0].termLengthIsZero\">\n";
htmlText += " <div class=\"sn-widget-list-content\">\n";
htmlText += " <span class=\"sn-widget-list-title sn-widget-list-title_wrap\">Enter the name of a person you want to mention</span>\n";
htmlText += " </div>\n";
htmlText += " </li>\n";
htmlText += " <li ng-if=\"items.length === 0 && items.loading && visible\">\n";
htmlText += " <div class=\"sn-widget-list-content sn-widget-list-content_static\">\n";
htmlText += " <span class=\"sn-widget-list-icon icon-loading\"></span>\n";
htmlText += " </div>\n";
htmlText += " <div class=\"sn-widget-list-content\">\n";
htmlText += " <span class=\"sn-widget-list-title\">Loading...</span>\n";
htmlText += " </div>\n";
htmlText += " </li>\n";
htmlText += " <li ng-if=\"items.length === 0 && !items.loading\">\n";
htmlText += " <div class=\"sn-widget-list-content\">\n";
htmlText += " <span class=\"sn-widget-list-title\">No users found</span>\n";
htmlText += " </div>\n";
htmlText += " </li>\n";
htmlText += " </ul>\n";
htmlText += " </div>\n";
htmlText += " </script>\n";
return htmlText;
}
That does create the potential for more than one template appearing in the source code, but other than the obvious bloat, that doesn’t really hurt anything.
Now we have to deal with the functions, one to fetch the data and one to handle the selection. We will add these to the link section of the provider, starting with the one that fetches the data:
scope.searchMembersAsync = function(term) {
scope.userSysId = window.NOW.user_id;
scope.members = [];
scope.members.loading = true;
clearTimeout(scope.typingTimer);
if (term.length === 0) {
scope.members = [{
termLengthIsZero: true
}];
scope.members.loading = false;
} else {
scope.typingTimer = setTimeout(function() {
scope.snMention.retrieveMembers('sys_id', scope.userSysId, term).then(function(members) {
scope.members = members;
scope.members.loading = false;
}, function () {
scope.members = [{
termLengthIsZero: true
}];
scope.members.loading = false;
});
}, 500);
}
};
This script required two things that we brought in via the client script function arguments: snMention and $timeout. In this context, $timeout can be replaced with the standard window functions setTimeout and clearTimeout, so that eliminates the need for that one. The other requirement, though, snMention, is still going to have to come in via a client script function argument, and then passed into the scope. I don’t like doing that, as that is yet another thing that the person using this is going to have to know, but I couldn’t see any other way around it. That makes the client script in our example widget now start out like this:
function($scope, snMention) {
var c = this;
$scope.snMention = snMention;
The other script we will need is the one that handles the selection:
scope.selectAtMention = function(field, item) {
if (!scope.mentionMap) {
scope.mentionMap = {};
}
if (!scope.mentionMap[field]) {
scope.mentionMap[field] = {};
}
if (item.termLengthIsZero) {
return (item.name || "") + "\n";
}
scope.mentionMap[field][item.name] = item.sys_id;
return "@[" + item.name + "]";
};
The major revision here is that we have added a field argument to the function call to allow for the fact that there could be more than one field on the form that contains @mentions. This way, mentions from one field are collected in a different object than mentions from another field. We also added checks for both the base mention map and the field-specific map so that they can be initialized as needed.
To test things out, I added a mention type form field to my form field test widget and gave it a whirl.
Everything seems to work, which is good, so I’ve bundled it all up into yet another Update Set just in case someone wants to dig into all of the details. I’m going to have to quit using the word final in relationship to this little form field experiment, as I keep finding new form field types to toss onto the pile; who knows what will pop up tomorrow …