How the Preferences Framework Works

From wiki.gpii
Jump to: navigation, search

The Preference Framework's Modular Components

Modular components, in the context of the Preferences Framework, are objects written in JavaScript that can be used to do one of the following tasks:

  1. Display one or more controls that allow a user to adjust their preferences (called Adjustors, which are assembled into Panels)
  2. Deliver a customized or transformed user interface based on a user’s preferences (called Enactors)
  3. Save a user’s preferences to some form of persistent storage, such as the GPII preferences server or the browser’s local data storage (called Data Stores)

Components in the preferences framework are modular because they are designed to only do one of these functions. They don’t mix together the three types of functionality. They also don’t expect to be used with a specific type or class of other components. For example, an adjustor can be used with any type of data store, and new stores can be plugged into the system without requiring changes to the adjustors. In software architecture terms, this makes them modular or loosely coupled.

As a result, the components in the Preferences Framework represent higher-level abstractions, rather than just traditional buttons and windows and other low-level user interface widgets. Adjusters, enactors, and data stores provide a set of the reusable “building blocks” that a developer who is making a preferences editor will need. They are semantic in nature, representing the kinds of adjustment, transformation, and persistence strategies that a developer will need to choose from when designing a preferences editor that is tailored to her audience.

In the case of adjusters, they often form a composition or grouping of lower-level user interface widgets into higher-level panels that can be assembled and connected with others to form a preference editor’s user interface. Not all components, though, present a user interface. Data stores and some enactors, for example, typically don’t have any user interface associated with them at all. Instead, they manage some essential piece of behaviour, logic, or data manipulation for the application, such as saving a user’s preferences.

Typically, the modular components that make up the Preferences Framework are built using standard web technologies—JavaScript, HTML, and CSS. This means that they can be imported into a web-based application (using ordinary <script> and <style> tags) and used directly in a JavaScript application. A developer can pick and choose which adjusters and enactors she wants to use, or can use them all together—she has the flexibility to decide what is most appropriate for her preference editor.

Aside from the individual adjusters, enactors, and data sources, the Preferences Framework also provides a set of abstractions that assist in the process of assembling the user interface for a preference editor. The Builder and schemas described on pages 38-39 of the PGA Design Deliverable report provide developers with a way of “weaving together” a collection of adjusters, enactors, and a data source using a declarative specification. Instead of asking the developer to write a lot of time-consuming, potentially brittle JavaScript by hand to “glue together” all the individual pieces of a preference editor, the Builder does it for them. The builder takes, as its input, a pair of JSON-based schemas that describe:

  1. All the preferences that the editor supports (i.e. that the user is able to edit using this tool)
  2. Which adjusters should be displayed in the preference editor
  3. How those adjusters should be bound to a user’s preference set
  4. Which enactors should be injected into the page to deliver on the user’s needs and preferences
  5. Which data source should be employed to save the user’s preferences

This declarative approach saves time, effort, and reduces the cost of change during the development process. But more importantly, it also enables user interfaces to be more easily generated programmatically. A Personal Control Panel is a good example of this; its job is to provide a means for the user to quickly adjust a device’s current settings. Typically, the user wouldn’t want to see all the possible preferences they could set, just the ones that are most likely to be changed regularly in the context of this particular device.

As a result of the preference framework’s declarative, schema-driven architecture, a Matchmaker can produce the JSON data needed by the Builder, delivering a completely customized, context-dependent set of adjusters tailored to the user on the fly. The Matchmaker produces a specification of the user interface—which settings and adjusters should be displayed—and the Builder does the work of actually connecting up all the required components and rendering them to the screen.

A Walkthrough of the Preferences Framework

Adjusters and Panels

How does a developer use the preferences framework to build their own preference editor? Typically, the developer of an editor such as an exploration tool will first decide which preferences she wants to support and the adjusters she wants to use for them. This may involve reusing existing adjusters as well as creating new adjusters as needed. For example, when we built the prototype exploration tool for PGA, we wanted to give the user a set of very simple "one click" adjusters, like these:

