Week 10: Widget Anatomy: HTML, CSS, Server Script & Client Controller

A widget is four files acting like one component. This week is about learning to read all four as a single unit — where server data becomes client data, and where the boundary between "runs once on the server" and "runs continuously in the browser" actually sits.

Module 10 of 25 Week 10 of 26 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Identify the four files that make up a widget and what each is responsible for
  • Explain how the server script's data object becomes the client controller's c.data
  • Build a working widget end to end and place it on a portal page

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 data object.
  • Client Controller — AngularJS controller-as-syntax JavaScript that runs in the browser, receiving the server's data as 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.

Server Script — my-open-incidents widget
(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

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

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.

Where does 'c' come from?

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

Hands-on

Build the my-open-incidents widget end to end

Requirements:

  1. Create a new widget in Service Portal > Widgets, using the server script, HTML template and client controller shown above.
  2. Add a state-based CSS class in the widget's CSS so "In Progress" rows render with a different background than "New" rows.
  3. Drop the widget into a container on the "Dashboard" page you built in Week 9.
  4. Log in as (or impersonate) a user with at least one incident logged against them, and confirm the widget shows real data.
  5. Test the empty state by impersonating a user with no incidents.
Hint

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?

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?

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?

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?

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.