While using Google Suite, I had this crazy idea! I wanted to replicate some of this email address entering magic in my next Appian application.
As I would rather not bother you with all the loops I had to go through, let’s put some plutonium and banana peels into our flux compensator and jump a few hours into the future.
Here you go:

Just enter some addresses and hit enter. All valid addresses are added on top.
Features
- Maximum number of addresses
- Address syntax validation using regex (Regex plugin required)
- Accept any separator when entering multiple addresses, even mixed
- Read-only mode with clickable addresses
- Additional validations
- Instructions text



Quirks & Tricks
There is an interesting trick in that code. When you enter some text in a text field and hit the enter button, it evaluates the saveInto, but it keeps the entered text, even if you set the value to NULL. I solve this by using two text fields, showing only one of them at the same time. When hitting enter, the code switches to the other one.
The component shows entered addresses in multiple rows and adjusts the number of items per row to the screen size.
I did not add a label, as I was not able to find a good generic way of implementation. Please add any icons or labels you need.
Interface Code
This code depends on the rule “PFX_SegmentList” that splits a list into segments. Find it here. You also need to install the regex plugin used to find valid addresses in the text entered.
Create the following rule inputs:
- value (List of Text String)
- saveInto (List of Save)
- placeholder (Text)
- marginBelow (Text)
- requiredMessage (Text)
- required (Boolean)
- validations (Any Type)
- instructions (Text)
- maxSelections (Integer)
- itemsColor (Text)
- showWhen (Boolean)
- readOnly (Boolean)
a!localVariables(
local!textFieldSwitchCounter: 0,
local!emailRegexPattern: "[\w-\.]+@([\w-]+\.)+[\w-]{2,255}",
{
/*
Create rows of columns
Each cell is made of a card layout containing
the email address plus the icon
*/
a!cardLayout(
padding: "EVEN_LESS",
showWhen: and(
ri!showWhen <> false,
or(
a!isNotNullOrEmpty(ri!value),
ri!readOnly = true
)
),
marginBelow: if(ri!readOnly, a!defaultValue(ri!marginBelow, "STANDARD"), "NONE"),
style: if(
ri!readOnly,
"#ffffff",
if(
count(ri!value) > a!defaultValue(ri!maxSelections, 999),
"ERROR",
"#d4d4d4"
)
),
contents: {
a!cardLayout(
showWhen: a!isNullOrEmpty(ri!value),
style: a!defaultValue(ri!itemsColor, if(ri!readOnly, "#f0f0f0", "#ffffff")),
shape: "ROUNDED",
padding: "EVEN_LESS",
showBorder: false,
marginBelow: "NONE",
contents: a!richTextDisplayField(
labelPosition: "COLLAPSED",
preventWrapping: true,
value: {
char(160),
a!richTextIcon(
icon: "info",
),
char(160),
"No email recipients available"
}
),
),
a!forEach(
/*
Turn the list of emails into a 2-dimensional matrix
and adjust the number of items per row to the screen size
*/
items: rule!PFX_SegmentList( /* Find that expression code here https://appian.rocks/2022/09/19/building-customisable-components/ */
items: ri!value,
enablePadding: true,
segmentSize: if(a!isPageWidth("PHONE"), 1,
if(a!isPageWidth({"TABLET_PORTRAIT"}), 3,
if(a!isPageWidth({"DESKTOP_NARROW", "TABLET_LANDSCAPE"}), 4,
if(a!isPageWidth({"DESKTOP"}), 6,
8
))))
),
/* Create a row of columns */
expression: a!columnsLayout(
spacing: "DENSE",
marginBelow: if(
fv!isLast,
"NONE",
"EVEN_LESS"
),
columns: {
/* Each address goes into a separate column */
a!forEach(
items: fv!item,
expression: a!columnLayout(
width: "NARROW",
contents: a!cardLayout(
showWhen: a!isNotNullOrEmpty(fv!item),
style: a!defaultValue(ri!itemsColor, if(ri!readOnly, "#f0f0f0", "#ffffff")),
shape: "ROUNDED",
padding: "EVEN_LESS",
showBorder: false,
marginBelow: "NONE",
contents: a!sideBySideLayout(
alignVertical: "MIDDLE",
spacing: "DENSE",
items: {
a!sideBySideItem(
item: a!richTextDisplayField(
labelPosition: "COLLAPSED",
preventWrapping: true,
value: {
char(160),
a!richTextIcon(
icon: "envelope-o",
color: "#777777"
),
char(160),
a!richTextItem(
text: fv!item,
link: a!safeLink(
showWhen: ri!readOnly = true,
uri: "mailto:" & fv!item,
openLinkIn: "NEW_TAB"
)
)
}
),
),
a!sideBySideItem(
item: a!richTextDisplayField(
labelPosition: "COLLAPSED",
value: {
a!richTextIcon(
icon: "times",
link: a!dynamicLink(
saveInto: a!save(
target: ri!value,
value: remove(ri!value, fv!index)
)
),
linkStyle: "STANDALONE",
color: "#777777",
caption: "Remove ",
altText: "Remove",
),
}
),
width: "MINIMIZE",
showWhen: ri!readOnly <> true
),
}
)
)
)
),
a!columnLayout(
/*
Show this only when there might be more than one item per row
to prevent excessive spacing between rows
*/
showWhen: not(a!isPageWidth("PHONE"))
)
},
)
),
}
),
/*
This switches between two text fields to get rid of the keyboard focus
and the text still shown in the text field.
*/
a!cardLayout(
showWhen: and(
ri!showWhen <> false,
ri!readOnly <> true,
mod(local!textFieldSwitchCounter, 2) = 0,
),
marginBelow: a!defaultValue(ri!marginBelow, "STANDARD"),
padding: "NONE",
showBorder: false,
contents: a!textField(
labelPosition: "COLLAPSED",
instructions: ri!instructions,
disabled: count(ri!value) >= a!defaultValue(ri!maxSelections, 999),
placeholder: a!defaultValue(ri!placeholder, "Type and hit enter to add a recipient ..."),
value: if(
count(ri!value) >= a!defaultValue(ri!maxSelections, 999),
"Maximum number of recipients reached",
null
),
required: if(a!isNotNullOrEmpty(ri!value), false, ri!required),
requiredMessage: ri!requiredMessage,
validations: if(
ri!readOnly,
null,
{
if(
count(ri!value) > a!defaultValue(ri!maxSelections, 999),
concat(
"Only ",
ri!maxSelections,
" items allowed"
),
null
),
ri!validations
}
),
saveInto: {
/*
Find and append all email addresses in the text.
The regex handles any king of divider like space, comma etc...
by just looking for valid email strings
Ignore any invalids and already known ones
Switch to the other text field in case the number of email addresses changed
*/
a!localVariables(
local!valueCount: if(
a!isNullOrEmpty(ri!value),
0,
count(ri!value)
),
{
a!save(
target: ri!saveInto,
value: filter(
a!isNotNullOrEmpty(_),
union(
touniformstring(ri!value),
touniformstring(
filter(
validateemailaddress(_),
index(regexsearch(local!emailRegexPattern, save!value, "gis"), "match", {}),
)
)
)
)
),
if(
local!valueCount <> count(ri!value),
a!save(
target: local!textFieldSwitchCounter,
value: local!textFieldSwitchCounter + 1
),
{}
)
}
)
},
inputPurpose: "EMAIL"
)
),
a!cardLayout(
showWhen: and(
ri!showWhen <> false,
ri!readOnly <> true,
mod(local!textFieldSwitchCounter, 2) = 1,
),
marginBelow: a!defaultValue(ri!marginBelow, "STANDARD"),
padding: "NONE",
showBorder: false,
contents: a!textField(
labelPosition: "COLLAPSED",
instructions: ri!instructions,
disabled: count(ri!value) >= a!defaultValue(ri!maxSelections, 999),
placeholder: a!defaultValue(ri!placeholder, "Type and hit enter to add a recipient ..."),
value: if(
count(ri!value) >= a!defaultValue(ri!maxSelections, 999),
"Maximum number of recipients reached",
null
),
required: if(a!isNotNullOrEmpty(ri!value), false, ri!required),
requiredMessage: ri!requiredMessage,
validations: if(
ri!readOnly,
null,
{
if(
count(ri!value) > a!defaultValue(ri!maxSelections, 999),
concat(
"Only ",
ri!maxSelections,
" items allowed"
),
null
),
ri!validations
}
),
saveInto: {
/*
Find and append all email addresses in the text.
The regex handles any king of divider like space, comma etc...
by just looking for valid email strings
Ignore any invalids and already known ones
Switch to the other text field in case the number of email addresses changed
*/
a!localVariables(
local!valueCount: if(
a!isNullOrEmpty(ri!value),
0,
count(ri!value)
),
{
a!save(
target: ri!saveInto,
value: filter(
a!isNotNullOrEmpty(_),
union(
touniformstring(ri!value),
touniformstring(
filter(
validateemailaddress(_),
index(regexsearch(local!emailRegexPattern, save!value, "gis"), "match", {}),
)
)
)
)
),
if(
local!valueCount <> count(ri!value),
a!save(
target: local!textFieldSwitchCounter,
value: local!textFieldSwitchCounter + 1
),
{}
)
}
)
},
inputPurpose: "EMAIL"
)
)
}
)
Summary
While this solution lacks some features Google provides like drag&drop, for me, it is a great example for a pretty complex custom UI component. And I learned how to combine multiple components seamlessly.
Have fun with it and share a screenshot of your use case.
Keep on rocking the digital transformation!
