aboutsummaryrefslogtreecommitdiff
path: root/apps/dreki_web/src/dreki_web_ui_json_form.erl
blob: 49f69f8f22693d2706e9321863abe9e96ff4c5fb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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>>.