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>>.
|