3rd-Party Declarations · Part 2 of 3

Guidance, validation, and error messaging

Part 1 left you with a working multi-row form. This part makes it self-explanatory and bullet-proof: placeholder hints on every field, the required-field convention, a submit gate that blocks incomplete declarations, and error messages that name exactly what's missing. Every name and formula has a copy button — paste, never retype.

9 steps 10 hint texts validation gate error UX
0 of 9 steps done

Before you start

PrerequisitePart 1 is complete: your app has the multi-row gallery galSystems with all 14 fields writing back into colSystems, plus the btnAddSystem / btnDeleteRow / lblSystemCount wiring. If any of that is missing, finish Part 1 first. (Wherever a guide page shows <YOUR-SITE-URL> or <IAM-DL-EMAIL>, substitute your own values — this part happens to need neither.)
The one rule from Part 1 still runs everythingValidation never looks at input controls — it reads only colSystems, which the OnChange write-back keeps current on every edit. That's why every formula on this page is a plain expression over the collection: no gallery state, no surprises.
How to read this guide — click vs enterEvery step uses the same three markers, so you always know where to look. A navy button like HintText is something you click in Power Apps — a menu, a button, or a property name. An amber chip like txtAppName is a value you enter — tap the chip to copy it (the ⧉ mark is the reminder), then paste it where the step says. A key cap like Ctrl+A is pressed on the keyboard. Dark boxes are complete formulas with their own Copy button. Paste, never retype.

Guidance — every field explains itself

B1Hint text on all ten text inputs

Ten pastes, one per input — the grey guidance a user sees in an empty field. Same motion each time: Tree view → expand galSystems → click the input → property dropdown (top-left, next to the fx bar) → HintTextCtrl+A → paste (with the quotes).

txtAppName · HintText

"Enter the system/application name"

txtVendor · HintText

"Enter the software vendor or service provider"

txtOwner · HintText

"Enter the business owner name"

txtPurpose · HintText

"Describe the business purpose of the application"

txtCustodian · HintText

"Enter the responsible IT contact"

txtAdminAccts · HintText

"List administrator accounts used to manage the system"

txtPermissions · HintText

"Describe available roles and access levels"

txtSupport · HintText

"Enter support email, phone number, or contact details"

txtAccessLink · HintText

"Enter the application URL or access portal link"

txtNumAccts · HintText

"Enter the total number of active user accounts"
Seeing "Placeholder" instead of HintText?You're on a modern control — this build uses classic controls throughout (property names like HintText, Default, BorderColor). Revisit Part 1's settings step and make sure "Modern controls and themes" is off, then re-insert the input as a classic one.
Check before moving onIn Preview, a fresh empty row shows grey guidance in all ten text boxes; type one character in any of them and the hint disappears, delete it and the hint returns.
B2Dropdown tooltips — and the empty-until-chosen check

Classic dropdowns have no placeholder, so their guidance rides on Tooltip. Four pastes: Tree view → expand galSystems → click the dropdown → property dropdown → TooltipCtrl+A → paste:

drpIAMRole · Tooltip

"Select IAM involvement level"

drpDivision · Tooltip

"Select the applicable division"

drpStatus · Tooltip

"Select the application status"

drpHandover · Tooltip

"Select the handover status"

While you're here, re-check AllowEmptySelection on all four: select drpIAMRole → property dropdown → AllowEmptySelection — the formula bar must read true. If it reads false, Ctrl+A → type true. Repeat for drpDivision, drpStatus and drpHandover.

Why AllowEmptySelection matters so muchWithout it, a classic dropdown auto-selects its first item — every new row would arrive with "Manage" pre-chosen, and the mandatory-field validation you're about to build could never catch an untouched dropdown. Empty-until-chosen is what makes "required" real.
Check before moving onIn Preview: hovering each dropdown shows its tooltip, and a freshly added row (+ Add System) shows all four dropdowns blank — nothing pre-selected.
B3Mark every field required

All 14 fields are mandatory, so all 14 labels say so. The first one, done together: Tree view → expand galSystems → click lblAppName → property dropdown → TextCtrl+A → paste:

lblAppName · Text

"Application Name *"

Now the other 13, same motion — each chip is the full Text value (tap to copy, quotes included):

  1. lblVendor"Vendor/Provider Name *"
  2. lblOwner"Business Owner *"
  3. lblPurpose"Application Purpose *"
  4. lblCustodian"IT Custodian *"
  5. lblAdminAccts"Admin Accounts Used *"
  6. lblPermissions"Permissions / Access Levels *"
  7. lblSupport"Support / Vendor Contact *"
  8. lblIAMRole"IAM Role *"
  9. lblAccessLink"Access Link *"
  10. lblDivision"Division *"
  11. lblStatus"Status *"
  12. lblHandover"Handover Status *"
  13. lblNumAccts"Number of Accounts *"
