Aria 3.2.6
C++23 MVVM framework — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
form.hpp
Go to the documentation of this file.
1#pragma once
2
3// FormField / FormGroup / FormValidator -- light client-side form helpers.
4//
5// FormField bundles the common trio:
6// Property<T> value
7// Validator<T> validator
8// Property<bool> is_valid
9// Property<std::string> error
10// Property<std::optional<aria::Error>> error_full
11//
12// Every FormField carries a `field_path` (e.g. "user.email") that flows
13// into its underlying Validator, so each emitted Error is routed back
14// to the cell with full provenance. See `docs/error-model.md` for the
15// rationale.
16
17#include "aria/error.hpp"
18#include "aria/property.hpp"
19#include "aria/subscription.hpp"
21#include "aria/validator.hpp"
22
23#include <functional>
24#include <memory>
25#include <string>
26#include <utility>
27#include <vector>
28
29namespace aria::binding {
30
31namespace detail {
32
33// Aggregates do not own their fields. NodeHandle is invalidated when an Aria
34// property is destroyed, so surviving fields can still trigger recomputation
35// without accessing a retired field. Custom reactive sources retain their
36// existing caller-managed lifetime contract.
37template<class Source, class Value, class Project = std::identity>
38auto form_reader(Source& source, Value expired, Project project = {}) {
39 if constexpr (std::derived_from<Source, ::aria::reactive::Node>) {
40 return [&source, handle = ::aria::reactive::detail::NodeHandle{&source},
41 expired = std::move(expired), project = std::move(project)] -> Value {
42 return handle ? project(source.get()) : expired;
43 };
44 } else {
45 return [&source, project = std::move(project)] -> Value { return project(source.get()); };
46 }
47}
48
49} // namespace detail
50
51template<PropertyValue T>
52class FormField {
53public:
65
68 explicit FormField(std::string field_path, T initial = T{})
69 : value(std::move(initial)),
71 initial_(value.get()) {
72 wire_();
73 }
74
75 [[nodiscard]] const std::string& field_path() const noexcept {
76 return validator.field_path();
77 }
78
80 std::string rule_id_value = {}) {
81 validator.rule(std::move(r), std::move(rule_id_value));
82 return *this;
83 }
84
85 template<std::predicate<const T&> P>
86 FormField& must(P&& predicate, std::string message,
87 std::string rule_id_value = {}) {
88 validator.must(std::forward<P>(predicate),
89 std::move(message),
90 std::move(rule_id_value));
91 return *this;
92 }
93
94 FormField& required(std::string message = "required")
95 requires requires(const T& v) { { v.empty() } -> std::convertible_to<bool>; }
96 {
97 return must([](const T& v) { return !v.empty(); },
98 std::move(message),
99 "required");
100 }
101
102 FormField& min_length(std::size_t n, std::string message)
103 requires requires(const T& v) { { v.size() } -> std::convertible_to<std::size_t>; }
104 {
105 return must([n](const T& v) { return v.size() >= n; },
106 std::move(message),
107 "min_length");
108 }
109
110 void reset(T v = T{}) {
111 initial_ = v;
112 value = std::move(v);
113 touched = false;
114 dirty = false;
115 }
116
117private:
118 void wire_() {
119 bag_ += validator.result().bind([this](const ValidationResult& r) {
120 is_valid = r.valid;
121 if (r.valid || r.errors.empty()) {
122 error = "";
123 error_full = std::nullopt;
124 } else {
125 error = r.errors.front().message;
126 error_full = r.errors.front();
127 }
128 });
129 bag_ += value.on_changed([this](const T& v) {
130 touched = true;
131 dirty = !(v == initial_);
132 });
133 }
134
135 T initial_;
136 SubscriptionBag bag_;
137};
138
140public:
143
144 template<typename Field>
145 void track(Field& f) {
146 fields_.push_back(FieldHooks{
147 detail::form_reader(f.is_valid, true),
148 detail::form_reader(f.dirty, false)
149 });
150 bag_ += f.is_valid.on_changed([this](bool) { recompute_(); });
151 bag_ += f.dirty.on_changed([this](bool) { recompute_(); });
152 recompute_();
153 }
154
155 void clear() {
156 fields_.clear();
157 bag_.clear();
158 is_valid = true;
159 is_dirty = false;
160 }
161
162private:
163 struct FieldHooks {
164 std::function<bool()> valid;
165 std::function<bool()> dirty;
166 };
167
168 void recompute_() {
169 bool all_valid = true;
170 bool any_dirty = false;
171 for (auto& f : fields_) {
172 all_valid = all_valid && f.valid();
173 any_dirty = any_dirty || f.dirty();
174 }
175 is_valid = all_valid;
176 is_dirty = any_dirty;
177 }
178
179 std::vector<FieldHooks> fields_;
180 SubscriptionBag bag_;
181};
182
183// ============================================================================
184// FormValidator -- form-level validation aggregator.
185//
186// Cross-field rules are keyed by `ValidationKey { "", rule_id }` so a
187// UI consumer can still route the message even though the rule is
188// not anchored to any single field. Per-field errors retain their
189// original key from the underlying Validator.
190// ============================================================================
192public:
200
201 template<typename Field>
202 void track(Field& f) {
203 auto pending = [&f] -> std::function<bool()> {
204 if constexpr (requires { f.validator.state(); }) {
205 return detail::form_reader(f.validator.state(), false,
206 [](const ValidationState& state) { return state.pending; });
207 } else {
208 return [] { return false; };
209 }
210 };
211 fields_.push_back(FieldHooks{
212 detail::form_reader(f.is_valid, true),
213 detail::form_reader(f.dirty, false),
214 pending(),
215 detail::form_reader(f.error_full, std::optional<::aria::Error>{}),
216 });
217 ++revision_;
218 bag_ += f.is_valid.on_changed ([this](bool) { recompute_(); });
219 bag_ += f.dirty.on_changed ([this](bool) { recompute_(); });
220 bag_ += f.error_full.on_changed ([this](const std::optional<::aria::Error>&) {
221 recompute_();
222 });
223 using ValueT = std::remove_reference_t<decltype(f.value.get())>;
224 bag_ += f.value.on_changed([this](const ValueT&) { recompute_(); });
225 if constexpr (requires { f.validator.state(); }) {
226 bag_ += f.validator.state().on_changed([this](const ValidationState&) { recompute_(); });
227 }
228 recompute_();
229 }
230
235 template<std::predicate Pred>
236 void rule(Pred predicate, std::string message,
237 std::string rule_id_value = {}) {
238 if (rule_id_value.empty()) {
239 rule_id_value = "form_rule_" + std::to_string(rules_.size());
240 }
241 rules_.push_back(Rule{
242 std::function<bool()>(std::move(predicate)),
243 std::move(message),
244 std::move(rule_id_value),
245 });
246 ++revision_;
247 recompute_();
248 }
249
250 void clear() {
251 ++revision_;
252 fields_.clear();
253 rules_.clear();
254 bag_.clear();
256 is_valid = true;
257 is_dirty = false;
258 is_pending = false;
259 first_error = "";
260 first_error_full = std::nullopt;
261 });
262 }
263
264private:
265 struct FieldHooks {
266 std::function<bool()> valid;
267 std::function<bool()> dirty;
268 std::function<bool()> pending;
269 std::function<std::optional<::aria::Error>()> error;
270 };
271 struct Rule {
272 std::function<bool()> predicate;
273 std::string message;
274 std::string rule_id;
275 };
276
277 void recompute_() {
278 const auto revision = revision_;
279 const auto rules = rules_; // A rule may clear or replace its own callable.
280 bool all_valid = true;
281 bool any_dirty = false;
282 bool any_pending = false;
283 std::optional<::aria::Error> headline;
284
285 for (const auto& r : rules) {
286 const bool passed = r.predicate();
287 if (revision != revision_) return;
288 if (!passed) {
289 all_valid = false;
290 if (!headline) {
292 ValidationKey{std::string{}, r.rule_id},
293 r.message);
294 e.source = "FormValidator";
295 headline = std::move(e);
296 }
297 }
298 }
299 for (auto& f : fields_) {
300 const bool v = f.valid();
301 const bool d = f.dirty();
302 const bool p = f.pending();
303 all_valid = all_valid && v;
304 any_dirty = any_dirty || d;
305 any_pending = any_pending || p;
306 if (!headline && !v) {
307 headline = f.error();
308 }
309 }
310
312 is_valid = all_valid;
313 is_dirty = any_dirty;
314 is_pending = any_pending;
315 first_error_full = headline;
316 first_error = headline ? headline->message : std::string{};
317 });
318 }
319
320 std::vector<FieldHooks> fields_;
321 std::vector<Rule> rules_;
322 SubscriptionBag bag_;
323 std::size_t revision_ = 0;
324};
325
326} // namespace aria::binding
Definition validator.hpp:135
std::function< std::optional< std::string >(const T &)> Rule
User-supplied rule body: returns a message when the value fails, std::nullopt when it passes.
Definition validator.hpp:142
const std::string & field_path() const noexcept
Definition validator.hpp:167
FormField(std::string field_path, T initial=T{})
Construct from (field_path, initial).
Definition form.hpp:68
FormField & must(P &&predicate, std::string message, std::string rule_id_value={})
Definition form.hpp:86
const std::string & field_path() const noexcept
Definition form.hpp:75
Property< bool > dirty
Definition form.hpp:64
Validator< T > validator
Definition form.hpp:55
FormField & min_length(std::size_t n, std::string message)
Definition form.hpp:102
Property< T > value
Definition form.hpp:54
Property< std::optional<::aria::Error > > error_full
Full first-error record.
Definition form.hpp:62
void reset(T v=T{})
Definition form.hpp:110
Property< bool > is_valid
Definition form.hpp:56
Property< bool > touched
Definition form.hpp:63
Property< std::string > error
First-error message string for trivial UI binding.
Definition form.hpp:59
FormField & required(std::string message="required")
Definition form.hpp:94
FormField & rule(typename Validator< T >::Rule r, std::string rule_id_value={})
Definition form.hpp:79
Definition form.hpp:139
Property< bool > is_dirty
Definition form.hpp:142
void track(Field &f)
Definition form.hpp:145
Property< bool > is_valid
Definition form.hpp:141
void clear()
Definition form.hpp:155
Definition form.hpp:191
void track(Field &f)
Definition form.hpp:202
Property< bool > is_dirty
Definition form.hpp:194
void clear()
Definition form.hpp:250
Property< bool > is_pending
Definition form.hpp:195
Property< std::string > first_error
First-error message (string). Empty when the form is valid.
Definition form.hpp:197
void rule(Pred predicate, std::string message, std::string rule_id_value={})
Add a cross-field rule.
Definition form.hpp:236
Property< std::optional<::aria::Error > > first_error_full
First-error record with kind / key / severity.
Definition form.hpp:199
Property< bool > is_valid
Definition form.hpp:193
Definition property.hpp:103
::aria::Subscription on_changed(std::function< void(const T &)> fn)
Run fn(new_value) every time the value changes.
Definition property.hpp:215
Definition binding_engine.hpp:25
auto batch(Fn &&fn) -> decltype(fn())
Sugar: batch([&]{ firstName = "..."; lastName = "..."; }).
Definition graph.hpp:369
validation_dsl::RulePart rule_id(std::string id) noexcept
Definition validation_key.hpp:96
Definition validation_key.hpp:110
static Error validation(ValidationKey k, std::string msg)
Hard validation error.
Definition error.hpp:170
std::string source
Free-form locator: which subsystem produced this.
Definition error.hpp:136
Definition validator.hpp:34
std::vector< Error > errors
Definition validator.hpp:36
bool valid
Definition validator.hpp:35
Definition validator.hpp:61