Skip to content

Patterns in the WinJS Codebase

Adam Comella edited this page Oct 6, 2015 · 71 revisions

There are a number of patterns that appear in the WinJS codebase. This page catalogues the patterns and, for the most part, you should follow them. As with all guidelines, there are exceptions to the rule. This document also describes the rationale behind each pattern so you can decide when you should and shouldn't follow it.

Table of Contents

Styling Hover

Guidance

When styling hover, you must consider the following rules:

  1. (LESS) Theme-independent hover styles, including styles inside high-contrast media queries, are simply prefixed with html.win-hoverable.

  2. (LESS) Theme-specific hover rules should be expressed inside of the ColorsHover LESS mixin. Note that the specificity of rules within the ColorsHover mixin increases by 1 element and 1 class.

  3. (LESS) Non-hover rules which include :hover in their selector do not go into the ColorsHover mixin. :focus and :active rules commonly fall into this category. For such rules, you'll need to consider the specificity of rules within the ColorsHover mixin and it's common to need to boost the specificity of the rule by 1 class. The convention is to duplicate the last class of the rule.

  4. (JavaScript/TypeScript) Modules which style hover must include the _Hoverable module.

  • TypeScript Caveat: In TypeScript, it's not enough to merely require the _Hoverable module. Because the _Hoverable module isn't actually used, TypeScript will omit the _Hoverable module from the generated JavaScript. Consequently, you have to access a property of the _Hoverable module to ensure that TypeScript will include it in the generated JavaScript. Like this:

    import _Hoverable = require("../../Utilities/_Hoverable");
    
    // Force-load Dependencies
    _Hoverable.isHoverable;

Let's do an example to illustrate rules (2) and (3). Suppose we start out with rules that look like this (specificity calculator):

.Colors(@theme) {
  // Hover rule
  // specificity: 0,0,3,0
  .win-appbar .win-overflow-button:hover {
    background-color: @listHover;
  }

  // Active rule (non-hover rule -- includes :hover just to make
  // sure we beat the :hover rule)
  // specificity: 0,0,4,0
  .win-appbar .win-overflow-button:active:hover {
    background-color: @listActive;
  }
}

As instructed by (1), we take the hover rule and put it into the ColorsHover mixin.

.ColorsHover(@theme) {
  // hover rule
  // specificity: 0,0,4,1
  .win-appbar .win-overflow-button:hover {
    background-color: @listHover;
  }
}

The active rule is a non-hover rule so it stays outside of the ColorsHover mixin as per (3). Recall that the ColorsHover mixin causes the rule's specificity to be boosted by 1 class and 1 element. To make sure the active rule continues to beat the hover rule, we boost its specificity by 1 class by duplicating its last class:

.Colors(@theme) {
  // active rule
  // specificity: 0,0,5,0
  .win-appbar .win-overflow-button.win-overflow-button:active:hover {
    background-color: @listHover;
  }
}

Rationale

