1. The Four Widget Files
Every widget record (sp_widget) is built from four editable
sections, each with a clear, single job:
- HTML Template — the widget's markup, written as an AngularJS template (directives like
ng-repeat, interpolation with{{ }}). - CSS / SCSS — styles scoped to this widget.
- Server Script — plain server-side Glide script (GlideRecord and everything from Weeks 4–8 all still apply) that runs once, before the page renders, and populates a
dataobject. - Client Controller — AngularJS controller-as-syntax JavaScript that runs in the browser, receiving the server's
dataas its starting state.
2. Server Script: Populating data
The server script runs exactly once per page load (per widget instance), with an
implicit data object available to write onto — anything you attach
to it becomes available to the client controller.
(function() {
data.incidents = [];
var gr = new GlideRecord('incident');
gr.addQuery('caller_id', gs.getUserID());
gr.addActiveQuery();
gr.orderByDesc('sys_created_on');
gr.setLimit(10);
gr.query();
while (gr.next()) {
data.incidents.push({
number: gr.getValue('number'),
shortDescription: gr.getValue('short_description'),
state: gr.getDisplayValue('state'),
sysId: gr.getUniqueValue()
});
}
})();
Note getValue()/getDisplayValue() instead of raw
field access — the server script's job is to hand the client plain,
JSON-serializable data, and GlideRecord field objects don't serialize cleanly on
their own.
3. HTML Template
My Open Incidents
-
{{inc.number}} — {{inc.shortDescription}}
{{inc.state}}
No open incidents.
c here is the client controller (next section) — every widget
template refers to its controller as c by convention, so
c.data is exactly the same object the server script populated,
now living in the browser.
4. Client Controller
function() {
/* widget controller */
var c = this;
// c.data already contains everything the server script attached —
// no explicit assignment needed for the initial page load.
}
This is the minimum viable controller — for a widget that only displays what the server already prepared, there's genuinely nothing else to write. The controller becomes essential once a widget needs client-side state, event handlers, or (as you'll see in Week 12) a way to ask the server for fresh data after the initial load.
Under the hood this is AngularJS's "controller as" syntax — the widget framework wires up controllerAs: 'c' for every widget automatically, so this in the controller function becomes accessible as c in the HTML template. You don't configure this yourself; it's part of how the widget framework bootstraps AngularJS for you.
5. Hands-on Exercise
Build the my-open-incidents widget end to end
Requirements:
- Create a new widget in Service Portal > Widgets, using the server script, HTML template and client controller shown above.
- Add a state-based CSS class in the widget's CSS so "In Progress" rows render with a different background than "New" rows.
- Drop the widget into a container on the "Dashboard" page you built in Week 9.
- Log in as (or impersonate) a user with at least one incident logged against them, and confirm the widget shows real data.
- Test the empty state by impersonating a user with no incidents.
Use the widget editor's built-in preview pane while building — it re-runs the server script and re-renders the template on save, which is much faster than round-tripping through the full portal page every time you make a change.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What are the four files that make up a widget, and what is each responsible for?
What are the four files that make up a widget, and what is each responsible for?
HTML Template (the AngularJS markup), CSS/SCSS (widget-scoped styles), Server Script (GlideRecord/Glide logic that runs once server-side and populates a data object), and Client Controller (AngularJS controller-as-syntax JavaScript that runs in the browser, starting from that server data).
Q2
Why does the server script use getValue()/getDisplayValue() instead of assigning raw GlideRecord fields onto data?
Why does the server script use getValue()/getDisplayValue() instead of assigning raw GlideRecord fields onto data?
The server script's job is to hand the client plain, JSON-serializable data — raw GlideElement field objects (from GlideRecord) don't serialize cleanly into something the browser-side controller can consume directly, so extracting plain strings/values with getValue() or getDisplayValue() is the correct pattern.
Q3
In the HTML template, what does c refer to, and where does the data in c.data actually come from?
In the HTML template, what does c refer to, and where does the data in c.data actually come from?
c refers to the widget's client controller, following AngularJS's "controller as" syntax that the widget framework wires up automatically. c.data is exactly the same data object the server script populated during the widget's server-side execution — it arrives in the browser already filled in, before the client controller's own code even runs.
Q4
Is a client controller required for every widget, even a purely display-only one?
Is a client controller required for every widget, even a purely display-only one?
Technically the controller function still needs to exist (even as a near-empty function assigning var c = this), but for a widget that only displays server-prepared data with no client-side interactivity, there's nothing further to write in it — the interesting client controller logic starts once a widget needs its own state, event handling, or a way to request fresh data after the page has loaded.