1 (function(angular
, $, _
) {
3 // Example usage: <div af-fieldset="myModel"><af-field name="do_not_email" /></div>
4 angular
.module('af').component('afField', {
6 afFieldset
: '^^afFieldset',
8 afRepeatItem
: '?^^afRepeatItem'
10 templateUrl
: '~/af/afField.html',
15 controller: function($scope
, $element
, crmApi4
, $timeout
, $location
) {
16 var ts
= $scope
.ts
= CRM
.ts('org.civicrm.afform'),
18 // Prefix used for SearchKit explicit joins
20 boolOptions
= [{id
: true, label
: ts('Yes')}, {id
: false, label
: ts('No')}],
21 // Used to store chain select options loaded on-the-fly
22 chainSelectOptions
= null,
23 // Only used for is_primary radio button
24 noOptions
= [{id
: true, label
: ''}];
26 // Attributes for each of the low & high date fields when using search_range
29 this.$onInit = function() {
30 var closestController
= $($element
).closest('[af-fieldset],[af-join],[af-repeat-item]');
31 $scope
.dataProvider
= closestController
.is('[af-repeat-item]') ? ctrl
.afRepeatItem
: ctrl
.afJoin
|| ctrl
.afFieldset
;
32 $scope
.fieldId
= ctrl
.fieldName
+ '-' + id
++;
34 $element
.addClass('af-field-type-' + _
.kebabCase(ctrl
.defn
.input_type
));
36 if (this.defn
.name
!== this.fieldName
) {
37 namePrefix
= this.fieldName
.substr(0, this.fieldName
.length
- this.defn
.name
.length
);
40 if (ctrl
.defn
.search_range
) {
41 // Initialize value as object unless using relative date select
42 var initialVal
= $scope
.dataProvider
.getFieldData()[ctrl
.fieldName
];
43 if (!_
.isArray($scope
.dataProvider
.getFieldData()[ctrl
.fieldName
]) &&
44 (ctrl
.defn
.input_type
!== 'Select' || !ctrl
.defn
.is_date
|| initialVal
!== '{}')
46 $scope
.dataProvider
.getFieldData()[ctrl
.fieldName
] = {};
48 // Initialize inputAttrs (only used for datePickers at the moment)
49 if (ctrl
.defn
.is_date
) {
50 this.inputAttrs
.push(ctrl
.defn
.input_attrs
|| {});
51 for (var i
= 1; i
<= 2; ++i
) {
52 var attrs
= _
.cloneDeep(ctrl
.defn
.input_attrs
|| {});
53 attrs
.placeholder
= attrs
['placeholder' + i
];
54 attrs
.timePlaceholder
= attrs
['timePlaceholder' + i
];
55 ctrl
.inputAttrs
.push(attrs
);
60 // is_primary field - watch others in this afRepeat block to ensure only one is selected
61 if (ctrl
.fieldName
=== 'is_primary' && 'repeatIndex' in $scope
.dataProvider
) {
62 $scope
.$watch('dataProvider.afRepeat.getEntityController().getData()', function (items
, prev
) {
63 var index
= $scope
.dataProvider
.repeatIndex
;
64 // Set first item to primary if there isn't a primary
65 if (items
&& !index
&& !_
.find(items
, 'is_primary')) {
66 $scope
.dataProvider
.getFieldData().is_primary
= true;
68 // Set this item to not primary if another has been selected
69 if (items
&& prev
&& items
.length
=== prev
.length
&& items
[index
].is_primary
&& prev
[index
].is_primary
&&
70 _
.filter(items
, 'is_primary').length
> 1
72 $scope
.dataProvider
.getFieldData().is_primary
= false;
77 // ChainSelect - watch control field & reload options as needed
78 if (ctrl
.defn
.input_type
=== 'ChainSelect') {
79 var controlField
= namePrefix
+ ctrl
.defn
.input_attrs
.control_field
;
80 $scope
.$watch('dataProvider.getFieldData()["' + controlField
+ '"]', function(val
) {
81 // After switching option list, remove invalid options
82 function validateValue() {
83 var options
= $scope
.getOptions(),
84 value
= $scope
.dataProvider
.getFieldData()[ctrl
.fieldName
];
85 if (_
.isArray(value
)) {
86 _
.remove(value
, function(item
) {
87 return !_
.find(options
, function(option
) {return option
.id
== item
;});
89 } else if (value
&& !_
.find(options
, function(option
) {return option
.id
== value
;})) {
90 $scope
.dataProvider
.getFieldData()[ctrl
.fieldName
] = '';
93 if (val
&& (typeof val
=== 'number' || val
.length
)) {
94 $('input[crm-ui-select]', $element
).addClass('loading').prop('disabled', true);
96 name
: ctrl
.afFieldset
.getFormName(),
97 modelName
: ctrl
.afFieldset
.getName(),
98 fieldName
: ctrl
.fieldName
,
99 joinEntity
: ctrl
.afJoin
? ctrl
.afJoin
.entity
: null,
100 values
: $scope
.dataProvider
.getFieldData()
102 crmApi4('Afform', 'getOptions', params
)
103 .then(function(data
) {
104 $('input[crm-ui-select]', $element
).removeClass('loading').prop('disabled', !data
.length
);
105 chainSelectOptions
= data
;
109 chainSelectOptions
= null;
115 // Wait for parent controllers to initialize
116 $timeout(function() {
117 // Unique field name = entity_name index . join . field_name
118 var entityName
= ctrl
.afFieldset
.getName(),
119 joinEntity
= ctrl
.afJoin
? ctrl
.afJoin
.entity
: null,
121 urlArgs
= $location
.search();
123 var index
= ctrl
.getEntityIndex();
124 uniquePrefix
= entityName
+ (index
? index
+ 1 : '') + (joinEntity
? '.' + joinEntity
: '') + '.';
126 // Set default value from url with uniquePrefix + fieldName
127 if (urlArgs
&& urlArgs
[uniquePrefix
+ ctrl
.fieldName
]) {
128 setValue(urlArgs
[uniquePrefix
+ ctrl
.fieldName
]);
130 // Set default value from url with fieldName only
131 else if (urlArgs
&& urlArgs
[ctrl
.fieldName
]) {
132 $scope
.dataProvider
.getFieldData()[ctrl
.fieldName
] = urlArgs
[ctrl
.fieldName
];
134 // Set default value based on field defn
135 else if (ctrl
.defn
.afform_default
) {
136 setValue(ctrl
.defn
.afform_default
);
141 // Set default value; ensure data type matches input type
142 function setValue(value
) {
143 if (ctrl
.defn
.input_type
=== 'Number' && ctrl
.defn
.search_range
) {
144 if (!_
.isPlainObject(value
)) {
146 '>=': +(('' + value
).split('-')[0] || 0),
147 '<=': +(('' + value
).split('-')[1] || 0),
150 } else if (ctrl
.defn
.input_type
=== 'Number') {
152 } else if (ctrl
.defn
.search_range
&& !_
.isPlainObject(value
)) {
154 '>=': ('' + value
).split('-')[0],
155 '<=': ('' + value
).split('-')[1] || '',
159 $scope
.dataProvider
.getFieldData()[ctrl
.fieldName
] = value
;
162 // Get the repeat index of the entity fieldset (not the join)
163 ctrl
.getEntityIndex = function() {
164 // If already in a join repeat, look up the outer repeat
165 if ('repeatIndex' in $scope
.dataProvider
&& $scope
.dataProvider
.afRepeat
.getRepeatType() === 'join') {
166 return $scope
.dataProvider
.outerRepeatItem
? $scope
.dataProvider
.outerRepeatItem
.repeatIndex
: 0;
168 return ctrl
.afRepeatItem
? ctrl
.afRepeatItem
.repeatIndex
: 0;
172 // Params for the Afform.submitFile API when uploading a file field
173 ctrl
.getFileUploadParams = function() {
175 modelName
: ctrl
.afFieldset
.getName(),
176 fieldName
: ctrl
.fieldName
,
177 joinEntity
: ctrl
.afJoin
? ctrl
.afJoin
.entity
: null,
178 entityIndex
: ctrl
.getEntityIndex(),
179 joinIndex
: ctrl
.afJoin
&& $scope
.dataProvider
.repeatIndex
|| null
183 $scope
.getOptions = function () {
184 return chainSelectOptions
|| ctrl
.defn
.options
|| (ctrl
.fieldName
=== 'is_primary' && ctrl
.defn
.input_type
=== 'Radio' ? noOptions
: boolOptions
);
187 $scope
.select2Options = function() {
189 results
: _
.transform($scope
.getOptions(), function(result
, opt
) {
190 result
.push({id
: opt
.id
, text
: opt
.label
});
195 // Getter/Setter function for fields of type select or entityRef.
196 $scope
.getSetSelect = function(val
) {
197 var currentVal
= $scope
.dataProvider
.getFieldData()[ctrl
.fieldName
];
199 if (arguments
.length
) {
200 if (ctrl
.defn
.is_date
) {
201 // The '{}' string is a placeholder for "choose date range"
203 val
= !_
.isPlainObject(currentVal
) ? {} : currentVal
;
206 // If search_range, this select is the "low" value (the high value uses ng-model without a getterSetter fn)
207 else if (ctrl
.defn
.search_range
) {
208 return ($scope
.dataProvider
.getFieldData()[ctrl
.fieldName
]['>='] = val
);
210 // A multi-select needs to split string value into an array
211 if (ctrl
.defn
.input_attrs
&& ctrl
.defn
.input_attrs
.multiple
) {
212 val
= val
? val
.split(',') : [];
214 return ($scope
.dataProvider
.getFieldData()[ctrl
.fieldName
] = val
);
217 if (_
.isArray(currentVal
)) {
218 return currentVal
.join(',');
220 if (ctrl
.defn
.is_date
) {
221 return _
.isPlainObject(currentVal
) ? '{}' : currentVal
;
223 // If search_range, this select is the "low" value (the high value uses ng-model without a getterSetter fn)
224 else if (ctrl
.defn
.search_range
) {
225 return currentVal
['>='];
232 })(angular
, CRM
.$, CRM
._
);