Developing the widget
In the previous post, you saw how to create and publish a basic widget. In this post we'll explore some of the decisions we made and things we discovered whilst building the Countdown widget.
Switching to TypeScript
A widget is developed as a set of HTML, CSS and JavaScript files. When it comes to JavaScript, we recommend you use TypeScript to add strong typing and other IDE features to your JavaScript development. If you don't know what TypeScript is, you can explore samples and other documentation at https://typescriptlang.org.
When you start working with external libraries from TypeScript, like jQuery ormoment.js, you need something called a TypeScript Definition File. You can configure your solution to download TypeScript Definition files from the GitHub repository at DefinitelyTyped. You do this by modifying the grunt file that gets created by the Team Services Extension Template. Grunt is a JavaScript based task runner that integrates nicely with Visual Studio 2015 (see Task runners in Visual Studio 2015 for more information). One of the methods in this file is called tsdinit. This task downloads the TypeScript definition files that you specify. In the final code for the Countdown widget, the method looks like this:
tsdinit: {
command: "tsd install jquery q knockout moment jasmine moment-timezone spectrum",
stdout: true,
stderr: true
},
The definition files are downloaded to the typings directory. In the root of this directory is a file called tsd.d.ts:
/// <reference path="../node_modules/vss-sdk/typings/tfs.d.ts" />
/// <reference path="../node_modules/vss-sdk/typings/vss.d.ts" />
/// <reference path="moment/moment.d.ts" />
/// <reference path="jasmine/jasmine.d.ts" />
/// <reference path="jquery/jquery.d.ts" />
/// <reference path="moment-timezone/moment-timezone.d.ts" />
/// <reference path="spectrum/spectrum.d.ts" />
This file serves as a container for all the files you want to reference. In your TypeScript file, you only have to reference this single file to get access to all TypeScript definition files that you use. Using TypeScript while developing your widget will definitely help you in writing better JavaScript. By using the Bindings of the Task Runner, the compile TypeScript command runs whenever you build your project. This adds an extra validation to your TypeScript code.
Using Node Package Manager
In your solution explorer you'll also find a file packages.json:
{
"devDependencies": {
"bower": "^1.7.2",
"grunt": "~0.4.5",
"grunt-contrib-copy": "~0.8.2",
"grunt-contrib-jasmine": "*",
"grunt-exec": "~0.4.6",
"grunt-typescript": "*",
"jasmine": "^2.4.1",
"jquery": "^2.1.4",
"moment": "^2.11.0",
"moment-timezone": "*",
"requirejs": "2.1.22",
"spectrum-colorpicker": "^1.8.0",
"tsd": "~0.6.5",
"vset": "^0.4.24",
"vss-sdk": "^0.91.3"
},
"name": "",
"private": true,
"version": "0.0.0"
}
As you can see, there is a list of devDependencies in this file. This file is parsed by the Node.js Package Manager: npm. The devDependencies are downloaded and stored in the node_modules folder (turn on Show All Files to see this folder). Files such as moment.js and the vss-sdk are required by the widget at run time. To get these files from the node-modules folder to the scrips/lib folder there is another task in the Grunt task runner:
copy: {
main: {
files: [
{
expand: true,
flatten: true,
src: [
'node_modules/vss-sdk/lib/VSS.SDK.js',
'node_modules/moment/moment.js',
'node_modules/jquery/dist/jquery.js',
'node_modules/spectrum-colorpicker/spectrum.js',
'node_modules/spectrum-colorpicker/spectrum.css',
'bower_components/datetimepicker/jquery.datetimepicker.js',
'bower_components/datetimepicker/jquery.datetimepicker.css',
],
dest: 'scripts/lib',
filter: 'isFile'
},
]
}
}
This task takes external dependencies from both Node.js and Bower (another Package Manager) and copies them to scripts/lib. After that you can reference the JavaScript files from your HTML files that contain the UI for your widget.
Using npm allows you to easily include libraries in your project without checking them in to version control. You can also specify the version number of a library and be assured that the correct version is downloaded. By copying only the files you need, you avoid including the whole node_modules folder.
Running Unit Tests
The extension template also adds a tests directory with a Jasmine specification file. Jasmine is a test framework that lets you write unit tests for JavaScript. With some configuration settings it also supports tests written in TypeScript. To be able to run the unit tests within Visual Studio, you need to install the Chutzpah extension. After that you can just right click in the spec file and choose Run JS Tests. A Jasmine test looks something like this:
describe("countdown ", function () {
it("from date before to date is valid", function () {
var calculator = new CountdownCalculator.CountdownCalculator(
moment("21122015", "DDMMYYYY"),
moment("01012016", "DDMMYYYY"));
expect(calculator.isValid()).toBe(true);
});
});
This test makes sure that a countdown from a date to a later date is valid. To be able to run this tests as TypeScript you need the chutzpah.json configuration file that is in the root of the sample project.
{
"Framework": "jasmine",
"TestFileTimeout": 60000,
"TestHarnessReferenceMode": "AMD",
"TypeScriptModuleKind": "AMD",
"Compile": {
"Mode": "External",
"Extensions": [ ".ts" ],
"ExtensionsWithNoOutput": [ ".d.ts" ]
},
"References": [
{ "Path": "./scripts/lib/moment.js" },
{ "Path": "./scripts/lib/VSS.SDK.js" },
{ "Path": "./node_modules/requirejs/require.js" }
],
"Tests": [
{
"Includes": [ "*/tests/*.ts" ],
"Excludes": [ "*.d.ts", "*/scripts/*/*.ts" ]
}]
}
This file makes sure that Chutzpah looks for the TypeScript files and includes a couple of required references. It also tells Chutzpah that we use a module loader of the AMD type namely RequireJS.
The Main widget HTML Page
The widget is started by Team Services loading the countdown.html page. This is specified in the vss-extension.json file with the URI property:
"id": "CountdownWidget",
"type": "ms.vss-dashboards-web.widget",
"targets": [
"ms.vss-dashboards-web.widget-catalog"
],
"properties": {
"name": "Countdown Widget",
"description": "Dashboard Widget that counts down to a configurable moment in time.",
"previewImageUrl": "img/Preview-Full.png",
"uri": "countdown.html",
"isNameConfigurable": true,
"supportedSizes": [
{
"rowSpan": 1,
"columnSpan": 1
}
],
"supportedScopes": [ "project_team" ]
}
The countdown.html file contains a couple of HTML elements that are later used to display the title of the widget and the countdown counter. There is also a small piece of JavaScript that starts the widget SDK:
<script type="text/javascript">
VSS.init(
{
explicitNotifyLoaded: true,
setupModuleLoader: true,
usePlatformScripts: true,
usePlatformStyles: true
});
VSS.ready(function () {
require(["scripts/main"], function (main) { });
});
</script>
The ready function uses RequireJS to load the main.js file when the SDK is initialized. The main JavaScript file of your widget has a structure like this:
/// <reference path='../typings/tsd.d.ts' />
VSS.require(["TFS/Dashboards/WidgetHelpers"], (WidgetHelpers) => {
VSS.register("CountdownWidget", () => {
var showCountdown = (widgetSettings) => {
.....
return WidgetHelpers.WidgetStatusHelper.Success();
};
return {
load: (widgetSettings) => {
return showCountdown(widgetSettings);
},
reload: (widgetSettings) => {
return showCountdown(widgetSettings);
}
};
});
VSS.notifyLoadSucceeded();
});
For the Countdown Widget, we register two widgets in main.ts: the regular Countdown and the Sprint Countdown Widget. We then return an instance of thecountdownWidget class that contains the load and reload methods. This helps us in avoiding duplicate code and separating concerns. The load method is called when your widget is displayed for the first time. The reload method is used when a user makes changes in the configuration page of your widget. The widgetSettings is an object that's managed by VS Team Services. This object contains the settings that a user configured and is persisted for you.
Using a Configuration page
The Countdown widget uses a configuration page where you can set the name of the widget and select the date and time you want to countdown to. You tell VS Team Services that you have a configuration page by adding another contribution point to the vss-extension.json file like this:
{
"id": "CountdownWidget.Configuration",
"type": "ms.vss-dashboards-web.widget-configuration",
"targets": [ "ms.vss-dashboards-web.widget-configuration", "ms.vss-dashboards-web.CountdownWidget" ],
"properties": {
"name": "Countdown Widget Configuration",
"description": "Configures Countdown Widget",
"uri": "countdownconfiguration.html"
}
}
The file countdownconfiguration.html is now loaded by the widget SDK when a user opens the configuration settings for the Countdown widget. The HTML file references the required JavaScript libraries such as moment.js, jQuery and of course the VSS SDK. It then kicks of the configuration file in the scripts folder. For the configuration settings, we use an interface add strong typing for the custom settings object and avoid errors:
interface ISettings {
countDownDate: string,
timezone: string,
name: string,
backgroundColor: string,
foregroundColor: string
}
Your configuration page communicates with Team Services by using the following methods:
widgetConfigurationContext.setSaveValidationCallBack(validate);
widgetConfigurationContext.getInitialWidgetState()
widgetConfigurationContext.notifyConfigurationChange(validate());
validate is a custom function that returns an object that signals if the configuration is valid (canSave:true) and that returns an object containing the custom configuration fields:
validate() {
var result = {
canSave: true,
settings: JSON.stringify(<ISettings>
{
foregroundColor: $("#foreground-color-input").val(),
backgroundColor: $("#background-color-input").val(),
countDownDate: $("#datetimepicker").val(),
timezone: $('select').val()
})
};
return result;
}
The reload method in the main JavaScript file is called whenever a user makes a change to the configuration settings resulting in a live update.
Another cool feature is the ability to use the VS Team Services REST API. For the Sprint End countdown we use this to get the current team iteration by callinggetTeamIterations with the "current" parameter:
private getCurrentIteration(): IPromise<Date> {
var deferred = Q.defer<Date>();
var webContext = VSS.getWebContext();
var teamContext: TFS_Core_Contracts.TeamContext = { projectId: webContext.project.id, teamId: webContext.team.id, project: "", team: "" };
var workClient: Work_Client.WorkHttpClient = Service.VssConnection
.getConnection()
.getHttpClient(Work_Client.WorkHttpClient, WebApi_Constants.ServiceInstanceTypes.TFS);
workClient.getTeamIterations(teamContext, "current").then(teamIterations => {
if (teamIterations.length > 0) {
deferred.resolve(teamIterations[0].attributes.finishDate);
}
else {
deferred.resolve(null);
}
});
return deferred.promise;
}
The result is then used to determine how many days are left until the end of your sprint. This code may look a little daunting at first. What it does is return a Promiseobject to signal to the caller that the result is loaded asynchronously. The caller can then add a continuation on the Promise object by using the .then(function()) syntax. This method return the finish date of the current iteration or null if it's not available.
Scopes
There is one other thing you need to remember while working on your extension. In your extension manifest you can define scopes that you want to access. Scopes are the permissions that an extension requests when the user installs the extension. The following extension snippet asks access to the Work item services:
{
...
"scopes":[
"vso.work"
]
}
What's important to remember is that a scope change can only be applied by removing your extension from the Marketplace and uploading it as a new extension.
Adding a new widget
When developing the main widget we realized that it would make sense to have one version with all options and one simpler that would default to show the remaining time for the current iteration. After some research we discovered that this can be achieved quite easily by refactoring the code to share as much of the implementation as possible. We updated main.ts to register two extensions:
/// <reference path='../typings/tsd.d.ts' />
/// <reference path="isettings.d.ts" />
import CountdownWidget = require("scripts/countdownwidget");
VSS.require(["TFS/Dashboards/WidgetHelpers"], (WidgetHelpers) => {
VSS.register("SprintEndCountdownWidget", () => {
var countdownWidget = new CountdownWidget.CountdownWiget(WidgetHelpers, true);
return countdownWidget;
})
VSS.notifyLoadSucceeded();
});
VSS.require(["TFS/Dashboards/WidgetHelpers"], (WidgetHelpers) => {
VSS.register("CountdownWidget", () => {
var countdownWidget = new CountdownWidget.CountdownWiget(WidgetHelpers, false);
return countdownWidget;
})
VSS.notifyLoadSucceeded();
});
The extension manifest also has to be updated. In the vss-extension.json we add a very similar section for the Sprint End countdown widget:
{
"id": "SprintEndCountdownWidget",
"type": "ms.vss-dashboards-web.widget",
"targets": [
"ms.vss-dashboards-web.widget-catalog"
],
"properties": {
"name": "Sprint end Countdown Widget",
"description": "Dashboard Widget that counts down to the end of the current iteration",
"previewImageUrl": "img/Preview-Sprint.png",
"uri": "countdown.html",
"isNameConfigurable": true,
"supportedSizes": [
{
"rowSpan": 1,
"columnSpan": 1
}
],
"supportedScopes": [ "project_team" ]
}
},
{
"id": "SprintEndCountdownWidget.Configuration",
"type": "ms.vss-dashboards-web.widget-configuration",
"targets": [ "ms.vss-dashboards-web.widget-configuration", "ms.vss-dashboards-web.SprintEndCountdownWidget" ],
"properties": {
"name": "Countdown Widget Configuration",
"description": "Configures Countdown Widget",
"uri": "countdownconfiguration.html"
}
}
Now we have two extensions that share the same HTML pages.
Check out the Widgets SDK for more information.
Check out the Countdown widget on the marketplace.
Coming up…
About the team
Wouter de Kort | Wouter de Kort works as the Principal Expert DevOps at Ordina where he runs a team of DevOps Consultants. He helps organizations to stay on the cutting edge of software development on the Microsoft stack. Wouter focuses on Application Lifecycle Management and software architecture. He loves solving complex problems and helping other developers to grow. Wouter authored a couple of books, is a Microsoft Certified Trainer and an ALM Ranger. You can find him on Twitter @wouterdekort and on his blog or at the various conferences where Wouter speaks. |
Mathias Olausson | Mathias Olausson works as the CTO at Solidify, specializing in DevOps and application lifecycle management practices. With over 15 years of experience as a software consultant and trainer, he has worked on numerous projects and in many organizations. Mathias has been a Microsoft Visual Studio ALM MVP for eight years and is active as a Microsoft ALM Ranger. Mathias is a frequent speaker on Visual Studio and Team Foundation Server at conferences and industry events, and he blogs at https://blogs.msmvps.com/molausson. |
Comments
- Anonymous
May 27, 2016
Thanks Anisha! Your getCurrentIteration implementation saved me a lot of digging arround :) - Anonymous
May 07, 2018
>> "To be able to run the unit tests within Visual Studio, you need to install the Chutzpah extension. ">> [...]>> "To be able to run this tests as TypeScript you need the chutzpah.json configuration file that is in the root of the sample project."What if we want to run unit tests as TypeScript but not in visual studio? Do we still need chutzpah?