Showing posts with label Client Scripts. Show all posts
Showing posts with label Client Scripts. Show all posts

Wednesday, July 15, 2026

Build a Memory Match Game on ServiceNow: A Fun Hackathon Project for Developers

Memory Match: a ServiceNow hackathon project banner

Most ServiceNow hackathon demos look the same: a catalog item with an approval flow, a Flow Designer automation that closes tickets a little faster, a dashboard nobody asked for. A few of us wanted to try something different for our next internal hackathon — could we build something that's actually fun to demo, while still exercising real Service Portal development skills? That question turned into a small browser game called Memory Match, built entirely as a native ServiceNow widget.

The idea itself is simple: a flip-card memory game with a live moves/time tracker and a saved leaderboard. What makes it a genuinely useful hackathon project isn't the game concept — it's that building one small, self-contained feature forces you through nearly every layer of Now Platform widget development: client-side state management, GlideRecord operations, ACL decisions, and the HTML/CSS/client/server split that every production widget shares.

From there, we sketched out a project plan the same way we would for any other hackathon build.

Project Plan for the Memory Match Widget

1. Requirements Gathering

  • Understand what a Service Portal widget can and can't do out of the box.
  • Define what the game needs to actually work:
    • A card grid that flips and checks for matches, entirely client-side for responsiveness.
    • A moves counter and a running timer.
    • A win state that lets the player save their score.
    • A persisted leaderboard, stored server-side so it survives page reloads and is visible to everyone.
    • A custom table to hold scores, since nothing in the base schema fits.
    • Sensible ACLs — this is worth flagging early: the moment you add write access for anonymous or lightly-authenticated users, you need to think through who can insert records and whether the client can be trusted to self-report a score without any server-side sanity checking.

2. Technology Stack

  • Frontend: Service Portal's built-in AngularJS 1.x widget framework — HTML template plus a client controller, no external UI library needed.
  • Backend: the widget's native Server Script, running standard server-side JavaScript.
  • Data: a custom table (u_memory_game_score) accessed through GlideRecord.
  • Hosting: none needed — it runs entirely inside the existing ServiceNow instance, on Service Portal.

3. Design and Architecture

  • HTML Template: the card grid, stats bar, win screen, and leaderboard table — all driven by Angular bindings against the client controller's scope.
  • Client Controller: owns all real-time gameplay — card flips, match checking, the timer — so nothing needs a server round-trip while the player is actually playing.
  • Server Script: only gets involved twice — once to save a finished score, once to fetch the current leaderboard.
  • CSS: a simple 3D flip transform for the cards, kept in the widget's own CSS field so it doesn't leak into the rest of the portal theme.

4. Implementation Steps

  1. Create the table: u_memory_game_score with player name, moves, and seconds fields.
  2. Build the widget shell: new Service Portal widget, wire up the four fields — HTML Template, CSS, Client Controller, Server Script.
  3. Implement gameplay: shuffle a deck, handle flip/match logic, run the timer with $interval.
  4. Wire up persistence: on win, call c.server.get() to save the score and pull back a fresh leaderboard.
  5. Add the page: drop the widget onto a portal page through Service Portal Designer.
  6. Test: confirm the full loop — play, win, save name, see the leaderboard update — works end to end, on both desktop and mobile.

5. Deployment and Maintenance

  • Build and test it in a personal developer instance first (free at developer.servicenow.com) before touching anything production-facing.
  • Move it between instances with a standard Update Set, like any other customization.
  • Review the table's ACLs periodically — especially if it ever moves from an internal demo to something with wider portal exposure.

With the plan settled, here's what the actual widget code looks like — the same pattern you'd use for any Service Portal widget, just applied to a game instead of a form.

The Widget, Piece by Piece

The HTML Template lays out the card grid, stats, and leaderboard using standard Angular directives:

<div class="mg-board" ng-if="!c.won">
  <div class="mg-card"
       ng-repeat="card in c.cards track by $index"
       ng-class="{flipped: card.flipped || card.matched}"
       ng-click="c.flipCard($index)">
    <div class="mg-card-inner">
      <div class="mg-card-front">?</div>
      <div class="mg-card-back">{{card.icon}}</div>
    </div>
  </div>