Preferences-Framework-Adjusters-Callout.png

Building these adjusters involved writing a bit of JavaScript, HTML, and a JSON-based component specification. The JavaScript code and JSON specification of an adjuster must conform to an API defined by the preferences framework so that it can be easily wired up to other components. In the case of the web-based preferences framework, this API is supported by Fluid Infusion, which provides facilities for defining flexible components and connecting them up together. Adjusters and enactors can be implemented using any standard web technology and wrapped as preference framework-compatible components if necessary.

Adjusters are responsible for adjusting one or more settings. They can be either fine-grained or coarse-grained, depending on the desired interaction. A fine-grained enactor usually provides a direct mapping between a user interface control and a value in the user’s N&P set. For example, the “text to speech voice speed” preference might be bound directly to a slider that allows the user to choose their preferred speed. Fine-grained adjustors can be very powerful, but also complicated for some users. As an alternative, “clustered” or coarse-grained adjusters like the ones in our exploration tool prototype provide simpler, quicker ways to adjust a collection of preferences all at once. Both types of adjusters are useful in different contexts, and may be mixed and matched freely.

In the case the “enlarge” adjuster shown above, pressing this button actually sets two different preferences at once: the text size and the line spacing.

Enlarge-Adjuster-Preferences.png

More specifically, the adjuster and its associated label (which is localizable) is combined into a Panel. The panel consists of a checkbox element, a primary icon, a label, and the checked state icon. Here’s what the HTML markup for this panel looks like:

 <input type="checkbox" id="increase-size" class="gc-explorationTool-enlarge-choice g-explorationTool-checkbox" />
 <label for="increase-size">
     <span class="g-explorationTool-panel-icon g-explorationTool-icon-enlarge"></span>
     <div class="gc-explorationTool-enlarge-labelText fl-icon-text"></div>
     <div class="fl-icon-check" role="image"></div>
 </label>

The panel’s JSON component specification describes how the panel’s markup should rendered and bound to the preference editor’s "model"—the user’s N&P set. Here’s an example of what the enlarge panel’s component specification looks like:

   fluid.defaults("gpii.explorationTool.panel.enlarge", {
       gradeNames: ["gpii.explorationTool.togglePanel", "autoInit"],
       preferenceMap: {
           "http://registry.gpii.net/common/lineSpace": {
               "model.lineSpace": "default",
               "range.min": "minimum",
               "range.max": "maximum"
           },
           "http://registry.gpii.net/common/fontSize": {
               "model.lineSpace": "default",
               "range.min": "minimum",
               "range.max": "maximum"
           }
       },
       selectors: {
           toggle: ".gc-explorationTool-enlarge-choice",
           label: ".gc-explorationTool-enlarge-labelText"
       },
       protoTree: {
           toggle: "${enabled}",
           label: {messagekey: "increaseSizeLabel"}
       }
   });

A primary schema is defined for the preference editor as a whole. The primary schema describes each of the supported preferences, including their default values and other schematic information such as enumerations, minimum and maximum values, and so on. The primary schema conforms to the JSON schema IETF specification. Here’s what the primary schema looks like for the some of the preferences in the exploration tool prototype:

 gpii.explorationTool.primarySchema = {
   "http://registry.gpii.net/common/textSize": {
       "type": "number",
       "default": 1.5,
       "minimum": 0.1,
       "maximum": 4,
       "divisibleBy": 0.1
   },
   http://registry.gpii.net/common/lineSpace": {
       "type": "number",
       "default": 1.5,
       "minimum": 0.1,
       "maximum": 4,
       "divisibleBy": 0.1
   }
 };

Enactors

Enactors are responsible for actually doing the work of satisfying a user’s needs and preferences by some means. This typically involves transforming, adapting, or otherwise enhancing a user interface to make it easier to read, control, or navigate. In practice, enactors tend to be fairly fine-grained; an enactor is usually responsible for making a particular adjustment to the use interface based on a single preference. For example, the "line space" enactor will widen or narrow the space between lines of text on a page based on the user’s line spacing preference. In some cases, a user may have specified a more general preference such as “enlarge this” or "make it easier to read," and the system will employ a collection of enactors to deliver this.

