-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 => <>}, 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">>), <>. field_id(Field, Opts) -> FB = as_binary(Field), BaseId = maps:get(id, Opts, <<"form">>), <>.