Merge branch 'open-master' into open702

Merge latest from master branch into topic branch for separating
out Couch adapter from persistence cache, in support of reusing
the latter for the WARP persistence adapter, WTD-702.
This commit is contained in:
Victor Woeltjen 2015-01-20 13:04:17 -08:00
commit 3df82775b5
17 changed files with 502 additions and 86 deletions

View File

@ -1,4 +1,7 @@
<div ng-controller="ExampleFormController">
<mct-toolbar structure="toolbar" ng-model="state" name="aToolbar">
</mct-toolbar>
<mct-form structure="form" ng-model="state" name="aForm">
</mct-form>

View File

@ -1,4 +1,4 @@
/*global define*/
/*global define,window*/
define(
[],
@ -10,6 +10,89 @@ define(
};
$scope.toolbar = {
name: "An example toolbar.",
sections: [
{
description: "First section",
items: [
{
name: "X",
description: "X coordinate",
control: "textfield",
pattern: "^\\d+$",
disabled: true,
size: 2,
key: "x"
},
{
name: "Y",
description: "Y coordinate",
control: "textfield",
pattern: "^\\d+$",
size: 2,
key: "y"
},
{
name: "W",
description: "Cell width",
control: "textfield",
pattern: "^\\d+$",
size: 2,
key: "w"
},
{
name: "H",
description: "Cell height",
control: "textfield",
pattern: "^\\d+$",
size: 2,
key: "h"
}
]
},
{
description: "Second section",
items: [
{
control: "button",
glyph: "1",
description: "Button A",
click: function () {
window.alert("A");
}
},
{
control: "button",
glyph: "2",
description: "Button B",
click: function () {
window.alert("B");
}
},
{
control: "button",
glyph: "3",
description: "Button C",
disabled: true,
click: function () {
window.alert("C");
}
}
]
},
{
items: [
{
control: "color",
key: "color"
}
]
}
]
};
$scope.form = {
name: "An example form.",
sections: [

View File

@ -73,7 +73,7 @@
{
"key": "ViewSwitcherController",
"implementation": "controllers/ViewSwitcherController.js",
"depends": [ "$scope" ]
"depends": [ "$scope", "$timeout" ]
},
{
"key": "BottomBarController",

View File

@ -13,7 +13,7 @@ define(
* of applicable views for a represented domain object.
* @constructor
*/
function ViewSwitcherController($scope) {
function ViewSwitcherController($scope, $timeout) {
// If the view capability gets refreshed, try to
// keep the same option chosen.
function findMatchingOption(options, selected) {
@ -32,10 +32,12 @@ define(
// Get list of views, read from capability
function updateOptions(views) {
$scope.ngModel.selected = findMatchingOption(
views || [],
($scope.ngModel || {}).selected
);
$timeout(function () {
$scope.ngModel.selected = findMatchingOption(
views || [],
($scope.ngModel || {}).selected
);
}, 0);
}
// Update view options when the in-scope results of using the

View File

@ -10,12 +10,15 @@ define(
describe("The view switcher controller", function () {
var mockScope,
mockTimeout,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]);
mockTimeout = jasmine.createSpy("$timeout");
mockTimeout.andCallFake(function (cb) { cb(); });
mockScope.ngModel = {};
controller = new ViewSwitcherController(mockScope);
controller = new ViewSwitcherController(mockScope, mockTimeout);
});
it("watches for changes in applicable views", function () {
@ -71,6 +74,19 @@ define(
expect(mockScope.ngModel.selected).not.toEqual(views[1]);
});
// Use of a timeout avoids infinite digest problems when deeply
// nesting switcher-driven views (e.g. in a layout.) See WTD-689
it("updates initial selection on a timeout", function () {
// Verify precondition
expect(mockTimeout).not.toHaveBeenCalled();
// Invoke the watch for set of views
mockScope.$watch.mostRecentCall.args[1]([]);
// Should have run on a timeout
expect(mockTimeout).toHaveBeenCalled();
});
});
}
);

View File

@ -12,6 +12,11 @@ directive, e.g.:
<mct-form ng-model="myModel" structure="myStructure" name="myForm">
</mct-form>
Using toolbars is similar:
<mct-toolbar ng-model="myModel" structure="myStructure" name="myToolbar">
</mct-toolbar>
The attributes utilized by this form are as follows:
* `ng-model`: The object which should contain the full form input. Individual
@ -55,6 +60,44 @@ Note that `pattern` may be specified as a string, to simplify storing
for structures as JSON when necessary. The string should be given in
a form appropriate to pass to a `RegExp` constructor.
## Toolbar structure
A toolbar's structure is described similarly to forms, except that there
is no notion of rows; instead, there are `items`.
{
"name": ... title to display for the form, as a string ...,
"sections": [
{
"name": ... title to display for the section ...,
"items": [
{
"name": ... title to display for this row ...,
"control": ... symbolic key for the control ...,
"key": ... field name in ng-model ...
"pattern": ... optional, reg exp to match against ...
"required": ... optional boolean ...
"options": [
"name": ... name to display (e.g. in a select) ...,
"value": ... value to store in the model ...
],
"disabled": ... true if control should be disabled ...
"size": ... size of the control (for textfields) ...
"click": ... function to invoke (for buttons) ...
"glyph": ... glyph to display (for buttons) ...
"text": ... text withiin control (for buttons) ...
},
... and other rows ...
]
},
... and other sections ...
]
}
Note that `pattern` may be specified as a string, to simplify storing
for structures as JSON when necessary. The string should be given in
a form appropriate to pass to a `RegExp` constructor.
## Adding controls
Four standard control types are included in the forms bundle:
@ -62,6 +105,8 @@ Four standard control types are included in the forms bundle:
* `textfield`: An area to enter plain text.
* `select`: A drop-down list of options.
* `checkbox`: A box which may be checked/unchecked.
* `color`: A color picker.
* `button`: A button.
* `datetime`: An input for UTC date/time entry; gives result as a
UNIX timestamp, in milliseconds since start of 1970, UTC.

View File

@ -7,6 +7,10 @@
"key": "mctForm",
"implementation": "MCTForm.js"
},
{
"key": "mctToolbar",
"implementation": "MCTToolbar.js"
},
{
"key": "mctControl",
"implementation": "MCTControl.js",
@ -30,6 +34,14 @@
"key": "textfield",
"templateUrl": "templates/controls/textfield.html"
},
{
"key": "button",
"templateUrl": "templates/controls/button.html"
},
{
"key": "color",
"templateUrl": "templates/controls/color.html"
},
{
"key": "composite",
"templateUrl": "templates/controls/composite.html"
@ -45,36 +57,6 @@
"key": "CompositeController",
"implementation": "controllers/CompositeController.js"
}
],
"templates": [
{
"key": "_checkbox",
"templateUrl": "templates/_checkbox.html"
},
{
"key": "_checkboxes",
"templateUrl": "templates/_checkboxes.html"
},
{
"key": "_datetime",
"templateUrl": "templates/_datetime.html"
},
{
"key": "_select",
"templateUrl": "templates/_select.html"
},
{
"key": "_selects",
"templateUrl": "templates/_selects.html"
},
{
"key": "_textfield",
"templateUrl": "templates/_textfield.html"
},
{
"key": "_textfields",
"templateUrl": "templates/_textfields.html"
}
]
}
}

View File

@ -0,0 +1,11 @@
<a href=""
class="t-btn l-btn s-btn s-icon-btn s-very-subtle"
ng-class="{ labeled: structure.text }"
ng-click="structure.click()">
<span class="ui-symbol icon">
{{structure.glyph}}
</span>
<span class="title-label" ng-if="structure.text">
{{structure.text}}
</span>
</a>

View File

@ -0,0 +1,3 @@
<input type="color"
name="mctControl"
ng-model="ngModel[field]">

View File

@ -4,6 +4,7 @@
ng-required="ngRequired"
ng-model="ngModel[field]"
ng-pattern="ngPattern"
size="{{structure.size}}"
name="mctControl">
</span>
</span>

View File

@ -0,0 +1,32 @@
<form name="tool-bar btn-bar contents" novalidate>
<div class="form">
<span ng-repeat="section in structure.sections"
class="control-group coordinates"
title="{{section.description}}">
<ng-form ng-repeat="item in section.items"
ng-class="{ 'input-labeled': item.name }"
class="inline"
title="{{item.description}}"
name="mctFormInner">
<label ng-if="item.name">
{{item.name}}:
</label>
<mct-control key="item.control"
ng-class="{ disabled: item.disabled }"
ng-model="ngModel"
ng-required="item.required"
ng-pattern="getRegExp(item.pattern)"
options="item.options"
structure="item"
field="item.key">
</mct-control>
</ng-form>
</span>
</div>
</form>

View File

@ -4,13 +4,10 @@
* Module defining MCTForm. Created by vwoeltje on 11/10/14.
*/
define(
[],
function () {
["./controllers/FormController"],
function (FormController) {
"use strict";
// Default ng-pattern; any non whitespace
var NON_WHITESPACE = /\S/;
/**
* The mct-form directive allows generation of displayable
* forms based on a declarative description of the form's
@ -37,45 +34,6 @@ define(
"templates/form.html"
].join("/");
function controller($scope) {
var regexps = [];
// ng-pattern seems to want a RegExp, and not a
// string (despite what documentation says) but
// we want form structure to be JSON-expressible,
// so we make RegExp's from strings as-needed
function getRegExp(pattern) {
// If undefined, don't apply a pattern
if (!pattern) {
return NON_WHITESPACE;
}
// Just echo if it's already a regexp
if (pattern instanceof RegExp) {
return pattern;
}
// Otherwise, assume a string
// Cache for easy lookup later (so we don't
// creat a new RegExp every digest cycle)
if (!regexps[pattern]) {
regexps[pattern] = new RegExp(pattern);
}
return regexps[pattern];
}
// Publish the form state under the requested
// name in the parent scope
$scope.$watch("mctForm", function (mctForm) {
if ($scope.name) {
$scope.$parent[$scope.name] = mctForm;
}
});
$scope.getRegExp = getRegExp;
}
return {
// Only show at the element level
restrict: "E",
@ -83,9 +41,8 @@ define(
// Load the forms template
templateUrl: templatePath,
// Use the controller defined above to
// populate/respond to changes in scope
controller: controller,
// Use FormController to populate/respond to changes in scope
controller: FormController,
// Initial an isolate scope
scope: {

View File

@ -0,0 +1,64 @@
/*global define,Promise*/
/**
* Module defining MCTForm. Created by vwoeltje on 11/10/14.
*/
define(
["./controllers/FormController"],
function (FormController) {
"use strict";
/**
* The mct-toolbar directive allows generation of displayable
* forms based on a declarative description of the form's
* structure.
*
* This directive accepts three attributes:
*
* * `ng-model`: The model for the form; where user input
* where be stored.
* * `structure`: The declarative structure of the toolbar.
* Describes what controls should be shown and where
* their values should be read/written in the model.
* * `name`: The name under which to expose the form's
* dirty/valid state. This is similar to ng-form's use
* of name, except this will be made available in the
* parent scope.
*
* @constructor
*/
function MCTForm() {
var templatePath = [
"platform/forms", //MCTForm.bundle.path,
"res", //MCTForm.bundle.resources,
"templates/toolbar.html"
].join("/");
return {
// Only show at the element level
restrict: "E",
// Load the forms template
templateUrl: templatePath,
// Use FormController to populate/respond to changes in scope
controller: FormController,
// Initial an isolate scope
scope: {
// The model: Where form input will actually go
ngModel: "=",
// Form structure; what sections/rows to show
structure: "=",
// Name under which to publish the form
name: "@"
}
};
}
return MCTForm;
}
);

View File

@ -0,0 +1,57 @@
/*global define*/
define(
[],
function () {
"use strict";
// Default ng-pattern; any non whitespace
var NON_WHITESPACE = /\S/;
/**
* Controller for mct-form and mct-toolbar directives.
* @constructor
*/
function FormController($scope) {
var regexps = [];
// ng-pattern seems to want a RegExp, and not a
// string (despite what documentation says) but
// we want form structure to be JSON-expressible,
// so we make RegExp's from strings as-needed
function getRegExp(pattern) {
// If undefined, don't apply a pattern
if (!pattern) {
return NON_WHITESPACE;
}
// Just echo if it's already a regexp
if (pattern instanceof RegExp) {
return pattern;
}
// Otherwise, assume a string
// Cache for easy lookup later (so we don't
// creat a new RegExp every digest cycle)
if (!regexps[pattern]) {
regexps[pattern] = new RegExp(pattern);
}
return regexps[pattern];
}
// Publish the form state under the requested
// name in the parent scope
$scope.$watch("mctForm", function (mctForm) {
if ($scope.name) {
$scope.$parent[$scope.name] = mctForm;
}
});
// Expose the regexp lookup
$scope.getRegExp = getRegExp;
}
return FormController;
}
);

View File

@ -0,0 +1,90 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/MCTToolbar"],
function (MCTToolbar) {
"use strict";
describe("The mct-toolbar directive", function () {
var mockScope,
mctToolbar;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]);
mockScope.$parent = {};
mctToolbar = new MCTToolbar();
});
it("is restricted to elements", function () {
expect(mctToolbar.restrict).toEqual("E");
});
it("watches for changes in form by name", function () {
// mct-form needs to watch for the form by name
// in order to convey changes in $valid, $dirty, etc
// up to the parent scope.
mctToolbar.controller(mockScope);
expect(mockScope.$watch).toHaveBeenCalledWith(
"mctForm",
jasmine.any(Function)
);
});
it("conveys form status to parent scope", function () {
var someState = { someKey: "some value" };
mockScope.name = "someName";
mctToolbar.controller(mockScope);
mockScope.$watch.mostRecentCall.args[1](someState);
expect(mockScope.$parent.someName).toBe(someState);
});
it("allows strings to be converted to RegExps", function () {
// This is needed to support ng-pattern in the template
mctToolbar.controller(mockScope);
// Should have added getRegExp to the scope,
// to convert strings to regular expressions
expect(mockScope.getRegExp("^\\d+$")).toEqual(/^\d+$/);
});
it("returns the same regexp instance for the same string", function () {
// Don't want new instances each digest cycle, for performance
var strRegExp = "^[a-z]\\d+$",
regExp;
// Add getRegExp to scope
mctToolbar.controller(mockScope);
regExp = mockScope.getRegExp(strRegExp);
// Same object instance each time...
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
});
it("passes RegExp objects through untouched", function () {
// Permit using forms to simply provide their own RegExp object
var regExp = /^\d+[a-d]$/;
// Add getRegExp to scope
mctToolbar.controller(mockScope);
// Should have added getRegExp to the scope,
// to convert strings to regular expressions
expect(mockScope.getRegExp(regExp)).toBe(regExp);
});
it("passes a non-whitespace regexp when no pattern is defined", function () {
// If no pattern is supplied, ng-pattern should match anything
mctToolbar.controller(mockScope);
expect(mockScope.getRegExp()).toEqual(/\S/);
expect(mockScope.getRegExp(undefined)).toEqual(/\S/);
});
});
}
);

