{"revision":"c10276e953456835cdbb1f3a60d58ae1980be353","document":{"id":42410,"path":"platform-user-interface/service-portal/adv-widget-tutorial.md","title":null,"bundle":null,"doc_type":null,"release":null,"body":"---\ntitle: Tutorial: Build a custom widget\ndescription: Follow this tutorial to build a custom widget that displays Service Catalog items. Use this tutorial as a model to help you understand the advanced scripting power of the Service Portal.Create the Quick Order widget to query items in the Service Catalog.After adding your widget and creating a basic template, you can define advanced client and server scripts that enable users to query data from an instance table. You can pass the data model between the client and server by querying data from the database, displaying it to the user, and sending any updates back to the server.Display a list of popular items to the user before any search terms are entered.Enable the user to view and purchase Service Catalog items in the Quick Order widget by embedding the SC Catalog Item widget.Angular Providers are reusable components that can be added to multiple widgets. Using the Widget Angular Providers table, create a directive that shows a category icon next to each result in the Quick Order widget.\nlocale: en-US\ncanonical_url: https://www.servicenow.com/docs/r/australia/platform-user-interface/service-portal/adv-widget-tutorial.html\nrelease: australia\nproduct: Service Portal\nclassification: service-portal\ntopic_type: concept\nlast_updated: \"2026-03-12\"\nreading_time_minutes: 10\nbreadcrumb: [Developing custom widgets, Service Portal, Configure UIs and portals, Configure user experiences]\n---\n\n# Tutorial: Build a custom widget\n\nFollow this tutorial to build a custom widget that displays Service Catalog items. Use this tutorial as a model to help you understand the advanced scripting power of the Service Portal.\n\nIn this tutorial, you will create the Quick Order widget. This widget:\n\n-   Displays popular items to the user prior to any search.\n-   Queries the Service Catalog and displays available options to the user.\n-   Includes an embedded SC Catalog Item widget, allowing the user to view and order items within the Quick Order widget.\n-   Uses an Angular Provider to display a category icon beside each queried item.\n\n**Parent Topic:**[Developing custom widgets](https://raw.githubusercontent.com/ServiceNow/ServiceNowDocs/australia/markdown/australia/platform-user-interface/service-portal/widget-dev-guide.md)\n\n## Create a widget and set up a template\n\nCreate the Quick Order widget to query items in the Service Catalog.\n\n### Before you begin\n\nRole required: admin or sp\\_admin\n\n### Procedure\n\n1.  Navigate to **All** &gt; **Service Portal** &gt; **Service Portal Configuration** and click **Widget Editor**.\n\n2.  Click **Create a new widget**.\n\n3.  Define the following values.\n\n    -   **Widget Name**: Quick Order\n    -   **Widget ID**: quick\\_order\n    -   **Create a test page**: Active\n    -   **Page ID**: quick\\_order\n    Adding a widget to a test page creates a record in each of the following tables:\n\n    -   sp\\_page\n    -   sp\\_container\n    -   sp\\_row\n    -   sp\\_column\n    -   sp\\_instance\n    -   sp\\_widget\n    **Note:** You can use the Page Editor in Service Portal Configuration to view the hierarchy of elements on your test page.\n\n4.  Click **Submit**.\n\n5.  From the **Widget Editor**, open the Quick Order widget.\n\n6.  Add the following simple template to the HTML field.\n\n    ```\n    <div class=\"panel panel-primary\">\n     <div class=\"panel-heading\">Request an item from the catalog</div>\n     <div class=\"panel-body\">\n       My catalog results\n     </div>\n    </div>\n    \n    ```\n\n7.  Click **Save**.\n\n8.  Preview your test page in a new tab using the following URL: `<yourInstanceUrl>/sp?id=quick_order`.\n\n    Your widget template displays on the test page.\n\n    \\[Omitted image \"quick-order-test.png\"\\] Alt text: Widget rendering on a page\n\n\n## Add a server script to query an instance table\n\nAfter adding your widget and creating a basic template, you can define advanced client and server scripts that enable users to query data from an instance table. You can pass the data model between the client and server by querying data from the database, displaying it to the user, and sending any updates back to the server.\n\n### Before you begin\n\nRole required: admin or sp\\_admin\n\n### Procedure\n\n1.  From the **Widget Editor**, open the Quick Order widget.\n\n2.  Select **Server Script** to open the server script field.\n\n3.  Replace the default server script with the following custom script.\n\n    ```javascript\n    (function() {\n    \tif (input.keywords != null && input.keywords != '')\n    \t\tdata.items = getCatalogItems(input.keywords);\n    \t\n    \tfunction getCatalogItems(keywords) {\n    \t\tvar sc = new GlideRecord('sc_cat_item');\n    \t\tsc.addActiveQuery();\n    \t\tsc.addQuery('123TEXTQUERY321', keywords);\n    \t\tsc.addQuery('sys_class_name', 'NOT IN', 'sc_cat_item_wizard,sc_cat_item_content');\n    \t\tsc.addQuery('sc_catalogs', 'e0d08b13c3330100c8b837659bba8fb4');\n    \t\tsc.setLimit(100);\n    \t\tsc.orderByDesc(\"ir_query_score\");\n    \t\tsc.query();\n    \t\tvar results = [];\n    \t\twhile (sc.next()) {\n    \t\t\tif (!$sp.canReadRecord(sc))\n    \t\t\t\tcontinue;\n    \n    \t\t\tvar item = {};\n    \t\t\t$sp.getRecordDisplayValues(item, sc, 'name,price,sys_id');\n    \t\t\titem.category = sc.getValue('category');\n    \t\t\tresults.push(item);\n    \t\t}\n    \t\treturn results;\n    \t}\n    })();\n    \n    ```\n\n    This script performs a keyword search on the sc\\_cat\\_item table using the 123TEXTQUERY321 query method.\n\n4.  Replace the HTML template with the following script:\n\n    ```\n    <div class=\"panel panel-primary\">\n     <div class=\"panel-heading\">Request an item from the catalog</div>\n     <div class=\"panel-body\">\n       <input class=\"form-control\" type=\"search\" placeholder=\"Start typing here to search the list of catalog items\" ng-model=\"c.data.keywords\" ng-change=\"c.server.update()\" ng-model-options=\"{debounce: 250}\" />\n       <ul class=\"list-group result-container\">\n         <li class=\"list-group-item\" ng-repeat=\"item in c.data.items\">\n           <a href>{{item.name}}</a><span class=\"pull-right\">{{item.price}}</span>\n         </li>\n       </ul>\n     </div>\n    </div>\n    \n    ```\n\n    This template adds a search field and displays the results of the query performed in the server script using the following Angular directives. To learn more about these directives, review the [Angular API Reference](https://docs.angularjs.org/api/ng/directive).\n\n    |Angular directive|Description|\n    |-----------------|-----------|\n    |ng-model|Automatically reads and writes value changes to the model variable `c.data.keywords`.|\n    |ng-model-options|Configures ng-model behavior. In this template, ng-model updates the model after a user has stopped typing for 250 milliseconds.|\n    |ng-change|Executes `c.server.update()` after the model value changes. This function posts the data object to the server script. After the script is executed, the data object is automatically updated with the new values from the server generated data object.|\n    |ng-repeat|Creates a template from the parent element and child elements. For each `item in c.data.items`, an instance of the template is created and the expressions `{{item.name}}` and `{{item.price}}` are replaced with the values from each item.|\n\n5.  Add the following script to the **CSS - SCSS** field:\n\n    ```css\n    .result-container {\n     margin-top: 10px;\n    }\n    \n    ```\n\n6.  Refresh your test page preview to view the changes.\n\n    As you type in the search box, matching catalog items appear. Try searching for **ipad**.\n\n    \\[Omitted image \"widget-tutorial-search.png\"\\] Alt text: Search displays results for iPad.\n\n\n## Manage the empty state of a widget\n\nDisplay a list of popular items to the user before any search terms are entered.\n\n### Before you begin\n\nRole required: admin or sp\\_admin\n\n### About this task\n\nBecause no search has been executed when the widget initializes, the server **input** variable is undefined. This empty state may cause confusion when a user first interacts with the widget. To solve this issue, give your widget something to display when the **input** variable is empty. This initial data can guide your users when initially interacting with your widget.\n\n### Procedure\n\n1.  From the **Widget Editor**, open the Quick Order widget.\n\n2.  Replace the existing server script with the following script:\n\n    ```javascript\n    (function() {\n        if (input.keywords != null && input.keywords != '')\n            data.items = getCatalogItems(input.keywords);\n        else data.items = getPopularItems();\n    \n        function getCatalogItems(keywords) {\n            var sc = new GlideRecord('sc_cat_item');\n            sc.addActiveQuery();\n            sc.addQuery('123TEXTQUERY321', keywords);\n            sc.addQuery('sys_class_name', 'NOT IN', 'sc_cat_item_wizard,sc_cat_item_content');\n            sc.addQuery('sc_catalogs', 'e0d08b13c3330100c8b837659bba8fb4');\n            sc.setLimit(100);\n            sc.orderByDesc(\"ir_query_score\");\n            sc.query();\n            var results = [];\n            while (sc.next()) {\n                if (!$sp.canReadRecord(sc))\n                    continue;\n    \n                var item = {};\n                $sp.getRecordDisplayValues(item, sc, 'name,price,sys_id');\n                item.category = sc.getValue('category');\n                results.push(item);\n            }\n            return results;\n        }\n    \n        function getPopularItems() {\n            var items = [];\n            var count = new GlideAggregate('sc_req_item');\n            count.addAggregate('COUNT', 'cat_item');\n            count.groupBy('cat_item');\n            count.addQuery('cat_item.sys_class_name', 'NOT IN', 'sc_cat_item_guide,sc_cat_item_wizard,sc_cat_item_content');\n            count.addQuery('cat_item.sc_catalogs', 'e0d08b13c3330100c8b837659bba8fb4');\n            count.orderByAggregate('COUNT', 'cat_item');\n            count.query();\n            while (count.next() && items.length < 9) {\n                if (!$sp.canReadRecord(\"sc_cat_item\", count.cat_item.sys_id.getDisplayValue()))\n                    continue; // user does not have permission to see this item\n    \n                var item = {};\n                item.name = count.cat_item.name.getDisplayValue();\n                item.category = count.cat_item.category.toString();\n                item.price = count.cat_item.price.getDisplayValue();\n                item.sys_id = count.cat_item.sys_id.getDisplayValue();\n                items.push(item);\n            }\n            return items;\n        }\n    })();\n    ```\n\n    This script introduces a new function `getPopularItems()` to query the database and return popular items when the **input** variable is empty.\n\n3.  Replace the HTML template with the following script:\n\n    ```\n    <div class=\"panel panel-primary\">\n      <div class=\"panel-heading\">Request an item from the catalog</div>\n      <div class=\"panel-body\">\n        <input class=\"form-control\" type=\"search\" placeholder=\"Start typing here to search the list of catalog items\" ng-model=\"c.data.keywords\" ng-change=\"c.server.update()\" ng-model-options=\"{debounce: 250}\" />\n        <h5 ng-if=\"!c.data.keywords\">Showing the most popular items</h5>\n        <ul class=\"list-group result-container\">\n          <li class=\"list-group-item\" ng-repeat=\"item in c.data.items\">\n            <a href>{{item.name}}</a><span class=\"pull-right\">{{item.price}}</span>\n          </li>\n        </ul>\n      </div>\n      <div class=\"panel-footer\" ng-if=\"c.data.keywords\">\n        <ng-pluralize count=\"c.data.items.length\"\n                     when=\"{'0': 'No items found for ',\n                         '1': 'One item matching ',\n                         'other': 'Found {} items matching '}\">\n        </ng-pluralize>\n        {{c.data.keywords}}\n      </div>\n    </div>\n    \n    ```\n\n    This script provides a template to display the popular items returned from the server script.\n\n4.  Refresh your test page preview to view the changes.\n\n    The widget displays popular items to the user prior to any search input.\n\n    \\[Omitted image \"empty-state.png\"\\] Alt text: A search box with the text \"Start typing here to search the list of catalog items\".\n\n\n## Embed an existing widget\n\nEnable the user to view and purchase Service Catalog items in the Quick Order widget by embedding the SC Catalog Item widget.\n\n### Before you begin\n\nRole required: admin or sp\\_admin\n\n### About this task\n\nInstead of duplicating code, you can embed widgets to leverage pre-existing functionality. The SC Catalog Item widget is a base system widget that enables the user to view and purchase Service Catalog items.\n\n### Procedure\n\n1.  Inspect the SC Catalog Item widget.\n\n    Before embedding the SC Catalog Item widget, inspect the widget to understand what data it needs access to. You may need to update your Quick Order widget client or server script to make sure that the correct data is passed to the embedded widget.\n\n    1.  Navigate to `<yourInstanceURL>/sp_config?id=widget_editor`.\n\n    2.  Open the SC Catalog Item widget.\n\n    3.  Note that the widget ID is `widget-sc-cat-item`.\n\n        You will use this ID to embed the widget model in the client script.\n\n    4.  Examine the server script.\n\n        Notice that the **data** object includes a sys\\_id property populated by either the **input** or **options** objects. If neither **input** nor **options** include a sys\\_id, the $sp.getParameter\\(\\) method retrieves the sys\\_id from the request query string.\n\n        \\[Omitted image \"embedded-widget.png\"\\] Alt text: Sample server script using an If Else statement to retrieve the sys\\_id.\n\n        To populate the **input** object, you can pass a catalog item sys\\_id from the Quick Order widget client script.\n\n2.  From the **Widget Editor**, open the Quick Order widget.\n\n3.  Replace the Quick Order widget client script with the following script.\n\n    ```javascript\n    function($location, spUtil) {\n        var c = this;\n    \n        c.select = function(item_id) {\n            if (c.openItem == item_id) {\n                c.openItem = null;\n                return;\n            }\n    \n            renderCatalogItemWidget(item_id);\n        }\n    \n        function renderCatalogItemWidget(item_id) {\n            c.catalogItemWidget = null;\n            spUtil.get(\"widget-sc-cat-item\", {\n                sys_id: item_id\n            }).then(function(response) {\n                c.catalogItemWidget = response;\n                c.openItem = item_id;\n            });\n        }\n    }\n    ```\n\n    This script uses `spUtil.get()` to retrieve the widget model by ID \\(`widget-sc-cat-item`\\) and define the `{sys_id: item_id}` object. This object posts to the server script as **input**.\n\n4.  Replace the HTML template with the following script:\n\n    ```\n    <div class=\"panel panel-primary\">\n      <div class=\"panel-heading\">Request an item from the catalog</div>\n      <div class=\"panel-body\">\n        <input class=\"form-control\" type=\"search\" placeholder=\"Start typing here to search the list of catalog items\" ng-model=\"c.data.keywords\" ng-change=\"c.server.update()\" ng-model-options=\"{debounce: 250}\" />\n        <h5 ng-if=\"!c.data.keywords\">Showing the most popular items</h5>\n        <ul class=\"list-group result-container\">\n          <li class=\"list-group-item\" ng-repeat=\"item in c.data.items\">\n            <a href ng-click=\"c.select(item.sys_id)\">{{item.name}}</a><span class=\"pull-right\">{{item.price}}</span>\n            <div class=\"catalog-item\" ng-if=\"item.sys_id == c.openItem\">\n                <sp-widget ng-if=\"c.catalogItemWidget\" widget=\"c.catalogItemWidget\" />\n            </div>\n          </li>\n        </ul>\n      </div>\n      <div class=\"panel-footer\" ng-if=\"c.data.keywords\">\n        <ng-pluralize count=\"c.data.items.length\"\n                     when=\"{'0': 'No items found for ',\n                         '1': 'One item matching ',\n                         'other': 'Found {} items matching '}\">\n        </ng-pluralize>\n        {{c.data.keywords}}\n      </div>\n    </div>\n    ```\n\n    This template:\n\n    -   Adds on-click behavior using the ng-click directive.\n    -   Displays the embedded SC Catalog Item widget using the sp-widget directive.\n5.  Replace the CSS with the following script:\n\n    ```css\n    .result-container {\n      margin-top: 10px;\n    }\n    \n    .catalog-item {\n      background-color: #f5f5f5;\n      padding: 10px;\n      @include border-top-radius($panel-border-radius);\n      @include border-bottom-radius($panel-border-radius);\n    }\n    \n    ```\n\n6.  Refresh your test page preview to view the changes.\n\n    When you select a search result, the item opens in the embedded SC Catalog Item widget.\n\n    \\[Omitted image \"embed-widget-result.png\"\\] Alt text: Test page showing a selected item displaying in an embedded widget.\n\n\n## Create a reusable directive and add it to a widget\n\nAngular Providers are reusable components that can be added to multiple widgets. Using the Widget Angular Providers table, create a directive that shows a category icon next to each result in the Quick Order widget.\n\n### Before you begin\n\nRole required: admin or sp\\_admin\n\n### About this task\n\nAngular Providers let you build angular directives and services that can be injected into your client script controller. The code in a Provider differs from a typical Angular directive or service because it must be anonymous, without being appended to a specific module.\n\n### Procedure\n\n1.  Navigate to **All** &gt; **Service Portal** &gt; **Service Portal Configuration** &gt; **Portal Tables** &gt; **Widget Angular Provider**.\n\n    The Widget Angular Providers table opens.\n\n2.  Select **New** to create a new record.\n\n3.  Fill out the form.\n\n    1.  Add the type and name.\n\n        -   **Type**: Directive\n        -   **Name**: categoryIcon\n    2.  Add the client script.\n\n        ```javascript\n        function() {\n            return {\n                template: '<span class=\"fa fa-stack fa-lg\"><i class=\"fa fa-circle fa-stack-2x\"></i><i class=\"fa fa-{{::icon}} fa-stack-1x fa-inverse\"></i></span>',\n                restrict: 'E',\n                replace: true,\n                scope: {\n                    category: '='\n                },\n                link: function(scope, element) {\n        \n                    var _iconMap = {\n                        \"b06546f23731300054b6a3549dbe5dd8\": \"tablet\",\n                        /* Tablets */\n                        \"15706fc0a0a0aa7007fc21e1ab70c2f\": \"question\",\n                        /* Can we help you? */\n                        \"d68eb4d637b1300054b6a3549dbe5db2\": \"mobile-phone\",\n                        /* Mobiles */\n                        \"109cdff8c6112276003b17991a09ad65\": \"print\",\n                        /* Office and Print */\n                        \"5d643c6a3771300054b6a3549dbe5db0\": \"print\",\n                        /* Printers */\n                        \"2c0b59874f7b4200086eeed18110c71f\": \"plug\",\n                        /* Peripherals */\n                        \"2809952237b1300054b6a3549dbe5dd4\": \"desktop\", \n                        /* Software */\n                    };\n        \n                    scope.icon = _iconMap[scope.category] || \"shopping-cart\";\n                }\n            }\n        }\n        ```\n\n        This script associates the sys\\_id of the Category record with the Service Catalog item. The icon that displays is the icon defined in the Category record in the Service Catalog.\n\n    3.  Select **Save**.\n\n4.  Associate the new Angular directive with the Quick Order Widget.\n\n    1.  Navigate to **All** &gt; **Service Portal** &gt; **Widgets**.\n\n    2.  Open the Quick Order widget.\n\n    3.  Under **Related Lists**, select **Angular Providers**.\n\n    4.  In the Angular Providers list, select **Edit** and associate the categoryIcon Angular Provider with the Quick Order widget.\n\n    5.  Select **Save**.\n\n5.  Add the categoryIcon directive to your Quick Order HTML template.\n\n    1.  From the **Widget Editor**, open the Quick Order widget.\n\n    2.  Replace the HTML template with the following script.\n\n        ```\n        <div class=\"panel panel-primary\">\n          <div class=\"panel-heading\">Request an item from the catalog</div>\n          <div class=\"panel-body\">\n            <input class=\"form-control\" type=\"search\" placeholder=\"Start typing here to search the list of catalog items\" ng-model=\"c.data.keywords\" ng-change=\"c.server.update()\" ng-model-options=\"{debounce: 250}\" />\n            <h5 ng-if=\"!c.data.keywords\">Showing the most popular items</h5>\n            <ul class=\"list-group result-container\">\n              <li class=\"list-group-item\" ng-repeat=\"item in c.data.items\">\n                <a href ng-click=\"c.select(item.sys_id)\"><category-icon category=\"item.category\" style=\"margin-right: 10px\"></category-icon>{{item.name}}</a><span class=\"pull-right\">{{item.price}}</span>\n                <div class=\"catalog-item\" ng-if=\"item.sys_id == c.openItem\">\n                    <sp-widget ng-if=\"c.catalogItemWidget\" widget=\"c.catalogItemWidget\" />\n                  </div>\n              </li>\n            </ul>\n          </div>\n          <div class=\"panel-footer\" ng-if=\"c.data.keywords\">\n            <ng-pluralize count=\"c.data.items.length\"\n                         when=\"{'0': 'No items found for ',\n                             '1': 'One item matching ',\n                             'other': 'Found {} items matching '}\">\n            </ng-pluralize>\n            {{c.data.keywords}}\n          </div>\n        </div>\n        \n        ```\n\n6.  Refresh your test page preview to view the changes.\n\n    A category icon displays beside each result.\n\n    \\[Omitted image \"category-icon.png\"\\] Alt text:\n\n\n"}}