Goal
To learn how to configure roles within repeaters on service forms.
Starting code
Start tag: lesson/10
Requirements
Where would we be without friends to rely on eh? The same is true for super heroes and no matter how super they are, a super great support person or two could be a real life (or day) saver. So let’s include some roles within our hero registration form.
Add the wizard layout to the form with the details on one tab, then two more tabs, the first being "Confidants" and the other being "Side kicks". Their respective details should be as outlined below.
Confidants
You can add zero or more confidants and each can have the following properties (all fields are mandatory unless otherwise stated):
- Standard individual name details
- Type that can be one of:
- Personal
- Professional
- Contact details, where at least one of the following must be entered otherwise the message “We need at least an email or mobile to contact you to save the day” is displayed:
- Mobile
- Trust level which must be one of:
- Can trust with my life
- Highly trustworthy
- Seems solid
- Keep an eye on this one
- Pretty sure they’ve turned
The confidants can be added using the slide out functionality and should show their name in summary/compact form. Also the text "Considered widening your Circle of Trust lately?" should be displayed when there are no confidants added.
Sidekicks
Similarly you can add up to seven sidekicks, and they can have the following information (all fields are mandatory):
- Name (as per hero details)
- Partnership type, which can be one of:
- We’re an official team
- We team up every now and then
- We’re seeing other super heroes right now
- Super powers (as per hero details)
The sidekicks can also use the slide out functionality and should show their name with their super powers underneath in summary/compact form. The text to show when no sidekicks have been added is "Doesn’t play well with others". An error message of "There’s no ‘i’ in team, but we’re getting a little carried away now aren’t we?" if they have more than seven sidekicks.
Steps
Wizardry
First let’s get the wizard part out of the way. A wizard is quite similar to the tabs we added in the view lesson so we won’t linger too long. We’ll use the Wizard component with the PresenterSection as it’s first child, then Section components as the next three children, one for each of our details, confidants and sidekicks tabs.
Have a go at making these changes to the "heroRegister" form before checking you end up with something similar to the configuration shown below and checking the layout in the application. Don’t forget the localised text entries for the section tabs and make sure the previous buttons are all still present too. Also notice the wizard tab comes with a review tab, which we’ll keep even not not explicitly stated in the requirements (we could exclude it using the "includeReview" parameter, which you might want to try out).
<record shortCode="heroRegister" domain="Hero">
<text shortCode="message" visible="false">
<platform:set-property visible="true" when-service-mode="View"/>
<textValue type="label" text="Your super hero is now registered and ready to save the world!"/>
</text>
<component:Wizard textKeyPrefix="hero.wizard">
<component:PresenterSection/>
<component:Section shortCode="details" textKeyPrefix="hero.tab.heroDetails">
<component:HeroDetails/>
</component:Section>
<component:Section shortCode="confidants" textKeyPrefix="hero.tab.confidants">
</component:Section>
<component:Section shortCode="sidekicks" textKeyPrefix="hero.tab.sidekicks">
</component:Section>
</component:Wizard>
</record>
With the wizard structure firmly in place it’s time to look at the role details.
Role component
We introduced child records and repeaters and their usage in the previous lesson, and roles definitely fall into this category. So we’ll need to add a repeater and a child record for our roles.
As always, the first port of call when adding new configuration is to look whether an appropriate tag and/or component exists to help. In our case we have the RoleRecord component, which seems a pretty obvious choice for the role itself. In terms of repeaters, the standard repeater should suffice. This gives us something like the following as our starting point for both of our roles.
<repeater min="TODO">
<component:RoleRecord domainName="TODO">
<slot:recordInfo>
</slot:recordInfo>
<slot:summary>
</slot:summary>
</component:RoleRecord>
</repeater>
Now let’s take a closer look at the requirements for our roles and the RoleRecord component documentation to see what we might put in each of these slots. Actually, it turns out we don’t need anything in these slots as we don’t need anything other than the record details which is just the default slot – in other words the non-slot children of the component element itself. We do however want to use our own custom summary text, but we’ll get to that later. For now we’ll just remove these slots and start putting in the content we want for our roles.
Confidant details
We’ll add a new "Confidants" component in the "heroes" application for the confidant repeater and RoleRecord and include a reference to this in the first empty Section component on the "heroRegistration" form. We’re also going to continue the pattern we saw in the last lesson and add any peripheral configuration such as data constraints and localised text right within the Confidant component file itself.
Have a look at the requirements for the confidant details above again and try to add in the configuration you think we’ll need. If we ignore the requirement of at least one email or mobile being added in the contact details for now there’s nothing we haven’t covered in previous lessons, so give it a solid go yourself before comparing your configuration with the Confidant component configuration shown below.
<components>
<component name="Confidant">
<repeater shortCode="confidants" min="0" textKeyPrefix="confidants">
<component:RoleRecord domainName="Confidant">
<component:IndividualName/>
<attribute attribute="Type" mandatory="true" dataConstraint="confidantType" textKeyPrefix="confidant.type"/>
<box shortCode="contactDetails" textKeyPrefix="confidant.contactDetails">
<repeater shortCode="emails" min="0" max="1" widget="list" textKeyPrefix="confidant.emails">
<component:Email/>
</repeater>
<repeater shortCode="mobiles" min="0" max="1" widget="list" textKeyPrefix="confidant.mobiles">
<component:MobilePhone/>
</repeater>
</box>
<attribute attribute="TrustLevel" mandatory="true" dataConstraint="confidantTrustLevel" textKeyPrefix="confidant.trustLevel"/>
</component:RoleRecord>
</repeater>
</component>
</components>
<dataConstraints>
<dataConstraint name="confidantType" type="string" restrict="true" sortBy="ordinal">
<value code="personal" textKey="constraintType.confidantType.personal"/>
<value code="professional" textKey="constraintType.confidantType.professional"/>
</dataConstraint>
<dataConstraint name="confidantTrustLevel" type="long" restrict="true" sortBy="ordinal">
<value code="1" textKey="constraintType.confidantTrustLevel.1"/>
<value code="2" textKey="constraintType.confidantTrustLevel.2"/>
<value code="3" textKey="constraintType.confidantTrustLevel.3"/>
<value code="4" textKey="constraintType.confidantTrustLevel.4"/>
<value code="5" textKey="constraintType.confidantTrustLevel.5"/>
</dataConstraint>
</dataConstraints>
<locales>
<locale name="en">
<text key="confidants.label">Confidants</text>
<text key="confidants.empty">Considered widening your Circle of Trust lately?</text>
<text key="confidants.add">Add Confidant</text>
<text key="confidant.type.label">Type</text>
<text key="confidant.trustLevel.label">Trust level</text>
<text key="confidant.contactDetails.label">Contact details</text>
<text key="confidant.emails.add">Add Email</text>
<text key="confidant.mobiles.add">Add Mobile</text>
<text key="constraintType.confidantType.personal">Personal</text>
<text key="constraintType.confidantType.professional">Professional</text>
<text key="constraintType.confidantTrustLevel.1">Can trust with my life</text>
<text key="constraintType.confidantTrustLevel.2">Highly trustworthy</text>
<text key="constraintType.confidantTrustLevel.3">Seems solid</text>
<text key="constraintType.confidantTrustLevel.4">Keep an eye on this one</text>
<text key="constraintType.confidantTrustLevel.5">Pretty sure they've turned</text>
</locale>
</locales>
That should give you something like screenshot Fig 10.1 below for the main confidant details and Fig 10.2 for the summary details view. Good job!


