Transformers
More often than not, the data provided by your end-users won't be in perfect shape. Extra trailing spaces, incorrectly formatted phone numbers, and other frequent irregularities that take time and effort to resolve.
importOK transformers are designed specifically to resolve this, by allowing you to apply common fixes. The transformation happens right after the file is uploaded and before the validation.
Let's see how we can extend our previous example, to make sure that all values have no leading or trailing spaces. For that purpose, we will be using the trim transformer.
1[ 2 { 3 "label": "First name", 4 "transformers": "trim" 5 }, 6 { 7 "label": "Last name", 8 "transformers": "trim" 9 },10 {11 "label": "Phone",12 "transformers": "trim"13 },14 {15 "label": "Email",16 "transformers": "trim"17 },18 {19 "label": "Country",20 "transformers": "trim"21 }22]
Chaining transformers
Let's see how we can add more transformers. We are looking to capitalize the first and last name, make sure that emails are always in lower case and that the country code is always in upper case. You can chain multiple transformers using the pipe syntax.
1[ 2 { 3 "label": "First name", 4 "transformers": "trim|capitalize" 5 }, 6 { 7 "label": "Last name", 8 "transformers": "trim|capitalize" 9 },10 {11 "label": "Phone",12 "transformers": "trim"13 },14 {15 "label": "Email",16 "transformers": "trim|lowercase"17 },18 {19 "label": "Country",20 "transformers": "trim|uppercase"21 }22]
Available transformers
| Transformer | Description |
|---|---|
as:*provider* |
Uses the specified data provider to convert a value to the right label, or even finding the right value if a label was provided. |
capitalize |
Capitalizes the first letter of the string. |
date |
Converts a date from Excel format or Unix timestamp into YYYY-MM-DD. |
integer |
Converts the string to a numeric string, by removing any non digits. In case the string contains no digits, it remains unchanged. |
lowercase |
Converts the string to lowercase letters. |
number |
Converts the string to a numeric string (with floating-point), by removing any non digits. In case the string contains no digits, it remains unchanged. |
replace:*a*,*b* |
Replaces all matches of a with b |
trim |
Removes trailing and leading spaces from the string. |
uppercase |
Converts the string to uppercase letters. |
Custom transformers
On top of the built-in transformers, you can always build your own transformers so that you can auto heal and format the data based on your requirements. To do this you will need to name your transformer and associate it with a callback to be called every time it is used. You can add as many transformers as necessary.
1{ 2 "transformers": { 3 "my-transformer": function(record, field) { 4 ... 5 }, 6 "my-transformer-with-single-arg": function(record, field, arg1) { 7 ... 8 }, 9 "my-transformer-with-multiple-args": function(record, field, arg1, arg2, arg3) {10 ...11 }12 }13}
Let's see how we can add a transformer called phone to reformat phone numbers using the google-libphonenumber library. Please note that you cannot overwrite a transformer that already exists and you will get an error if you try to do that.
1{ 2 "transformers": { 3 "phone": function (record, key) { 4 const value = record.get(key); 5 try { 6 const parsedValue = parsePhoneNumberWithError(value, { 7 defaultCountry: 'US' 8 }); 9 if (!parsedValue.isValid()) {10 return value;11 }12 13 if (parsedValue.country === defaultCountry) {14 return parsedValue.formatNational();15 }16 17 return parsedValue.formatInternational();18 } catch (e) {19 return value;20 }21 }22}
That's all! You can use the pipe syntax to use the new transformer.
1{ 2 "fields": [ 3 { 4 "label": "First name", 5 "transformers": "trim" 6 }, 7 { 8 "label": "Last name", 9 "transformers": "trim"10 },11 {12 "label": "Phone",13 "transformers": "trim|phone"14 },15 {16 "label": "Email",17 "transformers": "trim"18 }19 ]20}
Passing additional arguments
Transformers can be made more dynamic by accepting additional arguments. Let's enhance our example to accept a country code as the first argument. If no argument is provided we will default to US.
1{ 2 "transformers": { 3 "phone": function (record, key, defaultCountry) { 4 const value = record.get(key); 5 try { 6 const parsedValue = parsePhoneNumberWithError(value, { 7 defaultCountry: defaultCountry || 'US' 8 }); 9 if (!parsedValue.isValid()) {10 return value;11 }12 13 if (parsedValue.country === defaultCountry) {14 return parsedValue.formatNational();15 }16 17 return parsedValue.formatInternational();18 } catch (e) {19 return value;20 }21 }22}
You can now pass the country code as indicated below.
1{ 2 "fields": [ 3 { 4 "label": "First name", 5 "transformers": "trim" 6 }, 7 { 8 "label": "Last name", 9 "transformers": "trim"10 },11 {12 "label": "Phone",13 "transformers": "trim|phone:UK"14 },15 {16 "label": "Email",17 "transformers": "trim"18 }19 ]20}
Merging and Splitting Fields
Advanced transformers can read and write multiple fields on a record, enabling powerful data manipulation patterns. This allows you to combine multiple source columns into one field (merge) or break one source column into multiple fields (split).
Since every transformer receives the full ImportRecord as its first argument, you can:
- Read other fields via
record.get(fieldKey)to build combined values - Write to other fields via
record.set(fieldKey, value)to populate derived fields
Merging multiple fields into one
When your source file has separate fields that need to be combined, create a custom transformer to merge them.
For example, to combine separate first_name and last_name columns into a single full_name field:
1ImportOK.add('importok-wizard', { 2 fields: { 3 first_name: { 4 label: 'First Name', 5 transformers: 'trim', 6 }, 7 last_name: { 8 label: 'Last Name', 9 transformers: 'trim',10 },11 full_name: {12 label: 'Full Name',13 transformers: 'concat:first_name,last_name',14 },15 },16 17 transformers: {18 concat: (record, _key, ...sourceFields) => {19 return sourceFields20 .map(f => record.get(f) ?? '')21 .filter(Boolean)22 .join(' ');23 },24 },25});
The full_name field can be left unmapped in the mapping step since its value is derived entirely from the source fields.
Field definition order
Transformers execute in the order fields are defined. When merging fields, define source fields before the merged field. This ensures the merge transformer reads already-transformed values.
1fields: {2 first_name: { transformers: 'trim' }, // runs first3 last_name: { transformers: 'trim' }, // runs second4 full_name: { transformers: '...' }, // reads trimmed values5}
Splitting one field into multiple
When your source file has a combined field that needs to be separated, create a custom transformer to split it.
For example, to split a single full_name column into separate first_name and last_name fields:
1ImportOK.add('importok-wizard', { 2 fields: { 3 full_name: { 4 label: 'Full Name', 5 transformers: 'split_into:first_name,last_name', 6 }, 7 first_name: { 8 label: 'First Name', 9 },10 last_name: {11 label: 'Last Name',12 },13 },14 15 transformers: {16 split_into: (record, key, ...targetFields) => {17 const parts = (record.get(key) ?? '').split(' ');18 targetFields.forEach((field, i) => record.set(field, parts[i] ?? ''));19 return record.get(key) ?? '';20 },21 },22});
In the mapping step, map full_name to the source column and leave first_name and last_name unmapped since they're populated automatically by the transformer. Don't add a required validator to unmapped target fields.