The motivation for this funky hover styling guidance is that :hover styles get stuck rendered on an element after tapping on it with touch in webkit (#288). The fix is to disable WinJS's hover styles in such browsers when the user is using touch. The :hover styling guidance allows WinJS to enable and disable its hover rules as it sees fit.

Mechanics

The _Hoverable module adds the win-hoverable class to the html element during start up. If a touch event is detected in webkit, the win-hoverable class is removed. WinJS's hover rules are only in affect when the win-hoverable class is present. Thus WinJS's hover styles are disabled when removing the win-hoverable class.

The ColorsHover mixin prefixes rules with html.win-hoverable. For example:

.ColorsHover(@theme) {
  .win-appbar .win-overflow-button:hover {
    background-color: @listHover;
  }
}

expands to:

html.win-hoverable .win-appbar .win-overflow-button:hover {
  background-color: @listHover;
}

That prefixing explains why rules inside of the ColorsHover mixin have their specificity increased by 1 class (.win-hoverable) and 1 element (html).

getComputedStyle

Guidance

Don't call _Global.getComputedStyle.

Instead, call _ElementUtilities._getComputedStyle.

For JavaScript code, we have a jscs rule to enforce this guidance.

Rationale

Firefox's implementation of getComputedStyle has a bug where it returns null when called within an iframe that is display:none. This violates the getComputedStyle contract. _ElementUtilities._getComputedStyle is a helper which upholds the contract of always returning an object whose keys are CSS attributes that map to strings.

See #1253 for more details.

Pointer/Touch Events

Guidance

Don't register for pointer events directly. Never register for touch events directly.

Instead, register for pointer events via _ElementUtilities._addEventListener. This helper generates synthetic pointer events which work in all browsers regardless of whether they support pointer events or only touch events.

For JavaScript code, we have a jscs rule to enforce this guidance.

Rationale

Some browsers that WinJS cares about do not support pointer events. The _ElementUtilities._addEventListener helper enables you to sign up for pointer events that work in all browsers. For browsers that do not support pointer events, this helper polyfills them.

Focus/Blur Events

Guidance

Don't register for focus/blur events on elements directly (e.g. focus, blur, focusin, focus out).

Instead, register for focusin and focusout on elements using _ElementUtilities._addEventListener.

Exception: This helper is for focus/blur events on elements so if you want to listen to blur on window, you should just sign up for blur on window directly rather than using this helper.

Caveat: Under some circumstances, the helper may appear to fail to fire a focusout event. The helper is built on top of the browser's native focus/focusin events only so if there's a scenario where the browser fires its native blur/focusout event but not its native focus/focusin event, then the helper will appear to fail to fire a focusout event. For example, this can happen if an element loses focus due to it being deleted which causes the browser to move focus to body. Because body doesn't have a tabIndex by default, the browser will not fire a focus/focusin event and consequently the helper will not fire a focusout event.

Rationale

The behavior of the focus/blur events varies between browsers. Some examples of the variations:

  • Some browsers do not support focusin or focusout. They only support focus and blur.
  • In most browsers, focus and blur fire synchronously while in IE they fire asynchronously.
  • In IE, within the blur event handler, document.activeElement is the element that will be receiving focus. In most other browsers, document.activeElement is null in that case. This makes it difficult to determine what element will be receiving focus within the blur event handler.

The _ElementUtilities._addEventListener helper provides implementations of focusout and focusin which work consistently in all browsers. It provides a polyfill for browsers that do not support these events. Some characteristics of the implementation:

  • The events fire synchronously
  • Using target and relatedTarget off of the eventObject, you can determine both who is losing and who is gaining focus from within either event.
  • document.activeElement has a consistent value within these event handlers in all browsers.

Feature Detection

Guidance

Don't detect platforms. Examples:

// DON'T do these things:
if (hasWinRT) { ... }
if (isFirefox) { ... }
if (isXbox) { ... }

Instead, detect the particular features you are interested in using:

// DO these things 

if (_WinRT.Windows.UI.ViewManagement.InputPane) { ... }

var supportsCssGrid = !!("-ms-grid-row" in _Global.document.documentElement.style);
if (supportsCssGrid) { ... }
Notes on WinRT

When using a WinRT API, ensure that the API appears in _WinRT.js. For example, if you wanted to use Windows.UI.ViewManagement.InputPane, put that API into _WinRT.js and then you can feature detect it like this:

if (_WinRT.Windows.UI.ViewManagement.InputPane) {
  // Use Windows.UI.ViewManagement.InputPane
}

The _WinRT module ensures that it's safe to "dot into" each object of the API. Without the _WinRT module, you'd have to write much more verbose feature detection code. For example:

// _WinRT module saves you from this kind of verbosity
if (Windows && Windows.UI && Windows.UI.ViewManagement && Windows.UI.ViewManagement.InputPane) {
  // Use Windows.UI.ViewManagement.InputPane
}

Rationale

Detecting features rather than detecting platforms has a number of benefits including:

  • If a platform adds support for a feature that WinJS uses, WinJS will begin taking advantage of that feature in that platform without any change to the WinJS code.
  • If a platform removes support for a feature WinJS uses, WinJS will continue to work on that platform without any change to the WinJS code.

We've even gone so far as to feature detect bugs. Essentially, if we detect that a bug exists, we run code that works around the bug. When the browser fixes the bug, WinJS will automatically stop using the workaround in that browser. This is useful if there are downsides (e.g. performance) to the workaround.

Listening to Global Events

Guidance

Don't sign up for global events directly. For example:

// DON'T do this
window.addEventListener("wheel", handler);

Instead, sign up thru one of the helpers in _ElementUtilities. For example:

// DO this
_ElementUtilities._globalListener.addEventListener(element, "wheel", handler);

When used within a WinJS control, the root element of the control is typically passed as the first parameter to addEventListener:

// DO this within WinJS controls
_ElementUtilities._globalListener.addEventListener(this.element, "wheel", handler);

Note that when you register for events in this way, your event handler isn't sent the original event. Instead, it is sent a new event and the original event is available under eventObject.detail.originalEvent. For example, if your handler used to look like this:

window.addEventListener("wheel", function (eventObject) {
  var pageX = eventObject.pageX;
  // Use pageX...
});

It will now need to look like this:

window.addEventListener("wheel", function (eventObject) {
  eventObject = eventObject.detail.originalEvent; // Unwrap the event object
  var pageX = eventObject.pageX;
  // Use pageX...
});

There are a number of different helpers in _ElementUtilities for different global objects including:

  • window: _globalListener
    • window resize: _resizeNotifier (this should probably just be _globalListener but it predates everything else)
  • document.documentElement: _documentElementListener
  • Windows.UI.ViewManagement.InputPane.getForCurrentView(): _inputPaneListener

There's also Application._applicationListener for listening to events on WinJS.Application.

If you need to register for events on a global object which isn't listed, it's easy to create a new helper using GenericListener.

Rationale

This approach avoids memory leaks. If you were to register for an event on a global object directly and for some reason failed to unregister the handler when your object was done being used, your object would be leaked by the global object.

For example, suppose a control signs up for window resize directly. If the control was thrown away without dispose being called on it, the window would prevent the control from ever being garbage collected because the control is still listening to window resize.

The _ElementUtilities helpers work by indirectly signing clients up to the event. _ElementUtilities is the only one that registers for the event directly so only it can be leaked. However, this is okay because _ElementUtilities will exist for the lifetime of the application anyway. Elements interested in a particular event add a special class name to themselves indicating that they're interested in the event. When the event fires, _ElementUtilities queries the DOM for elements with the unique class name for that event and notifies those elements that the event has fired. In this way, elements sign up indirectly for events and avoid the risk of being leaked.

Styling Accent Color

Guidance

For a control to utilize accent color, it should:

  1. Import the accent color module
  2. Declare its accent color styles using the createAccentRule function. This function is designed to resemble declarative styling via CSS. Its signature looks like this:
createAccentRule(selector: string, props: { name: string; value: ColorTypes; }[])

Here's an example usage. Suppose you wanted to style the outline-color of the element with class name win-contentdialog-dialog to be the accent color. Here's how you would do that:

_Accents.createAccentRule(
  ".win-contentdialog-dialog", [
    { name: "outline-color", value: _Accents.ColorTypes.accent }
  ]
);

Controls should not call createAccentRule lazily. Instead, they should call createAccentRule eagerly when the WinJS JavaScript files are being loaded (i.e. outside of the lazy portion of their module). This ensures that createAccentRule will batch up accent color styles for all of the controls and dynamically generate a stylesheet one time. Dynamically generating CSS rules is expensive because it requires the browser to recalculate CSS formatting for the whole page so createAccentRule ideally only does this one time.

Rationale

In Windows 10, users are able to select a systemwide accent color which some WinJS controls consume. Ideally, controls would style against the accent color in CSS using a CSS variable. However, such a variable is not currently available.

The platform only exposes the accent color via the Windows.UI.ViewManagement.UISettings.getColorValue API. If each control had to call this API directly, it would result in unnecessarily complicated code. Each control would have to worry about updating all of its accent colors when the user switched accent colors. Certain styling scenarios would be extra complicated requiring the control to sign up for several events (e.g. using accent color on hover would require registering for the pointerenter and pointerleave events).

Instead, we've opted for the createAccentRule API which enables controls to express accent color styles in a declarative way resembling CSS.

Animations

Guidance

Don't use WinJS.UI.executeAnimation for doing programmatic animations.

Instead, use WinJS.UI.executeTransition.

Rationale

executeAnimation is based on CSS animations. To guarantee performance, you need to handwrite the keyframe and include it in the WinJS stylesheet. If you fail to do that, executeAnimation will generate the keyframe on the fly and add it to a dynamically generated stylesheet. This is a very expensive operation which will cause the browser to recalculate CSS formatting for the entire page.

Instead, use executeTransition which is based on CSS transitions. There are no keyframes involved so there's no risk of accidentally making executeTransition expensive.

Flexbox (LESS)

Guidance

Don't use flexbox styles directly. For example:

// DON'T do this
.win-someelement {
  display: flex;
  flex-direction: column;
}

Instead, use WinJS's LESS mixin for flexbox. For example:

// DO this
.win-someelement {
  #flex > .display-flex();
  #flex > .flex-direction(column);
}

Rationale

Different browsers support different notations for flexbox. For example, some browsers require vendor-prefixed names (e.g. -webkit-flex). IE10 strays furthest from the current standard. It supports an older version of flexbox which has all of the same functionality but under completely different names (e.g. align-items: flex-start is written as -ms-flex-align: start).

The LESS flexbox mixin takes care of handling all of these variations in flexbox notation for you so you don't have to worry about them.

Manipulating Styles (JavaScript)

Guidance

Don't manipulate the CSS attributes in this list directly. For example:

// DON'T do this
someElement.style.transform = "";

Instead, get the name of the property thru the _BaseUtils._browserStyleEquivalents helper and then manipulate that. For example:

// DO this
var transformName = _BaseUtils._browserStyleEquivalents["transform"].scriptName;
someElement.style[transformName] = "";

Rationale

Some browsers only support vendor-prefixed versions of properties. For example, Safari supports -webkit-transform but not transform. The _BaseUtils._browserStyleEquivalents helper maps unprefixed CSS attribute names to the name that will work in the current browser.

Lazy Modules

Guidance

When defining a module, run as much of the code lazily/on-demand as possible. For example, suppose we were defining the WinJS.UI.ShinyWidget. During start up, the only code that should run is enough code to publish a WinJS.UI.ShinyWidget property. The code for defining constants, helpers, the ShinyWidget class, etc. should only run the first time somebody tries to access the WinJS.UI.ShinyWidget property.

JavaScript

Don't write code like this because all of the code will run during start up:

define([
    '../Core/_Base',
], function shinyWidgetInit(_Base) {
    "use strict";

    // DON'T write code like this. All of this code will run
    // during start up.

    var constant1 = ...;
    var constant2 = ...;
    var constant3 = ...;

    function helper1() {
        ...
    }
    function helper2() {
        ...
    }
    function helper3() {
        ...
    }

    _Base.Namespace.define("WinJS.UI", {
        ShinyWidget: _Base.Class.define(function shinyWidget_ctor(element, options) {
            ...
        }, {
            instanceMember1: function () { },
            instanceMember2: function () { }
        }, {
            staticMember1: function () { },
            staticMember2: function () { },
        }
    });
});

Instead, use the _Base.Namespace._lazy so that none of the code inside of _lazy is run during start up -- it only gets run on demand the first time the WinJS.UI.ShinyWidget property is accessed.

define([
    '../Core/_Base',
], function shinyWidgetInit(_Base) {
    "use strict";

    // DO write code like this.

    _Base.Namespace.define("WinJS.UI", {
        ShinyWidget: _Base.Namespace._lazy(function () {

            // All of this code will run on demand the first time
            // the WinJS.UI.ShinyWidget property is accessed.

            var constant1 = ...;
            var constant2 = ...;
            var constant3 = ...;

            function helper1() {
                ...
            }
            function helper2() {
                ...
            }
            function helper3() {
                ...
            }

            var ShinyWidget = _Base.Class.define(function shinyWidget_ctor(element, options) {
                ...
            }, {
                instanceMember1: function () { },
                instanceMember2: function () { }
            }, {
                staticMember1: function () { },
                staticMember2: function () { },
            }
        });
        return ShinyWidget;
    });
});
TypeScript

Because TypeScript has built-in syntax for defining modules, we can't use the _Base.Namespace._lazy helper to make modules lazy like we can in JavaScript. We've come up with an alternate pattern which involves creating two files to make a TypeScript module lazy: one file which is loaded eagerly and the other file which is loaded lazily. Let's do the WinJS.UI.ShinyWidget example from above in TypeScript.

First, we'll look at the pattern of the eagerly loaded file. For controls, the convention is that this file goes in the Controls folder and its name does not begin with an underscore which indicates that it is a public module.

// Eagerly loaded file
// src/js/WinJS/Controls/ShinyWidget.ts

// All code that should be run during start up goes in here.

import _Base = require('../Core/_Base');

// Note that no members of _ShinyWidget are used in this file.
// It's only used for type information. Consequently, TypeScript
// will not include _ShinyWidget in the generated JavaScript and
// thus this file will not force the _ShinyWidget module to be
// loaded eagerly.
import _ShinyWidget = require('./ShinyWidget/_ShinyWidget');

var module: typeof _ShinyWidget = null;

_Base.Namespace.define("WinJS.UI", {
    ShinyWidget: {
        get: () => {
            if (!module) {
                // Load the _ShinyWidget module on demand the first time
                // somebody tries to access the WinJS.UI.ShinyWidget
                // property.
                require(["./ShinyWidget/_ShinyWidget"], (m: typeof _ShinyWidget) => {
                    module = m;
                });
            }
            return module.ShinyWidget;
        }
    }
});

Now let's look at the lazily loaded file. For controls, the convention is that we create a folder that has the same name as the control (e.g. ShinyWidget) and this folder goes into the Controls folder. Then the lazily loaded file goes inside of the ShinyWidget folder and the file name is the control name prefixed with an underscore (e.g. _ShinyWidget.ts) to indicate that it is not public -- consumers should reference the eagerly loaded file rather than the lazily loaded file.

// Lazily loaded file
// src/js/WinJS/Controls/ShinyWidget/_ShinyWidget.ts

// All code that should be run on demand the first time the
// WinJS.UI.ShinyWidget property is accessed should go in here.

var constant1 = ...;
var constant2 = ...;
var constant3 = ...;

function helper1() {
    ...
}
function helper2() {
    ...
}
function helper3() {
    ...
}

export class ShinyWidget {
    static staticMember1(): void { }
    static staticMember2(): void { }
    static staticMember3(): void { }

    instanceMember1(): void { }
    instanceMember2(): void { }
    instanceMember3(): void { }
}

Rationale

The benefit of lazy modules is in making start up time faster. Prior to lazy modules, all WinJS initialization code was run during start up (e.g. code to define constants, helper functions, classes). After lazy modules were introduced, only the minimum amount of code is run during start up to define the API surface of WinJS (i.e. everything under the WinJS namespace). All of the other initialization code is run on demand as the app accesses properties of the WinJS namespace. This improved WinJS's start up performance noticeably.

d.ts Files

Guidance

WinJS has a few different categories of TypeScript d.ts files:

This file represents the public API surface of WinJS (i.e. the WinJS namespace). It is consumed by apps written in TypeScript as well as by the WinJS unit tests which are written in TypeScript.

The dts-verifier tool helps us ensure that winjs.d.ts is an accurate representation of the WinJS API surface.

This file is consumed by the WinJS unit tests which are written in TypeScript. This file should contain any private WinJS APIs that are needed by the unit tests.

Per Module d.ts Files

Some WinJS modules written in JavaScript have d.ts files associated with them. Some examples:

These kinds of d.ts files are needed for WinJS modules which are written in JavaScript and are consumed by WinJS modules which are written in TypeScript. These d.ts files are only consumed internally by WinJS code.

Summary

Internal WinJS code uses WinJS thru modules whereas external code (e.g. apps, unit tests) uses WinJS thru the WinJS namespace which gets published off of window.

Localization

Localizing a String

Whenever a string needs to be localized, you add it to the resjson file for US English: strings/en-us/Microsoft.WinJS.resjson

The en-us resjson file contains key value pairs where the key represents a unique name for the string and the value is the english translation of the string.

The en-us resjson file regularly gets handed off to the localization team who ensures the strings are translated into the roughly 100 languages that WinJS supports. The resjson file for each language is stored in the strings folder.

Consuming a String

To use the localized version of a string, you generally have an object called strings near the top of your file which has a key per localized string used by the file. For example:

var strings = {
    get closeOverlay() { return _Resources._getWinJSString("ui/closeOverlay").value; },
};

And when you need to use the localized string, you write: strings.closeOverlay.

To understand how an app makes use of WinJS's localized resources, see:

Deprecating APIs

Guidelines

To deprecate an API:

Rationale

When we decide we want to get rid of an API, if we were to immediately delete that API from WinJS, it would be a painful experience for developers trying to upgrade. Their app would end up throwing exceptions or working incorrectly due to usages of APIs that were removed.

Instead, we first do a release to deprecate the API. This allows the API to continue working while warning developers that it may not be available in the future. It gives developers a period to smoothly transition from the old API to the new API.

After an API has been deprecated, we can delete it in the next release.

Dispose Pattern

Guidelines

See the Dispose Pattern Guidelines wiki page for details.

Rationale

When a developer is done with an instance of a WinJS control, the control may need to perform some cleanup. This may include:

  • Canceling timeouts, setImmediates, XHRs, and other asynchronous work
  • Unregistering from global events so that the control can be garbage collected

The dispose pattern is designed to solve this problem. Every WinJS control implements a dispose function. The developer can call dispose on the control to communicate that it is done with the control and to give the control the opportunity to perform cleanup.

High Contrast

Guidelines

It's best practice to define LESS variables for the colors of your WinJS control. You assign these variables different values depending on whether the page is in light theme, dark theme, or high contrast. This pattern enables you to define your CSS selectors in one location rather than having to repeat them once for the light theme, once for the dark theme, and once for high contrast. Here's an example from SplitView:

#win-splitview {
    .variableDefs(@theme) when not (@theme = highcontrast) {
        // Colors for light and dark theme. The .colorDefinitions mixin
        // gives us access to color variables which have the appropriate
        // color for the chosen theme.
        .colorDefinitions(@theme);
        
        @paneBackgroundColor: @chromeLow;
    }
    
    .variableDefs(highcontrast) {
        // Colors for high contrast.
        @paneBackgroundColor: ButtonFace;
    }
    
    // Define a mixin which creates rules that use the color variables.
    .stylesForTheme(@theme) {
        #win-splitview > .variableDefs(@theme);
        
        .win-splitview-pane {
            background-color: @paneBackgroundColor;
        }
    }
}

.Colors(@theme) {
    // Use the rules mixin for each theme (light/dark).
    #win-splitview > .stylesForTheme(@theme);
}

.HighContrast() {
    // Use the rules mixin for high contrast.
    #win-splitview > .stylesForTheme(highcontrast);
}

Use the following colors in high contrast styles:

  • -ms-hotlight
  • ButtonFace
  • ButtonText
  • GrayText
  • Highlight
  • HighlightText
  • Window
  • WindowText

Here's what they look like in High Contrast #1:

image

Background

The following background applies to Edge, IE10+, and Windows Store Apps.

When a Windows machine enters high contrast mode, the -ms-high-contrast-adjust property comes into play. Its default value is auto. When its value on an element is auto, the following CSS attributes are ignored on that element:

  • color
  • background-color
  • background-image

This gives you the opportunity to restyle these attributes for high contrast without worrying about the specificity of the non-high contrast styles. To style elements for high contrast, use a -ms-high-contrast media query.

Here's an example:

// These styles only apply in non-high contrast mode.
// They are ignored in high contrast mode because the
// default value of -ms-high-contrast-adjust is auto.
.myElement {
  background-color: gray;
  color: blue;
}

// These styles only apply in high contrast mode.
@media screen and (-ms-high-contrast: active) {
  .myElement {
    background-color: ButtonFace;
    color: ButtonText;
  }
}