Want the asterisk in brand red?A classic label is single-colour, so the star inherits the label's ink. That's fine — the red arrives where it counts, on the error borders and messages you build in B6–B8. Skip the temptation to stack 14 extra one-character labels.
Check before moving onEvery field label in the row card ends with * — count 14 in one row, and the layout hasn't wrapped or clipped anywhere.

Validation — the gate and the messages

B4The Submit button and the form-level error label

a) The button. Click scrMain in the Tree view (so the button lands on the screen, not inside the gallery), then + Insert (left rail) → type Button in the search box → click it. Double-click its name in the Tree view and paste btnSubmit. Position it via the Properties pane on the right → Position → X 1160 · Y 712, Size → Width 180 · Height 44. It should sit fully on the dark footer bar next to btnAddSystem — if it doesn't, check that recFooter's Y is 692.

Three properties, each via the property dropdown (top-left, next to the fx bar) → select the property → Ctrl+A → paste (Text keeps its quotes):

Text
"Submit Declaration"
Fill
gblColorBrand
Color
Color.White

Property dropdown → DisplayModeCtrl+A → paste:

btnSubmit · DisplayMode

If(CountRows(colSystems) = 0 || gblSubmitting, DisplayMode.Disabled, DisplayMode.Edit)

b) The label. Click scrMain in the Tree view again, then + Insert → type Text label in the search box → click it. Double-click its name in the Tree view and paste lblFormError. Properties pane → Position → X 500 · Y 718, Size → Width 640 · Height 32.

Two properties via the property dropdown → select → Ctrl+A → paste:

Color
Color.Red
Text
"" — for now; B8 wires the real formula
Why the DisplayMode guardAn empty declaration (zero rows) has nothing to submit, and gblSubmitting will lock the button during the SharePoint write in Part 3 — no double-clicks, no duplicate records.
Check before moving onWith one row in the gallery the button shows solid brand red and is clickable; delete the last remaining row with Delete Row → the button greys out; press + Add System and it wakes up again.
B5The validation gate on Submit

One paste — the whole gate. Tree view → click btnSubmit → property dropdown → OnSelectCtrl+A → paste:

btnSubmit · OnSelect

Set(gblShowErrors, true); If( CountRows(colSystems) > 0 && CountRows(Filter(colSystems, IsBlank(AppName) || IsBlank(Vendor) || IsBlank(BizOwner) || IsBlank(Purpose) || IsBlank(Custodian) || IsBlank(AdminAccts) || IsBlank(Permissions) || IsBlank(SupportContact) || IsBlank(IAMRole) || IsBlank(Division) || IsBlank(Status) || IsBlank(Handover) || IsBlank(AccessLink) || !IsMatch(AccessLink, "^(https?://)?([\w\-]+\.)+[\w\-]+(/\S*)?$") || IsBlank(NumAccts) || !IsMatch(NumAccts, "^\d+$"))) = 0, Set(gblShowConfirm, true) )
How the gate readsFirst it switches error display on (gblShowErrors — the red borders and messages of B6–B8 key off it). Then it counts invalid rows: a row fails if any of the 14 fields is blank, if the Access Link isn't URL-shaped, or if Number of Accounts isn't a whole number. Only a declaration with zero invalid rows sets gblShowConfirm — the confirmation popup you build in Part 3.
The URL pattern must stay a literalIsMatch only accepts a pattern typed directly into the formula — never move the regex into a variable "to tidy up"; it will stop compiling. The pattern is deliberately lenient: real inventory entries include bare domains like stc.com.bh, and those should pass.
A valid click does nothing visible yet — that's expectedgblShowConfirm flips to true but no popup exists until Part 3. Nothing is broken.
Check before moving onIn Preview with an incomplete row: click Submit Declaration → no popup, and the Variables pane (three-dots menu → Variables) shows gblShowErrors = true, gblShowConfirm = false. Fill one row completely — all four dropdowns chosen, Access Link https://example.com, Number of Accounts 5 — click again → gblShowConfirm = true. Then run OnStart (App → Run OnStart) to reset the flags; it re-seeds the gallery to one blank row, which is expected while building.
B6Red borders on all fourteen inputs

Every input signals its own state through BorderColor: resting grey normally, red once gblShowErrors is on and the field is empty (or invalid). The first paste, done together — Tree view → expand galSystems → click txtAppName → property dropdown → BorderColorCtrl+A → paste:

txtAppName · BorderColor

If(gblShowErrors && IsBlank(ThisItem.AppName), Color.Red, RGBA(201, 204, 213, 1))

The other eleven plain fields, same motion — each is a full paste, no editing. Select the control → property dropdown → BorderColorCtrl+A → paste:

txtVendor · BorderColor

