Last active
January 10, 2020 09:09
-
-
Save joenoon/98e4cd57e6e9b312c24eadad2de0a144 to your computer and use it in GitHub Desktop.
my liveview setup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
i have one MainLive live_view defined in router as live "/*path", MainLive . then i created | |
a router_live.ex which looks sort of like a normal router.ex with all the live routes, but | |
they all point to live_components. and that router is not used in the endpoint, its purely | |
to get the functionality with route matching and Routes.live_path etc. my MainLive | |
handle_params does a URI.parse(url) and then Phoenix.Router.route_info(MyAppWeb.RouterLive, | |
"GET", uri.path, uri.authority) and i come out with a route that i pass down. my MainLive | |
render does a header/footer and in the middle something like live_component(assigns.socket, | |
route.live_view, id: route.route, store: store) (route here is what i created in | |
handle_params… route.live_view is the LiveComponent defined in the router_live.ex for the | |
route that matched). ive liked how it worked out so far. i also have an “AppState” that i | |
pass down (thats the store assign), and it allows to keep the store/session at the top and | |
not recreate/relookup current user etc when navigating… basically like a react app. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"/> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/> | |
<%= csrf_meta_tag() %> | |
<link rel="icon" type="image/png" href="<%= Routes.static_path(@conn, "/images/favicon-32x32.png") %>" sizes="32x32" /> | |
<link rel="icon" type="image/png" href="<%= Routes.static_path(@conn, "/images/favicon-16x16.png") %>" sizes="16x16" /> | |
</head> | |
<body id="body_tag"> | |
<div id="phx_root"> | |
<%= render @view_module, @view_template, assigns %> | |
</div> | |
<script> | |
window._live_flash = <%= raw Jason.encode!(get_flash(@conn)) %> | |
</script> | |
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Browser, LiveSocket } from 'phoenix_live_view'; | |
... | |
Hooks.NewSession = { | |
mounted() { | |
const { el } = this; | |
// console.log('NewSession mounted', el.id); | |
Browser.setCookie('__phoenix_flash__', el.id + '; max-age=60000; path=/'); | |
}, | |
}; | |
Hooks.PageURLHook = { | |
// on url change | |
// - blur the active element | |
// - scroll to top | |
updated() { | |
const { el, prev, __view } = this; | |
const cur = el.dataset.url; | |
const same = prev === cur; | |
// console.log('PageURLHook update', prev, 'vs', cur, 'same', same); | |
this.prev = cur; | |
if (!same) { | |
__view.liveSocket.blurActiveElement(); | |
window.scrollTo(0, 0); | |
} | |
}, | |
}; | |
... | |
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content'); | |
const liveSocket = new LiveSocket('/live', Socket, { | |
hooks: Hooks, | |
params: { _csrf_token: csrfToken, _live_flash: window._live_flash }, | |
}); | |
liveSocket.connect(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyAppWeb.AppState.Macros do | |
defmacro subscribable(key, load_assigns: load_assigns_bool) do | |
if load_assigns_bool do | |
quote do | |
def subscribe(%{assigns: %{store: %{unquote(key) => val}}} = socket, unquote(key)) | |
when val != nil, | |
do: assign(socket, unquote(key), val) | |
def subscribe(socket, unquote(key)), do: load_assigns(socket, unquote(key)) | |
end | |
else | |
quote do | |
def subscribe(%{assigns: %{store: %{unquote(key) => val}}} = socket, unquote(key)), | |
do: assign(socket, unquote(key), val) | |
end | |
end | |
end | |
end | |
defmodule MyAppWeb.AppState do | |
import MyAppWeb.AppState.Macros | |
require Logger | |
use GenServer | |
alias MyApp.{Accounts, User, Repo} | |
import Phoenix.LiveView, only: [assign: 2, assign: 3] | |
defmodule State do | |
defstruct [ | |
:connected?, | |
:root_pid, | |
:session, | |
:new_session, | |
:flash, | |
:flash_seen, | |
:route, | |
:current_user | |
] | |
end | |
def start_link(socket, opts) do | |
{initial_state, _} = Keyword.pop(opts, :state, %{}) | |
name = via_tuple(socket) | |
state = | |
%State{root_pid: root_pid(socket), connected?: socket.connected?} | |
|> Map.merge(initial_state) | |
|> Map.from_struct() | |
state = | |
%Phoenix.LiveView.Socket{} | |
|> assign(state) | |
|> Phoenix.LiveView.Utils.clear_changed() | |
GenServer.start_link(__MODULE__, state, name: name) | |
end | |
@impl true | |
def init(%{} = state) do | |
Process.flag(:trap_exit, true) | |
{:ok, state} | |
end | |
def subscribe(socket) do | |
state = GenServer.call(via_tuple(socket), :state) | |
socket | |
|> assign(:store, state.assigns) | |
end | |
subscribable(:current_user, load_assigns: true) | |
subscribable(:root_pid, load_assigns: false) | |
subscribable(:session, load_assigns: false) | |
subscribable(:new_session, load_assigns: false) | |
subscribable(:flash_seen, load_assigns: false) | |
subscribable(:route, load_assigns: false) | |
subscribable(:flash, load_assigns: false) | |
defp load_assigns(socket, key) do | |
state = GenServer.call(via_tuple(socket), {:load, key}) | |
socket | |
|> assign(:store, state.assigns) | |
|> assign(key, state.assigns[key]) | |
end | |
defp via_tuple(socket), | |
do: {:via, Registry, {MyAppWeb.AppStateRegistry, root_pid(socket)}} | |
@impl true | |
def terminate(reason, state) do | |
Logger.debug( | |
"[#{if state.assigns.connected?, do: "LIVE", else: "SSR"}] #{inspect(self())} AppState terminated: #{ | |
inspect(reason) | |
}" | |
) | |
:ok | |
end | |
defp root_pid(socket) do | |
socket.root_pid || self() | |
end | |
def put(socket, key, value) do | |
state = GenServer.call(via_tuple(socket), {:put, key, value}) | |
socket | |
|> assign(:store, state.assigns) | |
|> assign(key, value) | |
end | |
def reset_session(socket, session) do | |
signed = Phoenix.LiveView.Utils.sign_flash(socket, %{"reset_session" => session}) | |
state = GenServer.call(via_tuple(socket), {:reset_session, signed, session}) | |
socket | |
|> Phoenix.LiveView.put_flash("reset_session", session) | |
|> assign(:store, state.assigns) | |
end | |
def put_flash(socket, key, value) do | |
state = GenServer.call(via_tuple(socket), {:put_flash, key, value}) | |
socket | |
|> Phoenix.LiveView.put_flash(key, value) | |
|> assign(:store, state.assigns) | |
|> assign(:flash, state.assigns.flash) | |
|> assign(:flash_seen, state.assigns.flash_seen) | |
end | |
def clear_flash(socket, key) do | |
state = GenServer.call(via_tuple(socket), {:clear_flash, key}) | |
socket | |
|> socket_delete_flash(key) | |
|> assign(:store, state.assigns) | |
|> assign(:flash, state.assigns.flash) | |
end | |
def clear_flash(socket) do | |
state = GenServer.call(via_tuple(socket), {:clear_flash}) | |
socket | |
|> socket_delete_flash() | |
|> assign(:store, state.assigns) | |
|> assign(:flash, state.assigns.flash) | |
end | |
def clear_next_flash(socket) do | |
socket | |
|> socket_delete_flash() | |
end | |
defp socket_delete_flash(%{private: %{flash: flash} = private} = socket, key) | |
when is_map(flash) and is_binary(key) do | |
%{socket | private: Map.put(private, :flash, Map.delete(flash, key))} | |
end | |
defp socket_delete_flash(socket, _key), do: socket | |
defp socket_delete_flash(%{private: private} = socket) do | |
%{socket | private: Map.delete(private, :flash)} | |
end | |
@impl true | |
def handle_call({:reset_session, signed, session}, _from, state) do | |
Logger.debug("AppState handle_call {:reset_session, #{inspect(session)}}") | |
broadcast!(state, "reset_session") | |
new_assigns = | |
state.assigns | |
|> Map.put(:session, session) | |
|> Map.put(:new_session, signed) | |
|> Map.put(:flash_seen, false) | |
|> Map.put(:current_user, nil) | |
|> case do | |
new_assigns -> | |
if new_assigns.session["user_id"] == state.assigns.session["user_id"] && | |
state.assigns.current_user do | |
new_assigns | |
|> Map.put(:current_user, state.assigns.current_user) | |
else | |
new_assigns | |
end | |
end | |
new_state = assign(state, new_assigns) | |
{:reply, new_state, new_state} | |
end | |
@impl true | |
def handle_call({:load, :current_user}, _from, state) do | |
broadcast!(state, "load current_user") | |
state = load_current_user(state) | |
{:reply, state, state} | |
end | |
@impl true | |
def handle_call(:state, _from, state) do | |
{:reply, state, state} | |
end | |
@impl true | |
def handle_call({:put, key, value}, _from, state) do | |
broadcast!(state, "put #{key}") | |
new_state = assign(state, key, value) | |
{:reply, new_state, new_state} | |
end | |
@impl true | |
def handle_call({:put_flash, key, value}, _from, state) do | |
broadcast!(state, "put_flash #{key}") | |
new_state = | |
state | |
|> assign(:flash, Map.put(state.assigns.flash, key, value)) | |
|> assign(:flash_seen, false) | |
{:reply, new_state, new_state} | |
end | |
@impl true | |
def handle_call({:clear_flash, key}, _from, state) do | |
broadcast!(state, "clear_flash #{key}") | |
new_state = assign(state, :flash, Map.delete(state.assigns.flash, key)) | |
{:reply, new_state, new_state} | |
end | |
@impl true | |
def handle_call({:clear_flash}, _from, state) do | |
broadcast!(state, "clear_flash") | |
new_state = assign(state, :flash, %{}) | |
{:reply, new_state, new_state} | |
end | |
@impl true | |
def handle_info({:broadcast, reason}, %{changed: changed, assigns: %{connected?: true}} = state) | |
when changed != %{} do | |
Logger.debug( | |
"[LIVE] [broadcast to #{inspect(state.assigns.root_pid)}] #{reason} #{ | |
inspect(state.changed) | |
}" | |
) | |
send(state.assigns.root_pid, {:updated_store, state.assigns}) | |
state = | |
state | |
|> Phoenix.LiveView.Utils.clear_changed() | |
{:noreply, state} | |
end | |
@impl true | |
def handle_info({:broadcast, _reason}, state) do | |
# Logger.debug( | |
# "[#{if state.assigns.connected?, do: "LIVE", else: "SSR"}] [broadcast noop] #{reason}" | |
# ) | |
{:noreply, state} | |
end | |
defp load_current_user( | |
%{assigns: %{current_user: nil, session: %{"user_id" => user_id}}} = state | |
) | |
when user_id != nil do | |
state | |
|> assign(:current_user, Accounts.get_user_by_id(user_id)) | |
end | |
defp load_current_user(state), do: state | |
defp broadcast!(%{assigns: %{connected?: true}}, reason) do | |
send(self(), {:broadcast, reason}) | |
end | |
defp broadcast!(_, _), do: nil | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# List all child processes to be supervised | |
children = [ | |
{Registry, keys: :unique, name: MyAppWeb.AppStateRegistry}, | |
... | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyAppWeb.ExampleComponent do | |
use MyAppWeb, :live_component | |
def update(assigns, socket) do | |
socket | |
|> assign(assigns) | |
|> AppState.subscribe(:current_user) | |
|> okreply() | |
end | |
def render(assigns) do | |
~L""" | |
<div><%= inspect(@current_user) %></div> | |
""" | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyAppWeb.FlashComponent do | |
use MyAppWeb, :live_component | |
def update(assigns, socket) do | |
socket | |
|> assign(assigns) | |
|> AppState.subscribe(:flash) | |
|> mark_as_seen() | |
|> okreply() | |
end | |
defp mark_as_seen(%{connected?: true, assigns: %{flash: flash}} = socket) | |
when flash != %{} do | |
socket | |
|> AppState.put(:flash_seen, true) | |
end | |
defp mark_as_seen(socket) do | |
socket | |
end | |
def handle_event("delete_flash", %{"flash_key" => key}, socket) do | |
socket | |
|> AppState.clear_flash(key) | |
|> noreply() | |
end | |
def render(assigns) do | |
~L""" | |
<div> | |
<%= if @flash["info"] do %> | |
<div class="container is-fluid app--flash"> | |
<div class="notification is-success"> | |
<button phx-click="delete_flash" phx-value-flash_key="info" class="delete"></button> | |
<%= @flash["info"] %> | |
</div> | |
</div> | |
<% end %> | |
<%= if @flash["error"] do %> | |
<div class="container is-fluid app--flash"> | |
<div class="notification is-danger"> | |
<button phx-click="delete_flash" phx-value-flash_key="error" class="delete"></button> | |
<%= @flash["error"] %> | |
</div> | |
</div> | |
<% end %> | |
</div> | |
""" | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyAppWeb.LiveHelpers do | |
require Logger | |
import Phoenix.LiveView, only: [redirect: 2, assign: 3, live_redirect: 2] | |
def execute_on_main(socket, fun) do | |
send(socket.root_pid || self(), {:execute_on_main, fun}) | |
socket | |
end | |
def smart_redirect_on_main(socket, opts) do | |
socket | |
|> assign(:did_redirect, true) | |
|> execute_on_main(fn main_socket -> | |
main_socket | |
|> case do | |
%{connected?: true} = main_socket -> | |
live_redirect(main_socket, opts) | |
main_socket -> | |
redirect(main_socket, opts) | |
end | |
end) | |
end | |
def handle_params_reply(socket) do | |
case socket do | |
%{redirected: {:redirect, _}} -> {:stop, socket} | |
_ -> {:noreply, socket} | |
end | |
end | |
def noreply(socket), do: {:noreply, socket} | |
def okreply(socket), do: {:ok, socket} | |
def require_assigns(socket, keys) do | |
Enum.each(keys, fn k -> | |
case socket.assigns do | |
%{^k => _} -> nil | |
_ -> raise "Missing required assign in #{inspect(socket.assigns[:route])}: #{k}" | |
end | |
end) | |
socket | |
end | |
def ensure_user(%{assigns: %{current_user: user}} = socket) when user != nil, | |
do: assign(socket, :allowed, true) | |
def ensure_user(socket) do | |
execute_on_main(socket, fn main_socket -> | |
Logger.warn("ensure_user: Access to page denied. You have been redirected.") | |
main_socket | |
|> redirect(to: "/") | |
|> MyAppWeb.AppState.put_flash( | |
"error", | |
"Please log in to continue." | |
) | |
end) | |
socket | |
|> assign(:allowed, false) | |
end | |
def copy_assign(socket, from_key, to_key) do | |
socket | |
|> assign(to_key, socket.assigns[from_key]) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyAppWeb.LiveView.Session do | |
@moduledoc """ | |
Joe Noon on 9/3/2019 | |
LiveView does not have a direct way to change the session, but does provide a way to set flash. | |
The application can use `put_flash(socket, "reset_session", %{...})`, and this plug will make that | |
the persisted session. | |
""" | |
@behaviour Plug | |
def init(opts), do: opts | |
def call(conn, _) do | |
conn | |
|> handle_session() | |
|> handle_reset_session() | |
end | |
def handle_session( | |
%{ | |
private: %{ | |
plug_session: plug_session | |
} | |
} = conn | |
) do | |
conn | |
|> Plug.Conn.put_private(:plug_session, merge_session(plug_session)) | |
end | |
def handle_reset_session( | |
%{ | |
private: %{ | |
plug_session: plug_session | |
} | |
} = conn | |
) do | |
conn | |
|> Plug.Conn.put_private(:plug_session, reset_session(plug_session)) | |
end | |
### Util functions | |
defp merge_session(%{"phoenix_flash" => %{"session" => %{} = new_session} = flash} = session) do | |
flash = Map.delete(flash, "session") | |
session | |
|> Map.put("phoenix_flash", flash) | |
|> Map.merge(new_session) | |
end | |
defp merge_session(session), do: session | |
defp reset_session(%{"phoenix_flash" => %{"reset_session" => %{} = new_session} = flash}) do | |
flash = Map.delete(flash, "reset_session") | |
new_session | |
|> Map.put("phoenix_flash", flash) | |
end | |
defp reset_session(session), do: session | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyAppWeb.MainLive do | |
require Logger | |
use MyAppWeb, :live_view | |
def mount(session, socket) do | |
AppState.start_link(socket, state: %{session: session, flash: get_flash(socket)}) | |
socket = | |
socket | |
|> AppState.subscribe() | |
{:ok, socket} | |
end | |
defp get_flash(%{connected?: false, private: %{conn_session: %{"phoenix_flash" => flash}}}) | |
when is_map(flash), | |
do: Map.take(flash, ["info", "error"]) | |
defp get_flash(%{connected?: true, private: %{connect_params: %{"_live_flash" => flash}}}) | |
when is_map(flash), | |
do: Map.take(flash, ["info", "error"]) | |
defp get_flash(_), do: %{} | |
defp cleanup_flash(%{assigns: %{flash_seen: true}} = socket) do | |
Logger.debug("[#{if socket.connected?, do: "LIVE", else: "SSR"}] [flash] cleanup now") | |
socket | |
|> AppState.clear_flash() | |
end | |
defp cleanup_flash(socket) do | |
Logger.debug("[#{if socket.connected?, do: "LIVE", else: "SSR"}] [flash] cleanup next") | |
socket | |
|> AppState.clear_next_flash() | |
end | |
def handle_params(params, url, socket) do | |
uri = URI.parse(url) | |
case Phoenix.Router.route_info(MyAppWeb.RouterLive, "GET", uri.path, uri.authority) do | |
:error -> | |
socket | |
|> redirect(to: "/") | |
|> handle_params_reply() | |
route_info -> | |
full_params = | |
Map.delete(params, "path") | |
|> Map.merge(route_info.path_params) | |
route = %{ | |
route: route_info.route, | |
live_view: route_info.plug_opts, | |
params: full_params, | |
uri: uri, | |
url: url | |
} | |
Logger.debug( | |
"[#{if socket.connected?, do: "LIVE", else: "SSR"}] [handle_params] url=#{route.url}" | |
) | |
socket | |
|> AppState.put(:route, route) | |
|> AppState.subscribe(:flash_seen) | |
|> cleanup_flash() | |
|> handle_params_reply() | |
end | |
end | |
def handle_info({:execute_on_main, fun}, socket) when is_function(fun, 1) do | |
fun.(socket) | |
|> handle_params_reply() | |
end | |
def handle_info({:updated_store, store}, socket) do | |
socket | |
|> assign(:store, store) | |
|> noreply() | |
end | |
def render(%{route: route, store: store} = assigns) do | |
~L""" | |
<%= live_component(@socket, MyAppWeb.NavbarComponent, id: "NavbarComponent", store: @store) %> | |
<%= live_component(@socket, MyAppWeb.FlashComponent, id: "FlashComponent", store: @store) %> | |
<main id="main"> | |
<%= | |
live_component(assigns.socket, route.live_view, | |
id: route.route, store: store | |
) | |
%> | |
</main> | |
<%= Phoenix.View.render MyAppWeb.LayoutView, "footer.html", assigns %> | |
<%= if store.new_session do %> | |
<div id="<%= store.new_session %>" phx-hook="NewSession"></div> | |
<% end %> | |
<div id="page-url-hook" data-url="url-<%= safe_id(route.url) %>" phx-hook="PageURLHook"></div> | |
""" | |
end | |
def render(assigns), do: ~L(<div></div>) | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyAppWeb do | |
@moduledoc """ | |
The entrypoint for defining your web interface, such | |
as controllers, views, channels and so on. | |
This can be used in your application as: | |
use MyAppWeb, :controller | |
use MyAppWeb, :view | |
The definitions below will be executed for every view, | |
controller, etc, so keep them short and clean, focused | |
on imports, uses and aliases. | |
Do NOT define functions inside the quoted expressions | |
below. Instead, define any helper function in modules | |
and import those modules here. | |
""" | |
def controller do | |
quote do | |
use Phoenix.Controller, namespace: MyAppWeb | |
import Plug.Conn | |
import MyAppWeb.Gettext | |
alias MyAppWeb.Router.Helpers, as: Routes | |
import Phoenix.LiveView.Controller, only: [live_render: 3] | |
end | |
end | |
def view do | |
quote do | |
use Phoenix.View, | |
root: "lib/my_app_web/templates", | |
namespace: MyAppWeb | |
# Import convenience functions from controllers | |
import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] | |
# Use all HTML functionality (forms, tags, etc) | |
use Phoenix.HTML | |
import MyAppWeb.HTMLHelpers | |
import MyAppWeb.ErrorHelpers | |
import MyAppWeb.Gettext | |
alias MyAppWeb.RouterLive.Helpers, as: Routes | |
alias MyAppWeb.AppState | |
import Phoenix.LiveView, | |
only: [ | |
live_render: 2, | |
live_render: 3, | |
live_link: 1, | |
live_link: 2, | |
live_component: 3, | |
live_component: 4 | |
] | |
end | |
end | |
def live_component do | |
quote do | |
use Phoenix.LiveComponent | |
use Phoenix.HTML | |
import MyAppWeb.LiveHelpers | |
import MyAppWeb.HTMLHelpers | |
import MyAppWeb.ErrorHelpers | |
import MyAppWeb.Gettext | |
alias MyAppWeb.RouterLive.Helpers, as: Routes | |
alias MyAppWeb.AppState | |
end | |
end | |
def live_view do | |
quote do | |
use Phoenix.LiveView | |
use Phoenix.HTML | |
import MyAppWeb.LiveHelpers | |
import MyAppWeb.HTMLHelpers | |
import MyAppWeb.ErrorHelpers | |
import MyAppWeb.Gettext | |
alias MyAppWeb.RouterLive.Helpers, as: Routes | |
alias MyAppWeb.AppState | |
end | |
end | |
def router do | |
quote do | |
use Phoenix.Router | |
import Plug.Conn | |
import Phoenix.Controller | |
import Phoenix.LiveView.Router | |
end | |
end | |
def channel do | |
quote do | |
use Phoenix.Channel | |
import MyAppWeb.Gettext | |
end | |
end | |
@doc """ | |
When used, dispatch to the appropriate controller/view/etc. | |
""" | |
defmacro __using__(which) when is_atom(which) do | |
apply(__MODULE__, which, []) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment