CRM-14798 - crmCaseType - Fix handling of labels+validation
authorTim Otten <totten@civicrm.org>
Tue, 1 Jul 2014 04:08:44 +0000 (21:08 -0700)
committerTim Otten <totten@civicrm.org>
Tue, 1 Jul 2014 04:39:04 +0000 (21:39 -0700)
In our UX, labels are supposed to turn red when a field becomes invalid, and
they're supposed to include a red "*" if required.  This adds an Angular
directive to enforce both.

css/civicrm.css
js/angular-crm-ui.js
partials/crmCaseType/caseTypeDetails.html

index 6ad2f09dcc7214b0d3fa326b1a6a47d69cd9c23b..d56789b8ce910b37577de704bb042d5bdf92674d 100644 (file)
@@ -4715,9 +4715,9 @@ span.crm-status-icon {
   text-decoration: line-through;
 }
 
-.crm-container .ng-invalid {
+.crm-container .ng-invalid.ng-dirty {
     border: 1px solid red;
 }
-.crm-container .ng-valid {
+.crm-container .ng-valid, .crm-container .ng-pristine {
     border: 1px solid #666;
 }
index 52c02a2739d3daab266396f85ab126596ec862e1..db8272200780c304ed0086d93a840c36020a2f0b 100644 (file)
@@ -1,7 +1,70 @@
 /// crmUi: Sundry UI helpers
 (function (angular, $, _) {
+  var idCount = 0;
 
   angular.module('crmUi', [])
+    // example: <form name="myForm">...<label crm-ui-label crm-for="myField">My Field</span>...<input name="myField"/>...</form>
+    //
+    // Label adapts based on <input required>, <input ng-required>, or any other validation.
+    //
+    // Note: This should work in the normal case where <label> and <input> are in roughly the same scope,
+    // but if the scopes are materially different then problems could arise.
+    .directive('crmUiLabel', function($parse) {
+      return {
+        scope: {
+          name: '@'
+        },
+        transclude: true,
+        template: '<span ng-class="cssClasses"><span ng-transclude></span> <span ng-show="crmRequired" class="crm-marker" title="This field is required.">*</span></span>',
+        link: function(scope, element, attrs) {
+          if (attrs.crmFor == 'name') {
+            throw new Error('Validation monitoring does not work for field name "name"');
+          }
+
+          // 1. Figure out form and input elements
+
+          var form = $(element).closest('form');
+          var formCtrl = scope.$parent.$eval(form.attr('name'));
+          var input = $('input[name="' + attrs.crmFor + '"],select[name="' + attrs.crmFor + '"],textarea[name="' + attrs.crmFor + '"]', form);
+          if (form.length != 1 || input.length != 1) {
+            if (console.log) console.log('Label cannot be matched to input element. Expected to find one form and one input.', form.length, input.length);
+            return;
+          }
+
+          // 2. Make sure that inputs are well-defined (with name+id).
+
+          if (!input.attr('id')) {
+            input.attr('id', 'crmUi_' + (++idCount));
+          }
+          $(element).attr('for', input.attr('id'));
+
+          // 3. Monitor is the "required" and "$valid" properties
+
+          if (input.attr('ng-required')) {
+            scope.crmRequired = scope.$parent.$eval(input.attr('ng-required'));
+            scope.$parent.$watch(input.attr('ng-required'), function(isRequired) {
+              scope.crmRequired = isRequired;
+            });
+          } else {
+            scope.crmRequired = input.prop('required');
+          }
+
+          var inputCtrl = form.attr('name') + '.' + input.attr('name');
+          scope.cssClasses = {};
+          scope.$parent.$watch(inputCtrl + '.$valid', function(newValue) {
+            //scope.cssClasses['ng-valid'] = newValue;
+            //scope.cssClasses['ng-invalid'] = !newValue;
+            scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
+          });
+          scope.$parent.$watch(inputCtrl + '.$pristine', function(newValue) {
+            //scope.cssClasses['ng-pristine'] = newValue;
+            //scope.cssClasses['ng-dirty'] = !newValue;
+            scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
+          });
+
+        }
+      };
+    })
 
     // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
     // example: <a crm-ui-lock
index c6c6dfc045e97f89a163dbcd29c33213ab0c6a78..c89a5332b30058d9221eecb0cf69f239aa09421f 100644 (file)
@@ -7,28 +7,58 @@ The original form used table layout; don't know if we have an alternative, CSS-b
 <table class="form-layout">
   <tbody>
   <tr>
-    <td class="label">Label</td>
+    <td class="label">
+      <label crm-ui-label crm-for="title">
+        Title
+      </label>
+    </td>
     <td>
-      <input type="text" ng-model="caseType.title" class="big crm-form-text"/>
+      <input
+        type="text"
+        name="title"
+        ng-model="caseType.title"
+        class="big crm-form-text"
+        required
+        />
     </td>
   </tr>
   <tr>
-    <td class="label">Name</td>
+    <td class="label">
+      <label crm-ui-label crm-for="caseTypeName">
+        Name
+      </label>
+    </td>
     <td>
-      <input type="text" ng-model="caseType.name" ng-disabled="locks.caseTypeName" class="big crm-form-text"/>
+      <input
+        type="text"
+        name="caseTypeName"
+        ng-model="caseType.name"
+        ng-disabled="locks.caseTypeName"
+        ng-pattern="/^[a-zA-Z0-9_]+$/"
+        required
+        class="big crm-form-text"/>
+
       <a crm-ui-lock binding="locks.caseTypeName"></a>
     </td>
   </tr>
   <tr>
-    <td class="label">Description</td>
+    <td class="label">
+      <label crm-ui-label crm-for="description">
+        Description
+      </label>
+    </td>
     <td>
-      <textarea ng-model="caseType.description" class="big crm-form-textarea"></textarea>
+      <textarea name="description" ng-model="caseType.description" class="big crm-form-textarea"></textarea>
     </td>
   </tr>
   <tr>
-    <td class="label">Enabled?</td>
+    <td class="label">
+      <label crm-ui-label crm-for="is_active">
+        Enabled?
+      </label>
+    </td>
     <td>
-      <input type="checkbox" ng-model="caseType.is_active" ng-true-value="1" ng-false-value="0"/>
+      <input name="is_active" type="checkbox" ng-model="caseType.is_active" ng-true-value="1" ng-false-value="0"/>
     </td>
   </tr>
   </tbody>