If(gblShowErrors && IsBlank(ThisItem.Vendor), Color.Red, RGBA(201, 204, 213, 1))

txtOwner · BorderColor

If(gblShowErrors && IsBlank(ThisItem.BizOwner), Color.Red, RGBA(201, 204, 213, 1))

txtPurpose · BorderColor

If(gblShowErrors && IsBlank(ThisItem.Purpose), Color.Red, RGBA(201, 204, 213, 1))

txtCustodian · BorderColor

If(gblShowErrors && IsBlank(ThisItem.Custodian), Color.Red, RGBA(201, 204, 213, 1))

txtAdminAccts · BorderColor

If(gblShowErrors && IsBlank(ThisItem.AdminAccts), Color.Red, RGBA(201, 204, 213, 1))

txtPermissions · BorderColor

If(gblShowErrors && IsBlank(ThisItem.Permissions), Color.Red, RGBA(201, 204, 213, 1))

txtSupport · BorderColor

If(gblShowErrors && IsBlank(ThisItem.SupportContact), Color.Red, RGBA(201, 204, 213, 1))

drpIAMRole · BorderColor

If(gblShowErrors && IsBlank(ThisItem.IAMRole), Color.Red, RGBA(201, 204, 213, 1))

drpDivision · BorderColor

If(gblShowErrors && IsBlank(ThisItem.Division), Color.Red, RGBA(201, 204, 213, 1))

drpStatus · BorderColor

If(gblShowErrors && IsBlank(ThisItem.Status), Color.Red, RGBA(201, 204, 213, 1))

drpHandover · BorderColor

If(gblShowErrors && IsBlank(ThisItem.Handover), Color.Red, RGBA(201, 204, 213, 1))

The last two carry their format checks as well — full pastes, no editing:

txtAccessLink · BorderColor

If(gblShowErrors && (IsBlank(ThisItem.AccessLink) || !IsMatch(ThisItem.AccessLink, "^(https?://)?([\w\-]+\.)+[\w\-]+(/\S*)?$")), Color.Red, RGBA(201, 204, 213, 1))

txtNumAccts · BorderColor

If(gblShowErrors && (IsBlank(ThisItem.NumAccts) || !IsMatch(ThisItem.NumAccts, "^\d+$")), Color.Red, RGBA(201, 204, 213, 1))
Why borders wait for a submit attemptKeying everything off gblShowErrors means a user filling the form top-to-bottom is never shouted at mid-entry — red appears only after they try to submit, and clears live as they fix each field (the write-back keeps colSystems current, and these formulas read ThisItem, which is the collection row).
Check before moving onIn Preview: click Submit Declaration with gaps in a row → red borders on exactly the empty and invalid fields, resting grey on the filled ones. Fill one bordered field and tab out — its border calms down on its own.
B7The per-row error message

Each row card gets one red line at its foot that names every offending field — far friendlier than 14 separate error labels, and far fewer controls in the template. Part 1 didn't create this label, so insert it now — into the gallery template, not onto the screen:

  1. Click any control inside the first row (e.g. txtAppName) so the gallery template is active.
  2. + Insert (left rail) → type Text label in the search box → click it.
  3. Look at the Tree view: the new label must appear nested under galSystems. If it sits at screen level instead, delete it, click inside the row again, and re-insert.
  4. Double-click the label's name in the Tree view → paste lblRowErrorEnter.
  5. Properties pane (right side) → Position → X 16 · Y 552, Size → Width 1294 · Height 40. Leave Auto height off (it's off by default) — the gallery template is fixed-height and a growing label would spill out of the card.
  6. Property dropdown → ColorCtrl+A → paste: Color.Red

Property dropdown → TextCtrl+A → paste:

lblRowError · Text

If(!gblShowErrors, "", With({missing: Concat( Filter( Table( {f: "Application Name", bad: IsBlank(ThisItem.AppName)}, {f: "Vendor/Provider Name", bad: IsBlank(ThisItem.Vendor)}, {f: "Business Owner", bad: IsBlank(ThisItem.BizOwner)}, {f: "Application Purpose", bad: IsBlank(ThisItem.Purpose)}, {f: "IT Custodian", bad: IsBlank(ThisItem.Custodian)}, {f: "Admin Accounts Used", bad: IsBlank(ThisItem.AdminAccts)}, {f: "Permissions/Access Levels", bad: IsBlank(ThisItem.Permissions)}, {f: "Support/Vendor Contact", bad: IsBlank(ThisItem.SupportContact)}, {f: "IAM Role", bad: IsBlank(ThisItem.IAMRole)}, {f: "Access Link", bad: IsBlank(ThisItem.AccessLink)}, {f: "Division", bad: IsBlank(ThisItem.Division)}, {f: "Status", bad: IsBlank(ThisItem.Status)}, {f: "Handover Status", bad: IsBlank(ThisItem.Handover)}, {f: "Number of Accounts", bad: IsBlank(ThisItem.NumAccts)} ), bad), f, ", ")}, If(missing <> "", "Missing: " & missing & ". ", "") & If(!IsBlank(ThisItem.AccessLink) && !IsMatch(ThisItem.AccessLink, "^(https?://)?([\w\-]+\.)+[\w\-]+(/\S*)?$"), "Access Link must be a valid URL (e.g. https://portal.vendor.com). ", "") & If(!IsBlank(ThisItem.NumAccts) && !IsMatch(ThisItem.NumAccts, "^\d+$"), "Number of Accounts must be a whole number.", "") ))

Then property dropdown → VisibleCtrl+A → paste:

lblRowError · Visible

gblShowErrors && !IsBlank(Self.Text)
How one label names every gapThe Table(…) builds fourteen tiny {field-name, is-it-bad} records, Filter keeps the bad ones, Concat strings their names together — then the two trailing Ifs append the URL and whole-number messages when those fields are filled but wrong. One formula, complete diagnosis.
Check before moving onSubmit with a half-filled row → the row's foot reads "Missing: …" naming exactly the empty fields; put abc in Access Link and the URL message joins on. Fix a named field and tab out — it drops off the list immediately.
B8The form-level message, and three finishing touches

a) Wire the footer label from B4. Tree view → click lblFormError → property dropdown → TextCtrl+A → paste:

