aboutsummaryrefslogtreecommitdiff
path: root/apps/dreki_web/src/ui/dreki_web_ui_json_form.erl
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dreki_web/src/ui/dreki_web_ui_json_form.erl')
-rw-r--r--apps/dreki_web/src/ui/dreki_web_ui_json_form.erl160
1 files changed, 160 insertions, 0 deletions
diff --git a/apps/dreki_web/src/ui/dreki_web_ui_json_form.erl b/apps/dreki_web/src/ui/dreki_web_ui_json_form.erl
new file mode 100644
index 0000000..51a9de4
--- /dev/null
+++ b/apps/dreki_web/src/ui/dreki_web_ui_json_form.erl
@@ -0,0 +1,160 @@
+-module(dreki_web_ui_json_form).
+-export([render_html/2]).
+-export([render/2]).
+-export([to_html/1]).
+
+-type dreki_form() :: #{
+ input => binary(),
+ label => binary()
+}.
+
+render_html(Schema, Opts) ->
+ Abstract = render(Schema, Opts),
+ to_html(Abstract).
+
+to_html(Atom) when is_atom(Atom) ->
+ as_binary(Atom);
+to_html(Binary) when is_binary(Binary) ->
+ Binary;
+to_html(Abstract) ->
+ to_html(Abstract, []).
+
+to_html([{Node, Attrs} | Rest], Acc) ->
+ BNode = as_binary(Node),
+ AttrsS = attrs_to_html(Attrs),
+ to_html(Rest, [[<<"<">>, BNode, <<" ">>, AttrsS, <<">">>] | Acc]);
+to_html([{Node, Attrs, Content} | Rest], Acc) ->
+ BNode = as_binary(Node),
+ AttrsS = attrs_to_html(Attrs),
+ logger:debug("Node ~p Attrs ~p Content ~p", [Node, Attrs, Content]),
+ to_html(Rest, [[<<"<">>, BNode, <<" ">>, AttrsS, <<">">>, to_html(Content), <<"</">>, BNode, <<">">>] | Acc]);
+to_html([List | Rest], Acc) when is_list(List) ->
+ to_html(Rest, [to_html(List) | Acc]);
+to_html([], Acc) ->
+ lists:reverse(Acc).
+
+attrs_to_html(Attrs) -> attrs_to_html(Attrs, []).
+attrs_to_html([{Attr, undefined} | Rest], Acc) ->
+ attrs_to_html(Rest, Acc);
+attrs_to_html([{Attr, false} | Rest], Acc) ->
+ attrs_to_html(Rest, Acc);
+attrs_to_html([{Attr, true} | Rest], Acc) ->
+ attrs_to_html(Rest, [[as_binary(Attr), <<" ">>] | Acc]);
+attrs_to_html([{Attr, Num} | Rest], Acc) when is_integer(Num) ->
+ attrs_to_html(Rest, [[as_binary(Attr), <<"=">>, Num, <<" ">>] | Acc]);
+attrs_to_html([{Attr, Value} | Rest], Acc) ->
+ attrs_to_html(Rest, [[as_binary(Attr), <<"=">>, <<"\"">>, as_binary(Value), <<"\"">>, <<" ">>] | Acc]);
+attrs_to_html([], Acc) ->
+ Acc.
+
+put_new(Key, Value, Map) ->
+ case maps:is_key(Key, Map) of
+ true ->
+ Map;
+ false ->
+ maps:put(Key, Value, Map)
+ end.
+
+render(Schema, Opts) ->
+ render(Schema, Opts, {'div', [{class, <<"mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">>}]}).
+
+render(Schema, Opts, {Elem, Attrs}) ->
+ [{Elem, Attrs, render(Schema, Opts, undefined)}];
+render(Schema, Opts0, undefined) ->
+ Opts = put_new(name, <<"form">>, Opts0),
+ lists:reverse(maps:fold(fun (Key, Value, Acc) ->
+ case render_property(Key, Value, Schema, Schema, Opts) of
+ {ok, Html} -> [Html | Acc];
+ ignore -> Acc
+ end
+ end, [], maps:get(properties, Schema))).
+
+render_property(Field, Config = #{enum := Enum}, Parent, Schema, Opts) ->
+ FormAttrs = maps:get(<<"dreki:form">>, Config, #{}),
+ Required = lists:member(Field, maps:get(required, Parent, [])),
+ Label = maps:get(label, FormAttrs, maps:get(title, Config, Field)),
+ InputOpts = #{required => Required, label => Label, values => Enum},
+ {ok, render_input(select, Field, undefined, InputOpts, Opts)};
+
+render_property(Field, Config = #{type := Type}, Parent, Schema, Opts) ->
+ FormAttrs = maps:get(<<"dreki:form">>, Config, #{}),
+ Input = maps:get(input, FormAttrs, input),
+ InputType = maps:get(input_type, FormAttrs, input_type(Type)),
+ Required = lists:member(Field, maps:get(required, Parent, [])),
+ Label = maps:get(label, FormAttrs, maps:get(title, Config, Field)),
+ InputOpts = #{required => Required, label => Label, input_type => InputType},
+ {ok, render_input(Input, Field, Type, InputOpts, Opts)};
+
+render_property(Key, SubSchema = #{<<"@schema">> := SubSchemaUrn}, Parent, Schema, Opts) ->
+ Name = maps:get(name, Opts),
+ KeyB = atom_to_binary(Key),
+ InnerForm = render(SubSchema, Opts#{name => <<Name/binary, "[", KeyB/binary, "]">>}, undefined),
+ Head = {'div', [{class, <<"sm:col-span-6 mb-3">>}], [
+ {h3, [{class, <<"text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">>}], maps:get(title, SubSchema, SubSchemaUrn)},
+ {p, [{class, <<"mt-1 text-sm text-gray-500 dark:text-gray-400">>}], maps:get(description, SubSchema, <<>>)}
+ ]},
+ {ok, {'div', [{class, <<"sm:col-span-6">>}],
+ [
+ {'div', [{class, <<"mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">>}], [Head, InnerForm]}
+ ]}};
+
+render_property(Field, #{'$ref' := Ref}, Parent, Schema = #{'$defs' := Defs}, Opts) ->
+ case maps:get(Ref, Defs, undefined) of
+ undefined -> logger:error("didn't get ref ~p", [Ref]);
+ ExpandedRef ->
+ logger:debug("Skipping ref for now: ~p ~p", [Ref, ExpandedRef])
+ end,
+ ignore.
+
+base_attributes(Field, IOpts, FOpts) ->
+ Name = field_name(Field, FOpts),
+ {Name, [
+ {id, field_id(Field, FOpts)},
+ {name, Name},
+ {required, maps:get(required, IOpts, false)}
+ ]}.
+
+render_input(select, Field, _, IOpts, FOpts) ->
+ {Name, Attributes} = base_attributes(Field, IOpts, FOpts),
+ OptionsHtml = lists:map(fun (Opt) -> {option, [], Opt} end, maps:get(values, IOpts, [])),
+ {'div', [{class, <<"json-field">>}],
+ [
+ label(Field, Name, maps:get(label, IOpts, Field)),
+ {select, Attributes, OptionsHtml}
+ ]};
+
+render_input(input, Field, Type, IOpts, FOpts) ->
+ {Name, Attributes0} = base_attributes(Field, IOpts, FOpts),
+ Attributes = [
+ {value, maps:get(value, IOpts, undefined)},
+ {placeholder, maps:get(placeholder, IOpts, undefined)},
+ {readonly, maps:get(readonly, IOpts, false)},
+ {autocomplete, maps:get(autocomplete, IOpts, <<"off">>)},
+ {type, maps:get(input_type, IOpts)},
+ {class, <<"shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-800">>}
+ | Attributes0],
+ {'div', [{class, <<"json-field sm:col-span-6">>}],
+ [
+ label(Field, Name, maps:get(label, IOpts, Field)),
+ {'div', [{class, <<"mt-1">>}], [{input, Attributes}]}
+ ]}.
+
+label(_Field, Name, Label) ->
+ {label, [{for, Name}, {class, <<"block text-sm font-medium dark:text-gray-400 text-gray-700 dark:text-gray-400">>}], Label}.
+
+as_binary(Atom) when is_atom(Atom) ->
+ atom_to_binary(Atom);
+as_binary(B) when is_binary(B) ->
+ B.
+
+input_type(_) -> <<"text">>.
+
+field_name(Field, Opts) ->
+ FB = as_binary(Field),
+ BaseName = maps:get(name, Opts, <<"form">>),
+ <<BaseName/binary, "[", FB/binary, "]">>.
+
+field_id(Field, Opts) ->
+ FB = as_binary(Field),
+ BaseId = maps:get(id, Opts, <<"form">>),
+ <<BaseId/binary, "-", FB/binary>>.