+
+
+
diff --git a/example/forms/src/ExampleFormController.js b/example/forms/src/ExampleFormController.js
index fd71122085..5534d26459 100644
--- a/example/forms/src/ExampleFormController.js
+++ b/example/forms/src/ExampleFormController.js
@@ -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: [
diff --git a/platform/commonUI/general/bundle.json b/platform/commonUI/general/bundle.json
index c88684991c..00e73a1568 100644
--- a/platform/commonUI/general/bundle.json
+++ b/platform/commonUI/general/bundle.json
@@ -73,7 +73,7 @@
{
"key": "ViewSwitcherController",
"implementation": "controllers/ViewSwitcherController.js",
- "depends": [ "$scope" ]
+ "depends": [ "$scope", "$timeout" ]
},
{
"key": "BottomBarController",
diff --git a/platform/commonUI/general/src/controllers/ViewSwitcherController.js b/platform/commonUI/general/src/controllers/ViewSwitcherController.js
index a7942fe627..05fec1bd1a 100644
--- a/platform/commonUI/general/src/controllers/ViewSwitcherController.js
+++ b/platform/commonUI/general/src/controllers/ViewSwitcherController.js
@@ -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
diff --git a/platform/commonUI/general/test/controllers/ViewSwitcherControllerSpec.js b/platform/commonUI/general/test/controllers/ViewSwitcherControllerSpec.js
index af0b99aa88..1895fb5302 100644
--- a/platform/commonUI/general/test/controllers/ViewSwitcherControllerSpec.js
+++ b/platform/commonUI/general/test/controllers/ViewSwitcherControllerSpec.js
@@ -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();
+ });
+
});
}
);
\ No newline at end of file
diff --git a/platform/forms/README.md b/platform/forms/README.md
index d435b023fd..034b687965 100644
--- a/platform/forms/README.md
+++ b/platform/forms/README.md
@@ -12,6 +12,11 @@ directive, e.g.:
+Using toolbars is similar:
+
+
+
+
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.
diff --git a/platform/forms/bundle.json b/platform/forms/bundle.json
index 5654d80f2b..d8e3730eca 100644
--- a/platform/forms/bundle.json
+++ b/platform/forms/bundle.json
@@ -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"
- }
]
}
}
\ No newline at end of file
diff --git a/platform/forms/res/templates/controls/button.html b/platform/forms/res/templates/controls/button.html
new file mode 100644
index 0000000000..730a67dd8b
--- /dev/null
+++ b/platform/forms/res/templates/controls/button.html
@@ -0,0 +1,11 @@
+
+
+ {{structure.glyph}}
+
+
+ {{structure.text}}
+
+
diff --git a/platform/forms/res/templates/controls/color.html b/platform/forms/res/templates/controls/color.html
new file mode 100644
index 0000000000..4e44c8e84f
--- /dev/null
+++ b/platform/forms/res/templates/controls/color.html
@@ -0,0 +1,3 @@
+
diff --git a/platform/forms/res/templates/controls/textfield.html b/platform/forms/res/templates/controls/textfield.html
index 57e61d3613..148f9e7005 100644
--- a/platform/forms/res/templates/controls/textfield.html
+++ b/platform/forms/res/templates/controls/textfield.html
@@ -4,6 +4,7 @@
ng-required="ngRequired"
ng-model="ngModel[field]"
ng-pattern="ngPattern"
+ size="{{structure.size}}"
name="mctControl">
diff --git a/platform/forms/res/templates/toolbar.html b/platform/forms/res/templates/toolbar.html
new file mode 100644
index 0000000000..17602268d8
--- /dev/null
+++ b/platform/forms/res/templates/toolbar.html
@@ -0,0 +1,32 @@
+
\ No newline at end of file
diff --git a/platform/forms/src/MCTForm.js b/platform/forms/src/MCTForm.js
index 8414938171..0de41797f7 100644
--- a/platform/forms/src/MCTForm.js
+++ b/platform/forms/src/MCTForm.js
@@ -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: {
diff --git a/platform/forms/src/MCTToolbar.js b/platform/forms/src/MCTToolbar.js
new file mode 100644
index 0000000000..c4642e4544
--- /dev/null
+++ b/platform/forms/src/MCTToolbar.js
@@ -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;
+ }
+);
\ No newline at end of file
diff --git a/platform/forms/src/controllers/FormController.js b/platform/forms/src/controllers/FormController.js
new file mode 100644
index 0000000000..ff7a1b0769
--- /dev/null
+++ b/platform/forms/src/controllers/FormController.js
@@ -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;
+ }
+);
\ No newline at end of file
diff --git a/platform/forms/test/MCTToolbarSpec.js b/platform/forms/test/MCTToolbarSpec.js
new file mode 100644
index 0000000000..d18ff40ed6
--- /dev/null
+++ b/platform/forms/test/MCTToolbarSpec.js
@@ -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/);
+ });
+
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/forms/test/controllers/FormControllerSpec.js b/platform/forms/test/controllers/FormControllerSpec.js
new file mode 100644
index 0000000000..538e4e5156
--- /dev/null
+++ b/platform/forms/test/controllers/FormControllerSpec.js
@@ -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/);
+ });
+
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/forms/test/suite.json b/platform/forms/test/suite.json
index f69c3a325e..61eff300e6 100644
--- a/platform/forms/test/suite.json
+++ b/platform/forms/test/suite.json
@@ -2,5 +2,6 @@
"MCTControl",
"MCTForm",
"controllers/CompositeController",
- "controllers/DateTimeController"
+ "controllers/DateTimeController",
+ "controllers/FormController"
]
\ No newline at end of file