</div>

The Client Controller handles every flip, entirely in the browser — no server call while the player is mid-game:

c.flipCard = function(index) {
  if (lockBoard) return;
  var card = c.cards[index];
  if (card.flipped || card.matched) return;

  card.flipped = true;
  flippedIndexes.push(index);

  if (flippedIndexes.length === 2) {
    c.moves++;
    lockBoard = true;
    var first = c.cards[flippedIndexes[0]];
    var second = c.cards[flippedIndexes[1]];

    if (first.icon === second.icon) {
      first.matched = true;
      second.matched = true;
      flippedIndexes = [];
      lockBoard = false;
      checkWin();
    } else {
      setTimeout(function() {
        $scope.$apply(function() {
          first.flipped = false;
          second.flipped = false;
          flippedIndexes = [];
          lockBoard = false;
        });
      }, 800);
    }
  }
};

And the Server Script is where GlideRecord does the only two things it needs to: save a score, and return the current top ten.

function getLeaderboard() {
  var results = [];
  var gr = new GlideRecord('u_memory_game_score');
  gr.orderBy('u_moves');
  gr.orderBy('u_seconds');
  gr.setLimit(10);
  gr.query();
  while (gr.next()) {
    results.push({
      name: gr.getValue('u_player_name'),
      moves: gr.getValue('u_moves'),
      seconds: gr.getValue('u_seconds')
    });
  }
  return results;
}

if (input.action === 'save_score') {
  var gr = new GlideRecord('u_memory_game_score');
  gr.initialize();
  gr.setValue('u_player_name', input.name);
  gr.setValue('u_moves', input.moves);
  gr.setValue('u_seconds', input.seconds);
  gr.insert();

  data.leaderboard = getLeaderboard();
} else if (input.action === 'get_leaderboard') {
  data.leaderboard = getLeaderboard();
}

That's the whole architecture: client owns the interaction, server owns the record of truth. It's a pattern you'll reuse constantly in real widget development — usually dressed up as "request status" or "approval state" instead of a game score.

Why This Works Well as a Hackathon Project

  • It demos itself. Judges don't need a walkthrough — they can just play it.
  • A working v1 is fast. A minimal playable version, no leaderboard, can be done in an hour or two.
  • It's memorable. Weeks later, "the team that built a game" outlasts "the team that streamlined another form" in people's memory.
  • It scales with skill level. Beginners ship a working game. More experienced developers can add difficulty scaling, real-time multiplayer, or tie the leaderboard into actual Performance Analytics data.

Additional Tips If You Try This

  • Lock down the leaderboard table: restrict write access appropriately once this leaves a sandbox — don't leave it open to any authenticated user by default.
  • Reuse the pattern for real training tools: the same widget skeleton works for an onboarding quiz, an incident-response speed drill, or a leaderboard tied to real ticket-resolution metrics.
  • Keep gameplay client-side: anything that needs to feel instant — flips, timers, animations — should never wait on a server round-trip.

If your next hackathon team is stuck between "another approval flow" and "something people will actually remember," build the game. It touches the same core skills as any production widget — just with a much better demo.

Monday, June 29, 2026

ServiceNow: Date and Time validation on layouts and custom forms

ServiceNow Date and Time validation

Date and time format, along with the system timezone setting for a ServiceNow instance, can be checked under System Properties > System.

Date and Time Format Strings

Date format uses the same format strings as Java's java.text.SimpleDateFormat class, with minor exceptions. Note that MM is months, while mm is minutes — a distinction that trips people up constantly.

Field Full Form Short Form
Year yyyy (4 digits) yy (2 digits), y (2 or 4 digits)
Month MMM (name or abbreviation) MM (2 digits), M (1 or 2 digits)
Day of Month dd (2 digits) d (1 or 2 digits)
Hour (1–12) hh (2 digits) h (1 or 2 digits)
Hour (0–23) HH (2 digits) H (1 or 2 digits)
Minute mm (2 digits) m (1 or 2 digits)
Second ss (2 digits) s (1 or 2 digits)
AM/PM a