Enactors are distinct from the user’s needs and preference set. The N&P set contains a set of values, either as generic common terms or as application-specific settings. The set does not specify exactly which enactor should do the work for a particular preference or setting. This is determined by the personalization system itself, and is conveyed by the developer in the auxiliary schema described below. The developer of a flexible web application, for example, is responsible for binding these preference/settings values to a particular set of enactors. This binding process is facilitated by the preferences framework.

To illustrate more clearly, let’s consider a web application that can "enlarge things" and "make them easier to read." The application will reuse (or implement from scratch) a collection of enactors that correspond with these capabilities. The appropriate enactors for these capabilities are then bound to particular terms. “Easier to read” might include making the font size larger, the line space wider, and the margins wider. The preferences framework provides a facility to bind a particular enactor (e.g. the "line space" enactor) to the appropriate term in the user’s N&P set (e.g. the http://registry.gpii.net/common/lineSpacing common term). Whenever the system encounters this preference in the user’s N&P set, the appropriate enactor is dispatched by the framework to do the work of adjusting the line spacing of the web page.

This binding between preferences and enactors is managed by the Builder component in the preferences framework. A developer who creates a flexible system will provide a specification of all the "capabilities" of their application—the needs that it is capable of satisfying—as well as the names of all of the concrete enactor components that it wants to integrate. The preferences framework builder component will assemble a "user interface enhancer" for the application, which can be linked to each page just like any ordinary JavaScript object. The UI Enhancer is a component responsible for:

  1. Reading the user’s preferences from the data store
  2. Invoking each appropriate enactor based on the preferences and settings contained in the user’s set

Generally, then, the choice of which enactor is responsible for enacting a user’s preferences is determined by the application developer in the auxiliary schema. Some enactors only work on the web or on a particular platform, so the developer will take this into account.

Here’s an example of the line spacing enactor in the preferences framework. It is composed of several small pieces of code. First, the JavaScript function that actually does the work of changing the page’s CSS to a larger line spacing:

 fluid.prefs.enactor.lineSpace.set = function (times, that) {
   // Calculating the initial size here rather than using a members expand because the "line-height"
   if (!that.initialSize) {
       that.initialSize = that.numerizeLineHeight();
   }
   // that.initialSize === 0 when the browser returned "lineHeight" css value is undefined,
   // which occurs when firefox detects "line-height" value on a hidden container.
   if (that.initialSize) {
       var targetLineSpace = times * that.initialSize;
       that.container.css("line-height", targetLineSpace);
   }
};

And secondly, the enactor’s component specification, which sets everything up:

 fluid.defaults("fluid.prefs.enactor.lineSpace", {
   gradeNames: ["fluid.viewComponent", "fluid.prefs.enactor", "autoInit"],
   preferenceMap: {
       "http://registry.gpii.net/common/lineSpace": {
           "model.value": "default"
       }
   },
   fontSizeMap: {},  // must be supplied by implementors    
   invokers: {
       set: {
           funcName: "fluid.prefs.enactor.lineSpace.set",
           args: ["{arguments}.0", "{that}"]
       },
        ...
   }
 });

Now that we have created an enactor to go with our adjuster, we need to bind them together. This is the job of the auxiliary schema. It defines which panels and enactors should actually be connected together to form the contents of the preferences editor. Here’s roughly what it looks like for the exploration tool:

 fluid.defaults("gpii.explorationTool.auxSchema", {
   gradeNames: ["fluid.prefs.auxSchema", "autoInit"],
   auxiliarySchema: {
       ...
       // The preference-specific information:
       "enlarge": {
           "type": "gpii.explorationTool.enlarge",
           "enactor": [
               {
                   "type": "fluid.prefs.enactors.lineSpace"
               },
               {
                   "type": "fluid.prefs.enactors.fontSize”
               }
           ],
           "panel": {
               "type": "gpii.explorationTool.panels.enlarge",
               "container": ".gc-prefsEditor-enlarge",  // the css selector in the template where the panel is rendered
               "template": "%prefix/explorationTool-enlarge.html",
               "message": "%prefix/enlarge.json"
           }
       }
   }
 });

Summary

As you can see from these examples, a preference editor is assembled from a series of modular components—adjusters, panels, and enactors. These are then woven together by defining a schema for each preference as well as its bindings to particular adjusters and enactors. This is done by in a declarative fashion using the primary and auxiliary schemas. The preference framework’s builder component takes care of processing these pieces and returning a full-fledged JavaScript component that can be linked into any web application.

By taking this architectural approach, the preferences framework promotes greater reusability of each component in the system across different preference editors and reduces the need for redundant, hard-to-test code.

To summarize, here are the main steps to build a preference editor using the preferences framework:

  1. Decide which preferences should be available for the user to edit
  2. Define a primary schema for the editor as a whole, which describes the range, default values, and other schematic information for preferences. In the future, this may be sourced directly from the GPII Common Terms Registry
  3. Reuse or implement adjuster panels that are created with:
    • HTML
    • CSS
    • JavaScript
    • a JSON component specification
  4. Reuse or implement enactors, which do the work of previewing or enacting one more user preferences
  5. Define an auxiliary schema, which binds together all the panels and enactors
  6. Use the Builder component to combine these schemas together into a working component that can be instantiated using a single line of JavaScript
  7. Link the appropriate HTML, CSS, and JavaScript resources into your web application


Integrating the Preferences Framework Into Existing Applications

Although the web platform provides many of the underlying tools to enable flexible and adaptive user interfaces, they aren’t always used by web developers. This can cause problems when plugging Enactors or other components into existing web applications that have been implemented hastily or without the appropriate accessibility considerations in mind. New frameworks and development tools can assist in this process to some degree, but ultimately developers need to be educated in how to develop flexible applications, and they need to be committed enough to follow the standards rather than writing their own ad-hoc components and poorly-tested infrastructure.

To this end, we have established a “checklist” or set of criteria for web developers who want to integrate enactors and other parts of the preferences framework into their applications. This tool is also useful as a set of standard development practices for developers of new preference editors. All of of the GPII-related web application development efforts follow these criteria closely.

Integration Readiness Checklist

  1. Maintain a clear separation between the content and presentation layers of an application
    • CSS must be separate from HTML markup
    • CSS and HTML should be independent of the application's server-side and client-side code
  2. Technical practices
    • Comprehensive support for ARIA in all JavaScript-enabled user interfaces
    • Validation of CSS, HTML, and code using appropriate W3C or third-party validators
    • Comprehensive support for WCAG 2.0 AA or higher through all user interfaces
    • All strings must be parameterized as properties files, JSON, or other parseable format for localization
    • Web content should implement AccessForAll accessibility metadata where appropriate.
    • Authoring tools must provide a means for authors to specify accessibile alternatives and AccessForAll metadata.
  3. Responsive and Responsible Design
    • Must support and test across multiple form factors, screen resolutions, and browsers
    • Must use relative sizing rather than absolute units to ensure cross-device scalability
  4. Testing
    • Comprehensive test plans for cross platform, cross browser, cross device testing
    • Regular and frequent testing with assistive technology by both developers and by end-users of these ATs
    • Unit tests to ensure the longevity of the integration and the modularity of the system
  5. Continuous integration
    • Features should be developed in individual, fine-grained development branches in order to ensure that features can be merged in or backed out as needed
    • Always-working builds; broken builds reduce the ability to test for accessibility and to accept contributions from the community
  6. Modular code design
    • No hardcoding of presentation or delivery format: application logic and content delivery mechanisms must be alterable and configuration without modifying substantial code, in order to enable personalization
  7. Open
    • Community and accessibility experts should be able to review and audit the underlying code for potential problems or errors
    • Reuse and contributions should be accepted from the community of users, stakeholders, and other makers