inquirex-ui
Widget rendering hints for Inquirex flow definitions.
inquirex-ui enriches an Inquirex Definition with framework-agnostic rendering
metadata — the widget DSL verb attaches a WidgetHint to each step node for each
rendering target (:desktop, :mobile, :tty, …), describing the preferred UI control.
Frontend adapters
(inquirex-tty, inquirex-js, inquirex-rails) consume these hints to pick the right
renderer. This gem produces enriched JSON — no HTML or JavaScript is generated here.
Installation
gem "inquirex-ui"
inquirex-ui depends only on inquirex (the core gem). Both have zero required
runtime dependencies beyond Ruby itself.
Inquirex.define vs Inquirex::UI.define
The two entry points share the same DSL — the only difference is whether nodes carry widget hints:
| Entry point | Node class | widget verb |
|
|---|---|---|---|
inquirex |
Inquirex.define |
Inquirex::Node |
not available |
inquirex-ui |
Inquirex::UI.define |
Inquirex::UI::Node |
available |
Both return an Inquirex::Definition that works identically with Inquirex::Engine,
to_json/from_json, MermaidExporter, etc. inquirex-ui is a strict superset —
everything you can write in Inquirex.define works unchanged inside
Inquirex::UI.define.
Use Inquirex.define when you only need the flow logic: server-side processing,
pure Ruby scripts, tests that don't care about rendering.
Use Inquirex::UI.define when the definition will be consumed by a frontend adapter
(inquirex-tty, inquirex-js, Rails views) that needs to know how to render each
question.
Quick Start
require "inquirex/ui"
definition = Inquirex::UI.define id: "tax-intake", version: "1.0.0" do
title: "Tax Preparation Intake"
start :filing_status
ask :filing_status do
type :enum
question "What is your filing status?"
single: "Single",
married_jointly: "Married Filing Jointly",
married_separately: "Married Filing Separately",
head_of_household: "Head of Household"
target: :desktop, type: :radio_group, columns: 2
target: :mobile, type: :dropdown
target: :tty, type: :select
transition to: :income
end
ask :income do
type :currency
question "Estimated annual income?"
# No widget call — WidgetRegistry provides :currency_input by default
transition to: :has_deductions
end
confirm :has_deductions do
question "Any deductions to claim?"
# WidgetRegistry default: :toggle (desktop) / :yes_no_buttons (mobile)
transition to: :deductions, if_rule: equals(:has_deductions, true)
transition to: :done
end
ask :deductions do
type :multi_enum
question "Select applicable deductions."
%w[Mortgage Charitable Medical]
target: :desktop, type: :checkbox_group, layout: :vertical
target: :tty, type: :multi_select
transition to: :done
end
say :done do
text "Thank you for completing the intake."
end
end
The definition object is a standard Inquirex::Definition — fully compatible with
Inquirex::Engine, to_json/from_json, and Inquirex::Graph::MermaidExporter.
Every step is an Inquirex::UI::Node (a subclass of Inquirex::Node) with widget hints
attached.
The widget DSL Verb
widget is available inside any step block when using Inquirex::UI.define. Call it
once per target context. The target: keyword defaults to :desktop, and the design
is open-ended — any adapter may introduce its own targets (:watch, :tv, :embed, …).
ask :priority do
type :enum
question "How urgent is this?"
low: "Low", medium: "Medium", high: "High"
target: :desktop, type: :radio_group, columns: 3
target: :mobile, type: :dropdown
target: :tty, type: :select
transition to: :next_step
end
type: names the widget. All remaining keyword arguments become the hint's options
hash and are passed through to the adapter unchanged.
target: can be any symbol. The three built-in targets are :desktop, :mobile, and
:tty. Additional targets (:watch, :embed, …) are valid — adapters can define their
own without any changes to this gem.
Widget DSL Method
| Signature | Purpose |
|---|---|
widget(target: :desktop, type:, **opts) |
Set rendering hint for the given target |
target: defaults to :desktop — you can omit it when only a single hint is needed:
type: :text_input, placeholder: "e.g. Alice"
When no widget call is made for a target, WidgetRegistry fills in the default for
that data type. Adapters may also call #effective_widget_hint_for(target:) on any node
to get the explicit hint or the registry fallback in one call.
Widget Registry (Auto-Defaults)
When no explicit widget call is made, WidgetRegistry provides a sensible default per
data type and rendering context:
| Data Type | :desktop default |
:mobile default |
:tty default |
|---|---|---|---|
:string |
text_input |
text_input |
text_input |
:text |
textarea |
textarea |
multiline |
:integer |
number_input |
number_input |
number_input |
:decimal |
number_input |
number_input |
number_input |
:currency |
currency_input |
currency_input |
number_input |
:boolean |
toggle |
yes_no_buttons |
yes_no |
:enum |
radio_group |
dropdown |
select |
:multi_enum |
checkbox_group |
checkbox_group |
multi_select |
:date |
date_picker |
date_picker |
text_input |
:email |
email_input |
email_input |
text_input |
:phone |
phone_input |
phone_input |
text_input |
Display steps (say, header, btw, warning) have no type, so they return nil
widget hints by default.
All Recognized Widget Types
Web / graphical (desktop + mobile):
text_input textarea number_input
currency_input toggle
radio_group dropdown checkbox_group
multi_select_dropdown date_picker email_input
phone_input
TTY — maps to tty-prompt methods:
| Widget type | tty-prompt method | Notes |
|---|---|---|
text_input |
prompt.ask |
Single-line text |
multiline |
prompt.multiline |
Multi-line text (:text type) |
number_input |
prompt.ask |
With numeric conversion |
yes_no |
prompt.yes? |
Boolean gate |
select |
prompt.select |
Single choice from list |
multi_select |
prompt.multi_select |
Multiple choices from list |
enum_select |
prompt.enum_select |
Numbered menu |
mask |
prompt.mask |
Hidden/password input |
slider |
prompt.slider |
Numeric range slider |
Adapters are not required to support every widget type and should fall back gracefully
(e.g. radio_group → dropdown in a TTY context).
WidgetHint
WidgetHint is an immutable Data class (Data.define) that pairs a widget type with
an options hash:
hint = Inquirex::UI::WidgetHint.new(type: :radio_group, options: { columns: 2 })
hint.type # => :radio_group
hint. # => { columns: 2 }
hint.to_h # => { "type" => "radio_group", "columns" => 2 }
Inquirex::UI::WidgetHint.from_h({ "type" => "radio_group", "columns" => 2 })
# => #<data Inquirex::UI::WidgetHint type=:radio_group, options={columns: 2}>
The options hash is merged inline with "type" in the serialized form — no extra nesting.
UI::Node
Inquirex::UI::Node extends Inquirex::Node with a widget_hints hash and two
accessor methods:
| Attribute / Method | Returns | Description |
|---|---|---|
widget_hints |
Hash{Symbol => WidgetHint}? |
All explicit hints keyed by target, or nil for display nodes |
widget_hint_for(target:) |
WidgetHint? |
Explicit hint for the given target, or nil |
effective_widget_hint_for(target:) |
WidgetHint? |
Explicit hint or registry default for the given target |
step = definition.step(:filing_status)
step.
# => { desktop: #<WidgetHint type=:radio_group, options={columns: 2}>,
# mobile: #<WidgetHint type=:dropdown, options={}> }
step.(target: :desktop)
# => #<data WidgetHint type=:radio_group, options={columns: 2}>
step.(target: :mobile)
# => #<data WidgetHint type=:dropdown, options={}>
Collecting steps produced by Inquirex::UI.define always return a non-nil value from
effective_widget_hint_for (registry fills in the gap). Display steps (say, header,
btw, warning) return nil.
JSON Serialization
Widget hints round-trip through JSON:
definition.to_json
# => '{"id":"tax-intake","start":"filing_status","steps":{"filing_status":{"verb":"ask",
# "type":"enum","question":"What is your filing status?","options":[...],"transitions":[...],
# "widget":{"desktop":{"type":"radio_group","columns":2},"mobile":{"type":"dropdown"},
# "tty":{"type":"select"}}},...}}'
Wire Format for a Step with Hints
{
"verb": "ask",
"type": "enum",
"question": "What is your filing status?",
"options": [
{ "value": "single", "label": "Single" },
{ "value": "married_jointly", "label": "Married Filing Jointly" }
],
"transitions": [{ "to": "income" }],
"widget": {
"desktop": { "type": "radio_group", "columns": 2 },
"mobile": { "type": "dropdown" },
"tty": { "type": "select" }
}
}
There is a single "widget" key whose value is an object keyed by target name. New
targets can be added without changing the schema. Display steps (say, header, btw,
warning) carry no "widget" key at all.
Deserializing UI Nodes
Inquirex::Definition.from_json restores the base Inquirex::Node class by default
(the core gem has no knowledge of inquirex-ui). To restore UI::Node instances:
json = definition.to_json
step_hash = JSON.parse(json)["steps"]["filing_status"]
restored = Inquirex::UI::Node.from_h(:filing_status, step_hash)
restored..type # => :radio_group
Engine Compatibility
Inquirex::UI.define returns a standard Inquirex::Definition. It works unchanged with
Inquirex::Engine:
engine = Inquirex::Engine.new(definition)
engine.answer("single") # :filing_status → :income
engine.answer(75_000.00) # :income → :has_deductions
engine.answer(true) # :has_deductions → :deductions
engine.answer(%w[Mortgage]) # :deductions → :done
engine.advance # past :done
engine.finished? # => true
engine.answers
# => { filing_status: "single", income: 75000.0, has_deductions: true, deductions: ["Mortgage"] }
For Adapter Authors
If you are building an adapter (TTY terminal, JS widget, Rails views):
- Parse the flow JSON to get step definitions.
- For each collecting step, read
step["widget"]— it is an object keyed by target name. - Look up your target (e.g.
step["widget"]["tty"]). The"type"key names the widget; additional keys are options. - If
"widget"is absent, callWidgetRegistry.default_hint_for(type)(Ruby) or implement the same lookup table in your target language. - Fall back gracefully — not every adapter supports every widget type.
# Example adapter lookup
def (step_hash, target: :desktop)
= step_hash["widget"]
raw = &.fetch(target.to_s, nil) || &.fetch("desktop", nil)
return Inquirex::UI::WidgetRegistry.default_hint_for(step_hash["type"], context: target) unless raw
Inquirex::UI::WidgetHint.from_h(raw)
end
Development
bundle install
bundle exec rspec # run specs (90 examples)
bundle exec rspec --format documentation
bundle exec rubocop # lint
The core inquirex gem is loaded from a relative path (../inquirex) in development.
See Gemfile for details.
License
MIT. See LICENSE.txt.