diff --git a/lib/live_view_events/send.ex b/lib/live_view_events/notify.ex
similarity index 53%
rename from lib/live_view_events/send.ex
rename to lib/live_view_events/notify.ex
index 8c16552..91e0d2d 100644
--- a/lib/live_view_events/send.ex
+++ b/lib/live_view_events/notify.ex
@@ -5,6 +5,31 @@ defmodule LiveViewEvents.Notify do
@doc """
Use this macro instead of the default `assigns(socket, assign)` in
+ `c:Phoenix.LiveComponent.update/2`.
+
+ This will detect if `c:Phoenix.LiveComponent.update/2` is being called
+ because of an event send with either `notify_to/2` or `notify_to/3`
+ and handle it with `c:Phoenix.LiveView.handle_info/2`. Otherwise,
+ it will assign the given `assigns` to `socket`.
+
+ If there is no `c:Phoenix.LiveView.handle_info/2` defined in the
+ component, sending an event won't raise an error but if there is one
+ defined and it cannot handle the received message, it will.
+
+ Furthermore, if the handler returns anything that is not a tuple
+ `{:noreply, socket}`, it'll raise an exception too.
+
+ ## Why using `c:Phoenix.LiveView.handle_info/2` in components?
+
+ In one word: consistency. Messages coming from the client are
+ handled by `c:Phoenix.LiveView.handle_event/3` or by
+ `c:Phoenix.LiveComponent.handle_event/3`. For messages sent from
+ the server are currently being handled by `c:Phoenix.LiveView.handle_info/2`
+ in live views, with not official way to do this but the hack this
+ library is based on.
+
+ The hack is basically send an update with `Phoenix.LiveView.send_update/3`
+ and handle it in `c:Phoenix.LiveComponent.update/2`
"""
defmacro handle_info_or_assign(socket, assigns) do
quote do
@@ -47,8 +72,9 @@ defmodule LiveViewEvents.Notify do
def notify_to(:self, message), do: notify_to(self(), message)
def notify_to(pid, message) when is_pid(pid), do: send(pid, message)
- def notify_to({module, id}, message) do
- Phoenix.LiveView.send_update(self(), module, %{:id => id, @assign_name_for_event => message})
+ def notify_to(target, message) when is_tuple(target) do
+ {pid, module, id} = process_tuple(target)
+ Phoenix.LiveView.send_update(pid, module, %{:id => id, @assign_name_for_event => message})
end
@doc """
@@ -59,10 +85,14 @@ defmodule LiveViewEvents.Notify do
def notify_to(:self, message, params), do: notify_to(self(), message, params)
def notify_to(pid, message, params) when is_pid(pid), do: send(pid, {message, params})
- def notify_to({module, id}, message, params) do
- Phoenix.LiveView.send_update(self(), module, %{
+ def notify_to(target, message, params) when is_tuple(target) do
+ {pid, module, id}=process_tuple(target)
+ Phoenix.LiveView.send_update(pid, module, %{
:id => id,
@assign_name_for_event => {message, params}
})
end
+
+ defp process_tuple({module, id}), do: {self(), module, id}
+ defp process_tuple({pid, _module, _id}=target) when is_pid(pid), do: target
end
diff --git a/test_app/.formatter.exs b/test_app/.formatter.exs
new file mode 100644
index 0000000..e945e12
--- /dev/null
+++ b/test_app/.formatter.exs
@@ -0,0 +1,5 @@
+[
+ import_deps: [:phoenix],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"]
+]
diff --git a/test_app/.gitignore b/test_app/.gitignore
new file mode 100644
index 0000000..89f67c1
--- /dev/null
+++ b/test_app/.gitignore
@@ -0,0 +1,27 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Temporary files, for example, from tests.
+/tmp/
+
+# Ignore package tarball (built via "mix hex.build").
+test_app-*.tar
+
diff --git a/test_app/README.md b/test_app/README.md
new file mode 100644
index 0000000..88ab700
--- /dev/null
+++ b/test_app/README.md
@@ -0,0 +1,18 @@
+# TestApp
+
+To start your Phoenix server:
+
+ * Run `mix setup` to install and setup dependencies
+ * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
+
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
+
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
+
+## Learn more
+
+ * Official website: https://www.phoenixframework.org/
+ * Guides: https://hexdocs.pm/phoenix/overview.html
+ * Docs: https://hexdocs.pm/phoenix
+ * Forum: https://elixirforum.com/c/phoenix-forum
+ * Source: https://github.com/phoenixframework/phoenix
diff --git a/test_app/config/config.exs b/test_app/config/config.exs
new file mode 100644
index 0000000..ee59105
--- /dev/null
+++ b/test_app/config/config.exs
@@ -0,0 +1,30 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+
+# General application configuration
+import Config
+
+# Configures the endpoint
+config :test_app, TestAppWeb.Endpoint,
+ url: [host: "localhost"],
+ render_errors: [
+ formats: [html: TestAppWeb.ErrorHTML, json: TestAppWeb.ErrorJSON],
+ layout: false
+ ],
+ pubsub_server: TestApp.PubSub,
+ live_view: [signing_salt: "Yv51WWUs"]
+
+# Configures Elixir's Logger
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{config_env()}.exs"
diff --git a/test_app/config/dev.exs b/test_app/config/dev.exs
new file mode 100644
index 0000000..ab4b449
--- /dev/null
+++ b/test_app/config/dev.exs
@@ -0,0 +1,62 @@
+import Config
+
+# For development, we disable any cache and enable
+# debugging and code reloading.
+#
+# The watchers configuration can be used to run external
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
+config :test_app, TestAppWeb.Endpoint,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}, port: 4000],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "/OOtixijCMkathudAQ6iWMDxAguNzdKOUBFOLZo/2h9IjzdYcX63RdVs4uvmdB3f",
+ watchers: []
+
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+# mix phx.gen.cert
+#
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+# https: [
+# port: 4001,
+# cipher_suite: :strong,
+# keyfile: "priv/cert/selfsigned_key.pem",
+# certfile: "priv/cert/selfsigned.pem"
+# ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
+
+# Watch static and templates for browser reloading.
+config :test_app, TestAppWeb.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"lib/test_app_web/(controllers|live|components)/.*(ex|heex)$"
+ ]
+ ]
+
+# Enable dev routes for dashboard and mailbox
+config :test_app, dev_routes: true
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/test_app/config/prod.exs b/test_app/config/prod.exs
new file mode 100644
index 0000000..1fe2d9e
--- /dev/null
+++ b/test_app/config/prod.exs
@@ -0,0 +1,7 @@
+import Config
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# Runtime production configuration, including reading
+# of environment variables, is done on config/runtime.exs.
diff --git a/test_app/config/runtime.exs b/test_app/config/runtime.exs
new file mode 100644
index 0000000..6b617b4
--- /dev/null
+++ b/test_app/config/runtime.exs
@@ -0,0 +1,82 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/test_app start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :test_app, TestAppWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :test_app, TestAppWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :test_app, TestAppWeb.Endpoint,
+ # https: [
+ # ...,
+ # port: 443,
+ # cipher_suite: :strong,
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+ # ]
+ #
+ # The `cipher_suite` is set to `:strong` to support only the
+ # latest and more secure SSL ciphers. This means old browsers
+ # and clients may not be supported. You can set it to
+ # `:compatible` for wider support.
+ #
+ # `:keyfile` and `:certfile` expect an absolute path to the key
+ # and cert in disk or a relative path inside priv, for example
+ # "priv/ssl/server.key". For all supported SSL configuration
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+ #
+ # We also recommend setting `force_ssl` in your endpoint, ensuring
+ # no data is ever sent via http, always redirecting to https:
+ #
+ # config :test_app, TestAppWeb.Endpoint,
+ # force_ssl: [hsts: true]
+ #
+ # Check `Plug.SSL` for all available options in `force_ssl`.
+end
diff --git a/test_app/config/test.exs b/test_app/config/test.exs
new file mode 100644
index 0000000..b75a84e
--- /dev/null
+++ b/test_app/config/test.exs
@@ -0,0 +1,14 @@
+import Config
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :test_app, TestAppWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "EdKwrnAZO/we479ohsBsJbnpaK+0LV2SONLY83a+FNvycfw2Rfp7C7WtVWSbrAZS",
+ server: false
+
+# Print only warnings and errors during test
+config :logger, level: :warning
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/test_app/lib/test_app.ex b/test_app/lib/test_app.ex
new file mode 100644
index 0000000..800559e
--- /dev/null
+++ b/test_app/lib/test_app.ex
@@ -0,0 +1,9 @@
+defmodule TestApp do
+ @moduledoc """
+ TestApp keeps the contexts that define your domain
+ and business logic.
+
+ Contexts are also responsible for managing your data, regardless
+ if it comes from the database, an external API or others.
+ """
+end
diff --git a/test_app/lib/test_app/application.ex b/test_app/lib/test_app/application.ex
new file mode 100644
index 0000000..d78dc9b
--- /dev/null
+++ b/test_app/lib/test_app/application.ex
@@ -0,0 +1,34 @@
+defmodule TestApp.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [
+ # Start the Telemetry supervisor
+ TestAppWeb.Telemetry,
+ # Start the PubSub system
+ {Phoenix.PubSub, name: TestApp.PubSub},
+ # Start the Endpoint (http/https)
+ TestAppWeb.Endpoint
+ # Start a worker by calling: TestApp.Worker.start_link(arg)
+ # {TestApp.Worker, arg}
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: TestApp.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ @impl true
+ def config_change(changed, _new, removed) do
+ TestAppWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/test_app/lib/test_app_web.ex b/test_app/lib/test_app_web.ex
new file mode 100644
index 0000000..182a6a9
--- /dev/null
+++ b/test_app/lib/test_app_web.ex
@@ -0,0 +1,111 @@
+defmodule TestAppWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, components, channels, and so on.
+
+ This can be used in your application as:
+
+ use TestAppWeb, :controller
+ use TestAppWeb, :html
+
+ The definitions below will be executed for every controller,
+ component, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define additional modules and import
+ those modules here.
+ """
+
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
+ quote do
+ use Phoenix.Router, helpers: false
+
+ # Import common connection and controller functions to use in pipelines
+ import Plug.Conn
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
+ def controller do
+ quote do
+ use Phoenix.Controller,
+ formats: [:html, :json],
+ layouts: [html: TestAppWeb.Layouts]
+
+ import Plug.Conn
+
+ unquote(verified_routes())
+ end
+ end
+
+ def live_view do
+ quote do
+ use Phoenix.LiveView,
+ layout: {TestAppWeb.Layouts, :app}
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def html do
+ quote do
+ use Phoenix.Component
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
+ end
+ end
+
+ defp html_helpers do
+ quote do
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # Core UI components and translation
+ import TestAppWeb.CoreComponents
+
+ # Shortcut for generating JS commands
+ alias Phoenix.LiveView.JS
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
+ end
+ end
+
+ def verified_routes do
+ quote do
+ use Phoenix.VerifiedRoutes,
+ endpoint: TestAppWeb.Endpoint,
+ router: TestAppWeb.Router,
+ statics: TestAppWeb.static_paths()
+ 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
diff --git a/test_app/lib/test_app_web/components/core_components.ex b/test_app/lib/test_app_web/components/core_components.ex
new file mode 100644
index 0000000..38a3b0d
--- /dev/null
+++ b/test_app/lib/test_app_web/components/core_components.ex
@@ -0,0 +1,661 @@
+defmodule TestAppWeb.CoreComponents do
+ @moduledoc """
+ Provides core UI components.
+
+ At the first glance, this module may seem daunting, but its goal is
+ to provide some core building blocks in your application, such as modals,
+ tables, and forms. The components are mostly markup and well documented
+ with doc strings and declarative assigns. You may customize and style
+ them in any way you want, based on your application growth and needs.
+
+ The default components use Tailwind CSS, a utility-first CSS framework.
+ See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
+ how to customize them or feel free to swap in another framework altogether.
+
+ Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
+ """
+ use Phoenix.Component
+
+ alias Phoenix.LiveView.JS
+
+ @doc """
+ Renders a modal.
+
+ ## Examples
+
+ <.modal id="confirm-modal">
+ This is a modal.
+
+
+ JS commands may be passed to the `:on_cancel` to configure
+ the closing/cancel event, for example:
+
+ <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
+ This is another modal.
+
+
+ """
+ attr :id, :string, required: true
+ attr :show, :boolean, default: false
+ attr :on_cancel, JS, default: %JS{}
+ slot :inner_block, required: true
+
+ def modal(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc """
+ Shows the flash group with standard titles and content.
+
+ ## Examples
+
+ <.flash_group flash={@flash} />
+ """
+ attr :flash, :map, required: true, doc: "the map of flash messages"
+
+ def flash_group(assigns) do
+ ~H"""
+ <.flash kind={:info} title="Success!" flash={@flash} />
+ <.flash kind={:error} title="Error!" flash={@flash} />
+ <.flash
+ id="client-error"
+ kind={:error}
+ title="We can't find the internet"
+ phx-disconnected={show(".phx-client-error #client-error")}
+ phx-connected={hide("#client-error")}
+ hidden
+ >
+ Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+
+ <.flash
+ id="server-error"
+ kind={:error}
+ title="Something went wrong!"
+ phx-disconnected={show(".phx-server-error #server-error")}
+ phx-connected={hide("#server-error")}
+ hidden
+ >
+ Hang in there while we get back on track
+ <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+ """
+ end
+
+ @doc """
+ Renders a simple form.
+
+ ## Examples
+
+ <.simple_form for={@form} phx-change="validate" phx-submit="save">
+ <.input field={@form[:email]} label="Email"/>
+ <.input field={@form[:username]} label="Username" />
+ <:actions>
+ <.button>Save
+
+
+ """
+ attr :for, :any, required: true, doc: "the datastructure for the form"
+ attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
+
+ attr :rest, :global,
+ include: ~w(autocomplete name rel action enctype method novalidate target multipart),
+ doc: "the arbitrary HTML attributes to apply to the form tag"
+
+ slot :inner_block, required: true
+ slot :actions, doc: "the slot for form actions, such as a submit button"
+
+ def simple_form(assigns) do
+ ~H"""
+ <.form :let={f} for={@for} as={@as} {@rest}>
+
+ <%= render_slot(@inner_block, f) %>
+
+ <%= render_slot(action, f) %>
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a button.
+
+ ## Examples
+
+ <.button>Send!
+ <.button phx-click="go" class="ml-2">Send!
+ """
+ attr :type, :string, default: nil
+ attr :class, :string, default: nil
+ attr :rest, :global, include: ~w(disabled form name value)
+
+ slot :inner_block, required: true
+
+ def button(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc """
+ Renders an input with label and error messages.
+
+ A `Phoenix.HTML.FormField` may be passed as argument,
+ which is used to retrieve the input name, id, and values.
+ Otherwise all attributes may be passed explicitly.
+
+ ## Types
+
+ This function accepts all HTML input types, considering that:
+
+ * You may also set `type="select"` to render a `