Example: HH:mm:ss

System timezone is used as the default for calendars and for users who haven't set their own. Olson/zoneinfo timezone values are accepted — for example Africa/Johannesburg, America/Toronto, Asia/Tokyo, Australia/Sydney, Europe/London, US/Eastern, or UTC.

Why Validation Matters Here Specifically

Appropriate validation on required date and time fields matters more than it might seem, because different users on the same instance can be working in different timezones with different personal date/time format settings. A validation script tied to the system default format, or hardcoded to one specific format string, can end up rejecting a date that's perfectly valid for the user who entered it — just not in the format the script assumed everyone uses.

Field-Level Validation Scripts

To restrict what gets accepted on a Date-type field so it conforms with the expected format, a Validation Script can be attached to that field type:

function validate(value) {
    if (!value) {
        return true;
    }
    return (getDateFromFormat(value, 'yyyy-MM-dd') != 0);
}

Client Scripts for Date/Time Fields

The same getDateFromFormat() function is commonly used in onChange and onSubmit client scripts:

function onChange(control, oldValue, newValue, isLoading) {
    if (isLoading || newValue == oldValue) {
        return;
    }
    if (getDateFromFormat(newValue, g_user_date_time_format) == 0) {
        g_form.showFieldMsg('yourField', 'Invalid date', 'error');
    }
}

function onSubmit() {
    if (getDateFromFormat(g_form.getValue('yourField'), g_user_date_time_format) == 0) {
        g_form.showFieldMsg('yourField', 'Invalid date', 'error');
        return false; // Stop the form submit event
    }
}

Use g_user_date_time_format (or g_user_date_format for date-only fields) rather than a hardcoded format string like 'yyyy-MM-dd HH:mm:ss'. These are built-in global variables that reflect the current logged-in user's actual date/time format preference — which can be different from the system default and different from another user's preference on the same instance. Validating against a hardcoded format works only by coincidence, when the user's personal setting happens to match it; validating against g_user_date_time_format works correctly regardless of what format that specific user has configured.

Important: This Doesn't Work Everywhere

getDateFromFormat(), g_user_date_format, and g_user_date_time_format are Core UI (classic platform UI) constructs only. They are not available in:

  • Agent Workspace
  • Service Portal
  • Mobile (Now Mobile / Mobile Agent)

If you reuse this exact pattern in a Workspace form or a Portal widget, it fails silently with getDateFromFormat is not defined in the browser console — not a helpful error for whoever inherits that code later. This is a real, documented platform gap, not an edge case, and it's worth flagging explicitly for anyone extending this pattern beyond classic UI forms.

For Workspace or Portal, the reliable approach is a GlideAjax call to a Script Include that performs the actual validation server-side using GlideDateTime, which doesn't have this UI-context restriction. This is the same pattern described below for custom UI pages — the difference is that for Workspace and Portal, it isn't an alternative approach, it's the only approach, since the client-side globals simply don't exist there.

Checking the Instance's Date/Time Format via Script

The date and time format settings can also be checked server-side, by querying the relevant system properties:

var rec = new GlideRecord('sys_properties');
rec.addQuery('name', 'glide.sys.date_format');
rec.query();
if (rec.next()) {
    var dateFormat = rec.value;
    gs.print(dateFormat);
}

var rec2 = new GlideRecord('sys_properties');
rec2.addQuery('name', 'glide.sys.time_format');
rec2.query();
if (rec2.next()) {
    var timeFormat = rec2.value;
    gs.print(timeFormat);
}

Custom Form Validation (UI Pages, Workspace, Portal)

For custom form validation outside classic UI forms — a UI page, a Workspace component, or a Portal widget — write a Script Include with the validation logic (mirroring the client-script logic above, but using GlideDateTime server-side instead of getDateFromFormat), and call it via GlideAjax from the client-side script. This keeps the validation logic consistent across classic UI, Workspace, and Portal, rather than maintaining two different validation implementations that can drift out of sync with each other.