View File

@ -0,0 +1,69 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/FormController"],
function (FormController) {
"use strict";
describe("The form controller", function () {
var mockScope,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]);
mockScope.$parent = {};
controller = new FormController(mockScope);
});
it("watches for changes in form by name", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"mctForm",
jasmine.any(Function)
);
});
it("conveys form status to parent scope", function () {
var someState = { someKey: "some value" };
mockScope.name = "someName";
mockScope.$watch.mostRecentCall.args[1](someState);
expect(mockScope.$parent.someName).toBe(someState);
});
it("allows strings to be converted to RegExps", function () {
// Should have added getRegExp to the scope,
// to convert strings to regular expressions
expect(mockScope.getRegExp("^\\d+$")).toEqual(/^\d+$/);
});
it("returns the same regexp instance for the same string", function () {
// Don't want new instances each digest cycle, for performance
var strRegExp = "^[a-z]\\d+$",
regExp;
// Add getRegExp to scope
regExp = mockScope.getRegExp(strRegExp);
// Same object instance each time...
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
});
it("passes RegExp objects through untouched", function () {
// Permit using forms to simply provide their own RegExp object
var regExp = /^\d+[a-d]$/;
// Should have added getRegExp to the scope,
// to convert strings to regular expressions
expect(mockScope.getRegExp(regExp)).toBe(regExp);
});
it("passes a non-whitespace regexp when no pattern is defined", function () {
// If no pattern is supplied, ng-pattern should match anything
expect(mockScope.getRegExp()).toEqual(/\S/);
expect(mockScope.getRegExp(undefined)).toEqual(/\S/);
});
});
}
);

View File

@ -2,5 +2,6 @@
"MCTControl",
"MCTForm",
"controllers/CompositeController",
"controllers/DateTimeController"
"controllers/DateTimeController",
"controllers/FormController"
]