Contact details validation
Now that the bulk of the confidant details are done let’s turn our attention to the contact details validation we ignored initially. The requirement is at least an email or mobile number must be added (i.e. both are allowed too). Repeaters come with their own standard validation (see the <platform:repeater-control> tag) but this relates to two different repeaters, so is slightly different.
What we want is to add some validation on the "contactDetails" box that encloses our two email and mobile sections that validates whether either of the child repeaters have a record. Read the validation best practice noting that we should definitely include this logic within a "validate" rule scope.
So now we know we want to add this logic using a "validate" scope rule, and we want this validation on the "contactDetails" box. The final piece of the puzzle we’ll need to know is that there is a <platform:add-error> tag for adding validation errors. This tag uses the "validate" rule scope by default, and has various options around the error code and level as well as the standard support for conditional execution.
In our case we can use the "when-expression" option to trigger the adding of this error, so we’ll end up with something like the following on our "contactDetails" box.
<platform:add-error code="confidant.contactDetails.atLeastOne" when-expression="!viewNode.select('*[type=repeater]').any { r -> r.hasActiveChildren() }"/>
Our expression uses the view node select method that takes a Vpath as an argument. Vpaths are used quite often so it’s important to read the documentation and understand how they work and be comfortable with the syntax and the options available. In our case we’re saying (relative to the "contactDetails" box view node) get all direct child view nodes that are repeaters. This gives us our two repeater view nodes – the one for the email and the other for the mobile. Then it uses Groovy’s any method to ask whether any of these have an active child (i.e. a record). If so, return false (note the "not" operator "!" at the start), otherwise return true.
Hopefully from the above description you can see that the add-error tag will "execute" – i.e. add the error – if none of the email or mobile repeaters have a child. Great stuff. Also add in an appropriate localised text entry for this error code.
Triggering rules on other view nodes
Once done, when the form is validated, the correct error will be displayed. Have a play registering super heroes with the configuration as it stands and note that the "We need at least an email or mobile to contact you to save the day" validation message occurs when you try to move on without adding an email or mobile. The problem though is that the message still persists after you add an email or mobile and remains until the validate scope is called again when trying to move on. Similarly if you remove the email or mobile the message doesn’t appear right away. This isn’t a good enough user experience for our liking so we also want our validation to be removed (or appear) early when we do something that directly relates to it, so in our case when we add (or remove) an email or mobile number.
So let’s look at additionally triggering this validation when we add or remove a child from the email or mobile repeaters. For this we can use the platform:validate tag, making use of the "add-child-complete" and "remove-child-complete" rule scopes and the "target" options. As always, read the documentation and attempt this yourself before checking what you have is similar to the following, configured against both of the email and mobile repeaters.
<platform:validate scope="remove-child-complete,add-child-complete" target-value=".."/>
Try it out in the application again and sit back in awe at the instant feedback you’ve now provided when the validation error is corrected or caused.
Sidekick details
Moving on to the Sidekicks tab, create a new Sidekicks component and use the same repeater and RoleRecord structure we did for the Confidants component. Next come the details – the name, partnership type and super powers. Let’s just add the Name attribute again as per the one in the HeroDetails component since it’s just a single attribute, but we’ll refactor the super powers repeater out into a standalone component that we can use within the HeroDetails and Sidekicks components.
Once done your Sidekicks component file should have content resembling the configuration shown below.
<components>
<component name="Sidekicks">
<repeater shortCode="sidekicks" min="0" max="7" textKeyPrefix="sidekicks">
<component:RoleRecord>
<attribute attribute="Name" textKeyPrefix="sidekick.name" mandatory="true">
<platform:validate-length min-length="2" error-code-too-short="sidekick.name.tooShort"/> <!-- Must be at least 2 characters -->
</attribute>
<attribute attribute="Type" dataConstraint="sidekickType" mandatory="true" textKeyPrefix="sidekick.type"/>
<component:SuperPowers/>
</component:RoleRecord>
</repeater>
</component>
</components>
<dataConstraints>
<dataConstraint name="sidekickType" type="string" restrict="true" sortBy="ordinal">
<value code="team" textKey="sidekickType.team"/>
<value code="casual" textKey="sidekickType.casual"/>
<value code="onABreak" textKey="sidekickType.onABreak"/>
</dataConstraint>
</dataConstraints>
<locales>
<locale name="en">
<text key="sidekick.name.label">Name</text>
<text key="sidekick.name.tooShort" parentKey="hero.name.tooShort"/>
<text key="sidekick.type.label">Partnership type</text>
<text key="sidekickType.team">We're an official team</text>
<text key="sidekickType.casual">We team up every now and then</text>
<text key="sidekickType.onABreak">We're seeing other super heroes right now</text>
</locale>
</locales>
Super powers component
And with a new SuperPowers component as shown below. Notice we pulled the associated configuration into the component like we’ve done with our new components above, as well as renaming the "heroPowers" data constraint to "superPowers" as a part of this refactor now that the powers can also relate to sidekicks as well as the heroes themselves (don’t forget to update the data constraint referenced in the search form too!).
<components>
<component name="SuperPowers">
<parameter name="textKeyPrefix" default="superPowers"/>
<!-- Multi-select pill box repeater using a restricted data constraint -->
<repeater shortCode="superPowers" min="1" max="5" widget="pillbox" textKeyPrefix="${textKeyPrefix}">
<ui:repeater-pillbox data-constraint="superPowers" value-attribute="Name"/>
<record domain="SuperPower" arrayName="powers" objectName="power">
<platform:set-domain-attribute scope="activate" attribute="StartDate" value-expression="activatedDate"/>
<attribute attribute="Name" dataConstraint="superPowers"/>
</record>
</repeater>
</component>
</components>
<dataConstraints>
<dataConstraint name="superPowers" type="string" restrict="true">
<value code="strength" textKey="superPower.strength"/>
<value code="speed" textKey="superPower.speed"/>
<value code="flight" textKey="superPower.flight"/>
<value code="invulnerability" textKey="superPower.invulnerability"/>
<value code="invisibility" textKey="superPower.invisibility"/>
<value code="senses" textKey="superPower.senses"/>
<value code="fighting" textKey="superPower.fighting"/>
<value code="weapons" textKey="superPower.weapons"/>
<value code="agility" textKey="superPower.agility"/>
<value code="other" textKey="superPower.other"/>
</dataConstraint>
</dataConstraints>
<locales>
<locale name="en">
<text key="superPowers.label">Super powers</text>
<text key="superPower.strength">Strength</text>
<text key="superPower.speed">Speed</text>
<text key="superPower.flight">Flight</text>
<text key="superPower.invulnerability">Invulnerability</text>
<text key="superPower.invisibility">Invisibility</text>
<text key="superPower.senses">Heightened senses</text>
<text key="superPower.fighting">Fighting skill</text>
<text key="superPower.weapons">Weaponary</text>
<text key="superPower.agility">Agility</text>
<text key="superPower.other">Other powerful stuff</text>
</locale>
</locales>
Custom repeater summary details
One thing you’ll notice when checking the functionality via the application though is that there aren’t any details that appear in the summary view, as shown in Fig 10.3 below.

