Afform - Implement client-side validation of required fields
authorColeman Watts <coleman@civicrm.org>
Thu, 26 May 2022 18:27:03 +0000 (14:27 -0400)
committerColeman Watts <coleman@civicrm.org>
Sat, 3 Sep 2022 02:54:09 +0000 (22:54 -0400)
ext/afform/core/Civi/Afform/AfformMetadataInjector.php
ext/afform/core/ang/af/afField.html
ext/afform/core/ang/af/afForm.component.js
ext/afform/core/ang/af/fields/ChainSelect.html
ext/afform/core/ang/af/fields/CheckBox.html
ext/afform/core/ang/af/fields/Date.html
ext/afform/core/ang/af/fields/EntityRef.html
ext/afform/core/ang/af/fields/File.html
ext/afform/core/ang/af/fields/Number.html
ext/afform/core/ang/af/fields/Text.html
ext/afform/core/ang/af/fields/TextArea.html

index bf39499559972cfb9368d8178ef21ac2299ee735..7b8eeafaf9b105eeb50433eb1de2b9797a43d7a1 100644 (file)
@@ -30,6 +30,11 @@ class AfformMetadataInjector {
         try {
           $module = \Civi::service('angular')->getModule(basename($path, '.aff.html'));
           $meta = \Civi\Api4\Afform::get(FALSE)->addWhere('name', '=', $module['_afform'])->setSelect(['join_entity', 'entity_type'])->execute()->first();
+
+          // Add ngForm directive to afForm controllers
+          foreach (pq('af-form[ctrl]') as $afForm) {
+            pq($afForm)->attr('ng-form', $module['_afform']);
+          }
         }
         catch (\Exception $e) {
         }
index 88e226c43b63e2a3fe973c79596f90650c97db1b..1965f74c7f629d868deac78b5aa62343ea6eac91 100644 (file)
@@ -1,5 +1,6 @@
 <label class="crm-af-field-label" ng-if=":: $ctrl.defn.label" for="{{:: fieldId }}">
   {{:: $ctrl.defn.label }}
+  <span class="crm-marker" title="{{:: ts('Required') }}" ng-if=":: $ctrl.defn.required">*</span>
 </label>
 <p class="crm-af-field-help-pre" ng-if=":: $ctrl.defn.help_pre">{{:: $ctrl.defn.help_pre }}</p>
 <div class="crm-af-field" ng-include="'~/af/fields/' + $ctrl.defn.input_type + '.html'"></div>
index dbaa9c910490f4bc89f970ddaa47d6bd71e0777e..eae28a02c7cec6321dbd96ae0719b3f43237b518 100644 (file)
@@ -4,6 +4,9 @@
     bindings: {
       ctrl: '@'
     },
+    require: {
+      ngForm: 'form'
+    },
     controller: function($scope, $element, $timeout, crmApi4, crmStatus, $window, $location, $parse, FileUploader) {
       var schema = {},
         data = {},
       }
 
       this.submit = function() {
+        if (!ctrl.ngForm.$valid) {
+          CRM.alert(ts('Please fill all required fields.'), ts('Form Error'));
+          return;
+        }
         status = CRM.status({});
         $element.block();
 
index d1763602ff3f5b056513ba6f8a9383e98057b669..ed756897ad8d8a359e217cd2cb055321bd5e3a74 100644 (file)
@@ -1 +1 @@
-<input class="form-control" crm-ui-select="{data: select2Options, multiple: $ctrl.defn.input_attrs.multiple, placeholder: $ctrl.defn.input_attrs.placeholder}" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" />
+<input class="form-control" ng-required="$ctrl.defn.required" crm-ui-select="{data: select2Options, multiple: $ctrl.defn.input_attrs.multiple, placeholder: $ctrl.defn.input_attrs.placeholder}" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" />
index f2edac2a4e5f552040899bac96f73028e367b690..169baa2599b73cd6db287e72c7954f505f7ac1f2 100644 (file)
@@ -4,4 +4,4 @@
     <label for="{{ fieldId + opt.id }}">{{:: opt.label }}</label>
   </li>
 </ul>
-<input type="checkbox" ng-if="!$ctrl.defn.options" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" />
+<input type="checkbox" ng-required="$ctrl.defn.required" ng-if="!$ctrl.defn.options" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" />
index 2707b64a4aa1954c6611542154ee219063c390cd..2722191d28815fbf2a1adafb0f5ae29b2b62d8f9 100644 (file)
@@ -1,6 +1,6 @@
 <input ng-if=":: !$ctrl.defn.search_range" class="form-control" crm-ui-datepicker=":: $ctrl.defn.input_attrs" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" />
 <div ng-if=":: $ctrl.defn.search_range" class="form-inline">
-  <input class="form-control" crm-ui-datepicker=":: $ctrl.inputAttrs[1]" id="{{:: fieldId }}1" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]['>=']" />
+  <input class="form-control" ng-required="$ctrl.defn.required" crm-ui-datepicker=":: $ctrl.inputAttrs[1]" id="{{:: fieldId }}1" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]['>=']" />
   <span class="af-field-range-sep">-</span>
   <input class="form-control" crm-ui-datepicker=":: $ctrl.inputAttrs[2]" id="{{:: fieldId }}2" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]['<=']" />
 </div>
