CRM-15856 - Convert date/time widgets to use ngModel (which supports validation).
authorTim Otten <totten@civicrm.org>
Mon, 2 Feb 2015 04:59:09 +0000 (20:59 -0800)
committerTim Otten <totten@civicrm.org>
Mon, 2 Feb 2015 05:27:19 +0000 (21:27 -0800)
css/civicrm.css
js/angular-crm-ui.js
js/angular-crmMailing/directives.js
partials/crmMailing/schedule.html
partials/crmUi/datetime.html [new file with mode: 0644]

index 3995a2925d2d7f246a381e4d8230a0c05c87f4b1..54508992c0d18f1d64450decd781056fd9028aa7 100644 (file)
@@ -4541,3 +4541,6 @@ span.crm-status-icon {
 .crm-container table.mergecontact thead th {
   width:30%;
 }
+.crm-container .crm-ui-datetime.ng-dirty input.incomplete {
+  border: 1px solid red;
+}
index 2bc70199310c7911af7a5f4a6e81a058281d61d7..d9f590678aefecb2c1b60a95790bce78b6f54d7e 100644 (file)
     })
 
     // Display a date widget.
-    // example: <input crm-ui-date="myobj.datefield" />
-    // example: <input crm-ui-date="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
-    // WISHLIST: use ngModel
+    // example: <input crm-ui-date ng-model="myobj.datefield" />
+    // example: <input crm-ui-date ng-model="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
     .directive('crmUiDate', function ($parse, $timeout) {
       return {
         restrict: 'AE',
+        require: 'ngModel',
         scope: {
-          crmUiDate: '@', // expression, model binding
           crmUiDateFormat: '@' // expression, date format (default: "yy-mm-dd")
         },
-        link: function (scope, element, attrs) {
+        link: function (scope, element, attrs, ngModel) {
           var fmt = attrs.crmUiDateFormat ? $parse(attrs.crmUiDateFormat)() : "yy-mm-dd";
-          var model = $parse(attrs.crmUiDate);
 
           element.addClass('dateplugin');
           $(element).datepicker({
             dateFormat: fmt
           });
 
-          var updateChildren = (function() {
-            element.off('change', updateParent);
-            $(element).datepicker('setDate', model(scope.$parent));
-            element.on('change', updateParent);
-          });
+          ngModel.$render = function $render() {
+            $(element).datepicker('setDate', ngModel.$viewValue);
+          };
           var updateParent = (function() {
             $timeout(function () {
-              model.assign(scope.$parent, $(element).val());
+              ngModel.$setViewValue(element.val());
             });
           });
 
-          updateChildren();
-          scope.$parent.$watch(attrs.crmUiDate, updateChildren);
           element.on('change', updateParent);
         }
       };
     })
 
     // Display a date-time widget.
-    // example: <div crm-ui-date-time="myobj.mydatetimefield"></div>
-    // WISHLIST: use ngModel
+    // example: <div crm-ui-date-time ng-model="myobj.mydatetimefield"></div>
     .directive('crmUiDateTime', function ($parse) {
       return {
         restrict: 'AE',
+        require: 'ngModel',
         scope: {
-          crmUiDateTime: '@'
+          ngRequired: '@'
         },
-        template: '<input crm-ui-date="dtparts.date" placeholder="{{dateLabel}}"/> <input crm-ui-time="dtparts.time" placeholder="{{timeLabel}}"/>',
-        link: function (scope, element, attrs) {
-          var model = $parse(attrs.crmUiDateTime);
+        templateUrl: '~/crmUi/datetime.html',
+        link: function (scope, element, attrs, ngModel) {
+          var ts = scope.ts = CRM.ts(null);
           scope.dateLabel = ts('Date');
           scope.timeLabel = ts('Time');
+          element.addClass('crm-ui-datetime');
 
-          var updateChildren = (function () {
-            var value = model(scope.$parent);
-            if (value) {
-              var dtparts = value.split(/ /);
+          ngModel.$render = function $render() {
+            if (!_.isEmpty(ngModel.$viewValue)) {
+              var dtparts = ngModel.$viewValue.split(/ /);
               scope.dtparts = {date: dtparts[0], time: dtparts[1]};
             }
             else {
               scope.dtparts = {date: '', time: ''};
             }
-          });
-          var updateParent = (function () {
-            model.assign(scope.$parent, scope.dtparts.date + " " + scope.dtparts.time);
-          });
+          };
+
+          function updateParent() {
+            var incompleteDateTime = _.isEmpty(scope.dtparts.date) ^ _.isEmpty(scope.dtparts.time);
+            ngModel.$setValidity('incompleteDateTime', !incompleteDateTime);
+
+            if (_.isEmpty(scope.dtparts.date) && _.isEmpty(scope.dtparts.time)) {
+              ngModel.$setViewValue(' ');
+            }
+            else {
+              //ngModel.$setViewValue(scope.dtparts.date + ' ' + scope.dtparts.time);
+              ngModel.$setViewValue((scope.dtparts.date ? scope.dtparts.date : '') + ' ' + (scope.dtparts.time ? scope.dtparts.time : ''));
+            }
+          }
 
-          updateChildren();
-          scope.$parent.$watch(attrs.crmUiDateTime, updateChildren);
           scope.$watch('dtparts.date', updateParent);
           scope.$watch('dtparts.time', updateParent);
+
+          function updateRequired() {
+            scope.required = scope.$parent.$eval(attrs.ngRequired);
+          }
+
+          if (attrs.ngRequired) {
+            updateRequired();
+            scope.$parent.$watch(attrs.ngRequired, updateRequired);
+          }
+
+          scope.reset = function reset() {
+            scope.dtparts = {date: '', time: ''};
+            ngModel.$setViewValue('');
+          };
         }
       };
     })
     })
 
     // Display a time-entry field.
-    // example: <input crm-ui-time="myobj.mytimefield" />
-    // WISHLIST: use ngModel
+    // example: <input crm-ui-time ng-model="myobj.mytimefield" />
     .directive('crmUiTime', function ($parse, $timeout) {
       return {
         restrict: 'AE',
+        require: 'ngModel',
         scope: {
-          crmUiTime: '@'
         },
-        link: function (scope, element, attrs) {
-          var model = $parse(attrs.crmUiTime);
-
+        link: function (scope, element, attrs, ngModel) {
           element.addClass('crm-form-text six');
-          $(element).timeEntry({show24Hours: true});
+          element.timeEntry({show24Hours: true});
+
+          ngModel.$render = function $render() {
+            element.timeEntry('setTime', ngModel.$viewValue);
+          };
 
-          var updateChildren = (function() {
-            element.off('change', updateParent);
-            $(element).timeEntry('setTime', model(scope.$parent));
-            element.on('change', updateParent);
-          });
           var updateParent = (function () {
             $timeout(function () {
-              model.assign(scope.$parent, element.val());
+              ngModel.$setViewValue(element.val());
             });
           });
-
-          updateChildren();
-          scope.$parent.$watch(attrs.crmUiTime, updateChildren);
           element.on('change', updateParent);
         }
       };
index f88d1afc44aa1057cf8f59f3fe7b522809751b69..b161cfa8c1f8a6a5d47d7c6d8e37b0d965a47be1 100644 (file)
   });
 
   // Represent a datetime field as if it were a radio ('schedule.mode') and a datetime ('schedule.datetime').
-  // example: <div crm-mailing-radio-date="mySchedule" crm-model="mailing.scheduled_date">...</div>
-  // FIXME: use ngModel instead of adhoc crmModel
+  // example: <div crm-mailing-radio-date="mySchedule" ng-model="mailing.scheduled_date">...</div>
   angular.module('crmMailing').directive('crmMailingRadioDate', function ($parse) {
     return {
-      link: function ($scope, element, attrs) {
-        var schedModel = $parse(attrs.crmModel);
+      require: 'ngModel',
+      link: function ($scope, element, attrs, ngModel) {
 
         var schedule = $scope[attrs.crmMailingRadioDate] = {
           mode: 'now',
           datetime: ''
         };
-        var updateChildren = (function () {
-          var sched = schedModel($scope);
-          if (sched) {
+
+        ngModel.$render = function $render() {
+          var sched = ngModel.$viewValue;
+          if (!_.isEmpty(sched)) {
             schedule.mode = 'at';
             schedule.datetime = sched;
           }
           else {
             schedule.mode = 'now';
           }
-        });
+        };
+
         var updateParent = (function () {
           switch (schedule.mode) {
             case 'now':
-              schedModel.assign($scope, null);
+              ngModel.$setViewValue(null);
               break;
             case 'at':
-              schedModel.assign($scope, schedule.datetime);
+              ngModel.$setViewValue(schedule.datetime);
               break;
             default:
               throw 'Unrecognized schedule mode: ' + schedule.mode;
           }
         });
 
-        $scope.$watch(attrs.crmModel, updateChildren);
         $scope.$watch(attrs.crmMailingRadioDate + '.mode', updateParent);
         $scope.$watch(attrs.crmMailingRadioDate + '.datetime', function (newValue, oldValue) {
           // automatically switch mode based on datetime entry
           if (oldValue != newValue) {
-            if (!newValue || newValue == " ") {
+            if (_.isEmpty(newValue) || newValue == " ") {
               schedule.mode = 'now';
             }
             else {
index a68afcd6fd3ea8bddf8087d0357feee81094369e..4dcdc66457ee1a611d5eb672a082f557ba0c1729 100644 (file)
@@ -1,4 +1,4 @@
-<div class="crmMailing-schedule-outer" crm-mailing-radio-date="schedule" crm-model="mailing.scheduled_date">
+<div class="crmMailing-schedule-outer" crm-mailing-radio-date="schedule" ng-model="mailing.scheduled_date">
   <div class="crmMailing-schedule-inner">
     <div>
       <input ng-model="schedule.mode" type="radio" name="send" value="now" id="schedule-send-now"/>
@@ -7,7 +7,7 @@
     <div>
       <input ng-model="schedule.mode" type="radio" name="send" value="at" id="schedule-send-at"/>
       <label for="schedule-send-at">{{ts('Send at:')}}</label>
-      <span crm-ui-date-time="schedule.datetime"></span>
+      <span crm-ui-date-time ng-model="schedule.datetime" ng-required="schedule.mode == 'at'"></span>
     </div>
   </div>
 </div>
diff --git a/partials/crmUi/datetime.html b/partials/crmUi/datetime.html
new file mode 100644 (file)
index 0000000..e7c455e
--- /dev/null
@@ -0,0 +1,18 @@
+<input
+  crm-ui-date
+  ng-model="dtparts.date"
+  ng-required="required"
+  ng-class="{incomplete: !dtparts.date && (required||dtparts.time)}"
+  placeholder="{{dateLabel}}"/>
+<input
+  crm-ui-time
+  ng-model="dtparts.time"
+  ng-required="required"
+  ng-class="{incomplete: !dtparts.time && (required||dtparts.date)}"
+  placeholder="{{timeLabel}}"/>
+<a
+  class="crm-hover-button"
+  ng-click="reset()"
+  ng-show="dtparts.date || dtparts.time"
+  title="Clear"
+  ><span class="icon ui-icon-close"></span></a>