jessi_graves
ServiceNow Employee
ServiceNow Employee

Welcome to the ServiceNow Knowledge19 Pre-Conference - Service Portal Advanced Class!

This is a dedicated Community Article for class communication, collaboration, and fun!!

Comments
Stijn Verhulst3
Kilo Guru

Hi Igor,

 

I can recommend you the following blog post which clarifies your question, even though it's more on the conceptual level of AngularJS; it also applies within the Service Portal of ServiceNow:

 

AngularJS - Controllers: The Comprehensive How To | Pluralsight | Pluralsight

Dan Conroy
ServiceNow Employee
ServiceNow Employee

My favourite explanation of why you don't need factories: https://blog.thoughtram.io/angular/2015/07/07/service-vs-factory-once-and-for-all.html (use services!)

Dan Conroy
ServiceNow Employee
ServiceNow Employee

https://www.codementor.io/cnorthfield/why-you-should-use-angularjs-s-controller-as-syntax-6i001lr5p

It becomes really clear if you start embedding directives in other directives and in widgets etc - can make it much easier to determine which scope you are actually in at any one time.

It is also easier to write 'c' than '$scope' every time 😄 and it makes it clear where you are extending the controller vs using the $scope 'service' for one of AngularJS's provided methods/attributes.

Dan Conroy
ServiceNow Employee
ServiceNow Employee
Stijn Verhulst3
Kilo Guru

Hi Benjamin,

initialize will basically set the default values, sys_id & others within the data structure of the GlideRecord object (= record within a database table on the platform) you're about to create, which then can be defined in detail hence the following script:

 

var inc = new GlideRecord('incident');
inc.initialize();
inc.short_description = 'Issue with server XYZ';
inc.category = 'hardware';
// ...

 

Though, in this script you don't have to explicitly use the initialize() function; as it will still work fine.

The moment when you must use the initialize() function is when you're applying a loop of record creations - updates such as in the code snippet below:

var inc = new GlideRecord('incident');

for (i=0; i < 5; i++) {

    inc.initialize();
    inc.short_description = "...";
    //...

}

 

Otherwise, the platform might get confused as of the object pointers and such (sys_id for example) need to be recalculated during the looping. If you wouldn't, it will always try to make use of the GlideRecord object you've created in the previous step of the loop; leading to possible failures.

Dan Conroy
ServiceNow Employee
ServiceNow Employee

They will both work, right?

Dan Conroy
ServiceNow Employee
ServiceNow Employee
Sarah Deady
Tera Contributor

Would someone be able to paste the contents from the ng-tags-input.js file here? Security policy on my computer is blocking it from running.

mike_visser
Tera Expert