As you can see in the RoleRecord component documentation regarding the "summary" slot, the "summaryType" parameter can be used to define the text key(s) used to display the summary details. The default "summaryType" value is "individual", which means the "roleRecord.individual.summary.header" text key is used for the main summary details heading text, which in turn points to the following text key:
<text key="roleRecord.individual.fullName" type="groovy"><![CDATA[
def nameDomain = domain.cssSelectOne("IndividualName[hasNext='false']")
return nameDomain ? nameDomain.getAttribute('FullName').asString('') : ''
]]></text>
Read the CSS selector documentation to see what kind of selection options are available on the “domain” variable available for text keys to use to resolve their values.
Instead of being a text-based text key, this one is Groovy based, so expects a String (the text to use) to be returned. As you can see this assumes an "IndividualName" domain is present within the role record and uses the "FullName" attribute as the summary details heading text.
For our sidekick, we don’t have an individual name domain, instead we just use a "Name" attribute right on the role record. Therefore, we have to use different text keys for the sidekick summary details, so let’s pass in a "summaryType" parameter value of "sidekick" into the RoleRecord component, and define the associated "roleRecord.sidekick.summary.header" and "roleRecord.sidekick.summary.detail" text keys. Leave the detail one blank for now and try to come up with the content for the "roleRecord.sidekick.summary.header" text key, before checking against the one shown below.
<text key="roleRecord.sidekick.summary.header" type="groovy">
return domain.getAttribute('Name').asString('')
</text>
Try the application once done and confirm that you now see the sidekick name in the summary view when adding one. Great. So what about the "with their super powers underneath" portion of the requirement? As you might’ve guessed, this is where the "roleRecord.sidekick.summary.detail" text key comes in. Before we get into the value we want to use for this though, let’s highlight a few things.
Resolved values in text keys
Firstly, the super hero name is the value entered by the user in the field. It is what it is. For this reason we can use the value right off the domain, which we did. The super powers though are backed by a restricted data constraint, meaning the displayed textual value is different from the raw value saved in the attribute. So in this case we need the resolved values, which means we can’t use the domain to get the value (or at least, we’d need to process it further if we did) so we’ll use the view nodes instead for these.
Secondly, it’s worth noting the view node that this text key is resolved against (i.e. the "viewNode" variable referenced in the text key content) is nested within the sidekick record view node itself. As you might expect it’s a view node within the "summary" slot of the RoleRecord component. This is important when considering the VPath to use to get to the super power attribute view nodes we want the resolved values for. In fact, let’s use the service transaction inspector tool to see exactly what view node it is and its properties. Open it up and select the summary content view node in the view tree, as well as expanding the super power view nodes that contain the values we want to display, as per the screenshot in Fig 10.4 below.