lblFormError · Text

If(!gblShowErrors, "", With({bad: CountRows(Filter(colSystems, IsBlank(AppName) || IsBlank(Vendor) || IsBlank(BizOwner) || IsBlank(Purpose) || IsBlank(Custodian) || IsBlank(AdminAccts) || IsBlank(Permissions) || IsBlank(SupportContact) || IsBlank(IAMRole) || IsBlank(Division) || IsBlank(Status) || IsBlank(Handover) || IsBlank(AccessLink) || !IsMatch(AccessLink, "^(https?://)?([\w\-]+\.)+[\w\-]+(/\S*)?$") || IsBlank(NumAccts) || !IsMatch(NumAccts, "^\d+$")))}, If(bad > 0, bad & " system record(s) have missing or invalid fields — fix the highlighted rows above.", "") ))

lblFormError · Visible

gblShowErrors && !IsBlank(Self.Text)

b) Smoother typing. On each of the ten text inputs: select it → property dropdown → DelayOutputCtrl+A → type true. The ten: txtAppName · txtVendor · txtOwner · txtPurpose · txtCustodian · txtAdminAccts · txtPermissions · txtSupport · txtAccessLink · txtNumAccts. Validation now re-evaluates a beat after you pause instead of on every keystroke.

c) Guard Delete Row during submission. Tree view → click btnDeleteRow → property dropdown → DisplayModeCtrl+A → paste:

btnDeleteRow · DisplayMode

If(gblSubmitting, DisplayMode.Disabled, DisplayMode.Edit)
Tab order, for freeIn Preview, Tab should walk the left column top-to-bottom, then the right column. Classic controls follow their insertion order — if Part 1 was built in guide order, this already works. If a field is visited out of turn, select it and re-check its position in the Tree view rather than reaching for TabIndex.
Check before moving onSubmit with two incomplete rows → the footer reads "2 system record(s) have missing or invalid fields…". Complete one of them → the count drops to 1 without another click. Complete both → the message disappears entirely.
B9The validation test script

A condensed test aimed at everything this page added. Run OnStart first (App → Run OnStart) for a clean single blank row, then F5:

  • Empty submit → every field in the row wears a red border, the row foot lists all 14 field names, the footer reads "1 system record(s) have missing or invalid fields…". No popup opens.
  • Bad URL → fill the row validly but put not a link in Access Link → the row message switches to the URL-specific line; only Access Link stays red-bordered.
  • Bad number → Number of Accounts refuses letters as you type (its Format is Number); paste 12a anyway → "Number of Accounts must be a whole number." appears. Replace with 25.
  • Live clearing → fix the remaining gaps one by one — each border calms and each name drops off the row message as you tab out; the footer count falls to nothing.
  • Valid pass → click Submit Declaration on the fully valid declaration → silence, and the Variables pane shows gblShowConfirm = true. That silent flag is Part 3's opening move: the confirmation popup, the SharePoint write, and the IAM email.
Hints still guidingWhile you're in Preview, glance at a fresh row: grey hint text in every box, tooltips on the dropdowns, an asterisk on every label — the guidance layer and the enforcement layer now tell the same story.
DoneValidation is complete: nothing incomplete or malformed can reach submission, and every error explains itself. On to Part 3 — confirm, submit, notify, and ship. 🏁