Eh, the router is not so much just a router (it used to be) but it grew it the entrance point of my modules.
One of the more simple examples:
defmodule MyServer.Routers.Help do
use MyServer.ModuleBehaviour
@doc false
defmacro __using__(opts) do
base_scope = Keyword.get(opts, :base_scope, [])
quote bind_quoted: [base_scope: base_scope] do
scope base_scope, MyServer.Help, as: :help do
put "/issue/:id/add_tag", IssueController, :add_tag, as: :issue
delete "/issue/:id/:tag", IssueController, :delete_tag, as: :issue
resources "/issue", IssueController, as: :issue
resources "/communication", CommunicationController, as: :communication
get "/", IndexController, :index
end
end
end
import MyServer.Gettext
def title(_conn, _view, _assigns), do: "Admin"
def menu(conn) do
happy_path(else: handle_noauth) do
@perm true = conn |> can?(index(%Perms.Help{}))
{:link, [:module, :help], gettext("Help"), MyServer.Router.Helpers.help_index_path(conn, :index)}
end
end
end
I’ve been meaning to encapsulate the using function for easier use, but it has been too easy to just copy/paste it around as I make new modules (bad bad I know). But I have a drop-down menu on my titlebar at the top of every page (part of the layout) that has a “Home” link, a “Modules” link (which dynamically fills up with the modules that the currently logged in user has access to, and it is also dynamically built so if I make a new module with use MyServer.ModuleBehaviour
then it will be automatically added to the module list, it is part of my Plugin system capabilities).
My MySever.ModuleBehaviour
is:
defmodule MyServer.ModuleBehaviour do
@type breadcrumb :: atom() | list(breadcrumb)
@type menu_link :: {:link, breadcrumb, String.t, String.t}
@type menu_dropdown :: {:dropdown, String.t, menu}
@type menu_dropdown_header :: {:dropdown_header, String.t}
@type menu_separator :: :separator
@type menuitem :: [] | nil | menu_link | menu_dropdown | menu_dropdown_header | menu_separator
@type menu :: menuitem | list(menuitem)
@callback title(conn::struct(), view::atom(), assigns::map()) :: binary()
@callback menu(conn::struct()) :: menu
defmacro __using__(_opts) do
quote bind_quoted: [] do
@behaviour MyServer.ModuleBehaviour
import MyServer.ModuleBehaviour
import Happy
import Canada
alias MyServer.Perms
end
end
def handle_noauth({:perm, false}) do
[]
end
end
So it is pretty basic right now, but the important parts are that it sets up the typespecs for the menu system (so you can see how that works there, it allows adding menu entries, dropdowns, separators, and I’ll just add more as I need). The permission checks (via happy and my PermissionEx system combined with canada) are cached in mnesia (with in-line ets access, I’ve used erlang a lot so I know the parts of mnesia to work around and when it is safe to do so for speed). And the most important part, it adds the @behaviour MyServer.ModuleBehaviour
behaviour, which is what registers it with my plugin system so it can be dynamically picked up (my plugin system is used to dynamically access and register permissions and modules so far, I try to minimize its use as it is a little bit magic, but it saves SOMUCH* work otherwise).
The modules have other optionally supported callbacks too, such as a module can define a sublayout that is applied within the main site layout, which then wraps each page within the module (so a module can add a custom bar at the top/bottom below the main titlebar for when the menu system is insufficient, only one of my modules so far is complicated enough to need it). The module views specify what module to point to to handle callbacks and such. The layout_view handles the title and menu and such, here is the menu function in the main layout module as it is the more complicated one to show:
def render_module_specific_menu(conn, assigns, loc) do
view_module = assigns.view_module
view_template = assigns.view_template
if not :erlang.function_exported(view_module, :menu, 4) do
_ = Logger.debug("Unhandled menu/4 for module: #{inspect view_module}")
nil
else
menu = view_module.menu(conn, assigns, view_module, view_template)
render_module_specific_menu(menu, conn, assigns, loc)
end
end
And let me also show the sublayout rendering as it fairly unique:
def render_sublayout(view_module, view_template, submodule, sublayout, assigns) do
sub_assigns = Map.merge(assigns, %{layout: {submodule, sublayout}, render_existing: {submodule, sublayout}})
sub_layout = render(view_module, view_template, sub_assigns)
if sub_layout === nil do
if :erlang.function_exported(view_module, :sublayout_module, 0) do
case view_module.sublayout_module() do
^submodule -> # The same as itself, don't infinite loop...
render(view_module, view_template, assigns)
parent_view_module ->
render_sublayout(view_module, view_template, parent_view_module, sublayout, assigns)
end
else
render(view_module, view_template, assigns)
end
else
sub_layout
end
end
Each modules should define a few various things, here is the most complicated one (excusing module-specific view stuff, just the callbacks), and one of the sub-views within that module and how it delegates up:
defmodule MyServer.MyModule.IndexView do
@moduledoc false
use MyServer.Web, :view
def breadcrumb(_conn, _assigns, MyServer.MyModule.IndexView, _template), do: [:module, :MyModule, :index]
def breadcrumb(_conn, _assigns, MyServer.MyModule.RequirementView, _template), do: [:module, :MyModule, :section1]
def breadcrumb(_conn, _assigns, MyServer.MyModule.SemesterView, _template), do: [:module, :MyModule, :section2]
def breadcrumb(_conn, _assigns, MyServer.MyModule.StudentView, _template), do: [:module, :MyModule, :section3]
def breadcrumb(_conn, _assigns, _view_module, _template), do: [:module, :MyModule]
def menu(conn, assigns, _view_module, _template), do: {:dropdown, gettext("MyModule"), [
{:link, breadcrumb(conn, assigns, MyServer.MyModule.IndexView, "index."), gettext("MyModule Home"), MyModule_index_path(conn, :index)},
:separator,
{:link, breadcrumb(conn, assigns, MyServer.MyModule.Section1View, "index."), gettext("Section 1"), MyModule_section1_path(conn, :index)},
{:link, breadcrumb(conn, assigns, MyServer.MyModule.Section2View, "index."), gettext("Section 2"), MyModule_section2_path(conn, :index)},
{:link, breadcrumb(conn, assigns, MyServer.MyModule.Section3View, "index."), gettext("Section 3"), MyModule_section3_path(conn, :index)},
]}
end
defmodule MyServer.MyModule.Section3View do
use MyServer.Web, :view
alias MyServer.MyModule.IndexView, as: Parent
defdelegate breadcrumb(conn, assigns, view_module, template), to: Parent
defdelegate menu(conn, assigns, view_module, template), to: Parent
def sublayout_module, do: MyServer.MyModule.IndexView
end
I of course do not have things named like that, just web-sanitization.
Let me whip up an example, it is a crappy gif with weird coloring and such, but:
The ‘Home’ is the root link, the Modules are dynamically filled out based on the user that is logged in, the menu that appears is the ‘menu’ callback when in a given module (if the module opts for a menu at all, which ‘home’ does not). The breadcrumb is used to colorize the given menu links showing what you are currently in and so forth. It is fairly simple overall, and should use a redesign, it ‘grew’ over time rather than be designed.