From this you can see where the "viewNode" that will be used in the "roleRecord.sidekick.summary.detail" text key is in the view tree in relation to the "Name" attribute view nodes relating to the super powers that we want to get our resolved values from.
The first common ancestor view node to all of these is the record view node itself, so if we start at the text node and select the record view node we’ll be able to then look "downwards" for the super power attribute view nodes. The best way to target the record view node from the text view node is using the VPath "type" predicate i.e. "@[type=record]".
From the sidekick record view node, we can get the super power "Name" attribute view nodes using a combination of the "attributeDomain" and "attributeName" VPath predicates i.e. "**[attributeDomain=SuperPower][attributeName=Name]". We don’t strictly require the "attributeName" predicate since it looks like there’s only one attribute view node under the super power record view nodes anyway, but it’s best to be safe and makes the VPath robust against any future changes.
Once we have these view nodes we can use the getResolvedAttributeStringValue() method to get the display value relating to them, combined with a little bit of Groovy goodness to collect and join them up and even exclude any blank or null values (although the pillbox UI ensures that only super power records with a selected value will be present, but again, it’s making it robust with very little extra effort). This results in the text key configuration shown below. Note also that we need the CDATA wrapper because of the presence of the "->" within its content which uses the reserved XML ">" character.
<text key="roleRecord.sidekick.summary.detail" type="groovy"><![CDATA[
return viewNode.select('@[type=record]/**[attributeDomain=SuperPower][attributeName=Name]').collect({ n -> n.resolvedAttributeStringValue }).findAll().join(', ')
]]></text>
Add sidekicks with an assortment of super powers to a hero registration and confirm the powers now appear in the summary view as expected.
Repeater control
One little thing left, the maximum sidekicks validation if more than seven are entered. The first thing you might notice is that it’s actually fairly difficult to even get the error message relating to too many sidekicks being added to appear. Why? Because the UI simply doesn’t let you add any more than the 7 defined as the "max" value on the repeater.
So we’re all good, right? Well, not quite. Just like with the YearOfArrival attribute back in lesson 2, the UI restriction should only be thought of as a convenience, not as satisfying the validation business requirement.
So how do we test our configuration? We’ll make use of the <platform:repeater-control/> tag and moving the hard maximum from the repeater to a "logical-max" option within the tag. So add the following line to the sidekicks repeater, removing the "max" attribute from the repeater itself.
<platform:repeater-control logical-max="7"/>
Now you should be able to add more than seven sidekicks and see the error that you get when you do as per Fig 10.5 below.
This is a standard message for too many children within a repeater, but we want our custom "There’s no ‘i’ in team, but we’re getting a little carried away now aren’t we?" message. As you can see from the <platform:repeater-control/> documentation this tag allows numerous additional options for configuring a repeater. Have a go at configuring the maximum sidekick validation yourself before comparing with the configuration below. Again, don’t forget the relevant localised text item for the actual textual validation message.
<platform:repeater-control logical-max="7" too-many-key="sidekicks.tooMany"/>
Brilliant stuff… now that we know our repeater maximum is protected by validation regardless of the UI behaviour we’ll clean up a little by removing the "logical-max" option from the <platform:repeater-control/> tag and putting the "max" value of "7" back on the repeater since it’s a nice user experience. No point letting them get somewhere we won’t let them continue from anyway after all.
Apply and view
Now that we’re done with the registration configuration, test the changes locally using the application, including save & exit, submitting and viewing super heroes with and without confidants and sidekicks. You’ll notice we also need to include the roles in the view hero details, which we can easily do by including the Section, Confidants and Sidekicks components on the "heroView" form, as we did for the "heroRegistration" one.
The other thing that becomes apparent is the duplication of tab text and the tab content headings, particularly when viewing the details when there are no roles, as per Fig 10.5 below.

