Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — 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
31template<PropertyValue T>
32class FormField {
33public:
45
48 explicit FormField(std::string field_path, T initial = T{})
49 : value(std::move(initial)),
51 initial_(value.get()) {
52 wire_();
53 }
54
55 [[nodiscard]] const std::string& field_path() const noexcept {
56 return validator.field_path();
57 }
58
60 std::string rule_id_value = {}) {
61 validator.rule(std::move(r), std::move(rule_id_value));
62 return *this;
63 }
64
65 template<std::predicate<const T&> P>
66 FormField& must(P&& predicate, std::string message,
67 std::string rule_id_value = {}) {
68 validator.must(std::forward<P>(predicate),
69 std::move(message),
70 std::move(rule_id_value));
71 return *this;
72 }
73
74 FormField& required(std::string message = "required")
75 requires requires(const T& v) { { v.empty() } -> std::convertible_to<bool>; }
76 {
77 return must([](const T& v) { return !v.empty(); },
78 std::move(message),
79 "required");
80 }
81
82 FormField& min_length(std::size_t n, std::string message)
83 requires requires(const T& v) { { v.size() } -> std::convertible_to<std::size_t>; }
84 {
85 return must([n](const T& v) { return v.size() >= n; },
86 std::move(message),
87 "min_length");
88 }
89
90 void reset(T v = T{}) {
91 initial_ = v;
92 value = std::move(v);
93 touched = false;
94 dirty = false;
95 }
96
97private:
98 void wire_() {
99 bag_ += validator.result().bind([this](const ValidationResult& r) {
100 is_valid = r.valid;
101 if (r.valid || r.errors.empty()) {
102 error = "";
103 error_full = std::nullopt;
104 } else {
105 error = r.errors.front().message;
106 error_full = r.errors.front();
107 }
108 });
109 bag_ += value.on_changed([this](const T& v) {
110 touched = true;
111 dirty = !(v == initial_);
112 });
113 }
114
115 T initial_;
116 SubscriptionBag bag_;
117};
118
120public:
123
124 template<typename Field>
125 void track(Field& f) {
126 fields_.push_back(FieldHooks{
127 [&f] { return f.is_valid.get(); },
128 [&f] { return f.dirty.get(); }
129 });
130 bag_ += f.is_valid.on_changed([this](bool) { recompute_(); });
131 bag_ += f.dirty.on_changed([this](bool) { recompute_(); });
132 recompute_();
133 }
134
135 void clear() {
136 fields_.clear();
137 bag_.clear();
138 is_valid = true;
139 is_dirty = false;
140 }
141
142private:
143 struct FieldHooks {
144 std::function<bool()> valid;
145 std::function<bool()> dirty;
146 };
147
148 void recompute_() {
149 bool all_valid = true;
150 bool any_dirty = false;
151 for (auto& f : fields_) {
152 all_valid = all_valid && f.valid();
153 any_dirty = any_dirty || f.dirty();
154 }
155 is_valid = all_valid;
156 is_dirty = any_dirty;
157 }
158
159 std::vector<FieldHooks> fields_;
160 SubscriptionBag bag_;
161};
162
163// ============================================================================
164// FormValidator -- form-level validation aggregator.
165//
166// Cross-field rules are keyed by `ValidationKey { "", rule_id }` so a
167// UI consumer can still route the message even though the rule is
168// not anchored to any single field. Per-field errors retain their
169// original key from the underlying Validator.
170// ============================================================================
172public:
180
181 template<typename Field>
182 void track(Field& f) {
183 auto pending = [&f] {
184 if constexpr (requires { f.validator.state(); }) return f.validator.state().get().pending;
185 else return false;
186 };
187 fields_.push_back(FieldHooks{
188 [&f] { return f.is_valid.get(); },
189 [&f] { return f.dirty.get(); },
190 std::move(pending),
191 [&f] { return f.error_full.get(); },
192 });
193 ++revision_;
194 bag_ += f.is_valid.on_changed ([this](bool) { recompute_(); });
195 bag_ += f.dirty.on_changed ([this](bool) { recompute_(); });
196 bag_ += f.error_full.on_changed ([this](const std::optional<::aria::Error>&) {
197 recompute_();
198 });
199 using ValueT = std::remove_reference_t<decltype(f.value.get())>;
200 bag_ += f.value.on_changed([this](const ValueT&) { recompute_(); });
201 if constexpr (requires { f.validator.state(); }) {
202 bag_ += f.validator.state().on_changed([this](const ValidationState&) { recompute_(); });
203 }
204 recompute_();
205 }
206
211 template<std::predicate Pred>
212 void rule(Pred predicate, std::string message,
213 std::string rule_id_value = {}) {
214 if (rule_id_value.empty()) {
215 rule_id_value = "form_rule_" + std::to_string(rules_.size());
216 }
217 rules_.push_back(Rule{
218 std::function<bool()>(std::move(predicate)),
219 std::move(message),
220 std::move(rule_id_value),
221 });
222 ++revision_;
223 recompute_();
224 }
225
226 void clear() {
227 ++revision_;
228 fields_.clear();
229 rules_.clear();
230 bag_.clear();
232 is_valid = true;
233 is_dirty = false;
234 is_pending = false;
235 first_error = "";
236 first_error_full = std::nullopt;
237 });
238 }
239
240private:
241 struct FieldHooks {
242 std::function<bool()> valid;
243 std::function<bool()> dirty;
244 std::function<bool()> pending;
245 std::function<std::optional<::aria::Error>()> error;
246 };
247 struct Rule {
248 std::function<bool()> predicate;
249 std::string message;
250 std::string rule_id;
251 };
252
253 void recompute_() {
254 const auto revision = revision_;
255 const auto rules = rules_; // A rule may clear or replace its own callable.
256 bool all_valid = true;
257 bool any_dirty = false;
258 bool any_pending = false;
259 std::optional<::aria::Error> headline;
260
261 for (const auto& r : rules) {
262 const bool passed = r.predicate();
263 if (revision != revision_) return;
264 if (!passed) {
265 all_valid = false;
266 if (!headline) {
268 ValidationKey{std::string{}, r.rule_id},
269 r.message);
270 e.source = "FormValidator";
271 headline = std::move(e);
272 }
273 }
274 }
275 for (auto& f : fields_) {
276 const bool v = f.valid();
277 const bool d = f.dirty();
278 const bool p = f.pending();
279 all_valid = all_valid && v;
280 any_dirty = any_dirty || d;
281 any_pending = any_pending || p;
282 if (!headline && !v) {
283 headline = f.error();
284 }
285 }
286
288 is_valid = all_valid;
289 is_dirty = any_dirty;
290 is_pending = any_pending;
291 first_error_full = headline;
292 first_error = headline ? headline->message : std::string{};
293 });
294 }
295
296 std::vector<FieldHooks> fields_;
297 std::vector<Rule> rules_;
298 SubscriptionBag bag_;
299 std::size_t revision_ = 0;
300};
301
302} // 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:48
FormField & must(P &&predicate, std::string message, std::string rule_id_value={})
Definition form.hpp:66
const std::string & field_path() const noexcept
Definition form.hpp:55
Property< bool > dirty
Definition form.hpp:44
Validator< T > validator
Definition form.hpp:35
FormField & min_length(std::size_t n, std::string message)
Definition form.hpp:82
Property< T > value
Definition form.hpp:34
Property< std::optional<::aria::Error > > error_full
Full first-error record.
Definition form.hpp:42
void reset(T v=T{})
Definition form.hpp:90
Property< bool > is_valid
Definition form.hpp:36
Property< bool > touched
Definition form.hpp:43
Property< std::string > error
First-error message string for trivial UI binding.
Definition form.hpp:39
FormField & required(std::string message="required")
Definition form.hpp:74
FormField & rule(typename Validator< T >::Rule r, std::string rule_id_value={})
Definition form.hpp:59
Definition form.hpp:119
Property< bool > is_dirty
Definition form.hpp:122
void track(Field &f)
Definition form.hpp:125
Property< bool > is_valid
Definition form.hpp:121
void clear()
Definition form.hpp:135
Definition form.hpp:171
void track(Field &f)
Definition form.hpp:182
Property< bool > is_dirty
Definition form.hpp:174
void clear()
Definition form.hpp:226
Property< bool > is_pending
Definition form.hpp:175
Property< std::string > first_error
First-error message (string). Empty when the form is valid.
Definition form.hpp:177
void rule(Pred predicate, std::string message, std::string rule_id_value={})
Add a cross-field rule.
Definition form.hpp:212
Property< std::optional<::aria::Error > > first_error_full
First-error record with kind / key / severity.
Definition form.hpp:179
Property< bool > is_valid
Definition form.hpp:173
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:200
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