Custom Screen Creation Tutorial - rollerderby/scoreboard GitHub Wiki
Note on existing custom screens: This page describes how to devolop custom screens in CRG 2025, which differs significantly from how this was done in earler versions of CRG. Most custom screens developed for these earlier versions should still work, though you may have to copy (parts of) some js files from CRG 2023 to your custom screen, if you use functions from those. The most likely candidates being javascript/utils.js
or json/WSutils.js
.
Screens that use the sbModify
HTML attribute will probably not work in CRG 2025 without adaptation. This will primarily affect custom modifications of the main display screen or broadcast overlay. Appending the contents of each sbModify
to the corresponding sbDisplay
separated by a colon should fix the screens. But depending on the amount of customization it might be worthwile to migrate the customization to the new version of the CRG screen instead of adjusting the modified screen in order to also include the new functionality from those screens.
All CRG frontend screens are web pages. This tutorial constructs a simple example screen as an illustration of how to construct CRG frontend pages. It assumes you already have CRG v2025 or newer installed on your computer.
While you can create the files for this tutorial anywhere on your computer, best practice would be the subdirectory of the /html/custom/
directory of your scoreboard installation that fits the purpose of the screen.
In addition to basic HTML boilerplate, such as <html>
and <head>
, your HTML page must link to two scripts. One is /json/core.js
, which provides the basic scoreboard functionality, and the other is /external/jquery/jquery.js
, which provides a link to the version of jQuery bundled with CRG. For the screen to work, jquery.js
has to be loaded first. Note that for a page named example.html
Javascript from example.js
and css from example.css
in the same folder will be automatically loaded (if present).
example.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Strict//EN">
<html>
<head>
<title>CRG Sample Page</title>
<script type="text/javascript" src="/external/jquery/jquery.js" ></script>
<script type="text/javascript" src="/json/core.js"></script>
</head>
<body>
<!-- Put stuff here -->
</body>
</html>
This tutorial won't talk much about CSS, because there's not much CSS unique to the scoreboard. Any good online tutorial should be able to do a better job explaining the basics than we will here.
That said, if you are constructing a web page intended to be used as a broadcast overlay, there is one critical aspect to consider - the background color. Some video systems prefer the background to be a solid color (often green). Others prefer a transparent background. Put one of these two lines in your CSS file to specify a background value in that case.
example.css:
body { background-color: rgba(0,255,0,1); } /* Solid Green Background */
body { background-color: rgba(0,0,0,0); } /* Transparent Background */
The CRG javascript library is set up so that screens can be written primarily in HTML with only the occasional small helper function being needed in JS. Thus for the initial template you don't need a js file. If your sceen gets more complex and you need some helper functions, e.g. to convert values, you can just add them to example.js
.
If you create the file above in your html/custom
directory, you will discover that it does absolutely nothing. This is because the HTML file has no content.
Let's fix that problem by replacing <!-- Put stuff here -->
with actual stuff.
Specifically, let's make a spot to display the period clock. As we go forward, don't erase the whole file! Just change the bits shown.
example.html:
<body>
<div sbDisplay="ScoreBoard.CurrentGame.Clock(Period).Time"></div>
</body>
Now we have an area called a div
with an attribute named sbDisplay
. sbDisplay
is a CRG specific attribute that under the hood causes the screen to register for any updates to the Websocket Channel given and display it's current value in the HTML element to which it is attached.
If you reload example.html
, you will probably find that your screen displays some seemingly random large number. If the period clock in your scoreboard is running, this number will be changing quickly, indicating that there is a connection. But presumably this is not quite what you aimed for.
The reason is that the scoreboard stores clock values in milliseconds and we thus have to convert the value to a more convenient format. Try this:
example.html:
<body>
<div sbDisplay="ScoreBoard.CurrentGame.Clock(Period).Time: sbToTime"></div>
</body>
This adds a second argument to the sbDisplay
attribute. For all the CRG specific attributes, multiple arguments are separated by a colon. In the case of sbDisplay
the second argument (if present) is treated as the name of a Javascript function that is used to convert the value for display. sbToTime
is a conversion function provided by CRG that converts a time in the internal milliseconds format to the minutes:seconds format you are used to seeing on CRG displays.
In order to use our screen to change something, we need a control of some sort. How about a button? Everyone likes buttons!
Let's add a button that will start a new jam, and another one which will call a timeout. That way, we'll be able to see the effect on the period clock.
example.html:
<body>
<div sbDisplay="ScoreBoard.CurrentGame.Clock(Period).Time: sbToTime"></div>
<button sbSet="ScoreBoard.CurrentGame.StartJam">Start Jam</button>
<button sbSet="ScoreBoard.CurrentGame.Timeout">Timeout</Button>
</body>
If you reload the screen you should see the two buttons and they should do what their label claims.
sbSet
is another of the CRG specific HTML attributes. When called with a command Websocket Channel as argument, it will cause the command to be executed.
Now we can start and stop our period clock, but what about correcting the value if we made a mistake? Let's add some more buttons.
example.html:
<body>
<div>
<button sbSet="ScoreBoard.CurrentGame.Clock(Period).Time: -1000: change">-1</button>
<span sbDisplay="ScoreBoard.CurrentGame.Clock(Period).Time: sbToTime"></span>
<button sbSet="ScoreBoard.CurrentGame.Clock(Period).Time: +1000: change">+1</button>
</div>
<button sbSet="ScoreBoard.CurrentGame.StartJam">Start Jam</button>
<button sbSet="ScoreBoard.CurrentGame.Timeout">Timeout</Button>
</body>
These buttons makes use of the optional second and third argument of sbSet
that are useful when the first argument is a value channel instead of a command channel. The second argument can be either a function or a value. If it is a function, the function is evaluated and the cahnnel is set to the result, if it is a value the channel is set to that value. The third argument sets additional flags that are supported by some channels. The change
flag is supported by all channels with numeric values and indicates that the value is to be changed by the given amount instead of set to that number.
The values are -1000
and +1000
because we are directly working on the internal millisecond values and 1000ms are 1s.
If we have to make a bigger change, clicking +1 or -1 repeatedly can quickly get tedious. Isn't there a way to make this more efficient? There is.
example.html:
<body>
<div>
<button sbSet="ScoreBoard.CurrentGame.Clock(Period).Time: -1000: change">-1</button>
<input type="text" sbControl="ScoreBoard.CurrentGame.Clock(Period).Time: sbToLongTime: sbFromTime"></input>
<button sbSet="ScoreBoard.CurrentGame.Clock(Period).Time: +1000: change">+1</button>
</div>
<button sbSet="ScoreBoard.CurrentGame.StartJam">Start Jam</button>
<button sbSet="ScoreBoard.CurrentGame.Timeout">Timeout</Button>
</body>
sbControl
is the next of the CRG specific attributes. It can be used with input
and select
elements. Again, the first argument is a channel that is connected to the element. In this case the connection is both ways: If the value in the channel changes, the screen is updated and if the user changes the element on the screen, the channel is updated. The (optional) second argument is a conversion function from channel to screen and the (also optional) third argument is a conversion function from screen to channel. sbToLongTime
is similar to sbToTime
but it will always display a minutes part, even if it is zero, whereas the latter will display only seconds for times below a minute. sbFromTime
is the inverse conversion from the human readable minutes:secoonds to the internal milliseconds. (It can deal with both the long and the short format.)
If you look at the HTML we have so far, you might notice that we are repeating a lot of common prefixes for those channel names. This does happen commonly, as parts of a screen will often deal with channels related to each other. CRG provides a way to avoid this repetition:
example.html:
<body sbContext="ScoreBoard.CurrentGame">
<div sbContext="Clock(Period)">
<button sbSet="Time: -1000: change">-1</button>
<input type="text" sbControl="Time: sbToLongTime: sbFromTime"></input>
<button sbSet="Time: +1000: change">+1</button>
</div>
<button sbSet="StartJam">Start Jam</button>
<button sbSet="Timeout">Timeout</Button>
</body>
sbContext
will store a prefix that is used for all channels accessed in the element it is placed in and any elements enclosed by it. As you can see in the example, multiple nested instances of sbContext
are chained together. If necessary, the prefix can be ignored for an individual channel or an inner sbContext
by prefacing it with a slash, e.g. /ScoreBoard.Settings.Setting(ScoreBoard.View_SwapTeams)
.
Now we have a screen that can access the period clock. But there are more clocks in the scoreboard. Obviously we can copy and paste this in order to add other clocks. But then if we later change the code, we have to make this change multiple times over. Wouldn't it be nice if we only had to maintain one copy and could tell CRG to repeat it for each clock it knows about?
example.html:
<div sbForeach="Clock">
<span sbDisplay="Name"></span>:
<button sbSet="Time: -1000: change">-1</button>
<input type="text" sbControl="Time: sbToLongTime: sbFromTime"></input>
<button sbSet="Time: +1000: change">+1</button>
</div>
And that's it. If clocks were added or removed, this code would autmatically add/remove them from the screen. (Though in CRG the set of available clocks is fixed, so you won't be able to see this here. But if you repeat screen elements for Periods or Jams, this is very useful.) Note that sbForeach
will implicitly insert an sbContext
for the respective instance.
Now, what if you don't want to display all of the clocks and want to control the sorting of the ones that are displayed?
example.html:
<div sbForeach="Clock: Jam, Period, Timeout: only">
<span sbDisplay="Name"></span>:
<button sbSet="Time: -1000: change">-1</button>
<input type="text" sbControl="Time: sbToLongTime: sbFromTime"></input>
<button sbSet="Time: +1000: change">+1</button>
</div>
As with other attributes, there are optional arguments that can help us. The second argument here is a list of ids (the part in parentheses in the channel name) and the elements corresponding to the channels with these ids will be displayed first and in the given order. By default, all other elements would then be appended afterwards, but giving only
as the third argument suppresses them.
Now this works fine for clocks, where we know all the possible ids ahead of time. But what if we don't have this luxury?
example.html:
<div sbForeach="Clock: -Intermission, -Lineup: name" sbAttr="name: Name">
<span sbDisplay="Name"></span>:
<button sbSet="Time: -1000: change">-1</button>
<input type="text" sbControl="Time: sbToLongTime: sbFromTime"></input>
<button sbSet="Time: +1000: change">+1</button>
</div>
Here the ids in the second argument are prefaced by -
which tells CRG to exclude the elements with these ids. (You can mix this with the former approach of listing ids to place at the front.) And the third argument now gives the name of an attribute that is to be used as a sorting key. sbAttr
is used to set this attribute - the first argument gives the name of the attribute, the second is the channel from which it is set and the optional third argument is a conversion function as we know it from other CRG attributes.
Notes: If you append ,num
to the attribute in the third argument of sbForeach
, sorting is done numerically instead of alphabetically. And instead of giving an attribute to sort by, you can also give a JS function that takes two HTML elements as arguments and returns true if the second argument should be placed before the first one.
Often you'll want to affect how screen elemnts look based on values in channels, not just their text. In our example screen we could highlight, which clocks are currently running.
example.html:
<div sbForeach="Clock: -Intermission, -Lineup: name" sbAttr="name: Name">
<span sbDisplay="Name" sbClass="sbActive: Running"></span>:
<button sbSet="Time: -1000: change">-1</button>
<input type="text" sbControl="Time: sbToLongTime: sbFromTime"></input>
<button sbSet="Time: +1000: change">+1</button>
</div>
sbClass
works very similar to sbAttr
. The first argument is the css class that will be toggled. The second argument is the channel that controls it and the optional third one is a conversion function. If it is omitted, CRG will check if the channel is either the boolean true
or the string "true"
. There are some special values for the conversion function as well: !
will negate the result of the default conversion. And any value that is neither that nor the name of a function will be treated as js code that CRG will append to return v
and evaluate. You can use this for simple comparisons like < 3
.
To round things off, let's add a simple popup window to our example screen:
example.html:
<body sbContext="ScoreBoard.CurrentGame">
<div sbForeach="Clock: -Intermission, -Lineup: name" sbAttr="name: Name">
<span sbDisplay="Name" sbClass="sbActive: Running"></span>:
<button sbSet="Time: -1000: change">-1</button>
<input type="text" sbControl="Time: sbToLongTime: sbFromTime"></input>
<button sbSet="Time: +1000: change">+1</button>
<button sbCall="openPopup">Popup</button>
</div>
<button sbSet="StartJam">Start Jam</button>
<button sbSet="Timeout">Timeout</Button>
<div class="sbTemplates">
<div id="ClockDialog" sbContext="Clock(*)">
The current clock value is <span sbDisplay="Time: sbToTime"></span>.
</div>
</div>
</body>
example.js:
function openPopup(k, v, elem, event) {
WS.SetupDialog($('#ClockDialog'), k, {
title: WS.state[k + '.Name'] + ' Clock',
width: 400,
buttons: {
Close: function () {
$(this).dialog('close');
},
},
});
}
The HTML for the popup is placed in a div
with class="sbTemplates"
. This lets CRG know to prepare it accordingly when setting up the screen. The popup itself is then given a unique id
and an sbContext
that has a wildcard (*
) in it to indicate that the clock id that will appear there will vary. The contents are then just normal CRG HTML.
In order to open the popup we use another CRG attribute: sbCall
. This will call the given javascript function.
In the javascript file we then define the function that opens the popup. For the sake of consistency sbCall
will call functions with the same set of parameters as are used for converter functions, even though not all of them will have a useful value:
-
k
: In converter functions this is the (enriched) name of the channel that the value is coming from or that will be changed. SincesbCall
does not invoke a specific channel it will instead pass the currentsbContext
. -
v
: Usually the value to be converted. ForsbCall
it's alwaysundefined
. -
elem
: The HTML element we are changing/which has been changed/clicked. In this case the button. -
event
: The JS event that triggered this function call. In this case the click event on the button.
Our function only makes a single call to the helper function WS.SetupDialog
. Its first argument is a jquery object containing the contents of the dialog template. The second argument is the sbcontext
to set for the dialog. This should match the context set in the HTML with any wildcards replaced by specific values. The final argument is a set of options for a jQuery UI Dialog Widget.
In order to set the dialog title, we retrieve the value of a channel by passing its name to WS.state[]
(in square brackets). In order to do this the channel must be registered by the screen. In this case this is done because we are displaying the value in the HTML. If we have to retrieve values in javascript functions that are not registered by the HTML, we can do so by calling WS.Register('<channel>')
outside of any function in the JS file.
The channel names passed to the conversion functions are enriched with some convenience funtionality. In the following we'll use ScoreBoard.CurrentGame.Team(1).Skater(abcdef).Penalty(3).Code
as example.
-
k.field
is the part following the last period.Code
in the example. - For any substring of the form
.<type>(<id>)
,k.<type>
will give'<id>'
. In the examplek.Team
is'1'
,k.Skater
is'abcdef'
, andk.Penalty
is'3'
. -
k.upTo('field')
will give the part of k up to and includingfield
and any id pertaining tofield
. In the example,k.upTo('Skater')
is'ScoreBoard.CurrentGame.Team(1).Skater(abcdef)'
,k.upTo('Game')
is'ScoreBoard.CurrentGame'
.
Let's close this tutorial by looking at some functionality that didn't fit onto the example screen but is still used relative often.
As you write a screen you may want to add multiple conditional css classes, multiple attributes, or multiple function calls to the same HTML element. But HTML doesn't allow multiple sbClass
, sbAttr
, or sbCall
attributes. Thus there is a syntax to add multiple instances to a single attribute: separate the instances by |
, e.g.
<span sbDisplay="Name" sbClass="sbActive: Running | noMoreJam: ^NoMoreJam"></span>
sbActive
is a CSS class styled by CRG. The default theme will add a green background to the element.
The ^
preceding the Channel is another convenience functionality: It tells CRG to discard the last part of the prefix set by sbContext
, specifically the last period and anything after it. You can use multiple ^
s to discard more elements of the context.
Sometimes you may want to base a displayed value on the values of multiple channels. In this case you can list all channels separated by commas, as in
<button sbClass="sbClickMe: InJam, Clock(Jam).Running: sbJamTooLong" sbSet="StopJam">Stop Jam</button>
In this case the element will be updated whenever one of the values changes. You will usually need a custom conversion function in order to handle such elements. Note that the conversion function will not necessarily be called with the value that's changed but with the first value that's set among the list of channels. This is often used when displaying a team name with the option to override it with an alternate name, as in
<span sbDisplay="AlternateName(operator), UniformColor, Name"></span>
When the alternate name is set, it is used. Else uniform colcor is checked and as final option we'll use the team name.
If this tutorial served it's purpose, you should now understand how create screens for CRG. But of course it doesn't cover all the details and at some point you'll need information that is not covered here. These are places where you might find further help:
- WebSocket Channels has a complete overview of all the channels available in the various versions of CRG.
- Frontend Library has a complete overview of all available HTML attributes and their parameters as well as CSS classes and variables supported by CRG.
- Looking at existing screens or screen components in the
html
folder of your CRG installation that do things similar to what you want to achieve might also help. - If none of these resources help, you can also open a ticket on github and tag it as
question
.