diff --git a/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/error_helpers.ex b/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/error_helpers.ex
new file mode 100644
index 00000000..3a19855d
--- /dev/null
+++ b/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/error_helpers.ex
@@ -0,0 +1,47 @@
+defmodule PhxWCStorybookWeb.ErrorHelpers do
+ @moduledoc """
+ Conveniences for translating and building error messages.
+ """
+
+ use Phoenix.HTML
+
+ @doc """
+ Generates tag for inlined form input errors.
+ """
+ def error_tag(form, field) do
+ Enum.map(Keyword.get_values(form.errors, field), fn error ->
+ content_tag(:span, translate_error(error),
+ class: "invalid-feedback",
+ phx_feedback_for: input_name(form, field)
+ )
+ end)
+ end
+
+ @doc """
+ Translates an error message using gettext.
+ """
+ def translate_error({msg, opts}) do
+ # When using gettext, we typically pass the strings we want
+ # to translate as a static argument:
+ #
+ # # Translate "is invalid" in the "errors" domain
+ # dgettext("errors", "is invalid")
+ #
+ # # Translate the number of files with plural rules
+ # dngettext("errors", "1 file", "%{count} files", count)
+ #
+ # Because the error messages we show in our forms and APIs
+ # are defined inside Ecto, we need to translate them dynamically.
+ # This requires us to call the Gettext module passing our gettext
+ # backend as first argument.
+ #
+ # Note we use the "errors" domain, which means translations
+ # should be written to the errors.po file. The :count option is
+ # set by Ecto and indicates we should also apply plural rules.
+ if count = opts[:count] do
+ Gettext.dngettext(PhxWCStorybookWeb.Gettext, "errors", msg, msg, count, opts)
+ else
+ Gettext.dgettext(PhxWCStorybookWeb.Gettext, "errors", msg, opts)
+ end
+ end
+end
diff --git a/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/error_view.ex b/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/error_view.ex
new file mode 100644
index 00000000..85445bb8
--- /dev/null
+++ b/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/error_view.ex
@@ -0,0 +1,16 @@
+defmodule PhxWCStorybookWeb.ErrorView do
+ use PhxWCStorybookWeb, :view
+
+ # If you want to customize a particular status code
+ # for a certain format, you may uncomment below.
+ # def render("500.html", _assigns) do
+ # "Internal Server Error"
+ # end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.html" becomes
+ # "Not Found".
+ def template_not_found(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/layout_view.ex b/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/layout_view.ex
new file mode 100644
index 00000000..4bc6e953
--- /dev/null
+++ b/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/layout_view.ex
@@ -0,0 +1,7 @@
+defmodule PhxWCStorybookWeb.LayoutView do
+ use PhxWCStorybookWeb, :view
+
+ # Phoenix LiveDashboard is available only in development by default,
+ # so we instruct Elixir to not warn if the dashboard route is missing.
+ @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}}
+end
diff --git a/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/page_view.ex b/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/page_view.ex
new file mode 100644
index 00000000..53724485
--- /dev/null
+++ b/apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/views/page_view.ex
@@ -0,0 +1,3 @@
+defmodule PhxWCStorybookWeb.PageView do
+ use PhxWCStorybookWeb, :view
+end
diff --git a/apps/phx_wc_storybook_web/lib/table.exs b/apps/phx_wc_storybook_web/lib/table.exs
new file mode 100644
index 00000000..31eabdce
--- /dev/null
+++ b/apps/phx_wc_storybook_web/lib/table.exs
@@ -0,0 +1,18 @@
+defmodule PhxWCStorybookWeb.Storybook.Components.Table do
+ # :live_component or :page are also available
+ use PhxLiveStorybook.Entry, :component
+
+ def function, do: &Phoenix.WebComponent.Table.wc_table/1
+ def description, do: "A markdown render element."
+
+ def stories do
+ [
+ %Story{
+ id: :default,
+ attributes: %{
+ cols: [%{label: "Name"}]
+ }
+ }
+ ]
+ end
+end
diff --git a/apps/phx_wc_storybook_web/mix.exs b/apps/phx_wc_storybook_web/mix.exs
new file mode 100644
index 00000000..11a5ad90
--- /dev/null
+++ b/apps/phx_wc_storybook_web/mix.exs
@@ -0,0 +1,68 @@
+defmodule PhxWCStorybookWeb.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :phx_wc_storybook_web,
+ version: "0.1.0",
+ build_path: "../../_build",
+ config_path: "../../config/config.exs",
+ deps_path: "../../deps",
+ lockfile: "../../mix.lock",
+ elixir: "~> 1.12",
+ elixirc_paths: elixirc_paths(Mix.env()),
+ compilers: Mix.compilers(),
+ start_permanent: Mix.env() == :prod,
+ aliases: aliases(),
+ deps: deps()
+ ]
+ end
+
+ # Configuration for the OTP application.
+ #
+ # Type `mix help compile.app` for more information.
+ def application do
+ [
+ mod: {PhxWCStorybookWeb.Application, []},
+ extra_applications: [:logger, :runtime_tools]
+ ]
+ end
+
+ # Specifies which paths to compile per environment.
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ # Specifies your project dependencies.
+ #
+ # Type `mix help deps` for examples and options.
+ defp deps do
+ [
+ {:phoenix, "~> 1.6.8"},
+ {:phoenix_html, "~> 3.0"},
+ {:phoenix_live_reload, "~> 1.2", only: :dev},
+ {:phoenix_live_view, "~> 0.17.5"},
+ {:phx_live_storybook, "~> 0.3.0"},
+ {:floki, ">= 0.30.0", only: :test},
+ {:phoenix_live_dashboard, "~> 0.6"},
+ {:tailwind, "~> 0.1.6", runtime: Mix.env() == :dev},
+ {:esbuild, "~> 0.4", runtime: Mix.env() == :dev},
+ {:telemetry_metrics, "~> 0.6"},
+ {:telemetry_poller, "~> 1.0"},
+ {:gettext, "~> 0.18"},
+ {:phx_wc_storybook, in_umbrella: true},
+ {:phoenix_webcomponent, in_umbrella: true},
+ {:jason, "~> 1.2"},
+ {:plug_cowboy, "~> 2.5"}
+ ]
+ end
+
+ # Aliases are shortcuts or tasks specific to the current project.
+ #
+ # See the documentation for `Mix` for more info on aliases.
+ defp aliases do
+ [
+ setup: ["deps.get"],
+ "assets.deploy": ["tailwind storybook --minify", "esbuild storybook --minify", "phx.digest"]
+ ]
+ end
+end
diff --git a/apps/phx_wc_storybook_web/priv/gettext/en/LC_MESSAGES/errors.po b/apps/phx_wc_storybook_web/priv/gettext/en/LC_MESSAGES/errors.po
new file mode 100644
index 00000000..cdec3a11
--- /dev/null
+++ b/apps/phx_wc_storybook_web/priv/gettext/en/LC_MESSAGES/errors.po
@@ -0,0 +1,11 @@
+## `msgid`s in this file come from POT (.pot) files.
+##
+## Do not add, change, or remove `msgid`s manually here as
+## they're tied to the ones in the corresponding POT file
+## (with the same domain).
+##
+## Use `mix gettext.extract --merge` or `mix gettext.merge`
+## to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: en\n"
diff --git a/apps/phx_wc_storybook_web/priv/gettext/errors.pot b/apps/phx_wc_storybook_web/priv/gettext/errors.pot
new file mode 100644
index 00000000..d6f47fa8
--- /dev/null
+++ b/apps/phx_wc_storybook_web/priv/gettext/errors.pot
@@ -0,0 +1,10 @@
+## This is a PO Template file.
+##
+## `msgid`s here are often extracted from source code.
+## Add new translations manually only if they're dynamic
+## translations that can't be statically extracted.
+##
+## Run `mix gettext.extract` to bring this file up to
+## date. Leave `msgstr`s empty as changing them here has no
+## effect: edit them in PO (`.po`) files instead.
+
diff --git a/apps/phx_wc_storybook_web/priv/static/favicon.ico b/apps/phx_wc_storybook_web/priv/static/favicon.ico
new file mode 100644
index 00000000..73de524a
Binary files /dev/null and b/apps/phx_wc_storybook_web/priv/static/favicon.ico differ
diff --git a/apps/phx_wc_storybook_web/priv/static/images/phoenix.png b/apps/phx_wc_storybook_web/priv/static/images/phoenix.png
new file mode 100644
index 00000000..9c81075f
Binary files /dev/null and b/apps/phx_wc_storybook_web/priv/static/images/phoenix.png differ
diff --git a/apps/phx_wc_storybook_web/priv/static/robots.txt b/apps/phx_wc_storybook_web/priv/static/robots.txt
new file mode 100644
index 00000000..26e06b5f
--- /dev/null
+++ b/apps/phx_wc_storybook_web/priv/static/robots.txt
@@ -0,0 +1,5 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/controllers/page_controller_test.exs b/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/controllers/page_controller_test.exs
new file mode 100644
index 00000000..93ccaf87
--- /dev/null
+++ b/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/controllers/page_controller_test.exs
@@ -0,0 +1,8 @@
+defmodule PhxWCStorybookWeb.PageControllerTest do
+ use PhxWCStorybookWeb.ConnCase
+
+ test "GET /", %{conn: conn} do
+ conn = get(conn, "/")
+ assert html_response(conn, 200) =~ "Phoenix WebComponent"
+ end
+end
diff --git a/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/views/error_view_test.exs b/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/views/error_view_test.exs
new file mode 100644
index 00000000..da5d4e88
--- /dev/null
+++ b/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/views/error_view_test.exs
@@ -0,0 +1,15 @@
+defmodule PhxWCStorybookWeb.ErrorViewTest do
+ use PhxWCStorybookWeb.ConnCase, async: true
+
+ # Bring render/3 and render_to_string/3 for testing custom views
+ import Phoenix.View
+
+ test "renders 404.html" do
+ assert render_to_string(PhxWCStorybookWeb.ErrorView, "404.html", []) == "Not Found"
+ end
+
+ test "renders 500.html" do
+ assert render_to_string(PhxWCStorybookWeb.ErrorView, "500.html", []) ==
+ "Internal Server Error"
+ end
+end
diff --git a/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/views/layout_view_test.exs b/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/views/layout_view_test.exs
new file mode 100644
index 00000000..de38f20f
--- /dev/null
+++ b/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/views/layout_view_test.exs
@@ -0,0 +1,8 @@
+defmodule PhxWCStorybookWeb.LayoutViewTest do
+ use PhxWCStorybookWeb.ConnCase, async: true
+
+ # When testing helpers, you may want to import Phoenix.HTML and
+ # use functions such as safe_to_string() to convert the helper
+ # result into an HTML string.
+ # import Phoenix.HTML
+end
diff --git a/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/views/page_view_test.exs b/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/views/page_view_test.exs
new file mode 100644
index 00000000..069edec3
--- /dev/null
+++ b/apps/phx_wc_storybook_web/test/phx_wc_storybook_web/views/page_view_test.exs
@@ -0,0 +1,3 @@
+defmodule PhxWCStorybookWeb.PageViewTest do
+ use PhxWCStorybookWeb.ConnCase, async: true
+end
diff --git a/apps/phx_wc_storybook_web/test/support/conn_case.ex b/apps/phx_wc_storybook_web/test/support/conn_case.ex
new file mode 100644
index 00000000..54de26ab
--- /dev/null
+++ b/apps/phx_wc_storybook_web/test/support/conn_case.ex
@@ -0,0 +1,37 @@
+defmodule PhxWCStorybookWeb.ConnCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ tests that require setting up a connection.
+
+ Such tests rely on `Phoenix.ConnTest` and also
+ import other functionality to make it easier
+ to build common data structures and query the data layer.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use PhxWCStorybookWeb.ConnCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # Import conveniences for testing with connections
+ import Plug.Conn
+ import Phoenix.ConnTest
+ import PhxWCStorybookWeb.ConnCase
+
+ alias PhxWCStorybookWeb.Router.Helpers, as: Routes
+
+ # The default endpoint for testing
+ @endpoint PhxWCStorybookWeb.Endpoint
+ end
+ end
+
+ setup _tags do
+ {:ok, conn: Phoenix.ConnTest.build_conn()}
+ end
+end
diff --git a/apps/phx_wc_storybook_web/test/test_helper.exs b/apps/phx_wc_storybook_web/test/test_helper.exs
new file mode 100644
index 00000000..869559e7
--- /dev/null
+++ b/apps/phx_wc_storybook_web/test/test_helper.exs
@@ -0,0 +1 @@
+ExUnit.start()
diff --git a/config/config.exs b/config/config.exs
index f54bdc5d..e17eef9a 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,11 +1,80 @@
+# This file is responsible for configuring your umbrella
+# and **all applications** and their dependencies with the
+# help of the Config module.
+#
+# Note that all applications in your umbrella share the
+# same configuration and dependencies, which is why they
+# all use the same configuration file. If you want different
+# configurations or dependencies per app, it is best to
+# move said applications out of the umbrella.
import Config
-config :phoenix, :json_library, Jason
+# Configure Mix tasks and generators
+config :phx_wc_storybook,
+ namespace: PhxWCStorybook
+
+config :phx_wc_storybook_web,
+ namespace: PhxWCStorybookWeb,
+ generators: [context_app: :phx_wc_storybook]
+
+# Configures the endpoint
+config :phx_wc_storybook_web, PhxWCStorybookWeb.Endpoint,
+ url: [host: "localhost"],
+ render_errors: [view: PhxWCStorybookWeb.ErrorView, accepts: ~w(html json), layout: false],
+ pubsub_server: PhxWCStorybook.PubSub,
+ live_view: [signing_salt: "HkF5qV0r"]
+
+config :phx_wc_storybook_web, PhxWCStorybookWeb.Storybook,
+ content_path:
+ Path.expand("../apps/phx_wc_storybook_web/lib/phx_wc_storybook_web/storybook", __DIR__),
+ js_path: "/assets/app.js",
+ css_path: "/assets/app.css"
+
+config :tailwind,
+ version: "3.1.6",
+ default: [
+ args: ~w(
+ --config=tailwind.config.js
+ --input=css/phoenix_webcomponent.css
+ --output=../priv/static/phoenix_webcomponent.css
+ ),
+ cd: Path.expand("../apps/phoenix_webcomponent/assets", __DIR__),
+ env: %{"NODE_PATH" => "#{Path.expand("../deps", __DIR__)}:#{Path.expand("../apps", __DIR__)}"}
+ ],
+ storybook: [
+ args: ~w(
+ --config=tailwind.config.js
+ --input=css/app.css
+ --output=../priv/static/assets/app.css
+ ),
+ cd: Path.expand("../apps/phx_wc_storybook_web/assets", __DIR__),
+ env: %{"NODE_PATH" => "#{Path.expand("../deps", __DIR__)}:#{Path.expand("../apps", __DIR__)}"}
+ ]
+# Configure esbuild (the version is required)
config :esbuild,
- version: "0.14.0",
+ version: "0.14.29",
default: [
args:
- ~w(priv/src/phoenix_webcomponent.js --bundle --minify --target=esnext --outdir=priv/static/),
- cd: Path.expand("../", __DIR__)
+ ~w(js/phoenix_webcomponent.js --bundle --target=es2021 --outdir=../priv/static/ --external:/fonts/* --external:/images/*),
+ cd: Path.expand("../apps/phoenix_webcomponent/assets", __DIR__),
+ env: %{"NODE_PATH" => "#{Path.expand("../deps", __DIR__)}:#{Path.expand("../apps", __DIR__)}"}
+ ],
+ storybook: [
+ args:
+ ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+ cd: Path.expand("../apps/phx_wc_storybook_web/assets", __DIR__),
+ env: %{"NODE_PATH" => "#{Path.expand("../deps", __DIR__)}:#{Path.expand("../apps", __DIR__)}"}
]
+
+# 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/config/dev.exs b/config/dev.exs
new file mode 100644
index 00000000..76232592
--- /dev/null
+++ b/config/dev.exs
@@ -0,0 +1,45 @@
+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 use it
+# with esbuild to bundle .js and .css sources.
+config :phx_wc_storybook_web, PhxWCStorybookWeb.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: {0, 0, 0, 0}, port: 4600],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "BM3gjYo7YUKjr9Ye7kqOjj4t4c4dAkezwSbPFN1AJE1Tqi/aw1Kt/fNszzGoSGi9",
+ watchers: [
+ # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
+ tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]},
+ esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
+ tailwind: {Tailwind, :install_and_run, [:storybook, ~w(--watch)]},
+ esbuild: {Esbuild, :install_and_run, [:storybook, ~w(--sourcemap=inline --watch)]}
+ ]
+
+# Watch static and templates for browser reloading.
+config :phx_wc_storybook_web, PhxWCStorybookWeb.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"priv/gettext/.*(po)$",
+ ~r"lib/phoenix_webcomponent/.*(ex)$",
+ ~r"lib/phx_wc_storybook_web/(live|views)/.*(ex)$",
+ ~r"lib/phx_wc_storybook_web/templates/.*(eex)$"
+ ]
+ ]
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
diff --git a/config/prod.exs b/config/prod.exs
new file mode 100644
index 00000000..5f8555d9
--- /dev/null
+++ b/config/prod.exs
@@ -0,0 +1,17 @@
+import Config
+
+# For production, don't forget to configure the url host
+# to something meaningful, Phoenix uses this information
+# when generating URLs.
+#
+# Note we also include the path to a cache manifest
+# containing the digested version of static files. This
+# manifest is generated by the `mix phx.digest` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :phx_wc_storybook_web, PhxWCStorybookWeb.Endpoint,
+ url: [host: "phoenix-webcomponent.gsmlg.org", port: 80],
+ cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Do not print debug messages in production
+config :logger, level: :info
diff --git a/config/runtime.exs b/config/runtime.exs
new file mode 100644
index 00000000..e9eae020
--- /dev/null
+++ b/config/runtime.exs
@@ -0,0 +1,40 @@
+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.
+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
+ """
+
+ config :phx_wc_storybook_web, PhxWCStorybookWeb.Endpoint,
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: String.to_integer(System.get_env("PORT") || "4000")
+ ],
+ secret_key_base: secret_key_base
+
+ # ## Using releases
+ #
+ # If you are doing OTP releases, you need to instruct Phoenix
+ # to start each relevant endpoint:
+ #
+ # config :phx_wc_storybook_web, PhxWCStorybookWeb.Endpoint, server: true
+ #
+ # Then you can assemble a release by calling `mix release`.
+ # See `mix help release` for more information.
+end
diff --git a/config/test.exs b/config/test.exs
new file mode 100644
index 00000000..5d4d1e4b
--- /dev/null
+++ b/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 :phx_wc_storybook_web, PhxWCStorybookWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "u4CGMLvZGj1B4C7in/ai2kZWRpAPYjbpWB5kNiGeYVbPuZsv2wO3DTR71X6QiJ6l",
+ server: false
+
+# Print only warnings and errors during test
+config :logger, level: :warn
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/lib/phoenix_webcomponent.ex b/lib/phoenix_webcomponent.ex
deleted file mode 100644
index d3801f2d..00000000
--- a/lib/phoenix_webcomponent.ex
+++ /dev/null
@@ -1,153 +0,0 @@
-defmodule Phoenix.WebComponent do
- @moduledoc """
- Provides a suit of html custom component for phoenix.
-
- This library provides three main functionalities:
-
- * Enhance form helper with manterial web componet
- * Enhance link helper with manterial web componet
- * Markdown render helper with `@gsmlg/lit/remark-element`
- * TopAppBar render top app bar with custom element.
-
- ## Form helper
-
- See `Phoenix.WebComponent.FormHelper`.
-
- ## JavaScript library
-
- This project provides javascript that define custom elements.
-
- To use the web component, you must load `priv/static/phoenix_webcomponent.js`
- into your build tool. Or through npm by install `phoenix_webcomponent`.
- The difference is npm version is not bundled.
-
- """
-
- @doc false
- defmacro __using__(_) do
- quote do
- import Phoenix.WebComponent.FormHelper
- import Phoenix.WebComponent.Link
- import Phoenix.WebComponent.Markdown
- import Phoenix.WebComponent.TopAppBar
- import Phoenix.WebComponent.Table
- end
- end
-
- @doc """
- Returns a list of attributes that make an element behave like a link.
- For example, to make a button work like a link:
-
-
-
- However, this function is more often used to create buttons that
- must invoke an action on the server, such as deleting an entity,
- using the relevant HTTP protocol:
-
-
-
- The `to` argument may be a string, a URI, or a tuple `{scheme, value}`.
- See the examples below.
- Note: using this function requires loading the JavaScript library
- at `priv/static/phoenix_html.js`. See the `Phoenix.HTML` module
- documentation for more information.
-
- ## Options
-
- * `:method` - the HTTP method for the link. Defaults to `:get`.
- * `:csrf_token` - a custom token to use when method is not `:get`.
- This is used to ensure the request was sent by the user who
- rendered the page. By default, CSRF tokens are generated through
- `Plug.CSRFProtection`. You can set this option to `false`, to
- disable token generation, or set it to your own token.
-
- When the `:method` is set to `:get` and the `:to` URL contains query
- parameters the generated form element will strip the parameters in
- accordance with the [W3C](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.3.4)
- form specification.
-
- ## Data attributes
-
- The following data attributes can also be manually set in the element:
- * `data-confirm` - shows a confirmation prompt before generating and
- submitting the form.
-
- ## Examples
-
- iex> link_attributes("/world")
- [data: [method: :get, to: "/world"]]
- iex> link_attributes(URI.parse("https://elixir-lang.org"))
- [data: [method: :get, to: "https://elixir-lang.org"]]
- iex> link_attributes("/product/1", method: :delete)
- [data: [csrf: Plug.CSRFProtection.get_csrf_token(), method: :delete, to: "/product/1"]]
-
- ## If the URL is absolute, only certain schemas are allowed to avoid JavaScript injection.
- For example, the following will fail
-
- iex> link_attributes("javascript:alert('hacked!')")
- ** (ArgumentError) unsupported scheme given as link. In case you want to link to an
- unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}
-
- You can however explicitly render those unsafe schemes by using a tuple:
-
- iex> link_attributes({:javascript, "alert('my alert!')"})
- [data: [method: :get, to: ["javascript", 58, "alert('my alert!')"]]]
-
- """
- def link_attributes(to, opts \\ []) do
- to = valid_destination!(to)
- method = Keyword.get(opts, :method, :get)
- data = [method: method, to: to]
-
- data =
- if method == :get do
- data
- else
- case Keyword.get(opts, :csrf_token, true) do
- true -> [csrf: Phoenix.HTML.Tag.csrf_token_value(to)] ++ data
- false -> data
- csrf when is_binary(csrf) -> [csrf: csrf] ++ data
- end
- end
-
- [data: data]
- end
-
- defp valid_destination!(%URI{} = uri) do
- valid_destination!(URI.to_string(uri))
- end
-
- defp valid_destination!({:safe, to}) do
- {:safe, valid_string_destination!(IO.iodata_to_binary(to))}
- end
-
- defp valid_destination!({other, to}) when is_atom(other) do
- [Atom.to_string(other), ?:, to]
- end
-
- defp valid_destination!(to) do
- valid_string_destination!(IO.iodata_to_binary(to))
- end
-
- @valid_uri_schemes ~w(http: https: ftp: ftps: mailto: news: irc: gopher:) ++
- ~w(nntp: feed: telnet: mms: rtsp: svn: tel: fax: xmpp:)
-
- for scheme <- @valid_uri_schemes do
- defp valid_string_destination!(unquote(scheme) <> _ = string), do: string
- end
-
- defp valid_string_destination!(to) do
- if not match?("/" <> _, to) and String.contains?(to, ":") do
- raise ArgumentError, """
- unsupported scheme given as link. In case you want to link to an
- unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}\
- """
- else
- to
- end
- end
-end
diff --git a/lib/phoenix_webcomponent/form_helper.ex b/lib/phoenix_webcomponent/form_helper.ex
deleted file mode 100644
index 67d5304a..00000000
--- a/lib/phoenix_webcomponent/form_helper.ex
+++ /dev/null
@@ -1,898 +0,0 @@
-defmodule Phoenix.WebComponent.FormHelper do
- @moduledoc ~S"""
- Helpers related to producing HTML forms.
-
- The functions in this module can be used in three
- distinct scenarios:
-
- * with changeset data - when information to populate
- the form comes from a changeset
-
- * with limited data - when a form is created without
- an underlying data layer. In this scenario, you can
- use the connection information (aka Plug.Conn.params)
- or pass the form values by hand
-
- * outside of a form - when the functions are used directly,
- outside of `form_for`
-
- We will explore all three scenarios below.
-
- ## With changeset data
-
- The entry point for defining forms in Phoenix is with
- the `form_for/4` function. For this example, we will
- use `Ecto.Changeset`, which integrates nicely with Phoenix
- forms via the `phoenix_ecto` package.
-
- Imagine you have the following action in your controller:
-
- def new(conn, _params) do
- changeset = User.changeset(%User{})
- render conn, "new.html", changeset: changeset
- end
-
- where `User.changeset/2` is defined as follows:
-
- def changeset(user, params \\ %{}) do
- Ecto.Changeset.cast(user, params, [:name, :age])
- end
-
- Now a `@changeset` assign is available in views which we
- can pass to the form:
-
- <%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>
-
-
-
-
- <%= wc_submit "Submit" %>
- <% end %>
-
- `form_for/4` receives the `Ecto.Changeset` and converts it
- to a form, which is passed to the function as the argument
- `f`. All the remaining functions in this module receive
- the form and automatically generate the input fields, often
- by extracting information from the given changeset. For example,
- if the user had a default value for age set, it will
- automatically show up as selected in the form.
-
- ### A note on `:errors`
-
- If no action has been applied to the changeset or action was set to `:ignore`,
- no errors are shown on the form object even if the changeset has a non-empty
- `:errors` value.
-
- This is useful for things like validation hints on form fields, e.g. an empty
- changeset for a new form. That changeset isn't valid, but we don't want to
- show errors until an actual user action has been performed.
-
- Ecto automatically applies the action for you when you call
- Repo.insert/update/delete, but if you want to show errors manually you can
- also set the action yourself, either directly on the `Ecto.Changeset` struct
- field or by using `Ecto.Changeset.apply_action/2`.
-
- ## With limited data
-
- `form_for/4` expects as first argument any data structure that
- implements the `Phoenix.WebComponent.FormData` protocol. By default,
- Phoenix implements this protocol for `Plug.Conn` and `Atom`.
-
- This is useful when you are creating forms that are not backed
- by any kind of data layer. Let's assume that we're submitting a
- form to the `:new` action in the `FooController`:
-
- <%= form_for @conn, Routes.foo_path(@conn, :new), [as: :foo], fn f -> %>
- <%= wc_text_input f, :for %>
- <%= wc_submit "Search" %>
- <% end %>
-
- `form_for/4` uses the `Plug.Conn` to set input values from the
- request parameters.
-
- Alternatively, if you don't have a connection, you can pass `:foo`
- as the form data source and explicitly pass the value for every input:
-
- <%= form_for :foo, Routes.foo_path(MyApp.Endpoint, :new), fn f -> %>
- <%= wc_text_input f, :for, value: "current value" %>
- <%= wc_submit "Search" %>
- <% end %>
-
- ## Without form data
-
- Sometimes we may want to generate a `text_input/3` or any other
- tag outside of a form. The functions in this module also support
- such usage by simply passing an atom as first argument instead
- of the form.
-
- <%= wc_text_input :user, :name, value: "This is a prepopulated value" %>
-
-
- """
-
- import Phoenix.HTML
- import Phoenix.HTML.Tag
- import Phoenix.HTML.Form, except: [options_for_select: 2]
-
- ## Form helpers
-
- @doc """
- Generates a text input.
-
- The form should either be a `Phoenix.WebComponent.Form` emitted
- by `form_for` or an atom.
-
- All given options are forwarded to the underlying input,
- default values are provided for id, name and value if
- possible.
-
- ## Examples
-
- # Assuming form contains a User schema
- wc_text_input(form, :name)
- #=>
-
- wc_text_input(:user, :name)
- #=>
-
- """
- def wc_text_input(form, field, opts \\ []) do
- generic_input(:text, form, field, opts)
- end
-
- @doc """
- Generates an email input.
-
- Auto add pattern="[^@]+@[^@]+" to check format
-
- See `text_input/3` for example and docs.
- """
- def wc_email_input(form, field, opts \\ []) do
- opts = opts |> Keyword.put_new(:pattern, "[^@]+@[^@]+")
- generic_input(:email, form, field, opts)
- end
-
- @spec wc_number_input(atom | Phoenix.HTML.Form.t(), atom | binary, keyword) ::
- {:safe, [binary | list | 60 | 62, ...]}
- @doc """
- Generates a number input.
-
- See `text_input/3` for example and docs.
- """
- def wc_number_input(form, field, opts \\ []) do
- generic_input(:number, form, field, opts)
- end
-
- @doc """
- Generates a password input.
-
- For security reasons, the form data and parameter values
- are never re-used in `password_input/3`. Pass the value
- explicitly if you would like to set one.
-
- See `text_input/3` for example and docs.
- """
- def wc_password_input(form, field, opts \\ []) do
- opts =
- opts
- |> Keyword.put_new(:"label-text", humanize(field))
- |> Keyword.put_new(:type, "password")
- |> Keyword.put_new(:id, input_id(form, field))
- |> Keyword.put_new(:name, input_name(form, field))
-
- errors =
- case form do
- %{errors: errors} -> errors |> Keyword.get_values(field)
- _ -> []
- end
-
- {translate_error, opts} = opts |> Keyword.pop(:translate_error)
-
- opts =
- unless Enum.empty?(errors) do
- opts = opts |> Keyword.put_new(:invalid, true)
-
- errorString =
- Enum.map(errors, fn {msg, opts} ->
- if is_function(translate_error) do
- translate_error.({msg, opts})
- else
- msg
- end
- end)
- |> Enum.join(" ")
-
- opts |> Keyword.put(:"validity-message", errorString)
- else
- opts
- end
-
- tag(:"bx-input", opts)
- end
-
- @doc """
- Generates an url input.
-
- See `text_input/3` for example and docs.
- """
- def wc_url_input(form, field, opts \\ []) do
- generic_input(:url, form, field, opts)
- end
-
- @doc """
- Generates a search input.
-
- See `text_input/3` for example and docs.
- """
- def wc_search_input(form, field, opts \\ []) do
- generic_input(:search, form, field, opts)
- end
-
- @doc """
- Generates a telephone input.
-
- See `text_input/3` for example and docs.
- """
- def wc_telephone_input(form, field, opts \\ []) do
- generic_input(:tel, form, field, opts)
- end
-
- @doc """
- Generates a color input.
-
- Warning: this feature isn't available in all browsers.
- Check `http://caniuse.com/#feat=input-color` for further information.
-
- See `text_input/3` for example and docs.
- """
- def wc_color_input(form, field, opts \\ []) do
- generic_input(:color, form, field, opts)
- end
-
- @doc """
- Generates a range input.
-
- See `text_input/3` for example and docs.
- """
- def wc_range_input(form, field, opts \\ []) do
- generic_input(:range, form, field, opts)
- end
-
- @doc """
- Generates a date input.
-
- Warning: this feature isn't available in all browsers.
- Check `http://caniuse.com/#feat=input-datetime` for further information.
-
- See `text_input/3` for example and docs.
- """
- def wc_date_input(form, field, opts \\ []) do
- opts =
- opts
- |> Keyword.put_new(:"label-text", humanize(field))
- |> Keyword.put_new(:id, input_id(form, field))
- |> Keyword.put_new(:name, input_name(form, field))
- |> Keyword.put_new(:value, input_value(form, field))
- |> Keyword.update!(:value, &maybe_html_escape/1)
- |> Keyword.put_new(:"date-format", "Y-m-d")
-
- errors =
- case form do
- %{errors: errors} -> errors |> Keyword.get_values(field)
- _ -> []
- end
-
- {translate_error, opts} = opts |> Keyword.pop(:translate_error)
-
- opts =
- unless Enum.empty?(errors) do
- opts = opts |> Keyword.put_new(:invalid, true)
-
- errorString =
- Enum.map(errors, fn {msg, opts} ->
- if is_function(translate_error) do
- translate_error.({msg, opts})
- else
- msg
- end
- end)
- |> Enum.join(" ")
-
- opts |> Keyword.put(:"validity-message", errorString)
- else
- opts
- end
-
- {format, opts} = Keyword.pop(opts, :"date-format")
- {name, opts} = Keyword.pop(opts, :name)
- {value, opts} = Keyword.pop(opts, :value)
-
- content_tag(:"bx-date-picker", "date-format": format, name: name, value: value) do
- content_tag(:"bx-date-picker-input", "", opts ++ [kind: "single", value: value])
- end
- end
-
- defp generic_input(type, form, field, opts)
- when is_list(opts) and (is_atom(field) or is_binary(field)) do
- opts =
- opts
- |> Keyword.put_new(:"label-text", humanize(field))
- |> Keyword.put_new(:type, type)
- |> Keyword.put_new(:id, input_id(form, field))
- |> Keyword.put_new(:name, input_name(form, field))
- |> Keyword.put_new(:value, input_value(form, field))
- |> Keyword.update!(:value, &maybe_html_escape/1)
-
- errors =
- case form do
- %{errors: errors} -> errors |> Keyword.get_values(field)
- _ -> []
- end
-
- {translate_error, opts} = opts |> Keyword.pop(:translate_error)
-
- opts =
- unless Enum.empty?(errors) do
- opts = opts |> Keyword.put_new(:invalid, true)
-
- errorString =
- Enum.map(errors, fn {msg, opts} ->
- if is_function(translate_error) do
- translate_error.({msg, opts})
- else
- msg
- end
- end)
- |> Enum.join(" ")
-
- opts |> Keyword.put(:"validity-message", errorString)
- else
- opts
- end
-
- tag(:"bx-input", opts)
- end
-
- defp maybe_html_escape(nil), do: nil
- defp maybe_html_escape(value), do: html_escape(value)
-
- @doc """
- Generates a textarea input.
-
- All given options are forwarded to the underlying input,
- default values are provided for id, name and textarea
- content if possible.
-
- ## Examples
-
- # Assuming form contains a User schema
- textarea(form, :description)
- #=>
-
- ## New lines
-
- Notice the generated textarea includes a new line after
- the opening tag. This is because the HTML spec says new
- lines after tags must be ignored and all major browser
- implementations do that.
-
- So in order to avoid new lines provided by the user
- from being ignored when the form is resubmitted, we
- automatically add a new line before the text area
- value.
- """
- def wc_textarea(form, field, opts \\ []) do
- {value, opts} = Keyword.pop(opts, :value, input_value(form, field))
-
- opts =
- opts
- |> Keyword.put_new(:"label-text", humanize(field))
- |> Keyword.put_new(:id, input_id(form, field))
- |> Keyword.put_new(:name, input_name(form, field))
- |> Keyword.put_new(:value, value)
-
- errors =
- case form do
- %{errors: errors} -> errors |> Keyword.get_values(field)
- _ -> []
- end
-
- {translate_error, opts} = opts |> Keyword.pop(:translate_error)
-
- opts =
- unless Enum.empty?(errors) do
- opts = opts |> Keyword.put_new(:invalid, true)
-
- errorString =
- Enum.map(errors, fn {msg, opts} ->
- if is_function(translate_error) do
- translate_error.({msg, opts})
- else
- msg
- end
- end)
- |> Enum.join(" ")
-
- opts |> Keyword.put(:"validity-message", errorString)
- else
- opts
- end
-
- content_tag(:"bx-textarea", "", opts)
- end
-
- @doc """
- Generates a file input.
-
- It requires the given form to be configured with `multipart: true`
- when invoking `form_for/4`, otherwise it fails with `ArgumentError`.
-
- See `wc_text_input/3` for example and docs.
- """
- def wc_file_input(form, field, opts \\ []) do
- if match?(%Phoenix.HTML.Form{}, form) and !form.options[:multipart] do
- raise ArgumentError,
- "file_input/3 requires the enclosing form_for/4 " <>
- "to be configured with multipart: true"
- end
-
- opts =
- opts
- |> Keyword.put_new(:type, :file)
- |> Keyword.put_new(:id, input_id(form, field))
- |> Keyword.put_new(:name, input_name(form, field))
-
- opts =
- if opts[:multiple] do
- Keyword.update!(opts, :name, &"#{&1}[]")
- else
- opts
- end
-
- tag(:input, opts)
- end
-
- @doc """
- Generates a wc_submit button to send the form.
-
- ## Examples
-
- wc_submit do: "Submit"
- #=>
-
- """
- def wc_submit([do: _] = block_option), do: wc_submit([], block_option)
-
- @doc """
- Generates a wc_submit button to send the form.
-
- All options are forwarded to the underlying button tag.
- When called with a `do:` block, the button tag options
- come first.
-
- ## Examples
-
- wc_submit "Submit"
- #=>
-
- wc_submit "Submit", class: "btn"
- #=>
-
- wc_submit [class: "btn"], do: "Submit"
- #=>
-
- """
- def wc_submit(value, opts \\ [])
-
- def wc_submit(opts, [do: _] = block_option) do
- opts = Keyword.put_new(opts, :type, "submit")
- opts = Keyword.put_new(opts, :unelevated, true)
-
- content_tag(:"bx-btn", opts, block_option)
- end
-
- def wc_submit(value, opts) do
- opts = Keyword.put_new(opts, :type, "submit")
- opts = Keyword.put_new(opts, :unelevated, true)
-
- content_tag(:"bx-btn", value, opts)
- end
-
- @doc """
- Generates a reset input to reset all the form fields to
- their original state.
-
- All options are forwarded to the underlying input tag.
-
- ## Examples
-
- wc_reset "Reset"
- #=>
-
- wc_reset "Reset", class: "btn"
- #=>
-
- """
- def wc_reset(value, opts \\ []) do
- opts =
- opts
- |> Keyword.put_new(:type, "reset")
- |> Keyword.put_new(:value, value)
-
- tag(:"bx-btn", opts)
- end
-
- @doc """
- Generates a radio button.
-
- Invoke this function for each possible value you want
- to be sent to the server.
-
- ## Examples
-
- # Assuming form contains a User schema
- wc_radio_button(form, :role, "admin")
- #=>
-
- ## Options
-
- All options are simply forwarded to the underlying HTML tag.
- """
- def wc_radio_button(form, field, value, opts \\ []) do
- escaped_value = html_escape(value)
-
- opts =
- opts
- |> Keyword.put_new(:type, "radio")
- |> Keyword.put_new(:id, input_id(form, field, escaped_value))
- |> Keyword.put_new(:name, input_name(form, field))
-
- opts =
- if escaped_value == html_escape(input_value(form, field)) do
- Keyword.put_new(opts, :checked, true)
- else
- opts
- end
-
- tag(:input, [value: escaped_value] ++ opts)
- end
-
- @doc """
- Generates a wc_checkbox.
-
- This function is useful for sending boolean values to the server.
-
- ## Examples
-
- # Assuming form contains a User schema
- wc_checkbox(form, :famous)
- #=>
- #=>
-
- ## Options
-
- * `:checked_value` - the value to be sent when the wc_checkbox is checked.
- Defaults to "true"
-
- * `:hidden_input` - controls if this function will generate a hidden input
- to wc_submit the unchecked value or not. Defaults to "true"
-
- * `:unchecked_value` - the value to be sent when the wc_checkbox is unchecked,
- Defaults to "false"
-
- * `:value` - the value used to check if a wc_checkbox is checked or unchecked.
- The default value is extracted from the form data if available
-
- All other options are forwarded to the underlying HTML tag.
-
- ## Hidden fields
-
- Because an unchecked wc_checkbox is not sent to the server, Phoenix
- automatically generates a hidden field with the unchecked_value
- *before* the wc_checkbox field to ensure the `unchecked_value` is sent
- when the wc_checkbox is not marked. Set `hidden_input` to false If you
- don't want to send values from unchecked wc_checkbox to the server.
- """
- def wc_checkbox(form, field, opts \\ []) do
- opts =
- opts
- |> Keyword.put_new(:type, "checkbox")
- |> Keyword.put_new(:id, input_id(form, field))
- |> Keyword.put_new(:name, input_name(form, field))
-
- {value, opts} = Keyword.pop(opts, :value, input_value(form, field))
- {checked_value, opts} = Keyword.pop(opts, :checked_value, true)
- {unchecked_value, opts} = Keyword.pop(opts, :unchecked_value, false)
- {hidden_input, opts} = Keyword.pop(opts, :hidden_input, true)
-
- # We html escape all values to be sure we are comparing
- # apples to apples. After all we may have true in the data
- # but "true" in the params and both need to match.
- checked_value = html_escape(checked_value)
- unchecked_value = html_escape(unchecked_value)
-
- opts =
- Keyword.put_new_lazy(opts, :checked, fn ->
- value = html_escape(value)
- value == checked_value
- end)
-
- if hidden_input do
- hidden_opts = [type: "hidden", value: unchecked_value]
-
- html_escape([
- tag(:input, hidden_opts ++ Keyword.take(opts, [:name, :disabled, :form])),
- tag(:input, [value: checked_value] ++ opts)
- ])
- else
- html_escape([
- tag(:input, [value: checked_value] ++ opts)
- ])
- end
- end
-
- @doc """
- Generates a select tag with the given `options`.
-
- `options` are expected to be an enumerable which will be used to
- generate each respective `option`. The enumerable may have:
-
- * keyword lists - each keyword list is expected to have the keys
- `:key` and `:value`. Additional keys such as `:disabled` may
- be given to customize the option
-
- * two-item tuples - where the first element is an atom, string or
- integer to be used as the option label and the second element is
- an atom, string or integer to be used as the option value
-
- * atom, string or integer - which will be used as both label and value
- for the generated select
-
- ## Optgroups
-
- If `options` is map or keyword list where the first element is a string,
- atom or integer and the second element is a list or a map, it is assumed
- the key will be wrapped in an `