diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index b7c999bc20..8451210d35 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -941,6 +941,12 @@ look at field (see below) to determine which field in the model should be modified. * `ngRequired`: True if input is required. * `ngPattern`: The pattern to match against (for text entry) +* `ngBlur`: A function that may be invoked to evaluate the expression + associated with the `ng-blur` attribute associated with the control. + * This should be called when the control has lost focus; for controls + which simply wrap or augment `input` elements, this should be fired + on `blur` events associated with those elements, while more complex + custom controls may fire this at the end of more specific interactions. * `options`: The options for this control, as passed from the `options` property of an individual row definition. * `field`: Name of the field in `ngModel` which will hold the value for this diff --git a/platform/commonUI/general/res/templates/controls/datetime-field.html b/platform/commonUI/general/res/templates/controls/datetime-field.html index 2c0423c32b..e9b394c530 100644 --- a/platform/commonUI/general/res/templates/controls/datetime-field.html +++ b/platform/commonUI/general/res/templates/controls/datetime-field.html @@ -1,7 +1,29 @@ +
diff --git a/platform/commonUI/general/res/templates/controls/time-controller.html b/platform/commonUI/general/res/templates/controls/time-controller.html index e44a9ff77c..335dee61c3 100644 --- a/platform/commonUI/general/res/templates/controls/time-controller.html +++ b/platform/commonUI/general/res/templates/controls/time-controller.html @@ -20,12 +20,14 @@ at runtime from the About dialog for additional information. -->
-
+
C @@ -36,12 +38,15 @@   -
+ + +
diff --git a/platform/commonUI/general/src/controllers/DateTimeFieldController.js b/platform/commonUI/general/src/controllers/DateTimeFieldController.js index 325a112e13..ef0827e515 100644 --- a/platform/commonUI/general/src/controllers/DateTimeFieldController.js +++ b/platform/commonUI/general/src/controllers/DateTimeFieldController.js @@ -53,7 +53,9 @@ define( formatter.parse($scope.textValue) !== value) { $scope.textValue = formatter.format(value); $scope.textInvalid = false; + $scope.lastValidValue = $scope.textValue; } + $scope.pickerModel = { value: value }; } function updateFromView(textValue) { @@ -61,6 +63,17 @@ define( if (!$scope.textInvalid) { $scope.ngModel[$scope.field] = formatter.parse(textValue); + $scope.lastValidValue = $scope.textValue; + } + } + + function updateFromPicker(value) { + if (value !== $scope.ngModel[$scope.field]) { + $scope.ngModel[$scope.field] = value; + updateFromModel(value); + if ($scope.ngBlur) { + $scope.ngBlur(); + } } } @@ -69,10 +82,18 @@ define( updateFromModel($scope.ngModel[$scope.field]); } + function restoreTextValue() { + $scope.textValue = $scope.lastValidValue; + updateFromView($scope.textValue); + } + + $scope.restoreTextValue = restoreTextValue; + $scope.picker = { active: false }; $scope.$watch('structure.format', setFormat); $scope.$watch('ngModel[field]', updateFromModel); + $scope.$watch('pickerModel.value', updateFromPicker); $scope.$watch('textValue', updateFromView); } diff --git a/platform/commonUI/general/src/controllers/TimeRangeController.js b/platform/commonUI/general/src/controllers/TimeRangeController.js index cdcdb7f8d0..b036bd3fc7 100644 --- a/platform/commonUI/general/src/controllers/TimeRangeController.js +++ b/platform/commonUI/general/src/controllers/TimeRangeController.js @@ -175,6 +175,13 @@ define( updateViewFromModel($scope.ngModel); } + function updateFormModel() { + $scope.formModel = { + start: (($scope.ngModel || {}).outer || {}).start, + end: (($scope.ngModel || {}).outer || {}).end + }; + } + function updateOuterStart(t) { var ngModel = $scope.ngModel; @@ -192,6 +199,7 @@ define( ngModel.inner.end ); + updateFormModel(); updateViewForInnerSpanFromModel(ngModel); updateTicks(); } @@ -213,6 +221,7 @@ define( ngModel.inner.start ); + updateFormModel(); updateViewForInnerSpanFromModel(ngModel); updateTicks(); } @@ -223,6 +232,14 @@ define( updateTicks(); } + function updateBoundsFromForm() { + $scope.ngModel = $scope.ngModel || {}; + $scope.ngModel.outer = { + start: $scope.formModel.start, + end: $scope.formModel.end + }; + } + $scope.startLeftDrag = startLeftDrag; $scope.startRightDrag = startRightDrag; $scope.startMiddleDrag = startMiddleDrag; @@ -230,10 +247,13 @@ define( $scope.rightDrag = rightDrag; $scope.middleDrag = middleDrag; + $scope.updateBoundsFromForm = updateBoundsFromForm; + $scope.ticks = []; // Initialize scope to defaults updateViewFromModel($scope.ngModel); + updateFormModel(); $scope.$watchCollection("ngModel", updateViewFromModel); $scope.$watch("spanWidth", updateSpanWidth); diff --git a/platform/commonUI/general/test/controllers/DateTimeFieldControllerSpec.js b/platform/commonUI/general/test/controllers/DateTimeFieldControllerSpec.js index 714c7d072e..7d71095772 100644 --- a/platform/commonUI/general/test/controllers/DateTimeFieldControllerSpec.js +++ b/platform/commonUI/general/test/controllers/DateTimeFieldControllerSpec.js @@ -67,21 +67,13 @@ define( mockScope.ngModel = { testField: 12321 }; mockScope.field = "testField"; mockScope.structure = { format: "someFormat" }; + mockScope.ngBlur = jasmine.createSpy('blur'); controller = new DateTimeFieldController( mockScope, mockFormatService ); - }); - - it("updates models from user-entered text", function () { - var newText = "1977-05-25 17:30:00"; - - mockScope.textValue = newText; - fireWatch("textValue", newText); - expect(mockScope.ngModel.testField) - .toEqual(mockFormat.parse(newText)); - expect(mockScope.textInvalid).toBeFalsy(); + fireWatch("ngModel[field]", mockScope.ngModel.testField); }); it("updates text from model values", function () { @@ -91,16 +83,55 @@ define( expect(mockScope.textValue).toEqual("1977-05-25 17:30:00"); }); + describe("when valid text is entered", function () { + var newText; + + beforeEach(function () { + newText = "1977-05-25 17:30:00"; + mockScope.textValue = newText; + fireWatch("textValue", newText); + }); + + it("updates models from user-entered text", function () { + expect(mockScope.ngModel.testField) + .toEqual(mockFormat.parse(newText)); + expect(mockScope.textInvalid).toBeFalsy(); + }); + + it("does not indicate a blur event", function () { + expect(mockScope.ngBlur).not.toHaveBeenCalled(); + }); + }); + + describe("when a date is chosen via the date picker", function () { + var newValue; + + beforeEach(function () { + newValue = 12345654321; + mockScope.pickerModel.value = newValue; + fireWatch("pickerModel.value", newValue); + }); + + it("updates models", function () { + expect(mockScope.ngModel.testField).toEqual(newValue); + }); + + it("fires a blur event", function () { + expect(mockScope.ngBlur).toHaveBeenCalled(); + }); + }); + it("exposes toggle state for date-time picker", function () { expect(mockScope.picker.active).toBe(false); }); describe("when user input is invalid", function () { - var newText, oldValue; + var newText, oldText, oldValue; beforeEach(function () { newText = "Not a date"; oldValue = mockScope.ngModel.testField; + oldText = mockScope.textValue; mockScope.textValue = newText; fireWatch("textValue", newText); }); @@ -116,6 +147,11 @@ define( it("does not modify user input", function () { expect(mockScope.textValue).toEqual(newText); }); + + it("restores valid text values on request", function () { + mockScope.restoreTextValue(); + expect(mockScope.textValue).toEqual(oldText); + }); }); it("does not modify valid but irregular user input", function () { diff --git a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js index 85e77e4889..861f28ed45 100644 --- a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js +++ b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js @@ -91,6 +91,39 @@ define( .toHaveBeenCalledWith("ngModel", jasmine.any(Function)); }); + describe("when changes are made via form entry", function () { + beforeEach(function () { + mockScope.ngModel = { + outer: { start: DAY * 2, end: DAY * 3 }, + inner: { start: DAY * 2.25, end: DAY * 2.75 } + }; + mockScope.formModel = { + start: DAY * 10000, + end: DAY * 11000 + }; + // These watches may not exist, but Angular would fire + // them if they did. + fireWatchCollection("formModel", mockScope.formModel); + fireWatch("formModel.start", mockScope.formModel.start); + fireWatch("formModel.end", mockScope.formModel.end); + }); + + it("does not immediately make changes to the model", function () { + expect(mockScope.ngModel.outer.start) + .not.toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.outer.end) + .not.toEqual(mockScope.formModel.end); + }); + + it("updates model bounds on request", function () { + mockScope.updateBoundsFromForm(); + expect(mockScope.ngModel.outer.start) + .toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.outer.end) + .toEqual(mockScope.formModel.end); + }); + }); + describe("when dragged", function () { beforeEach(function () { mockScope.ngModel = { diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js index b46ba6e7a1..78ac8c194d 100644 --- a/platform/forms/src/MCTControl.js +++ b/platform/forms/src/MCTControl.js @@ -79,6 +79,9 @@ define( // Used to choose which form control to use key: "=", + // Allow controls to trigger blur-like events + ngBlur: "&", + // The state of the form value itself ngModel: "=",