index 2874725fbf2528da616c1288a0cf259f9442db06..70cb8d2284cbd1b65d147b26340db92d5929dddc 100644 (file)
@@ -1 +1 @@
-<input class="form-control" id="{{:: fieldId }}" ng-model="getSetSelect" ng-model-options="{getterSetter: true}" crm-entityref="{entity: $ctrl.defn.fk_entity, select: {multiple: !!$ctrl.defn.input_attrs.multiple, placeholder: $ctrl.defn.input_attrs.placeholder}}" >
+<input class="form-control" id="{{:: fieldId }}" ng-required="$ctrl.defn.required" ng-model="getSetSelect" ng-model-options="{getterSetter: true}" crm-entityref="{entity: $ctrl.defn.fk_entity, select: {multiple: !!$ctrl.defn.input_attrs.multiple, placeholder: $ctrl.defn.input_attrs.placeholder}}" >
index 1412e80138344b6f49c156f465476574af4e49bb..c02d56d8bb1e6520cc57dfc2f4569f9bbffa3ce1 100644 (file)
@@ -1,3 +1,4 @@
 <input type="file" nv-file-select
+       ng-required="$ctrl.defn.required"
        uploader="$ctrl.afFieldset.afFormCtrl.fileUploader"
        options="{crmApiParams: $ctrl.getFileUploadParams}">
index 2675bcc8652bb63bda42c5bfa1b60f2bd1571d3c..3980ec65a8cb6cde94cc8aa999c2d6751217b546 100644 (file)
@@ -1,4 +1,4 @@
-<input ng-if=":: !$ctrl.defn.search_range" class="form-control" type="number" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}" >
+<input ng-if=":: !$ctrl.defn.search_range" class="form-control" ng-required="$ctrl.defn.required" type="number" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}" >
 <div ng-if=":: $ctrl.defn.search_range" class="form-inline">
   <input class="form-control" type="number" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]['>=']" placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}" >
   <span class="af-field-range-sep">-</span>
index 862fa07322d2f04b185786b32754899a774a4a6e..fe9c3cf138f8e501a98bd78acd7889ae6baa9a1b 100644 (file)
@@ -1,4 +1,4 @@
-<input ng-if=":: !$ctrl.defn.search_range" class="form-control" type="text" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}" >
+<input ng-if=":: !$ctrl.defn.search_range" class="form-control" type="text" ng-required="$ctrl.defn.required" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}" >
 <div ng-if=":: $ctrl.defn.search_range" class="form-inline">
   <input class="form-control" type="text" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]['>=']" placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}" >
   <span class="af-field-range-sep">-</span>
index 6ded2d4f1ad5ab2764bca31dddf72aa6db21c186..3d10e3acbf3457f98b434762a256ed1c39e9c3d7 100644 (file)
@@ -1 +1 @@
-<textarea class="crm-form-textarea" id="{{:: fieldId }}" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" ></textarea>
+<textarea class="crm-form-textarea" id="{{:: fieldId }}" ng-required="$ctrl.defn.required" ng-model="dataProvider.getFieldData()[$ctrl.fieldName]" ></textarea>