Goal
HTML生成モジュールを作成する。
Dev-Environment
OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.5
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.5
Wait a minute
最初に書いておきます。失敗談の記事です。
Phoenix_HTMLライブラリを読んで、
HTMLの生成を自分で実装してみようと思った。
HTMLの生成を自分で実装してみようと思った。
今回の内容だと、何かの役に立つって内容ではないです。
検証用なのでかなり大雑把に作っています。
検証用なのでかなり大雑把に作っています。
Index
Generate HTML
Generate HTML
最初のステップは、ただpタグの文字列を返すだけです。
first step
defmodule GenerateHtml do
def p_tag do
"<p>"
end
end
iex> GenerateHtml.p_tag
"<p>"
次は、アトムを文字列に変換して出力します。
2th step
defmodule GenerateHtml do
def tag(:p) do
"<#{:p}>"
end
end
iex> GenerateHtml.tag(:p)
"<p>"
少し汎用的にタグの生成をできるようにします。
3th step
defmodule GenerateHtml do
def tag(name) when is_atom(name) do
"<#{name}>"
end
def close_tag(name) when is_atom(name) do
"</#{name}>"
end
end
iex> GenerateHtml.tag(:p)
"<p>"
iex> GenerateHtml.close_tag(:p)
"</p>"
CSSのclassを指定できるようにします。
4th step
defmodule GenerateHtml do
def tag(name) when is_atom(name) do
tag(name, [])
end
def tag(name, attrs) when is_atom(name) and is_list(attrs) do
"<#{name} #{:class}=\"#{Keyword.get_values(attrs, :class)}\">"
end
def close_tag(name) when is_atom(name) do
"</#{name}>"
end
end
iex> GenerateHtml.tag(:p, class: "hoge")
"<p class=\"hoge\">"
このままでは、classの指定しかできません。
タグのオプションやidなど他にも指定できた方が良いものが多くありますね。
タグのオプションやidなど他にも指定できた方が良いものが多くありますね。
5th step
defmodule GenerateHtml do
def tag(name) when is_atom(name) do
tag(name, [])
end
def tag(name, attrs) when is_atom(name) and is_list(attrs) do
"<#{name}#{build_attrs(attrs)}>"
end
def close_tag(name) when is_atom(name) do
"</#{name}>"
end
defp build_attrs(attrs) when is_list(attrs) do
Enum.reduce(attrs, "", &attrs_mapper/2)
end
defp attrs_mapper({key, value}, acc) when is_atom(value) or is_binary(value),
do: acc <> " #{key}=\"#{value}\""
defp attrs_mapper({key, value}, acc) when is_list(value),
do: attrs_mapper({key, Enum.join(value, " ")}, acc)
end
iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], id: :foo)
"<p class=\"hoge huge\" id=\"foo\">"
data-[name]と言ったように-(ハイフン)で区切られたオプションを指定したい場合はどうしましょう?
実は変数名やアトムで-(ハイフン)を使うとエラーになります。
実は変数名やアトムで-(ハイフン)を使うとエラーになります。
こんな感じに…
iex> data-type = "value"
** (CompileError) iex:3: illegal pattern
iex> data_type = :data-type
** (RuntimeError) undefined function: type/0
キーとバリューの値にあたる部分をタプルにして、ネストとして処理してみます。
6th step
defmodule GenerateHtml do
def tag(name) when is_atom(name) do
tag(name, [])
end
def tag(name, attrs) when is_atom(name) and is_list(attrs) do
"<#{name}#{build_attrs(attrs)}>"
end
def close_tag(name) when is_atom(name) do
"</#{name}>"
end
defp build_attrs(attrs) when is_list(attrs) do
Enum.reduce(attrs, "", &attrs_mapper/2)
end
defp attrs_mapper({key, value}, acc) when is_atom(value) or is_binary(value),
do: acc <> " #{key}=\"#{value}\""
defp attrs_mapper({key, value}, acc) when is_list(value),
do: attrs_mapper({key, Enum.join(value, " ")}, acc)
defp attrs_mapper({key, {nest_key, value}}, acc),
do: attrs_mapper({"#{key}-#{nest_key}", value}, acc)
end
iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], data: {:type, "value"})
"<p class=\"hoge huge\" data-type=\"value\">"
valueの部分にtrue、false、nilが来た場合の処理を追加します。
関数の定義している順番にパターンマッチしているようなので、定義する順番に気を付けて下さい。
関数の定義している順番にパターンマッチしているようなので、定義する順番に気を付けて下さい。
7th step
defmodule GenerateHtml do
def tag(name) when is_atom(name) do
tag(name, [])
end
def tag(name, attrs) when is_atom(name) and is_list(attrs) do
"<#{name}#{build_attrs(attrs)}>"
end
def close_tag(name) when is_atom(name) do
"</#{name}>"
end
defp build_attrs(attrs) when is_list(attrs) do
Enum.reduce(attrs, "", &attrs_mapper/2)
end
defp attrs_mapper({key, true}, acc),
do: attrs_mapper({key, key}, acc)
defp attrs_mapper({_key, false}, acc),
do: acc
defp attrs_mapper({_key, nil}, acc),
do: acc
defp attrs_mapper({key, value}, acc) when is_atom(value) or is_binary(value),
do: acc <> " #{key}=\"#{value}\""
defp attrs_mapper({key, value}, acc) when is_list(value),
do: attrs_mapper({key, Enum.join(value, " ")}, acc)
defp attrs_mapper({key, {nest_key, value}}, acc),
do: attrs_mapper({"#{key}-#{nest_key}", value}, acc)
end
iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], data: {:type, "value"}, hoge: true, huge: false, foo: nil, bar: true)
"<p class=\"hoge huge\" data-type=\"value\" hoge=\"hoge\" bar=\"bar\">"
ここまでやって、ようやく気が付きました。
再帰で処理しないとだめだこれ…っとorz
再帰で処理しないとだめだこれ…っとorz
とりあえず今回はここまで~
値の部分がネストのネストになっていたらどうするかなど、対応しないといけないパターンが他にもある。
それにリストとタプルが混在していて分かり辛いので、リストの方へ統一しようと思います。
それにリストとタプルが混在していて分かり辛いので、リストの方へ統一しようと思います。
ちなみに、以下のようなパターンに対応していない。
iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], data: {:type, "value", "value2"}, hoge: true, huge: false, foo: nil, bar: true)
** (FunctionClauseError) no function clause matching in GenerateHtml.attrs_mapper/2
(generate_html_dsl) lib/generate_html.ex:18: GenerateHtml.attrs_mapper({:data, {:type, "value", "value2"}}, " class=\"hoge huge\"")
(elixir) lib/enum.ex:1261: Enum."-reduce/3-lists^foldl/2-0-"/3
(generate_html_dsl) lib/generate_html.ex:7: GenerateHtml.tag/2
iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], data: {:type, "value", :input, "value2"}, hoge: true, huge: false, foo: nil, bar: true)
** (FunctionClauseError) no function clause matching in GenerateHtml.attrs_mapper/2
(generate_html_dsl) lib/generate_html.ex:18: GenerateHtml.attrs_mapper({:data, {:type, "value", :input, "value2"}}, " class=\"hoge huge\"")
(elixir) lib/enum.ex:1261: Enum."-reduce/3-lists^foldl/2-0-"/3
(generate_html_dsl) lib/generate_html.ex:7: GenerateHtml.tag/2
そういうわけで結局、再帰的に処理をする形に修正すると思います。
Speaking to oneself
とりあえず、やってみたが…
何故、Phoenix_HTMLライブラリでは再帰で処理しているのか理解した。
何故、Phoenix_HTMLライブラリでは再帰で処理しているのか理解した。
設計思想の一部でも理解できたので、悪くはないでしょう。
最終的に何がやりたいのかって話ですが…
ページネーション用のHTML生成に使えたらな~ってのと、EExのEngineについて知りたかった。
ページネーション用のHTML生成に使えたらな~ってのと、EExのEngineについて知りたかった。
追々、記事にできたらします。ノシ