3. Replace src/main.luau
luastra/ui · luastra/data · controlled input · about 20 minutescontrolled fields → validation → modal → confirmation
Every visible value and error comes from Luau state; the modal opens only after normalized values pass both validation layers.
When to use it
Replace the complete entry module. Keep modal visibility in state and use the same dismissal action for Escape, backdrop dismissal, and the visible cancel button.
Focused example
--!strict
local Data = require("luastra/data")
local UI = require("luastra/ui")
local nameSchema = Data.string { minBytes = 2, maxBytes = 60, trim = true }
local emailSchema = Data.string { minBytes = 3, maxBytes = 160, trim = true }
local App = require("luastra/app")
local Root = {}
local name = ""
local email = ""
local nameError: string? = nil
local emailError: string? = nil
local modalOpen = false
local status = "Complete both fields"
local submittedName: string? = nil
local function validEmail(value: string): boolean
return string.match(value, "^[^%s@]+@[^%s@]+%.[^%s@]+$") ~= nil
end
local function validate(): boolean
local checkedName = Data.decode(nameSchema, name)
local checkedEmail = Data.decode(emailSchema, email)
if checkedName.success then nameError = nil
else nameError = "Enter 2 to 60 bytes." end
if checkedEmail.success then emailError = nil
else emailError = "Enter an email address." end
if checkedEmail.success and not validEmail(checkedEmail.value) then
emailError = "Use a format such as name@example.com."
end
if not checkedName.success or not checkedEmail.success or emailError ~= nil then
status = "Fix the highlighted fields"
return false
end
name = checkedName.value
email = checkedEmail.value
modalOpen = true
status = "Review the normalized values"
return true
end
function Root.render(): UI.Node
return UI.Screen {
id = "profile",
width = "content",
padding = "responsive",
UI.Card {
id = "profile/form",
gap = "md",
padding = "lg",
surface = "elevated",
UI.Text { id = "profile/title", text = "Create profile", variant = "title" },
UI.Field {
id = "profile/name-field",
gap = "xs",
role = "group",
label = "Name field",
UI.Text { id = "profile/name-label", text = "Name" },
UI.TextInput {
id = "profile/name",
label = "Name",
value = name,
onInput = "form.name",
autoComplete = "name",
enterKeyHint = "next",
required = true,
errorId = nameError ~= nil and "profile/name-error" or nil,
},
UI.Text {
id = "profile/name-error",
text = nameError or "",
role = "alert",
tone = "error",
hidden = nameError == nil,
},
},
UI.Field {
id = "profile/email-field",
gap = "xs",
role = "group",
label = "Email field",
UI.Text { id = "profile/email-label", text = "Email" },
UI.TextInput {
id = "profile/email",
label = "Email",
value = email,
onInput = "form.email",
inputType = "email",
inputMode = "email",
autoComplete = "email",
enterKeyHint = "done",
required = true,
errorId = emailError ~= nil and "profile/email-error" or nil,
},
UI.Text {
id = "profile/email-error",
text = emailError or "",
role = "alert",
tone = "error",
hidden = emailError == nil,
},
},
UI.Text { id = "profile/status", text = status, role = "status" },
UI.Actions {
id = "profile/actions",
responsive = true,
UI.Button {
id = "profile/review",
text = "Review",
onTap = "form.review",
appearance = "primary",
},
},
},
UI.Modal {
id = "profile/confirm-modal",
label = "Confirm profile",
open = modalOpen,
onDismiss = "form.cancel-review",
UI.Stack {
id = "profile/confirm-content",
gap = "md",
padding = "lg",
UI.Text { id = "profile/confirm-title", text = "Confirm profile", variant = "heading" },
UI.Text { id = "profile/confirm-name", text = "Name: " .. name },
UI.Text { id = "profile/confirm-email", text = "Email: " .. email },
UI.Actions {
id = "profile/confirm-actions",
responsive = true,
UI.Button {
id = "profile/confirm",
text = "Confirm",
onTap = "form.confirm",
appearance = "primary",
},
UI.Button {
id = "profile/cancel",
text = "Keep editing",
onTap = "form.cancel-review",
appearance = "secondary",
},
},
},
},
}
end
function Root.handle(action: string, target: string, value: string)
if action == "form.name" and target == "profile/name" then
name = value
nameError = nil
elseif action == "form.email" and target == "profile/email" then
email = value
emailError = nil
elseif action == "form.review" and target == "profile/review" then
validate()
elseif action == "form.cancel-review"
and (target == "profile/cancel" or target == "profile/confirm-modal") then
modalOpen = false
status = "Continue editing"
elseif action == "form.confirm" and target == "profile/confirm" and modalOpen then
modalOpen = false
submittedName = name
status = "Profile confirmed for " .. name
end
end
function Root.snapshot()
return {
name = name,
email = email,
nameError = nameError,
emailError = emailError,
modalOpen = modalOpen,
status = status,
submittedName = submittedName,
}
end
return App.compose {
render = function(_outputs) return Root.render() end,
handle = Root.handle,
snapshot = Root.snapshot,
}