diff --git a/lib/frankt.ex b/lib/frankt.ex index 03ef1fa..26e8c1b 100644 --- a/lib/frankt.ex +++ b/lib/frankt.ex @@ -68,89 +68,58 @@ defmodule Frankt do """ @type response_handler :: ((params :: map(), socket :: Phoenix.Socket.t()) -> any()) - defmacro __using__(opts) do + defmacro __using__(_opts) do quote do - Module.register_attribute __MODULE__, :responses, accumulate: true - Module.put_attribute __MODULE__, :gettext, unquote(Keyword.get(opts, :gettext)) - - import Frankt, only: [defresponse: 2] - - @before_compile unquote(__MODULE__) + def handle_in("frankt-action", params = %{"action" => action}, socket) do + [handler_name, handler_fn] = String.split(action, ":") + handler_module = Frankt.__handler__(__MODULE__, handler_name) + gettext = Frankt.__gettext__(__MODULE__) + data = Map.get(params, "data", %{}) + + Frankt.__execute_action__(handler_module, String.to_existing_atom(handler_fn), data, socket, gettext) + {:noreply, socket} + end end end - @doc """ - Define a response to client-side triggers. - - This macro generates the code that call the response handler `function` when `message` is received. - - Take into account that to make a certain response behave differently depending on the received - params you must pattern match in the response handler `function` instead of using a second - `defresponse`. For more information take a look at `t:response_handler/0`. - """ - @spec defresponse(message :: String.t(), function :: response_handler()) :: any() - defmacro defresponse(message, function) do - quote do - Module.put_attribute(__MODULE__, :responses, unquote(message)) - - def execute_response(unquote(message), params, socket) do - Frankt.__execute_response__(unquote(function), params, socket, @gettext) + @doc false + def __execute_action__(module, fun, params, socket, gettext) do + invoke_action = fn -> + unless function_exported?(module, fun, 2) do + raise "Frankt is trying to execute an action, but the handler module does not define the appropriate function. Please define a '#{fun}/2' function in your ยท#{module} module." end + apply(module, fun, [params, socket]) end - end - - # Before the compilation takes place we need to generate the module's `use` - # handler so it can be used inside the Frankt channel with the response - # handlers exported correctly. - # It's important to point that this process should be done just before the - # compilation takes place to: - # * Be able to generate a bit more of code that go into the compilation - # * Be able to read responses storage to kow which ones needs to inject in the - # socket channel. - defmacro __before_compile__(_) do - quote do - defmacro __using__(_) do - Enum.map(@responses, fn message -> - quote do - def handle_in(unquote(message), params, socket) do - unquote(__MODULE__).execute_response(unquote(message), params, socket) - end - end - end) - end + if gettext do + locale = + case Map.get(socket.assigns, :locale) do + nil -> raise "You have configured Frankt to use Gettext for i18n, but the response does not know which locale to use. Please store the desired locale into a `locale` assign in the socket." + locale -> locale + end + Gettext.with_locale(gettext, locale, invoke_action) + else + invoke_action.() end end - @doc """ - Build the topic name for the Frankt channel. - - The topic name is used when connecting clients to Frankt. It can also be used in other - circumstances such broadcasting server-side updates for certain users. - - The `client` variable can be any value used to identify each connection (for example the - connected user ID). This variable will be base16 encoded for privacy. - """ - @spec topic_name(client :: String.t()) :: String.t() - def topic_name(client), do: "frankt:#{:md5 |> :crypto.hash(client) |> Base.encode16()}" - @doc false - def __execute_response__(function, params, socket, nil) do - function.(params, socket) - {:noreply, socket} - end - def __execute_response__(function, params, socket, gettext) do - Gettext.with_locale(gettext, get_locale(socket), fn -> - function.(params, socket) - {:noreply, socket} - end) + def __handler__(frankt_module, name) when is_binary(name) do + handlers = Application.get_env(:frankt, frankt_module) + if is_nil(handlers) do + raise "You have not configured any handlers for Frankt. Please set at least one handler in your configuration." + end + case get_in(handlers, [:handlers, String.to_existing_atom(name)]) do + nil -> "Frankt can not find a handler for '#{name}'. Please, chech that you are using the correct name or define a new handler in your configuration." + handler -> handler + end end - defp get_locale(socket) do - case Map.get(socket.assigns, :locale) do - nil -> raise "You have configured Frankt to use Gettext for i18n, but the response does not know which locale to use. Please store the desired locale into a `locale` assign in the socket." - locale -> locale - end + @doc false + def __gettext__(frankt_module) do + :frankt + |> Application.get_env(frankt_module, []) + |> Keyword.get(:gettext) end end diff --git a/lib/test/action_test.ex b/lib/test/action_test.ex new file mode 100644 index 0000000..7d88109 --- /dev/null +++ b/lib/test/action_test.ex @@ -0,0 +1,30 @@ +defmodule Frankt.ActionTest do + @moduledoc """ + Conveniences for testing Frankt actions. + + Frankt tests are actually channel tests. For more information take a look at + `Phoenix.ChannelTest`. + """ + + @doc false + defmacro __using__(_opts) do + quote do + import Frankt.ActionTest + end + end + + @doc """ + Call a Frankt action. + + Pushes a mesasge into the channel which triggers the Frankt `handle_in/3` function and then + dispatches to the corresponding action. + After pushing the message to Frankt you can check the response by using + `Phoenix.ChannelTest.assert_push/3`. + """ + @spec frankt_action(socket :: Socket.t, action :: String.t, payload :: map()) :: reference() + defmacro frankt_action(socket, action, payload \\ %{}) do + quote do + push(unquote(socket), "frankt-action", %{action: unquote(action), data: unquote(payload)}) + end + end +end diff --git a/package.json b/package.json index 8a4cf77..570a038 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "homepage": "https://github.com/acutario/frankt#readme", "dependencies": { - "phoenix": "^1.3.0" + "phoenix": "^1.3.0", + "deep-compact": "^1.1.0" } } diff --git a/priv/static/dom.js b/priv/static/dom.js index 2cadb58..3e5cdbb 100644 --- a/priv/static/dom.js +++ b/priv/static/dom.js @@ -1,3 +1,5 @@ +import deepCompact from 'deep-compact'; + const k_r_submitter = /^(?:submit|button|image|reset|file)$/i; const k_r_success_contrls = /^(?:input|select|textarea|keygen)/i; const brackets = /(\[[^\[\]]*\])/g; @@ -20,8 +22,7 @@ function serializer(result, key, value) { if (matches) { const keys = parse_keys(key); hash_assign(result, keys, value); - } - else { + } else { // Non bracket notation can make assignments directly. const existing = result[key]; @@ -33,11 +34,10 @@ function serializer(result, key, value) { // assignment could go through `hash_assign`. if (existing) { if (!Array.isArray(existing)) { - result[key] = [ existing ]; + result[key] = [existing]; } result[key].push(value); - } - else { + } else { result[key] = value; } } @@ -60,7 +60,7 @@ export function serialize(form, options) { //Object store each radio and set if it's empty or not const radio_store = {}; - for (let i=0 ; i