Collections
A collection is a powerfull built in field type which craetes an array / list of grouped fields. If you for example want to collect a list of participants for a dinner, you could set that up like this in the config:
{
id: "participants",
type: "collection",
isRequired: true,
init: true,
fields: [
{
id: "name",
label: "Name",
type: "text",
isRequired: true
},
{
id: "vegan",
label: "Is vegan?",
type: "checkbox"
}
]
}
And than render it like this:
render={({ fieldProps, errors }) => {
return (
<>
<div>
{fieldProps.fields.participants ? fieldProps.fields.participants.map((subFields, index) => (
<div key={`participants-${index}`}>
{subFields.name}
<br />
{subFields.vegan}
<br />
<button
type="button"
onClick={() => fieldProps.onCollectionAction("participants", "remove", index)}
>
Remove entry
</button>
</div>)
) : null}
<button
type="button"
onClick={() => fieldProps.onCollectionAction("participants", "add")}
>
Add entry
</button>
</div>
{errors && errors.participants ? (
<div style={{ color: "red" }}>
Please add at least one entry!
</div>
) : null}
</>
);
}}
Collection have following options:
- id: This will be the key for the array in the data structure
- isRequired: If this is set, than at least one entry has to be added
- init: If this is true, one initial empty entry will be set
- min: Add a number for the minimum amount of entries
- max: Add a number for the maximum amount of entries
- uniqEntries: If true, all entries have to be uniq
- setInitialData: A callback which adds initial data to a newly created collection entry, with following params:
- data: The collections data
- alldata: All the data in the form
- type: In case of a union type collection, the type of the new entry
Computed Collections
Sometimes you want to compute values based on the data of the individual collection entries, you can do this with
the itemData
on the computedValue
function:
{
id: "factorials",
type: "collection",
init: true,
fields: [
{
id: "factor1",
label: "Factor 1",
type: "number"
},
{
id: "factor2",
label: "Factor 2",
type: "number"
},
{
id: "result",
label: "Result of Factor 1 x Factor 2",
type: "number",
isDisabled: true,
computedValue: (data, itemData) => {
let result = 0;
if (itemData.factor1 && itemData.factor2) {
result = Number(itemData.factor1) * Number(itemData.factor2);
}
return result !== 0 ? result : "";
}
}
]
}
## Union Type Collections
Like in GraphQL, a collection can have multiple different field configs, so called types. You define them like this:
{
id: "meals",
type: "collection",
isRequired: true,
init: "food",
fields: {
food: [
{
id: "name",
label: "Food Name",
type: "text",
isRequired: true
},
{
id: "calories",
label: "Calories",
type: "text"
}
],
drink: [
{
id: "name",
label: "Drink Name",
type: "text",
isRequired: true
},
{
id: "alcohol",
label: "Alcohol Percentage",
type: "text"
},
{
id: "fullName",
label: "Full Name",
type: "text",
isDisabled: true,
computedValue: (data, itemData) => {
return `${itemData.name} ${itemData.alcohol}%`;
}
}
]
}
}
The difference is that you need to put the field config array into an object with the keys for each type.
One thing to than change is in the Form render funtion, you need to explicitely define which type to add, something like this:
<button
type="button"
onClick={() => fieldProps.onCollectionAction("collection9", "add", "food")}
>
+ Food
</button>
Collection actions
In the examples above, we used onCollectionAction()
a lot. The function params are:
- path: The unique path of the collection you want to manipulate
- action: What you want to do. Currently you can use
add
,remove
,move
, 'duplicate' andsort
- index: The index of the collection entry or if the action is sort, the Lodash sortBy rule which can be an array of fieldkeys or a sort function
- index2: A second index. Currently only needed for
move
actions where it will be the destination (check the drag and drop example in the demos for an advanced example)
Collection Rules
Implementing validations for fields in complicated collections can be time consuming, that's why you now get collection rules which simplify this task a lot.
Let's start with an example to get a basic understanding of how you define collection validation rules:
{
id: "players",
type: "collection",
init: true,
min: 11,
max: 11,
rules: {
"position": {
"goalkeeper": { exactCount: 1 },
"defender": { minCount: 2, sameCountAs: "midfield" },
"striker": { minCount: 1 }
},
"prename,lastname": { "": { isUnique: true } }
},
fields: [
{
id: "prename",
type: "text",
label: "Prename",
isRequired: true
},
{
id: "lastname",
type: "text",
label: "Lastname",
isRequired: true
},
{
id: "position",
type: "radio",
label: "Position",
options: [
{ value: "goalkeeper", text: "Goalkeeper" },
{ value: "defender", text: "Defender" },
{ value: "midfield", text: "Midfield" },
{ value: "striker", text: "Striker" }
],
isRequired: true
}
]
}
The example is a collection for entering a football team. The collection is defined as containing exactly 11 team members.
The user needs to enter pre- and lastname and a position for each team member. To make shure the team has at least one
goalkeeper, a first rule is defined for the field position
as "goalkeeper": { exactCount: 1 }
, additionally we define
the defender position to have at least two members and for the team to be balanced, the midfield needs to have the same
amount: "defender": { minCount: 2, sameCountAs: "midfield" }
. The striker than is set to have at least one member. Finally,
we define that the combination of the prename
and lastname
fields have to be unique: "prename,lastname": { "": { isUnique: true } }
.
With these basic rules, we make shure that the entered team makes sense.
So let's dissect the basic structure of rules and later list the possible options. First, we always define which fields are affected.
This can either be a single field fieldname
or a combination of fields field1,field2
. For collections with nested fields, we can use
dot separated path syntax like this: coords.longitude
After selecting the fields, we select the affected values. For example to have exactly one value of "goalkeeper", we define that
like this: "position": { "goalkeeper": { exactCount: 1 } }
. As with fields, we can add multiple values by separating them with a
comma like this: ms,mr
. Sometimes we don't want to target specific values with our rules, in such a case we can just leave the
value field empty like this: "spending": { "": { biggerSumAs: "income" } }
.
And lastly, we need to define one or multiple rules like this: "position": { "defender": { minCount: 2, sameCountAs: "midfield" } }
.
Rule options
There are various rule options for different types of rule. Some play with the count of certain entries, some with calculations of field
values like the sameSumAs
option, some are collection specific rules of field properties like the isUnique
option, than we have
options to require or disallow certain values based on other values set in the collection.
- maxCount: Define the maximum count of a certain field value:
"position": { "defender": { maxCount: 3 } }
- minCount: Define the minimum count of a certain field value:
"position": { "striker": { minCount: 1 } }
- exactCount: Define the exact count of a certain field value:
"position": { "goalkeeper": { exactCount: 1 } }
- sameCountAs: Define two values to occur the same amount of times:
"position": { "defender": { sameCountAs: "midfield" } }
- differentCountAs: Define two values to occur different amount of times:
"position": { "midfield": { differentCountAs: "striker" } }
- sameSumAs: Define the sum of all values of a certain field to be the same as another fields sum:
"spending": { "": { sameSumAs: "income" } }
- differentSumAs: Define the sum of all values of a certain field to be different as another fields sum:
"spending": { "": { differentSumAs: "income" } }
- biggerSumAs: Define the sum of all values of a certain field to be bigger than another fields sum:
"spending": { "": { biggerSumAs: "income" } }
- smallerSumAs: Define the sum of all values of a certain field to be small than another fields sum:
"spending": { "": { smallerSumAs: "income" } }
- isUnique: Define a combination of fields to be unique:
"prename,lastname": { "": { isUnique: true } }
- disallow: Disallow values if a certain other value is set:
"gender": { "ms": { disallow: "mr" } }
- require: Require certain values if a certain other value is set:
"gender": { "ms": { require: "mr" } }
You can combine all this rules freely, just make sure that you don't setup rules which can never be valid in combination!
Additionally you can add error codes to each rule set, to render different error messages depending on which rule has been broken.
"prename,lastname": { "": { isUnique: true, errorCode: "uniqueNames" } }
If you miss a certain useful rule, you can define your own custom rule handlers like this:
<Form
customRuleHandlers={{
disallowOddNumbers: ({ fieldValueCombos, fieldValidationData, valueRules, get }) => {
// "age": { "": { disallowOddNumbers: true } }
let ruleConformsToData = true;
if (valueRules.disallowOddNumbers) {
fieldValueCombos.forEach(fieldValueCombo => {
fieldValidationData.forEach(d => {
const value = get(d, fieldValueCombo[0]);
if (value && value % 2 === 1) ruleConformsToData = false;
});
});
}
return ruleConformsToData;
}
}}
config={...}
render={...}
/>