/*! * ngTagsInput v3.2.0 * http://mbenford.github.io/ngTagsInput * * Copyright (c) 2013-2017 Michael Benford * License: MIT * * Generated at 2017-04-15 17:08:51 -0300 */ (function() { 'use strict'; var KEYS = { backspace: 8, tab: 9, enter: 13, escape: 27, space: 32, up: 38, down: 40, left: 37, right: 39, delete: 46, comma: 188 }; var MAX_SAFE_INTEGER = 9007199254740991; var SUPPORTED_INPUT_TYPES = ['text', 'email', 'url']; var tagsInput = angular.module('ngTagsInput', []); /** * @ngdoc directive * @name tagsInput * @module ngTagsInput * * @description * Renders an input box with tag editing support. * * @param {string} ngModel Assignable Angular expression to data-bind to. * @param {boolean=} [useStrings=false] Flag indicating that the model is an array of strings (EXPERIMENTAL). * @param {string=} [template=NA] URL or id of a custom template for rendering each tag. * @param {string=} [templateScope=NA] Scope to be passed to custom templates - of both tagsInput and * autoComplete directives - as $scope. * @param {string=} [displayProperty=text] Property to be rendered as the tag label. * @param {string=} [keyProperty=text] Property to be used as a unique identifier for the tag. * @param {string=} [type=text] Type of the input element. Only 'text', 'email' and 'url' are supported values. * @param {string=} [text=NA] Assignable Angular expression for data-binding to the element's text. * @param {number=} tabindex Tab order of the control. * @param {string=} [placeholder=Add a tag] Placeholder text for the control. * @param {number=} [minLength=3] Minimum length for a new tag. * @param {number=} [maxLength=MAX_SAFE_INTEGER] Maximum length allowed for a new tag. * @param {number=} [minTags=0] Sets minTags validation error key if the number of tags added is less than minTags. * @param {number=} [maxTags=MAX_SAFE_INTEGER] Sets maxTags validation error key if the number of tags added is greater * than maxTags. * @param {boolean=} [allowLeftoverText=false] Sets leftoverText validation error key if there is any leftover text in * the input element when the directive loses focus. * @param {string=} [removeTagSymbol=×] (Obsolete) Symbol character for the remove tag button. * @param {boolean=} [addOnEnter=true] Flag indicating that a new tag will be added on pressing the ENTER key. * @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key. * @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key. * @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus. * @param {boolean=} [addOnPaste=false] Flag indicating that the text pasted into the input field will be split into tags. * @param {string=} [pasteSplitPattern=,] Regular expression used to split the pasted text into tags. * @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes. * @param {string=} [allowedTagsPattern=.+] Regular expression that determines whether a new tag is valid. * @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into the new tag * input box instead of being removed when the backspace key is pressed and the input box is empty. * @param {boolean=} [addFromAutocompleteOnly=false] Flag indicating that only tags coming from the autocomplete list * will be allowed. When this flag is true, addOnEnter, addOnComma, addOnSpace and addOnBlur values are ignored. * @param {boolean=} [spellcheck=true] Flag indicating whether the browser's spellcheck is enabled for the input field or not. * @param {expression=} [tagClass=NA] Expression to evaluate for each existing tag in order to get the CSS classes to be used. * The expression is provided with the current tag as $tag, its index as $index and its state as $selected. The result * of the evaluation must be one of the values supported by the ngClass directive (either a string, an array or an object). * See https://docs.angularjs.org/api/ng/directive/ngClass for more information. * @param {expression=} [onTagAdding=NA] Expression to evaluate that will be invoked before adding a new tag. The new * tag is available as $tag. This method must return either a boolean value or a promise. If either a false value or a rejected * promise is returned, the tag will not be added. * @param {expression=} [onTagAdded=NA] Expression to evaluate upon adding a new tag. The new tag is available as $tag. * @param {expression=} [onInvalidTag=NA] Expression to evaluate when a tag is invalid. The invalid tag is available as $tag. * @param {expression=} [onTagRemoving=NA] Expression to evaluate that will be invoked before removing a tag. The tag * is available as $tag. This method must return either a boolean value or a promise. If either a false value or a rejected * promise is returned, the tag will not be removed. * @param {expression=} [onTagRemoved=NA] Expression to evaluate upon removing an existing tag. The removed tag is available as $tag. * @param {expression=} [onTagClicked=NA] Expression to evaluate upon clicking an existing tag. The clicked tag is available as $tag. */ tagsInput.directive('tagsInput', ["$timeout", "$document", "$window", "$q", "tagsInputConfig", "tiUtil", function($timeout, $document, $window, $q, tagsInputConfig, tiUtil) { function TagList(options, events, onTagAdding, onTagRemoving) { var self = {}, getTagText, setTagText, canAddTag, canRemoveTag; getTagText = function(tag) { return tiUtil.safeToString(tag[options.displayProperty]); }; setTagText = function(tag, text) { tag[options.displayProperty] = text; }; canAddTag = function(tag) { var tagText = getTagText(tag); var valid = tagText && tagText.length >= options.minLength && tagText.length <= options.maxLength && options.allowedTagsPattern.test(tagText) && !tiUtil.findInObjectArray(self.items, tag, options.keyProperty || options.displayProperty); return $q.when(valid && onTagAdding({ $tag: tag })).then(tiUtil.promisifyValue); }; canRemoveTag = function(tag) { return $q.when(onTagRemoving({ $tag: tag })).then(tiUtil.promisifyValue); }; self.items = []; self.addText = function(text) { var tag = {}; setTagText(tag, text); return self.add(tag); }; self.add = function(tag) { var tagText = getTagText(tag); if (options.replaceSpacesWithDashes) { tagText = tiUtil.replaceSpacesWithDashes(tagText); } setTagText(tag, tagText); return canAddTag(tag) .then(function() { self.items.push(tag); events.trigger('tag-added', { $tag: tag }); }) .catch(function() { if (tagText) { events.trigger('invalid-tag', { $tag: tag }); } }); }; self.remove = function(index) { var tag = self.items[index]; return canRemoveTag(tag).then(function() { self.items.splice(index, 1); self.clearSelection(); events.trigger('tag-removed', { $tag: tag }); return tag; }); }; self.select = function(index) { if (index < 0) { index = self.items.length - 1; } else if (index >= self.items.length) { index = 0; } self.index = index; self.selected = self.items[index]; }; self.selectPrior = function() { self.select(--self.index); }; self.selectNext = function() { self.select(++self.index); }; self.removeSelected = function() { return self.remove(self.index); }; self.clearSelection = function() { self.selected = null; self.index = -1; }; self.getItems = function() { return options.useStrings ? self.items.map(getTagText): self.items; }; self.clearSelection(); return self; } function validateType(type) { return SUPPORTED_INPUT_TYPES.indexOf(type) !== -1; } return { restrict: 'E', require: 'ngModel', scope: { tags: '=ngModel', text: '=?', templateScope: '=?', tagClass: '&', onTagAdding: '&', onTagAdded: '&', onInvalidTag: '&', onTagRemoving: '&', onTagRemoved: '&', onTagClicked: '&', }, replace: false, transclude: true, templateUrl: 'ngTagsInput/tags-input.html', controller: ["$scope", "$attrs", "$element", function($scope, $attrs, $element) { $scope.events = tiUtil.simplePubSub(); tagsInputConfig.load('tagsInput', $scope, $attrs, { template: [String, 'ngTagsInput/tag-item.html'], type: [String, 'text', validateType], placeholder: [String, 'Add a tag'], tabindex: [Number, null], removeTagSymbol: [String, String.fromCharCode(215)], replaceSpacesWithDashes: [Boolean, true], minLength: [Number, 3], maxLength: [Number, MAX_SAFE_INTEGER], addOnEnter: [Boolean, true], addOnSpace: [Boolean, false], addOnComma: [Boolean, true], addOnBlur: [Boolean, true], addOnPaste: [Boolean, false], pasteSplitPattern: [RegExp, /,/], allowedTagsPattern: [RegExp, /.+/], enableEditingLastTag: [Boolean, false], minTags: [Number, 0], maxTags: [Number, MAX_SAFE_INTEGER], displayProperty: [String, 'text'], keyProperty: [String, ''], allowLeftoverText: [Boolean, false], addFromAutocompleteOnly: [Boolean, false], spellcheck: [Boolean, true], useStrings: [Boolean, false] }); $scope.tagList = new TagList($scope.options, $scope.events, tiUtil.handleUndefinedResult($scope.onTagAdding, true), tiUtil.handleUndefinedResult($scope.onTagRemoving, true)); this.registerAutocomplete = function() { var input = $element.find('input'); return { addTag: function(tag) { return $scope.tagList.add(tag); }, getTags: function() { return $scope.tagList.items; }, getCurrentTagText: function() { return $scope.newTag.text(); }, getOptions: function() { return $scope.options; }, getTemplateScope: function() { return $scope.templateScope; }, on: function(name, handler) { $scope.events.on(name, handler, true); return this; } }; }; this.registerTagItem = function() { return { getOptions: function() { return $scope.options; }, removeTag: function(index) { if ($scope.disabled) { return; } $scope.tagList.remove(index); } }; }; }], link: function(scope, element, attrs, ngModelCtrl) { var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace, KEYS.delete, KEYS.left, KEYS.right], tagList = scope.tagList, events = scope.events, options = scope.options, input = element.find('input'), validationOptions = ['minTags', 'maxTags', 'allowLeftoverText'], setElementValidity, focusInput; setElementValidity = function() { ngModelCtrl.$setValidity('maxTags', tagList.items.length <= options.maxTags); ngModelCtrl.$setValidity('minTags', tagList.items.length >= options.minTags); ngModelCtrl.$setValidity('leftoverText', scope.hasFocus || options.allowLeftoverText ? true : !scope.newTag.text()); }; focusInput = function() { $timeout(function() { input[0].focus(); }); }; ngModelCtrl.$isEmpty = function(value) { return !value || !value.length; }; scope.newTag = { text: function(value) { if (angular.isDefined(value)) { scope.text = value; events.trigger('input-change', value); } else { return scope.text || ''; } }, invalid: null }; scope.track = function(tag) { return tag[options.keyProperty || options.displayProperty]; }; scope.getTagClass = function(tag, index) { var selected = tag === tagList.selected; return [ scope.tagClass({$tag: tag, $index: index, $selected: selected}), { selected: selected } ]; }; scope.$watch('tags', function(value) { if (value) { tagList.items = tiUtil.makeObjectArray(value, options.displayProperty); if (options.useStrings) { return; } scope.tags = tagList.items; } else { tagList.items = []; } }); scope.$watch('tags.length', function() { setElementValidity(); // ngModelController won't trigger validators when the model changes (because it's an array), // so we need to do it ourselves. Unfortunately this won't trigger any registered formatter. ngModelCtrl.$validate(); }); attrs.$observe('disabled', function(value) { scope.disabled = value; }); scope.eventHandlers = { input: { keydown: function($event) { events.trigger('input-keydown', $event); }, focus: function() { if (scope.hasFocus) { return; } scope.hasFocus = true; events.trigger('input-focus'); }, blur: function() { $timeout(function() { var activeElement = $document.prop('activeElement'), lostFocusToBrowserWindow = activeElement === input[0], lostFocusToChildElement = element[0].contains(activeElement); if (lostFocusToBrowserWindow || !lostFocusToChildElement) { scope.hasFocus = false; events.trigger('input-blur'); } }); }, paste: function($event) { $event.getTextData = function() { var clipboardData = $event.clipboardData || ($event.originalEvent && $event.originalEvent.clipboardData); return clipboardData ? clipboardData.getData('text/plain') : $window.clipboardData.getData('Text'); }; events.trigger('input-paste', $event); } }, host: { click: function() { if (scope.disabled) { return; } focusInput(); } }, tag: { click: function(tag) { events.trigger('tag-clicked', { $tag: tag }); } } }; events .on('tag-added', scope.onTagAdded) .on('invalid-tag', scope.onInvalidTag) .on('tag-removed', scope.onTagRemoved) .on('tag-clicked', scope.onTagClicked) .on('tag-added', function() { scope.newTag.text(''); }) .on('tag-added tag-removed', function() { scope.tags = tagList.getItems(); // Ideally we should be able call $setViewValue here and let it in turn call $setDirty and $validate // automatically, but since the model is an array, $setViewValue does nothing and it's up to us to do it. // Unfortunately this won't trigger any registered $parser and there's no safe way to do it. ngModelCtrl.$setDirty(); focusInput(); }) .on('invalid-tag', function() { scope.newTag.invalid = true; }) .on('option-change', function(e) { if (validationOptions.indexOf(e.name) !== -1) { setElementValidity(); } }) .on('input-change', function() { tagList.clearSelection(); scope.newTag.invalid = null; }) .on('input-focus', function() { element.triggerHandler('focus'); ngModelCtrl.$setValidity('leftoverText', true); }) .on('input-blur', function() { if (options.addOnBlur && !options.addFromAutocompleteOnly) { tagList.addText(scope.newTag.text()); } element.triggerHandler('blur'); setElementValidity(); }) .on('input-keydown', function(event) { var key = event.keyCode, addKeys = {}, shouldAdd, shouldRemove, shouldSelect, shouldEditLastTag; if (tiUtil.isModifierOn(event) || hotkeys.indexOf(key) === -1) { return; } addKeys[KEYS.enter] = options.addOnEnter; addKeys[KEYS.comma] = options.addOnComma; addKeys[KEYS.space] = options.addOnSpace; shouldAdd = !options.addFromAutocompleteOnly && addKeys[key]; shouldRemove = (key === KEYS.backspace || key === KEYS.delete) && tagList.selected; shouldEditLastTag = key === KEYS.backspace && scope.newTag.text().length === 0 && options.enableEditingLastTag; shouldSelect = (key === KEYS.backspace || key === KEYS.left || key === KEYS.right) && scope.newTag.text().length === 0 && !options.enableEditingLastTag; if (shouldAdd) { tagList.addText(scope.newTag.text()); } else if (shouldEditLastTag) { tagList.selectPrior(); tagList.removeSelected().then(function(tag) { if (tag) { scope.newTag.text(tag[options.displayProperty]); } }); } else if (shouldRemove) { tagList.removeSelected(); } else if (shouldSelect) { if (key === KEYS.left || key === KEYS.backspace) { tagList.selectPrior(); } else if (key === KEYS.right) { tagList.selectNext(); } } if (shouldAdd || shouldSelect || shouldRemove || shouldEditLastTag) { event.preventDefault(); } }) .on('input-paste', function(event) { if (options.addOnPaste) { var data = event.getTextData(); var tags = data.split(options.pasteSplitPattern); if (tags.length > 1) { tags.forEach(function(tag) { tagList.addText(tag); }); event.preventDefault(); } } }); } }; }]); /** * @ngdoc directive * @name tiTagItem * @module ngTagsInput * * @description * Represents a tag item. Used internally by the tagsInput directive. */ tagsInput.directive('tiTagItem', ["tiUtil", function(tiUtil) { return { restrict: 'E', require: '^tagsInput', template: '', scope: { $scope: '=scope', data: '=' }, link: function(scope, element, attrs, tagsInputCtrl) { var tagsInput = tagsInputCtrl.registerTagItem(), options = tagsInput.getOptions(); scope.$$template = options.template; scope.$$removeTagSymbol = options.removeTagSymbol; scope.$getDisplayText = function() { return tiUtil.safeToString(scope.data[options.displayProperty]); }; scope.$removeTag = function() { tagsInput.removeTag(scope.$index); }; scope.$watch('$parent.$index', function(value) { scope.$index = value; }); } }; }]); /** * @ngdoc directive * @name autoComplete * @module ngTagsInput * * @description * Provides autocomplete support for the tagsInput directive. * * @param {expression} source Expression to evaluate upon changing the input content. The input value is available as * $query. The result of the expression must be a promise that eventually resolves to an array of strings. * @param {string=} [template=NA] URL or id of a custom template for rendering each element of the autocomplete list. * @param {string=} [displayProperty=tagsInput.displayText] Property to be rendered as the autocomplete label. * @param {number=} [debounceDelay=100] Amount of time, in milliseconds, to wait before evaluating the expression in * the source option after the last keystroke. * @param {number=} [minLength=3] Minimum number of characters that must be entered before evaluating the expression * in the source option. * @param {boolean=} [highlightMatchedText=true] Flag indicating that the matched text will be highlighted in the * suggestions list. * @param {number=} [maxResultsToShow=10] Maximum number of results to be displayed at a time. * @param {boolean=} [loadOnDownArrow=false] Flag indicating that the source option will be evaluated when the down arrow * key is pressed and the suggestion list is closed. The current input value is available as $query. * @param {boolean=} [loadOnEmpty=false] Flag indicating that the source option will be evaluated when the input content * becomes empty. The $query variable will be passed to the expression as an empty string. * @param {boolean=} [loadOnFocus=false] Flag indicating that the source option will be evaluated when the input element * gains focus. The current input value is available as $query. * @param {boolean=} [selectFirstMatch=true] Flag indicating that the first match will be automatically selected once * the suggestion list is shown. * @param {expression=} [matchClass=NA] Expression to evaluate for each match in order to get the CSS classes to be used. * The expression is provided with the current match as $match, its index as $index and its state as $selected. The result * of the evaluation must be one of the values supported by the ngClass directive (either a string, an array or an object). * See https://docs.angularjs.org/api/ng/directive/ngClass for more information. */ tagsInput.directive('autoComplete', ["$document", "$timeout", "$sce", "$q", "tagsInputConfig", "tiUtil", function($document, $timeout, $sce, $q, tagsInputConfig, tiUtil) { function SuggestionList(loadFn, options, events) { var self = {}, getDifference, lastPromise, getTagId; getTagId = function() { return options.tagsInput.keyProperty || options.tagsInput.displayProperty; }; getDifference = function(array1, array2) { return array1.filter(function(item) { return !tiUtil.findInObjectArray(array2, item, getTagId(), function(a, b) { if (options.tagsInput.replaceSpacesWithDashes) { a = tiUtil.replaceSpacesWithDashes(a); b = tiUtil.replaceSpacesWithDashes(b); } return tiUtil.defaultComparer(a, b); }); }); }; self.reset = function() { lastPromise = null; self.items = []; self.visible = false; self.index = -1; self.selected = null; self.query = null; }; self.show = function() { if (options.selectFirstMatch) { self.select(0); } else { self.selected = null; } self.visible = true; }; self.load = tiUtil.debounce(function(query, tags) { self.query = query; var promise = $q.when(loadFn({ $query: query })); lastPromise = promise; promise.then(function(items) { if (promise !== lastPromise) { return; } items = tiUtil.makeObjectArray(items.data || items, getTagId()); items = getDifference(items, tags); self.items = items.slice(0, options.maxResultsToShow); if (self.items.length > 0) { self.show(); } else { self.reset(); } }); }, options.debounceDelay); self.selectNext = function() { self.select(++self.index); }; self.selectPrior = function() { self.select(--self.index); }; self.select = function(index) { if (index < 0) { index = self.items.length - 1; } else if (index >= self.items.length) { index = 0; } self.index = index; self.selected = self.items[index]; events.trigger('suggestion-selected', index); }; self.reset(); return self; } function scrollToElement(root, index) { var element = root.find('li').eq(index), parent = element.parent(), elementTop = element.prop('offsetTop'), elementHeight = element.prop('offsetHeight'), parentHeight = parent.prop('clientHeight'), parentScrollTop = parent.prop('scrollTop'); if (elementTop < parentScrollTop) { parent.prop('scrollTop', elementTop); } else if (elementTop + elementHeight > parentHeight + parentScrollTop) { parent.prop('scrollTop', elementTop + elementHeight - parentHeight); } } return { restrict: 'E', require: '^tagsInput', scope: { source: '&', matchClass: '&' }, templateUrl: 'ngTagsInput/auto-complete.html', controller: ["$scope", "$element", "$attrs", function($scope, $element, $attrs) { $scope.events = tiUtil.simplePubSub(); tagsInputConfig.load('autoComplete', $scope, $attrs, { template: [String, 'ngTagsInput/auto-complete-match.html'], debounceDelay: [Number, 100], minLength: [Number, 3], highlightMatchedText: [Boolean, true], maxResultsToShow: [Number, 10], loadOnDownArrow: [Boolean, false], loadOnEmpty: [Boolean, false], loadOnFocus: [Boolean, false], selectFirstMatch: [Boolean, true], displayProperty: [String, ''] }); $scope.suggestionList = new SuggestionList($scope.source, $scope.options, $scope.events); this.registerAutocompleteMatch = function() { return { getOptions: function() { return $scope.options; }, getQuery: function() { return $scope.suggestionList.query; } }; }; }], link: function(scope, element, attrs, tagsInputCtrl) { var hotkeys = [KEYS.enter, KEYS.tab, KEYS.escape, KEYS.up, KEYS.down], suggestionList = scope.suggestionList, tagsInput = tagsInputCtrl.registerAutocomplete(), options = scope.options, events = scope.events, shouldLoadSuggestions; options.tagsInput = tagsInput.getOptions(); shouldLoadSuggestions = function(value) { return value && value.length >= options.minLength || !value && options.loadOnEmpty; }; scope.templateScope = tagsInput.getTemplateScope(); scope.addSuggestionByIndex = function(index) { suggestionList.select(index); scope.addSuggestion(); }; scope.addSuggestion = function() { var added = false; if (suggestionList.selected) { tagsInput.addTag(angular.copy(suggestionList.selected)); suggestionList.reset(); added = true; } return added; }; scope.track = function(item) { return item[options.tagsInput.keyProperty || options.tagsInput.displayProperty]; }; scope.getSuggestionClass = function(item, index) { var selected = item === suggestionList.selected; return [ scope.matchClass({$match: item, $index: index, $selected: selected}), { selected: selected } ]; }; tagsInput .on('tag-added tag-removed invalid-tag input-blur', function() { suggestionList.reset(); }) .on('input-change', function(value) { if (shouldLoadSuggestions(value)) { suggestionList.load(value, tagsInput.getTags()); } else { suggestionList.reset(); } }) .on('input-focus', function() { var value = tagsInput.getCurrentTagText(); if (options.loadOnFocus && shouldLoadSuggestions(value)) { suggestionList.load(value, tagsInput.getTags()); } }) .on('input-keydown', function(event) { var key = event.keyCode, handled = false; if (tiUtil.isModifierOn(event) || hotkeys.indexOf(key) === -1) { return; } if (suggestionList.visible) { if (key === KEYS.down) { suggestionList.selectNext(); handled = true; } else if (key === KEYS.up) { suggestionList.selectPrior(); handled = true; } else if (key === KEYS.escape) { suggestionList.reset(); handled = true; } else if (key === KEYS.enter || key === KEYS.tab) { handled = scope.addSuggestion(); } } else { if (key === KEYS.down && scope.options.loadOnDownArrow) { suggestionList.load(tagsInput.getCurrentTagText(), tagsInput.getTags()); handled = true; } } if (handled) { event.preventDefault(); event.stopImmediatePropagation(); return false; } }); events.on('suggestion-selected', function(index) { scrollToElement(element, index); }); } }; }]); /** * @ngdoc directive * @name tiAutocompleteMatch * @module ngTagsInput * * @description * Represents an autocomplete match. Used internally by the autoComplete directive. */ tagsInput.directive('tiAutocompleteMatch', ["$sce", "tiUtil", function($sce, tiUtil) { return { restrict: 'E', require: '^autoComplete', template: '', scope: { $scope: '=scope', data: '=' }, link: function(scope, element, attrs, autoCompleteCtrl) { var autoComplete = autoCompleteCtrl.registerAutocompleteMatch(), options = autoComplete.getOptions(); scope.$$template = options.template; scope.$index = scope.$parent.$index; scope.$highlight = function(text) { if (options.highlightMatchedText) { text = tiUtil.safeHighlight(text, autoComplete.getQuery()); } return $sce.trustAsHtml(text); }; scope.$getDisplayText = function() { return tiUtil.safeToString(scope.data[options.displayProperty || options.tagsInput.displayProperty]); }; } }; }]); /** * @ngdoc directive * @name tiTranscludeAppend * @module ngTagsInput * * @description * Re-creates the old behavior of ng-transclude. Used internally by tagsInput directive. */ tagsInput.directive('tiTranscludeAppend', function() { return function(scope, element, attrs, ctrl, transcludeFn) { transcludeFn(function(clone) { element.append(clone); }); }; }); /** * @ngdoc directive * @name tiAutosize * @module ngTagsInput * * @description * Automatically sets the input's width so its content is always visible. Used internally by tagsInput directive. */ tagsInput.directive('tiAutosize', ["tagsInputConfig", function(tagsInputConfig) { return { restrict: 'A', require: 'ngModel', link: function(scope, element, attrs, ctrl) { var threshold = tagsInputConfig.getTextAutosizeThreshold(), span, resize; span = angular.element(''); span.css('display', 'none') .css('visibility', 'hidden') .css('width', 'auto') .css('white-space', 'pre'); element.parent().append(span); resize = function(originalValue) { var value = originalValue, width; if (angular.isString(value) && value.length === 0) { value = attrs.placeholder; } if (value) { span.text(value); span.css('display', ''); width = span.prop('offsetWidth'); span.css('display', 'none'); } element.css('width', width ? width + threshold + 'px' : ''); return originalValue; }; ctrl.$parsers.unshift(resize); ctrl.$formatters.unshift(resize); attrs.$observe('placeholder', function(value) { if (!ctrl.$modelValue) { resize(value); } }); } }; }]); /** * @ngdoc directive * @name tiBindAttrs * @module ngTagsInput * * @description * Binds attributes to expressions. Used internally by tagsInput directive. */ tagsInput.directive('tiBindAttrs', function() { return function(scope, element, attrs) { scope.$watch(attrs.tiBindAttrs, function(value) { angular.forEach(value, function(value, key) { attrs.$set(key, value); }); }, true); }; }); /** * @ngdoc service * @name tagsInputConfig * @module ngTagsInput * * @description * Sets global configuration settings for both tagsInput and autoComplete directives. It's also used internally to parse and * initialize options from HTML attributes. */ tagsInput.provider('tagsInputConfig', function() { var globalDefaults = {}, interpolationStatus = {}, autosizeThreshold = 3; /** * @ngdoc method * @name tagsInputConfig#setDefaults * @description Sets the default configuration option for a directive. * * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'. * @param {object} defaults Object containing options and their values. * * @returns {object} The service itself for chaining purposes. */ this.setDefaults = function(directive, defaults) { globalDefaults[directive] = defaults; return this; }; /** * @ngdoc method * @name tagsInputConfig#setActiveInterpolation * @description Sets active interpolation for a set of options. * * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'. * @param {object} options Object containing which options should have interpolation turned on at all times. * * @returns {object} The service itself for chaining purposes. */ this.setActiveInterpolation = function(directive, options) { interpolationStatus[directive] = options; return this; }; /** * @ngdoc method * @name tagsInputConfig#setTextAutosizeThreshold * @description Sets the threshold used by the tagsInput directive to re-size the inner input field element based on its contents. * * @param {number} threshold Threshold value, in pixels. * * @returns {object} The service itself for chaining purposes. */ this.setTextAutosizeThreshold = function(threshold) { autosizeThreshold = threshold; return this; }; this.$get = ["$interpolate", function($interpolate) { var converters = {}; converters[String] = function(value) { return value; }; converters[Number] = function(value) { return parseInt(value, 10); }; converters[Boolean] = function(value) { return value.toLowerCase() === 'true'; }; converters[RegExp] = function(value) { return new RegExp(value); }; return { load: function(directive, scope, attrs, options) { var defaultValidator = function() { return true; }; scope.options = {}; angular.forEach(options, function(value, key) { var type, localDefault, validator, converter, getDefault, updateValue; type = value[0]; localDefault = value[1]; validator = value[2] || defaultValidator; converter = converters[type]; getDefault = function() { var globalValue = globalDefaults[directive] && globalDefaults[directive][key]; return angular.isDefined(globalValue) ? globalValue : localDefault; }; updateValue = function(value) { scope.options[key] = value && validator(value) ? converter(value) : getDefault(); }; if (interpolationStatus[directive] && interpolationStatus[directive][key]) { attrs.$observe(key, function(value) { updateValue(value); scope.events.trigger('option-change', { name: key, newValue: value }); }); } else { updateValue(attrs[key] && $interpolate(attrs[key])(scope.$parent)); } }); }, getTextAutosizeThreshold: function() { return autosizeThreshold; } }; }]; }); /*** * @ngdoc service * @name tiUtil * @module ngTagsInput * * @description * Helper methods used internally by the directive. Should not be called directly from user code. */ tagsInput.factory('tiUtil', ["$timeout", "$q", function($timeout, $q) { var self = {}; self.debounce = function(fn, delay) { var timeoutId; return function() { var args = arguments; $timeout.cancel(timeoutId); timeoutId = $timeout(function() { fn.apply(null, args); }, delay); }; }; self.makeObjectArray = function(array, key) { if (!angular.isArray(array) || array.length === 0 || angular.isObject(array[0])) { return array; } var newArray = []; array.forEach(function(item) { var obj = {}; obj[key] = item; newArray.push(obj); }); return newArray; }; self.findInObjectArray = function(array, obj, key, comparer) { var item = null; comparer = comparer || self.defaultComparer; array.some(function(element) { if (comparer(element[key], obj[key])) { item = element; return true; } }); return item; }; self.defaultComparer = function(a, b) { // I'm aware of the internationalization issues regarding toLowerCase() // but I couldn't come up with a better solution right now return self.safeToString(a).toLowerCase() === self.safeToString(b).toLowerCase(); }; self.safeHighlight = function(str, value) { str = self.encodeHTML(str); value = self.encodeHTML(value); if (!value) { return str; } function escapeRegexChars(str) { return str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } var expression = new RegExp('&[^;]+;|' + escapeRegexChars(value), 'gi'); return str.replace(expression, function(match) { return match.toLowerCase() === value.toLowerCase() ? '' + match + '' : match; }); }; self.safeToString = function(value) { return angular.isUndefined(value) || value == null ? '' : value.toString().trim(); }; self.encodeHTML = function(value) { return self.safeToString(value) .replace(/&/g, '&') .replace(//g, '>'); }; self.handleUndefinedResult = function(fn, valueIfUndefined) { return function() { var result = fn.apply(null, arguments); return angular.isUndefined(result) ? valueIfUndefined : result; }; }; self.replaceSpacesWithDashes = function(str) { return self.safeToString(str).replace(/\s/g, '-'); }; self.isModifierOn = function(event) { return event.shiftKey || event.ctrlKey || event.altKey || event.metaKey; }; self.promisifyValue = function(value) { value = angular.isUndefined(value) ? true : value; return $q[value ? 'when' : 'reject'](); }; self.simplePubSub = function() { var events = {}; return { on: function(names, handler, first) { names.split(' ').forEach(function(name) { if (!events[name]) { events[name] = []; } var method = first ? [].unshift : [].push; method.call(events[name], handler); }); return this; }, trigger: function(name, args) { var handlers = events[name] || []; handlers.every(function(handler) { return self.handleUndefinedResult(handler, true)(args); }); return this; } }; }; return self; }]); /* HTML templates */ tagsInput.run(["$templateCache", function($templateCache) { $templateCache.put('ngTagsInput/tags-input.html', "

" ); $templateCache.put('ngTagsInput/tag-item.html', " " ); $templateCache.put('ngTagsInput/auto-complete.html', "

 

" ); $templateCache.put('ngTagsInput/auto-complete-match.html', "" ); }]); }());

Sarah Deady
Tera Contributor

Thanks very much!

mike_visser
Tera Expert

No problem 🙂

 

Igor Kozlov
Tera Expert

Does anyone have an actual (London+) information about extending existing service/directive?

 

Here some interesting articles, but they kinda outdated 

https://community.servicenow.com/community?id=community_article&sys_id=955c66a1dbd0dbc01dcaf3231f961...

https://github.com/platform-experience/form-decoration

caroleanderson
ServiceNow Employee
ServiceNow Employee
caroleanderson
ServiceNow Employee
ServiceNow Employee

MODULE 5 UPDATE SETS

caroleanderson
ServiceNow Employee
ServiceNow Employee

SERVICE PORTAL ADVANCED DEMO DATA AND UPDATE SETS

daya2
Kilo Explorer

On platform if IP access control is enabled. Can we create a portal page which will be accessible outside allowed IP address ?

Chris Sanford1
Kilo Guru

Attaching all of the content you need to migrate the lab content to ANY instance. I have the global update set, cloud events update set with demo data, and the xml files of the presenter demo users and demo KB articles, which were not captured in the demo data as they are in the global domain.

Inactive_Us1890
Tera Contributor

thank you!!!!

Stijn Verhulst3
Kilo Guru

That type of functionality is currently not supported.

Version history
Last update:
‎04-03-2019 02:35 PM
Updated by: