Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shared event handlers #571

Merged
merged 24 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 0.1.0-rc.1

### Enhancements
* Added Shared Event Handlers which are global event handlers shared among all pages.
That's a simple model to work with where a layout, component, or multiple pages may share the same event handler,
for example a newsletter subscription form in a component called in a layout doesn't need to duplicate the same
event handler in all pages.

### Breaking Changes
* Remove Page Event Handlers in favor of Shared Event Handlers.
With Shared Event Handlers, it doesn't make sense to have page event handlers unless overriding becomes a neccessity.
The data is automatically migrated in a best-effort way, duplicated event handler names (from multiple pages) are
consolidated into a single shared event handler. See the migration `V002` for more info.

## 0.1.0-rc.0 (2024-08-02)

### Enhancements
Expand Down
3 changes: 2 additions & 1 deletion lib/beacon/boot.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ defmodule Beacon.Boot do
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_live_data_module(config.site) end),
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_layouts_modules(config.site) end),
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_error_page_module(config.site) end),
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_pages_modules(config.site, per_page: 20) end)
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_pages_modules(config.site, per_page: 20) end),
Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_event_handlers_module(config.site) end)
# TODO: load main pages (order_by: path, per_page: 10) to avoid SEO issues
]

Expand Down
102 changes: 49 additions & 53 deletions lib/beacon/content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ defmodule Beacon.Content do
alias Beacon.Content.ComponentSlot
alias Beacon.Content.ComponentSlotAttr
alias Beacon.Content.ErrorPage
alias Beacon.Content.EventHandler
alias Beacon.Content.Layout
alias Beacon.Content.LayoutEvent
alias Beacon.Content.LayoutSnapshot
alias Beacon.Content.LiveData
alias Beacon.Content.LiveDataAssign
alias Beacon.Content.Page
alias Beacon.Content.PageEvent
alias Beacon.Content.PageEventHandler
alias Beacon.Content.PageField
alias Beacon.Content.PageSnapshot
alias Beacon.Content.PageVariant
Expand Down Expand Up @@ -702,7 +702,7 @@ defmodule Beacon.Content do

@doc false
def create_page_snapshot(page, event) do
page = repo(page).preload(page, [:variants, :event_handlers])
page = repo(page).preload(page, :variants)

attrs = %{
"site" => page.site,
Expand Down Expand Up @@ -1025,14 +1025,14 @@ defmodule Beacon.Content do
defp extract_page_snapshot(%{schema_version: 1, page: %Page{} = page}) do
page
|> repo(page).reload()
|> repo(page).preload([:variants, :event_handlers], force: true)
|> repo(page).preload([:variants], force: true)
|> maybe_add_leading_slash()
end

defp extract_page_snapshot(%{schema_version: 2, page: %Page{} = page}) do
page
|> repo(page).reload()
|> repo(page).preload([:variants, :event_handlers], force: true)
|> repo(page).preload([:variants], force: true)
|> maybe_add_leading_slash()
end

Expand Down Expand Up @@ -3521,77 +3521,73 @@ defmodule Beacon.Content do

## Example

iex> change_page_event_handler(page_event_handler, %{name: "form-submit"})
%Ecto.Changeset{data: %PageEventHandler{}}
iex> change_event_handler(event_handler, %{name: "form-submit"})
%Ecto.Changeset{data: %EventHandler{}}

"""
@doc type: :page_event_handlers
@spec change_page_event_handler(PageEventHandler.t(), map()) :: Changeset.t()
def change_page_event_handler(%PageEventHandler{} = event_handler, attrs \\ %{}) do
PageEventHandler.changeset(event_handler, attrs)
@doc type: :event_handlers
@spec change_event_handler(EventHandler.t(), map()) :: Changeset.t()
def change_event_handler(%EventHandler{} = event_handler, attrs \\ %{}) do
EventHandler.changeset(event_handler, attrs)
end

@doc """
Creates a new page event handler and returns the page with updated `:event_handlers` association.
Lists all event handlers for a given Beacon site.
"""
@doc type: :page_event_handlers
@spec create_event_handler_for_page(Page.t(), %{name: binary(), code: binary()}) :: {:ok, Page.t()} | {:error, Changeset.t()}
def create_event_handler_for_page(page, attrs) do
changeset =
page
|> Ecto.build_assoc(:event_handlers)
|> PageEventHandler.changeset(attrs)
|> validate_page_event_handler(page)

transact(repo(page), fn ->
with {:ok, %PageEventHandler{}} <- repo(page).insert(changeset),
%Page{} = page <- repo(page).preload(page, :event_handlers, force: true),
%Page{} = page <- Lifecycle.Page.after_update_page(page) do
{:ok, page}
end
end)
@spec list_event_handlers(Site.t()) :: [EventHandler.t()]
def list_event_handlers(site) do
repo(site).all(from eh in EventHandler, where: [site: ^site])
end

@doc """
Updates a page event handler and returns the page with updated `:event_handlers` association.
Creates a new event handler.
"""
@doc type: :page_event_handlers
@spec update_event_handler_for_page(Page.t(), PageEventHandler.t(), map()) :: {:ok, Page.t()} | {:error, Changeset.t()}
def update_event_handler_for_page(page, event_handler, attrs) do
@doc type: :event_handlers
@spec create_event_handler(%{name: binary(), code: binary(), site: Site.t()}) ::
{:ok, EventHandler.t()} | {:error, Changeset.t()}
def create_event_handler(attrs) do
changeset =
event_handler
|> PageEventHandler.changeset(attrs)
|> validate_page_event_handler(page)
%EventHandler{}
|> EventHandler.changeset(attrs)
|> validate_event_handler()

transact(repo(page), fn ->
with {:ok, %PageEventHandler{}} <- repo(page).update(changeset),
%Page{} = page <- repo(page).preload(page, :event_handlers, force: true),
%Page{} = page <- Lifecycle.Page.after_update_page(page) do
{:ok, page}
end
end)
site = Changeset.get_field(changeset, :site)

changeset
|> repo(site).insert()
|> tap(&maybe_broadcast_updated_content_event(&1, :event_handler))
end

@doc """
Updates an event handler with the given attrs.
"""
@doc type: :event_handlers
@spec update_event_handler(EventHandler.t(), map()) :: {:ok, EventHandler.t()} | {:error, Changeset.t()}
def update_event_handler(event_handler, attrs) do
event_handler
|> EventHandler.changeset(attrs)
|> validate_event_handler()
|> repo(event_handler).update()
|> tap(&maybe_broadcast_updated_content_event(&1, :event_handler))
end

defp validate_page_event_handler(changeset, page) do
defp validate_event_handler(changeset) do
code = Changeset.get_field(changeset, :code)
metadata = %Beacon.Template.LoadMetadata{site: page.site, path: page.path}
variable_names = ["socket", "event_params"]
imports = ["Phoenix.Socket"]

do_validate_template(changeset, :code, :elixir, code, metadata, variable_names, imports)
do_validate_template(changeset, :code, :elixir, code, nil, variable_names, imports)
end

@doc """
Deletes a page event handler and returns the page with updated `:event_handlers` association.
Deletes an event handler.
"""
@doc type: :page_event_handlers
@spec delete_event_handler_from_page(Page.t(), PageEventHandler.t()) :: {:ok, Page.t()} | {:error, Changeset.t()}
def delete_event_handler_from_page(page, event_handler) do
with {:ok, %PageEventHandler{}} <- repo(page).delete(event_handler),
%Page{} = page <- repo(page).preload(page, :event_handlers, force: true),
%Page{} = page <- Lifecycle.Page.after_update_page(page) do
{:ok, page}
end
@doc type: :event_handlers
@spec delete_event_handler(EventHandler.t()) :: {:ok, EventHandler.t()} | {:error, Changeset.t()}
def delete_event_handler(event_handler) do
event_handler
|> repo(event_handler).delete()
|> tap(&maybe_broadcast_updated_content_event(&1, :event_handler))
end

# PAGE VARIANTS
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Beacon.Content.PageEventHandler do
defmodule Beacon.Content.EventHandler do
@moduledoc """
Beacon's representation of a LiveView [handle_event/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_event/3).

Expand All @@ -17,31 +17,29 @@ defmodule Beacon.Content.PageEventHandler do

import Ecto.Changeset

alias Beacon.Content.Page
alias Beacon.Types.Site
alias Ecto.UUID

@type t :: %__MODULE__{
id: UUID.t(),
name: binary(),
code: binary(),
page_id: UUID.t(),
page: Page.t(),
site: Site.t(),
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}

schema "beacon_page_event_handlers" do
schema "beacon_event_handlers" do
field :name, :string
field :code, :string

belongs_to :page, Page
field :site, Site

timestamps()
end

@doc false
def changeset(%__MODULE__{} = event_handler, attrs) do
fields = ~w(name code)a
fields = ~w(name code site)a

event_handler
|> cast(attrs, fields)
Expand Down
2 changes: 1 addition & 1 deletion lib/beacon/content/live_data_assign.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Beacon.Content.LiveDataAssign do
@moduledoc """
Dynamic key/value assigns to be used by `Beacon.Content.Page` templates and updated with `Beacon.Content.PageEventHandler`s.
Dynamic key/value assigns to be used by `Beacon.Content.Page` templates and updated with `Beacon.Content.EventHandler`s.

LiveDataAssigns don't exist on their own, but exist as part of a `Beacon.Content.LiveData` struct.

Expand Down
1 change: 0 additions & 1 deletion lib/beacon/content/page.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ defmodule Beacon.Content.Page do
belongs_to :layout, Content.Layout

has_many :variants, Content.PageVariant
has_many :event_handlers, Content.PageEventHandler

embeds_many :helpers, Helper

Expand Down
13 changes: 13 additions & 0 deletions lib/beacon/loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ defmodule Beacon.Loader do
Loader.Stylesheet.module_name(site)
end

def fetch_event_handlers_module(site) do
Loader.EventHandlers.module_name(site)
end

def fetch_layouts_modules(site) do
Enum.map(Content.list_published_layouts(site), fn layout ->
fetch_layout_module(layout.site, layout.id)
Expand Down Expand Up @@ -189,6 +193,10 @@ defmodule Beacon.Loader do
GenServer.call(worker(site), :reload_stylesheet_module, @timeout)
end

def reload_event_handlers_module(site) do
GenServer.call(worker(site), :reload_event_handlers_module, @timeout)
end

def reload_layouts_modules(site) do
Enum.map(Content.list_published_layouts(site), &reload_layout_module(&1.site, &1.id))
end
Expand Down Expand Up @@ -288,6 +296,11 @@ defmodule Beacon.Loader do
{:noreply, config}
end

def handle_info({:content_updated, :event_handler, %{site: site}}, config) do
reload_event_handlers_module(site)
{:noreply, config}
end

def handle_info(msg, config) do
raise inspect(msg)
Logger.warning("Beacon.Loader can not handle the message: #{inspect(msg)}")
Expand Down
30 changes: 30 additions & 0 deletions lib/beacon/loader/event_handlers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Beacon.Loader.EventHandlers do
@moduledoc false
alias Beacon.Loader

def module_name(site), do: Loader.module_name(site, "EventHandlers")

def build_ast(site, event_handlers) do
module = module_name(site)
functions = Enum.map(event_handlers, &build_fn/1)

quote do
defmodule unquote(module) do
import Beacon.Web, only: [assign: 2, assign: 3, assign_new: 3]

(unquote_splicing(functions))
end
end
end

defp build_fn(event_handler) do
%{site: site, name: name, code: code} = event_handler
Beacon.safe_code_check!(site, code)

quote do
def handle_event(unquote(name), var!(event_params), var!(socket)) do
unquote(Code.string_to_quoted!(code))
end
end
end
end
11 changes: 6 additions & 5 deletions lib/beacon/loader/page.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
defmodule Beacon.Loader.Page do
@moduledoc false

require Logger
alias Beacon.Content
alias Beacon.Lifecycle
alias Beacon.Loader
alias Beacon.Template.HEEx

require Logger

def module_name(site, page_id), do: Loader.module_name(site, "Page#{page_id}")

def build_ast(site, page) do
Expand Down Expand Up @@ -96,7 +97,7 @@ defmodule Beacon.Loader.Page do

defp interpolate_raw_schema_record(schema, page) when is_map(schema) do
render = fn key, value, page ->
case Beacon.Content.render_snippet(value, %{page: page, live_data: %{}}) do
case Content.render_snippet(value, %{page: page, live_data: %{}}) do
{:ok, new_value} ->
{key, new_value}

Expand Down Expand Up @@ -124,10 +125,10 @@ defmodule Beacon.Loader.Page do
end

defp handle_event(page) do
%{site: site, event_handlers: event_handlers} = page
event_handlers = Content.list_event_handlers(page.site)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still one step missing here :)

Whenever an event handler changes we'd need to broadcast a message to recompile all page modules so I'm wondering if it would be better to create a new EventHandler module containing all handle_event and on the handle_event of PageLive here

result =
Beacon.apply_mfa(
page_module,
:handle_event,
[event_name, event_params, socket],
context: %{site: site, page_id: page_id, live_path: live_path}
)

We just replace page_module with event_handler_module.

In this case whenever an event handler changes we only recompile the single event handler module and save resources by not duplicating the same event_handler on all page modules.

Wdyt?


Enum.map(event_handlers, fn event_handler ->
Beacon.safe_code_check!(site, event_handler.code)
Beacon.safe_code_check!(page.site, event_handler.code)

quote do
def handle_event(unquote(event_handler.name), var!(event_params), var!(socket)) do
Expand Down
8 changes: 8 additions & 0 deletions lib/beacon/loader/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ defmodule Beacon.Loader.Worker do
stop(result, config)
end

def handle_call(:reload_event_handlers_module, _from, config) do
%{site: site} = config
event_handlers = Content.list_event_handlers(site)
ast = Loader.EventHandlers.build_ast(site, event_handlers)
result = compile_module(site, ast, "event_handlers")
stop(result, config)
end

def handle_call({:reload_layout_module, layout_id}, _from, config) do
%{site: site} = config
layout = Beacon.Content.get_published_layout(site, layout_id)
Expand Down
Loading
Loading