25#include <system_error>
29#include "../../attributes.hpp"
30#include "../../core/completion.hpp"
42namespace morph::forms {
45using Submitter = std::function<async::Completion<std::string>(std::string_view actionType, std::string bodyJson)>;
49 std::function<async::Completion<std::string>(std::string_view optionsAction, std::string bodyJson)>;
63 std::function<bool(std::string_view bodyJson)>
accepts;
69namespace detail::engine {
82 _model{std::move(model)},
83 _choices{std::move(choices)},
84 _options{std::move(options)},
85 _values{.locale = _options.locale,
86 .displayOffsetMinutes = _options.displayOffsetMinutes,
87 .translations = _options.translations,
88 .bcp47 = _options.bcp47} {}
96 [[nodiscard]] FormModel
const& model() const noexcept {
return _model; }
100 [[nodiscard]] FormSessionOptions
const& options() const noexcept {
return _options; }
104 [[nodiscard]] ValueContext
const& values() const noexcept {
return _values; }
108 [[nodiscard]]
ChoiceFetcher const& choices() const noexcept {
return _choices; }
112 void onProgrammatic(std::function<
void()> hook) { _programmatic = std::move(hook); }
115 void markProgrammatic()
const {
123 void adopt(std::vector<std::unique_ptr<FieldState>>
const* fields)
noexcept { _fields = fields; }
128 [[nodiscard]] FieldState* topLevel(std::string_view name)
const;
135 reactive::Runtime* _rt;
138 FormSessionOptions _options;
139 ValueContext _values;
140 std::function<void()> _programmatic;
141 std::vector<std::unique_ptr<FieldState>>
const* _fields =
nullptr;
154 RowState(detail::engine::SessionCore& core,
FieldSpec const& arraySpec, std::string
const& arrayPath,
164 [[nodiscard]] std::uint64_t
key() const noexcept {
return _key; }
168 [[nodiscard]] std::span<std::unique_ptr<FieldState>
const>
fields() const noexcept {
return _fields; }
177 [[nodiscard]]
bool blank()
const;
184 std::vector<std::unique_ptr<FieldState>> _fields;
197 std::string
path, std::vector<std::unique_ptr<FieldState>>
const* siblings,
FieldState const* parent)
200 _path{std::move(
path)},
203 _text{core.runtime(), std::string{}},
204 _unit{core.runtime(),
spec.entryUnit},
205 _engaged{core.runtime(), false},
206 _rows{core.runtime(), std::vector<std::shared_ptr<
RowState>>{}},
207 _encoded{core.runtime(), [
this] {
return computeEncoding(); }},
208 _error{core.runtime(),
209 [
this] -> std::optional<FieldError> {
210 Encoding const& encoding = _encoded.get();
211 if (encoding.has_value() && !encoding->has_value()) {
212 return encoding->error();
216 _visible{core.runtime(),
221 return _parent !=
nullptr ||
224 _readonly{core.runtime(),
226 if (_spec->readOnly || (_parent !=
nullptr && _parent->readonly().get())) {
229 return _parent ==
nullptr &&
232 _required{core.runtime(), [
this] {
233 return _spec->required ||
234 (_parent ==
nullptr &&
240 std::make_unique<FieldState>(core, child, _path +
"." + child.name, &_members,
this));
261 [[nodiscard]] std::string
const&
path() const noexcept {
return _path; }
266 [[nodiscard]] std::string
const&
label() const noexcept {
return _label; }
270 [[nodiscard]] std::string
const&
help() const noexcept {
return _help; }
274 [[nodiscard]] std::string
const&
placeholder() const noexcept {
return _placeholder; }
282 [[nodiscard]] std::string
const&
draft()
const {
return _text.get(); }
320 if (_spec->truncated) {
321 return detail::engine::trim(_text.get()).empty();
324 return std::ranges::all_of(_members, [](
auto const& entry) {
return entry->blank(); });
327 if (_spec->truncated) {
328 return detail::engine::trim(_text.get()).empty() && !_engaged.get();
330 return _rows.get().empty() && !_engaged.get();
332 return detail::engine::trim(_text.get()).empty();
338 Encoding const& encoding = _encoded.get();
339 if (!encoding.has_value()) {
342 if (!encoding->has_value()) {
351 [[nodiscard]] std::vector<ChoiceOption>
const&
options()
const {
353 return _fetched->get();
355 return _spec->enumOptions;
362 if (_optionsQuery !=
nullptr && !_optionsQuery->pending() && _optionsQuery->error() !=
nullptr) {
363 _optionsQuery->refetch();
369 [[nodiscard]]
bool optionsPending()
const {
return _optionsQuery !=
nullptr && _optionsQuery->pending(); }
375 std::size_t
const current = _unit.peek();
376 if (index >= _spec->units.size() || index == current || current >= _spec->units.size()) {
380 convertDraft(_text.peek(), _spec->units.at(current), _spec->units.at(index), _core->values().locale);
381 _core->runtime().batch([&] {
383 if (!detail::engine::trim(_text.peek()).empty()) {
384 _text.set(converted.value_or(std::string{}));
391 [[nodiscard]] std::span<std::unique_ptr<FieldState>
const>
members() const noexcept {
return _members; }
398 std::ranges::find_if(_members, [&](
auto const& entry) {
return entry->spec().name == name; });
399 return found == _members.end() ? nullptr : found->get();
404 [[nodiscard]] std::vector<std::shared_ptr<RowState>>
const&
rows()
const {
return _rows.get(); }
412 auto row = std::make_shared<RowState>(*_core, *_spec, _path, _nextRowKey++,
this);
414 std::uint64_t
const key = row->key();
415 _core->runtime().batch([&] {
416 _rows.mutate([&](
auto&
rows) {
rows.push_back(std::move(row)); });
426 _rows.mutate([&](
auto&
rows) { std::erase_if(
rows, [&](
auto const& row) {
return row->key() == key; }); });
443 std::expected<void, std::string>
edit(detail::engine::Json
const& value) {
444 if (!value.is_null() && !_spec->truncated) {
446 return std::unexpected(std::string{
"an object field takes a JSON object"});
449 return std::unexpected(std::string{
"a list field takes a JSON array"});
452 _core->runtime().batch([&] {
load(&value); });
461 void load(detail::engine::Json
const* value) {
462 using detail::engine::Json;
463 bool const present = value !=
nullptr && !value->is_null();
464 if (_spec->truncated) {
465 _text.set(present ? detail::engine::toJson(*value) : std::string{});
466 _engaged.set(present && value->is_array());
470 for (
auto const& entry : _members) {
471 entry->load(present ? detail::engine::member(*value, entry->spec().name) :
nullptr);
476 std::vector<std::shared_ptr<RowState>> loaded;
477 if (present && value->is_array()) {
478 for (Json
const& element : value->get_array()) {
479 auto row = std::make_shared<RowState>(*_core, *_spec, _path, _nextRowKey++,
this);
480 for (
auto const& cell : row->fields()) {
481 cell->load(detail::engine::member(element, cell->spec().
name));
484 loaded.push_back(std::move(row));
487 _rows.set(std::move(loaded));
488 _engaged.set(present && value->is_array());
491 std::string
draft = present ?
decodeScalar(*_spec, *value, _core->values()) : std::string{};
492 std::size_t
const unit = _spec->entryUnit;
493 if (
unit != 0 && unit < _spec->units.size() && !
draft.empty()) {
496 .value_or(std::string{});
498 _text.set(std::move(
draft));
500 _engaged.set(_spec->blankAs ==
BlankAs::Empty && present && value->is_string());
506 _text.set(std::string{});
507 _unit.set(_spec->entryUnit);
509 for (
auto const& entry : _members) {
520 for (
auto const& entry : _members) {
524 _engage = std::make_unique<reactive::Effect>(_core->runtime(), [
this] {
525 if (!detail::engine::trim(_text.get()).empty() && !_engaged.peek()) {
530 if (_spec->kind ==
FieldKind::Choice && _spec->choice.has_value() && _core->choices()) {
532 _optionsQuery = std::make_unique<reactive::Query<std::string, std::string>>(
534 [
this](std::string
const& request) {
return _core->choices()(_spec->choice->optionsAction, request); },
535 [
this] {
return optionsRequest(); });
536 _fetched = std::make_unique<reactive::Computed<std::vector<ChoiceOption>>>(runtime, [
this] {
537 std::optional<std::string>
const& reply = _optionsQuery->value();
538 return reply.has_value() ? parseOptions(*reply, *_spec->choice) : std::vector<ChoiceOption>{};
540 _clearStale = std::make_unique<reactive::Effect>(runtime, [
this] { clearStaleSelection(); });
547 void appendSnapshot(std::string& out)
const {
548 std::string
const&
text = _text.peek();
549 out += std::to_string(
text.size());
552 out += std::to_string(_unit.peek());
553 out += _engaged.peek() ?
'+' :
'-';
554 for (
auto const& entry : _members) {
555 entry->appendSnapshot(out);
557 for (
auto const& row : _rows.peek()) {
559 for (
auto const& cell :
row->fields()) {
560 cell->appendSnapshot(out);
566 [[nodiscard]] std::string resolveSlot(i18n::FieldSlot slot, std::string
const& literal)
const {
567 auto const& translations = _core->options().translations;
568 std::string_view
const locale = _core->options().bcp47;
569 auto const explicitKey = i18n::explicitFieldKey(_spec->i18nKey, slot);
570 if (_parent ==
nullptr) {
571 return render::resolveText(translations, locale, explicitKey,
572 i18n::fieldKey(_core->model().actionType(), _spec->name, slot), literal);
574 if (translations && explicitKey) {
575 if (
auto hit = translations(*explicitKey, locale)) {
576 return *std::move(hit);
582 [[nodiscard]] FieldState* sibling(std::string_view name)
const {
583 if (_siblings ==
nullptr) {
587 std::ranges::find_if(*_siblings, [&](
auto const& entry) {
return entry->spec().name == name; });
588 return found == _siblings->end() ? nullptr : found->get();
592 [[nodiscard]] std::optional<std::string> optionsRequest()
const {
593 std::string body =
"{";
596 for (std::string
const& parent : _spec->choice.value().dependsOn) {
597 FieldState
const*
const source = sibling(parent);
598 if (source ==
nullptr) {
601 Encoding const& encoding = source->encoded().get();
602 if (!encoding.has_value() || !encoding->has_value()) {
605 body += (first ?
"" :
",") + detail::engine::quote(parent) +
":" + (*encoding)->json;
614 void clearStaleSelection() {
615 if (_optionsQuery->pending() || _optionsQuery->error() !=
nullptr) {
619 std::optional<std::string>
const request = optionsRequest();
623 std::vector<ChoiceOption>
const& current = _fetched->get();
624 std::string_view
const selected = detail::engine::trim(_text.get());
625 if (selected.empty() ||
626 std::ranges::any_of(current, [&](ChoiceOption
const& option) { return option.valueJson == selected; })) {
629 _core->runtime().batch([&] {
630 _text.set(std::string{});
631 _core->markProgrammatic();
636 [[nodiscard]] std::expected<std::string, FieldError> encodeMembers(
637 std::span<std::unique_ptr<FieldState>
const> members)
const {
638 std::string json =
"{";
640 for (
auto const& entry : members) {
641 Encoding const& encoding = entry->encoded().get();
642 if (!encoding.has_value()) {
643 if (entry->spec().required) {
644 return std::unexpected(
fieldError(
"incomplete", _core->values()));
648 if (!encoding->has_value()) {
649 return std::unexpected(encoding->error());
651 json += (first ?
"" :
",") + detail::engine::quote(entry->spec().name) +
":" + (*encoding)->json;
659 [[nodiscard]]
Encoding computeEncoding()
const {
660 if (_spec->truncated) {
661 std::string_view
const draft = detail::engine::trim(_text.get());
662 bool const array = _spec->kind == FieldKind::ObjectArray;
663 if (draft.empty() && !_engaged.get()) {
664 return (array && _spec->required)
665 ?
Encoding{WireValue{.json =
"[]", .value = ui::Value{ui::ValueList{}}}}
668 if (array && (draft ==
"[]" || draft.empty())) {
669 return WireValue{.json =
"[]", .value = ui::Value{ui::ValueList{}}};
671 return std::unexpected(
fieldError(
"unrepresentable", _core->values()));
673 if (_spec->kind == FieldKind::Object) {
677 auto json = encodeMembers(_members);
679 return std::unexpected(json.error());
681 return WireValue{.json = *std::move(json), .value = ui::Value{}};
683 if (_spec->kind == FieldKind::ObjectArray) {
684 auto const& rows = _rows.get();
685 if (rows.empty() && !_engaged.get() && !_spec->required) {
688 std::string json =
"[";
689 for (std::size_t i = 0; i < rows.size(); ++i) {
690 auto row = encodeMembers(rows.at(i)->fields());
692 return std::unexpected(
row.error());
694 json += (i == 0 ?
"" :
",") + *row;
696 return WireValue{.json = json +
"]", .value = ui::Value{}};
698 return encodeScalar(*_spec, _text.get(), _unit.get(), _core->values());
703 detail::engine::SessionCore* _core;
704 FieldSpec
const* _spec;
706 std::vector<std::unique_ptr<FieldState>>
const* _siblings;
707 FieldState
const* _parent;
710 std::string _placeholder;
711 std::uint64_t _nextRowKey = 1;
712 reactive::Signal<std::string> _text;
713 reactive::Signal<std::size_t> _unit;
714 reactive::Signal<bool> _engaged;
715 reactive::Signal<std::vector<std::shared_ptr<RowState>>> _rows;
716 std::vector<std::unique_ptr<FieldState>> _members;
717 reactive::Computed<Encoding> _encoded;
718 reactive::Computed<std::optional<FieldError>> _error;
719 reactive::Computed<bool> _visible;
720 reactive::Computed<bool> _readonly;
721 reactive::Computed<bool> _required;
722 std::unique_ptr<reactive::Effect> _engage;
723 std::unique_ptr<reactive::Query<std::string, std::string>> _optionsQuery;
724 std::unique_ptr<reactive::Computed<std::vector<ChoiceOption>>> _fetched;
725 std::unique_ptr<reactive::Effect> _clearStale;
732 _fields.push_back(std::make_unique<FieldState>(
733 core, member, arrayPath +
"#" + std::to_string(
key) +
"." + member.name, &_fields, parent));
737inline RowState::~RowState() =
default;
740 auto const found = std::ranges::find_if(_fields, [&](
auto const& entry) {
return entry->spec().name == name; });
741 return found == _fields.end() ? nullptr : found->get();
745 return std::ranges::all_of(_fields, [](
auto const& entry) {
return entry->blank(); });
749 for (
auto const& entry : _fields) {
754namespace detail::engine {
756inline FieldState* SessionCore::topLevel(std::string_view name)
const {
757 if (_fields ==
nullptr) {
760 auto const found = std::ranges::find_if(*_fields, [&](
auto const& entry) {
return entry->spec().name == name; });
761 return found == _fields->end() ? nullptr : found->get();
764inline RuleLookup SessionCore::lookup()
const {
765 return [
this](std::string_view name) {
766 FieldState
const*
const field = topLevel(name);
767 return field ==
nullptr ? RuleOperand{} : field->ruleOperand();
787 : _core{std::make_unique<detail::engine::SessionCore>(runtime, std::move(model), std::move(choices),
788 std::move(options))},
789 _submit{std::move(submit)},
790 _assembled{runtime, [this] {
return assemble(); }},
793 std::optional<std::string>
const& assembled = _assembled.get();
794 if (!assembled.has_value()) {
797 auto const& accepts = _core->options().accepts;
798 return !accepts || accepts(*assembled);
800 _body{runtime, [
this] {
return _ready.get() ? _assembled.get() : std::optional<std::string>{}; }},
802 [
this](std::string body) {
return _submit(_core->model().actionType(), std::move(body)); }},
803 _attempts{runtime, std::uint64_t{0}} {
805 throw std::invalid_argument{
"FormSession: a Submitter is required"};
807 for (FieldSpec
const& spec : _core->model().fields()) {
808 _fields.push_back(std::make_unique<FieldState>(*_core, spec, spec.name, &_fields,
nullptr));
810 _core->adopt(&_fields);
811 _core->onProgrammatic([
this] { _lastSubmitted = _body.peek(); });
812 for (std::size_t i = 0; i < _core->model().groups().size(); ++i) {
813 _collapsed.push_back(std::make_unique<reactive::Signal<bool>>(runtime,
false));
814 _tabs.push_back(std::make_unique<reactive::Signal<std::size_t>>(runtime, std::size_t{0}));
816 for (
auto const& field : _fields) {
820 _autoSubmit = std::make_unique<reactive::Effect>(runtime, [
this] { autoSubmit(); });
823 ~FormSession() =
default;
824 FormSession(FormSession
const&) =
delete;
825 FormSession& operator=(FormSession
const&) =
delete;
826 FormSession(FormSession&&) =
delete;
827 FormSession& operator=(FormSession&&) =
delete;
847 [[nodiscard]] std::span<std::unique_ptr<FieldState>
const>
fields() const noexcept {
return _fields; }
852 [[nodiscard]]
static std::optional<std::size_t>
rowIndexOf(std::string_view digits) {
853 std::size_t parsed = 0;
854 char const*
const end = std::to_address(digits.end());
855 auto const result = std::from_chars(std::to_address(digits.begin()), end, parsed);
856 if (digits.empty() || result.ec != std::errc{} || result.ptr != end) {
868 std::span<std::unique_ptr<FieldState>
const> scope = _fields;
869 std::string_view rest = path;
870 auto const missing = [&] {
871 return std::out_of_range{
"FormSession::field: no field '" + std::string{path} +
"'"};
873 while (!rest.empty()) {
874 auto const dot = rest.find(
'.');
875 std::string_view segment = rest.substr(0, dot);
876 rest = dot == std::string_view::npos ? std::string_view{} : rest.substr(dot + 1);
877 std::optional<std::size_t> index;
878 if (
auto const bracket = segment.find(
'['); bracket != std::string_view::npos) {
879 if (!segment.ends_with(
']')) {
882 index =
rowIndexOf(segment.substr(bracket + 1, segment.size() - bracket - 2));
883 if (!index.has_value()) {
886 segment = segment.substr(0, bracket);
889 std::ranges::find_if(scope, [&](
auto const& entry) {
return entry->spec().name == segment; });
890 if (found == scope.end()) {
893 current = found->get();
894 if (index.has_value()) {
895 auto const& rows = current->rows();
896 if (*index >= rows.size() || rest.empty()) {
899 scope = rows.at(*index)->fields();
901 scope = current->members();
902 if (!rest.empty() && scope.empty()) {
907 if (current ==
nullptr) {
916 [[nodiscard]]
bool ready()
const {
return _ready.get(); }
920 [[nodiscard]] std::optional<std::string>
body()
const {
return _body.get(); }
926 std::optional<std::string>
const body = _body.peek();
927 if (!
body.has_value()) {
930 _lastSubmitted =
body;
931 if (_mutation.run(*
body)) {
932 _attempts.set(_attempts.peek() + 1);
941 auto const dom = detail::engine::parseJson(bodyJson);
942 if (!dom || !dom->is_object()) {
946 for (
auto const&
field : _fields) {
947 field->load(detail::engine::member(*dom,
field->spec().name));
949 _core->markProgrammatic();
960 auto const dom = detail::engine::parseJson(bodyJson);
961 if (!dom || !dom->is_object()) {
964 std::vector<std::size_t> loaded;
966 for (std::size_t index = 0; index < _fields.size(); ++index) {
968 if (
field.snapshot() == _loaded.at(index)) {
969 field.load(detail::engine::member(*dom,
field.spec().name));
970 loaded.push_back(index);
973 _core->markProgrammatic();
975 for (std::size_t
const index : loaded) {
976 _loaded.at(index) = _fields.at(index)->snapshot();
985 void assign(std::string_view path, std::string_view jsonValue) {
987 auto const dom = detail::engine::parseJson(jsonValue);
993 _core->markProgrammatic();
996 for (std::size_t index = 0; index < _fields.size(); ++index) {
997 std::string
const& name = _fields.at(index)->spec().name;
998 if (path.starts_with(name) &&
999 (path.size() == name.size() || path.at(name.size()) ==
'.' || path.at(name.size()) ==
'[')) {
1000 _loaded.at(index) = _fields.at(index)->snapshot();
1008 for (
auto const&
field : _fields) {
1011 _core->markProgrammatic();
1018 [[nodiscard]]
bool pending()
const {
return _mutation.pending(); }
1022 [[nodiscard]] std::optional<std::string>
const&
lastReply()
const {
return _mutation.lastResult(); }
1026 [[nodiscard]] std::exception_ptr
lastError()
const {
return _mutation.error(); }
1031 [[nodiscard]] std::uint64_t
successCount()
const {
return _mutation.successCount(); }
1055 FieldGroupSpec const& spec = detail::engine::elementAt(_core->model().groups(), group);
1059 return render::resolveText(_core->options().translations, _core->options().bcp47, std::nullopt,
1066 for (
auto const& field : _fields) {
1067 _loaded.push_back(field->snapshot());
1071 [[nodiscard]] std::optional<std::string> assemble()
const {
1072 std::string json =
"{";
1074 for (
auto const& field : _fields) {
1075 Encoding
const& encoding = field->encoded().get();
1076 bool const required = field->required().get();
1077 std::string literal;
1078 if (!encoding.has_value()) {
1079 if (field->spec().blankAs == BlankAs::Empty && field->engaged().get() && !required) {
1081 } else if (required) {
1082 return std::nullopt;
1086 }
else if (!encoding->has_value()) {
1087 return std::nullopt;
1089 literal = (*encoding)->json;
1091 json += (first ?
"" :
",") + detail::engine::quote(field->spec().name) +
":" + literal;
1094 if (
gatingVerdict(_core->model().rules(), _core->lookup()) == Tri::False) {
1095 return std::nullopt;
1101 if (_core->model().submitMode() != SubmitMode::Automatic) {
1107 if (std::ranges::any_of(_fields, [](
auto const& field) {
return field->optionsPending(); })) {
1112 if (_mutation.pending()) {
1115 std::optional<std::string>
const& body = _body.get();
1116 if (!body.has_value() || body == _lastSubmitted) {
1119 _lastSubmitted = body;
1120 std::string
const toSend = *body;
1121 runtime().untracked([&] {
1122 if (_mutation.run(toSend)) {
1123 _attempts.set(_attempts.peek() + 1);
1128 std::unique_ptr<detail::engine::SessionCore> _core;
1130 std::vector<std::unique_ptr<FieldState>> _fields;
1131 std::vector<std::unique_ptr<reactive::Signal<bool>>> _collapsed;
1132 std::vector<std::unique_ptr<reactive::Signal<std::size_t>>> _tabs;
1133 reactive::Computed<std::optional<std::string>> _assembled;
1134 reactive::Computed<bool> _ready;
1135 reactive::Computed<std::optional<std::string>> _body;
1136 reactive::Mutation<std::string, std::string> _mutation;
1137 reactive::Signal<std::uint64_t> _attempts;
1138 std::optional<std::string> _lastSubmitted;
1140 std::vector<std::string> _loaded;
1141 std::unique_ptr<reactive::Effect> _autoSubmit;
Node text(TextOptions const &options)
A text node.
Handle< T > row()
The innermost row, {"row": ""}.
A derived value: computed lazily on first read, cached, and recomputed only after a source it read re...
The owner of a signal graph: every node takes one, and every operation runs on its owner.
A source value. Reading it with get() inside a tracked run subscribes that run.
std::string const & name() const noexcept
The element's id.
Declarative control: Query, Mutation and Subscription turn server interaction into reactive state,...
FormModel: the immutable runtime model of one action's form, read from the schema JSON schemaJson<A>(...
@ Choice
A set fetched by executing an options action.
@ ObjectArray
A list of nested objects.
SubmitMode
When a form submits.
The renderer-side translation-catalog seam over morph's derived message keys (morph::forms::i18n,...
std::function< std::optional< std::string >(std::string_view key, std::string_view bcp47Locale)> TranslationProvider
A host-supplied catalog lookup: key/bcp47Locale -> translated text, or std::nullopt on a miss.
std::string resolveText(const TranslationProvider &provider, std::string_view bcp47Locale, const std::optional< std::string > &explicitKey, std::string_view derivedKey, std::string_view schemaLiteral)
Resolves one display slot's text: explicit key, then derived key, then the schema literal.
x-rules read into RuleExpr trees, translated into the UI document's expression language (expr/1) and ...
std::function< RuleOperand(std::string_view field)> RuleLookup
Reads one field by wire name, for a rule. The session's lookup is tracked, so a verdict computed insi...
Tri readonlyVerdict(std::span< RuleExpr const > rules, std::string_view field, RuleLookup const &lookup)
Whether field is frozen: as soon as one readonlyWhen naming it is true.
@ False
Definitely does not hold.
Tri requiredVerdict(std::span< RuleExpr const > rules, std::string_view field, RuleLookup const &lookup)
Whether field is required by a rule: as soon as one requiredWhen naming it is true.
Tri visibleVerdict(std::span< RuleExpr const > rules, std::string_view field, RuleLookup const &lookup)
Whether field is shown: hidden as soon as one visibleWhen naming it is false.
Tri gatingVerdict(std::span< RuleExpr const > rules, RuleLookup const &lookup)
Whether the gating rules let the form submit.
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
Signal<T>, Computed<T> and Effect: the nodes of a reactive graph.
void request(Store< ViewState, Msg > &store, bridge::BridgeHandler< Model, Sharing > &handler, async::CallbackScope const &scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg)
Executes action and sends its result, or its failure, to store as one message.
The locale facts both control-edge conversions need, in one designated-initialisable aggregate.
Exact text helpers the forms engine shares: JSON quoting and reading, canonical decimal comparison an...
The forms engine's encoders and decoders: a field's draft text to its exact wire literal,...
Encoding encodeScalar(FieldSpec const &spec, std::string_view draft, std::size_t unit, ValueContext const &context)
Encodes one scalar field's draft (every kind but Object and ObjectArray).
FieldError fieldError(std::string_view code, ValueContext const &context)
The error for code, its message resolved through context's translations.
std::optional< std::string > convertDraft(std::string_view draft, UnitOption const &from, UnitOption const &target, render::NumericLocale const &locale)
Converts a draft from one unit to another exactly, rounding half away from zero to the target unit's ...
std::string decodeScalar(FieldSpec const &spec, detail::engine::Json const &value, ValueContext const &context)
The draft a control shows for the wire value value — the inverse of encodeScalar, so encoding the res...
std::optional< std::expected< WireValue, FieldError > > Encoding
A draft's encoding: std::nullopt when blank, else the value or why it does not encode.
std::string defaultPlaceholder(FieldSpec const &spec)
The placeholder a control shows when the schema declares none.