diff options
Diffstat (limited to 'apps/dreki_web/src/dreki_web_ui_json_form.erl')
-rw-r--r-- | apps/dreki_web/src/dreki_web_ui_json_form.erl | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/apps/dreki_web/src/dreki_web_ui_json_form.erl b/apps/dreki_web/src/dreki_web_ui_json_form.erl new file mode 100644 index 0000000..49f69f8 --- /dev/null +++ b/apps/dreki_web/src/dreki_web_ui_json_form.erl @@ -0,0 +1,127 @@ +-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([], 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. + +render(Schema, Opts) -> + 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(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, [{for, 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">>} + | Attributes0], + HtmlNode = {'div', [{class, <<"json-field">>}], [ + {label, [{for, Name}, {class, <<"block text-sm font-medium text-gray-700">>}], maps:get(label, IOpts, Field)}, + {'div', [{class, <<"mt-1">>}], [{input, Attributes}]} + ]}, + HtmlNode. + +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>>. |