Ideally we only want this heading text to appear once. So which to remove? There are a few things to consider here. In the context of accessibility for example, what section does it make more sense for the heading to be a part of? The tab already has the text associated to it, so really it’s the repeater that should keep its heading and the tab content heading should go. Fortunately this presents a simple solution since the "tabs" widget will only include tab text if you only specify a "shortlabel" text key type and not a "label" one (used for the tab content heading). So if we change all of our tab-related localised text entries to use the ".shortlabel" suffix instead of ".label" then they should all be consistent and not have any duplication. This means renaming the following localised text entry keys:
- hero.tab.heroDetails.label
- hero.tab.confidants.label
- hero.tab.sidekicks.label
- tab.filings.label
Now go over the application again making sure things are looking nice and consistent.
Wrap up
Here’s a summary of what we covered in this lesson. Hopefully most of it sounds familiar 😉
- Converting an editable registration form (as opposed to a view form) to the wizard layout
- The RoleRecord component and its summary and main content details slots
- A bit more on validation best practises and triggering validation on a view node based on changes or events triggered by actions on one or more different view nodes
- VPaths & CSS selectors for selecting view nodes relative to another view node within the view tree
- Accessing resolved values for attribute view nodes
- Customising repeater record summary details
- The
<platform:repeater-control/>tag for common repeater configuration options - Fixing up tab/section headings

