68 | //
69 | plugin(({ addVariant }) =>
70 | addVariant("phx-click-loading", [
71 | ".phx-click-loading&",
72 | ".phx-click-loading &",
73 | ]),
74 | ),
75 | plugin(({ addVariant }) =>
76 | addVariant("phx-submit-loading", [
77 | ".phx-submit-loading&",
78 | ".phx-submit-loading &",
79 | ]),
80 | ),
81 | plugin(({ addVariant }) =>
82 | addVariant("phx-change-loading", [
83 | ".phx-change-loading&",
84 | ".phx-change-loading &",
85 | ]),
86 | ),
87 |
88 | // Embeds Heroicons (https://heroicons.com) into your app.css bundle
89 | // See your `CoreComponents.icon/1` for more information.
90 | //
91 | plugin(function ({ matchComponents, theme }) {
92 | let iconsDir = path.join(__dirname, "../deps/heroicons/optimized");
93 | let values = {};
94 | let icons = [
95 | ["", "/24/outline"],
96 | ["-solid", "/24/solid"],
97 | ["-mini", "/20/solid"],
98 | ["-micro", "/16/solid"],
99 | ];
100 | icons.forEach(([suffix, dir]) => {
101 | fs.readdirSync(path.join(iconsDir, dir)).forEach((file) => {
102 | let name = path.basename(file, ".svg") + suffix;
103 | values[name] = { name, fullPath: path.join(iconsDir, dir, file) };
104 | });
105 | });
106 | matchComponents(
107 | {
108 | hero: ({ name, fullPath }) => {
109 | let content = fs
110 | .readFileSync(fullPath)
111 | .toString()
112 | .replace(/\r?\n|\r/g, "");
113 | let size = theme("spacing.6");
114 | if (name.endsWith("-mini")) {
115 | size = theme("spacing.5");
116 | } else if (name.endsWith("-micro")) {
117 | size = theme("spacing.4");
118 | }
119 | return {
120 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
121 | "-webkit-mask": `var(--hero-${name})`,
122 | mask: `var(--hero-${name})`,
123 | "mask-repeat": "no-repeat",
124 | "background-color": "currentColor",
125 | "vertical-align": "middle",
126 | display: "inline-block",
127 | width: size,
128 | height: size,
129 | };
130 | },
131 | },
132 | { values },
133 | );
134 | }),
135 | ],
136 | };
137 |
--------------------------------------------------------------------------------
/live_react_examples/assets/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "lib": ["dom", "dom.iterable", "ES2020"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "types": ["vite/client"],
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "esnext",
13 | "moduleResolution": "bundler",
14 | "isolatedModules": true,
15 | "resolveJsonModule": true,
16 | "noEmit": true,
17 | "jsx": "react",
18 | "sourceMap": true,
19 | "declaration": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "incremental": true,
23 | "noFallthroughCasesInSwitch": true,
24 |
25 | "paths": {
26 | "@/*": ["./*"]
27 | }
28 | },
29 | "include": ["js/*", "react/**/*"]
30 | }
31 |
--------------------------------------------------------------------------------
/live_react_examples/assets/vite.config.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { defineConfig } from "vite";
3 | import tailwindcss from "@tailwindcss/vite";
4 |
5 | import react from "@vitejs/plugin-react";
6 | import liveReactPlugin from "live_react/vite-plugin";
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig(({ command }) => {
10 | const isDev = command !== "build";
11 |
12 | return {
13 | base: isDev ? undefined : "/assets",
14 | publicDir: "static",
15 | plugins: [
16 | react(),
17 | liveReactPlugin(),
18 | tailwindcss(),
19 | ],
20 | ssr: {
21 | // we need it, because in SSR build we want no external
22 | // and in dev, we want external for fast updates
23 | noExternal: isDev ? undefined : true,
24 | },
25 | resolve: {
26 | alias: {
27 | "@": path.resolve(__dirname, "."),
28 | },
29 | },
30 | optimizeDeps: {
31 | // these packages are loaded as file:../deps/
imports
32 | // so they're not optimized for development by vite by default
33 | // we want to enable it for better DX
34 | // more https://vitejs.dev/guide/dep-pre-bundling#monorepos-and-linked-dependencies
35 | include: ["live_react", "phoenix", "phoenix_html", "phoenix_live_view"],
36 | },
37 | build: {
38 | commonjsOptions: { transformMixedEsModules: true },
39 | target: "es2020",
40 | outDir: "../priv/static/assets", // emit assets to priv/static/assets
41 | emptyOutDir: true,
42 | sourcemap: isDev, // enable source map in dev build
43 | // manifest: false, // do not generate manifest.json
44 | rollupOptions: {
45 | input: {
46 | app: path.resolve(__dirname, "./js/app.js"),
47 | },
48 | output: {
49 | // remove hashes to match phoenix way of handling asssets
50 | entryFileNames: "[name].js",
51 | chunkFileNames: "[name].js",
52 | assetFileNames: "[name][extname]",
53 | },
54 | },
55 | },
56 | };
57 | });
58 |
--------------------------------------------------------------------------------
/live_react_examples/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 |
7 | # General application configuration
8 | import Config
9 |
10 | config :live_react,
11 | ssr_module: LiveReact.SSR.NodeJS
12 |
13 | config :live_react_examples,
14 | generators: [timestamp_type: :utc_datetime]
15 |
16 | # Configures the endpoint
17 | config :live_react_examples, LiveReactExamplesWeb.Endpoint,
18 | url: [host: "localhost"],
19 | adapter: Bandit.PhoenixAdapter,
20 | render_errors: [
21 | formats: [html: LiveReactExamplesWeb.ErrorHTML, json: LiveReactExamplesWeb.ErrorJSON],
22 | layout: false
23 | ],
24 | pubsub_server: LiveReactExamples.PubSub,
25 | live_view: [signing_salt: "vR6Y0p5z"]
26 |
27 | # Configures Elixir's Logger
28 | config :logger, :console,
29 | format: "$time $metadata[$level] $message\n",
30 | metadata: [:request_id]
31 |
32 | # Use Jason for JSON parsing in Phoenix
33 | config :phoenix, :json_library, Jason
34 |
35 | # Import environment specific config. This must remain at the bottom
36 | # of this file so it overrides the configuration defined above.
37 | import_config "#{config_env()}.exs"
38 |
--------------------------------------------------------------------------------
/live_react_examples/config/dev.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we can use it
8 | # to bundle .js and .css sources.
9 | config :live_react_examples, LiveReactExamplesWeb.Endpoint,
10 | # Binding to loopback ipv4 address prevents access from other machines.
11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
12 | http: [ip: {127, 0, 0, 1}, port: 4000],
13 | check_origin: false,
14 | code_reloader: true,
15 | debug_errors: true,
16 | secret_key_base: "zvJnWbYbr/b5+CzCCcin8jP0cIVLqXs6/vt2WiC5d/nVE8npISnhItLe1AIAP7Vn",
17 | watchers: [
18 | npm: ["run", "dev", cd: Path.expand("../assets", __DIR__)]
19 | ]
20 |
21 | # ## SSL Support
22 | #
23 | # In order to use HTTPS in development, a self-signed
24 | # certificate can be generated by running the following
25 | # Mix task:
26 | #
27 | # mix phx.gen.cert
28 | #
29 | # Run `mix help phx.gen.cert` for more information.
30 | #
31 | # The `http:` config above can be replaced with:
32 | #
33 | # https: [
34 | # port: 4001,
35 | # cipher_suite: :strong,
36 | # keyfile: "priv/cert/selfsigned_key.pem",
37 | # certfile: "priv/cert/selfsigned.pem"
38 | # ],
39 | #
40 | # If desired, both `http:` and `https:` keys can be
41 | # configured to run both http and https servers on
42 | # different ports.
43 |
44 | # Watch static and templates for browser reloading.
45 | config :live_react_examples, LiveReactExamplesWeb.Endpoint,
46 | reloadable_appps: [:live_react, :live_react_examples_web, :live_react_examples],
47 | live_reload: [
48 | notify: [
49 | live_views: [
50 | ~r"lib/live_react_examples_web/core_components.ex$",
51 | ~r"lib/live_react_examples_web/(live|components)/.*(ex|heex)$"
52 | ]
53 | ],
54 | patterns: [
55 | ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
56 | ~r"lib/live_react_examples_web/(controllers|live|components)/.*(ex|heex)$"
57 | ]
58 | ]
59 |
60 | config :live_react,
61 | vite_host: "http://localhost:5173",
62 | ssr_module: LiveReact.SSR.ViteJS,
63 | ssr: true
64 |
65 | # Enable dev routes for dashboard and mailbox
66 | config :live_react_examples, dev_routes: true
67 |
68 | # Do not include metadata nor timestamps in development logs
69 | config :logger, :console, format: "[$level] $message\n"
70 |
71 | # Set a higher stacktrace during development. Avoid configuring such
72 | # in production as building large stacktraces may be expensive.
73 | config :phoenix, :stacktrace_depth, 20
74 |
75 | # Initialize plugs at runtime for faster development compilation
76 | config :phoenix, :plug_init_mode, :runtime
77 |
78 | config :phoenix_live_view,
79 | # Include HEEx debug annotations as HTML comments in rendered markup
80 | debug_heex_annotations: true,
81 | # Enable helpful, but potentially expensive runtime checks
82 | enable_expensive_runtime_checks: true
83 |
--------------------------------------------------------------------------------
/live_react_examples/config/prod.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # Note we also include the path to a cache manifest
4 | # containing the digested version of static files. This
5 | # manifest is generated by the `mix assets.deploy` task,
6 | # which you should run after static files are built and
7 | # before starting your production server.
8 | config :live_react_examples, LiveReactExamplesWeb.Endpoint,
9 | cache_static_manifest: "priv/static/cache_manifest.json"
10 |
11 | # Do not print debug messages in production
12 | config :logger, level: :info
13 |
14 | config :live_react,
15 | ssr_module: LiveReact.SSR.NodeJS,
16 | ssr: true
17 |
18 | # Runtime production configuration, including reading
19 | # of environment variables, is done on config/runtime.exs.
20 |
--------------------------------------------------------------------------------
/live_react_examples/config/runtime.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # config/runtime.exs is executed for all environments, including
4 | # during releases. It is executed after compilation and before the
5 | # system starts, so it is typically used to load production configuration
6 | # and secrets from environment variables or elsewhere. Do not define
7 | # any compile-time configuration in here, as it won't be applied.
8 | # The block below contains prod specific runtime configuration.
9 |
10 | # ## Using releases
11 | #
12 | # If you use `mix release`, you need to explicitly enable the server
13 | # by passing the PHX_SERVER=true when you start it:
14 | #
15 | # PHX_SERVER=true bin/live_react_examples start
16 | #
17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
18 | # script that automatically sets the env var above.
19 | if System.get_env("PHX_SERVER") do
20 | config :live_react_examples, LiveReactExamplesWeb.Endpoint, server: true
21 | end
22 |
23 | if config_env() == :prod do
24 | # The secret key base is used to sign/encrypt cookies and other secrets.
25 | # A default value is used in config/dev.exs and config/test.exs but you
26 | # want to use a different value for prod and you most likely don't want
27 | # to check this value into version control, so we use an environment
28 | # variable instead.
29 | secret_key_base =
30 | System.get_env("SECRET_KEY_BASE") ||
31 | raise """
32 | environment variable SECRET_KEY_BASE is missing.
33 | You can generate one by calling: mix phx.gen.secret
34 | """
35 |
36 | host = System.get_env("PHX_HOST") || "example.com"
37 | port = String.to_integer(System.get_env("PORT") || "4000")
38 |
39 | config :live_react_examples, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
40 |
41 | config :live_react_examples, LiveReactExamplesWeb.Endpoint,
42 | url: [host: host, port: 443, scheme: "https"],
43 | http: [
44 | # Enable IPv6 and bind on all interfaces.
45 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
46 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
47 | # for details about using IPv6 vs IPv4 and loopback vs public addresses.
48 | ip: {0, 0, 0, 0, 0, 0, 0, 0},
49 | port: port
50 | ],
51 | secret_key_base: secret_key_base
52 |
53 | # ## SSL Support
54 | #
55 | # To get SSL working, you will need to add the `https` key
56 | # to your endpoint configuration:
57 | #
58 | # config :live_react_examples, LiveReactExamplesWeb.Endpoint,
59 | # https: [
60 | # ...,
61 | # port: 443,
62 | # cipher_suite: :strong,
63 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
64 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
65 | # ]
66 | #
67 | # The `cipher_suite` is set to `:strong` to support only the
68 | # latest and more secure SSL ciphers. This means old browsers
69 | # and clients may not be supported. You can set it to
70 | # `:compatible` for wider support.
71 | #
72 | # `:keyfile` and `:certfile` expect an absolute path to the key
73 | # and cert in disk or a relative path inside priv, for example
74 | # "priv/ssl/server.key". For all supported SSL configuration
75 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
76 | #
77 | # We also recommend setting `force_ssl` in your config/prod.exs,
78 | # ensuring no data is ever sent via http, always redirecting to https:
79 | #
80 | # config :live_react_examples, LiveReactExamplesWeb.Endpoint,
81 | # force_ssl: [hsts: true]
82 | #
83 | # Check `Plug.SSL` for all available options in `force_ssl`.
84 | end
85 |
--------------------------------------------------------------------------------
/live_react_examples/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :live_react_examples, LiveReactExamplesWeb.Endpoint,
6 | http: [ip: {127, 0, 0, 1}, port: 4002],
7 | secret_key_base: "1TSUMeDi3xh+wePzvzKMq73p/bD2psOzg340hjtEcR8WGPxm0qINVteU03whCTcS",
8 | server: false
9 |
10 | # Print only warnings and errors during test
11 | config :logger, level: :warning
12 |
13 | # Initialize plugs at runtime for faster test compilation
14 | config :phoenix, :plug_init_mode, :runtime
15 |
16 | # Enable helpful, but potentially expensive runtime checks
17 | config :phoenix_live_view,
18 | enable_expensive_runtime_checks: true
19 |
--------------------------------------------------------------------------------
/live_react_examples/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml app configuration file generated for live-react-examples on 2024-06-29T19:14:48+02:00
2 | #
3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4 | #
5 |
6 | app = 'live-react-examples'
7 | primary_region = 'cdg'
8 | kill_signal = 'SIGTERM'
9 |
10 | [build]
11 |
12 | [env]
13 | PHX_HOST = 'live-react-examples.fly.dev'
14 | PORT = '8080'
15 |
16 | [http_service]
17 | internal_port = 8080
18 | force_https = true
19 | auto_stop_machines = true
20 | auto_start_machines = true
21 | min_machines_running = 0
22 | processes = ['app']
23 |
24 | [http_service.concurrency]
25 | type = 'connections'
26 | hard_limit = 1000
27 | soft_limit = 1000
28 |
29 | [[vm]]
30 | size = 'shared-cpu-1x'
31 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamples do
2 | @moduledoc """
3 | LiveReactExamples keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 |
10 | @url "https://github.com/mrdotb/live_react/blob/main/live_react_examples"
11 | @raw_url "https://raw.githubusercontent.com/mrdotb/live_react/main/live_react_examples"
12 | @dead_views "/lib/live_react_examples_web/controllers/page_html"
13 | @live_views "/lib/live_react_examples_web/live"
14 | @react "/assets/react-components"
15 |
16 | def demo(name)
17 |
18 | def demo(:simple) do
19 | %{
20 | view_type: "DeadView",
21 | raw_view_url: "#{@raw_url}/#{@dead_views}/simple.html.heex",
22 | view_url: "#{@url}/#{@dead_views}/simple.html.heex",
23 | raw_react_url: "#{@raw_url}/#{@react}/simple.jsx",
24 | react_url: "#{@url}/#{@react}/simple.jsx"
25 | }
26 | end
27 |
28 | def demo(:simple_props) do
29 | %{
30 | view_type: "DeadView",
31 | raw_view_url: "#{@raw_url}/#{@dead_views}/simple_props.html.heex",
32 | view_url: "#{@url}/#{@dead_views}/simple_props.html.heex",
33 | raw_react_url: "#{@raw_url}/#{@react}/simple-props.jsx",
34 | react_url: "#{@url}/#{@react}/simple-props.jsx"
35 | }
36 | end
37 |
38 | def demo(:typescript) do
39 | %{
40 | view_type: "DeadView",
41 | raw_view_url: "#{@raw_url}#{@dead_views}/typescript.html.heex",
42 | view_url: "#{@url}#{@dead_views}/typescript.html.heex",
43 | raw_react_url: "#{@raw_url}#{@react}/typescript.tsx",
44 | react_url: "#{@url}#{@react}/typescript.tsx",
45 | react_language: "tsx"
46 | }
47 | end
48 |
49 | def demo(:lazy) do
50 | %{
51 | view_type: "DeadView",
52 | raw_view_url: "#{@raw_url}#{@dead_views}/lazy.html.heex",
53 | view_url: "#{@url}#{@dead_views}/lazy.html.heex",
54 | raw_react_url: "#{@raw_url}#{@react}/lazy.jsx",
55 | react_url: "#{@url}#{@react}/lazy.jsx"
56 | }
57 | end
58 |
59 | def demo(:counter) do
60 | %{
61 | raw_view_url: "#{@raw_url}#{@live_views}/counter.ex",
62 | view_url: "#{@url}#{@live_views}/counter.ex",
63 | view_language: "elixir",
64 | raw_react_url: "#{@raw_url}#{@react}/counter.jsx",
65 | react_url: "#{@url}#{@react}/counter.jsx"
66 | }
67 | end
68 |
69 | def demo(:log_list) do
70 | %{
71 | raw_view_url: "#{@raw_url}#{@live_views}/log_list.ex",
72 | view_url: "#{@url}#{@live_views}/log_list.ex",
73 | view_language: "elixir",
74 | raw_react_url: "#{@raw_url}#{@react}/log-list.jsx",
75 | react_url: "#{@url}#{@react}/log-list.jsx"
76 | }
77 | end
78 |
79 | def demo(:flash_sonner) do
80 | %{
81 | raw_view_url: "#{@raw_url}#{@live_views}/flash_sonner.ex",
82 | view_url: "#{@url}#{@live_views}/flash_sonner.ex",
83 | view_language: "elixir",
84 | raw_react_url: "#{@raw_url}#{@react}/flash-sonner.jsx",
85 | react_url: "#{@url}#{@react}/flash-sonner.jsx"
86 | }
87 | end
88 |
89 | def demo(:ssr) do
90 | %{
91 | raw_view_url: "#{@raw_url}#{@live_views}/ssr.ex",
92 | view_url: "#{@url}#{@live_views}/ssr.ex",
93 | view_language: "elixir",
94 | raw_react_url: "#{@raw_url}#{@react}/ssr.jsx",
95 | react_url: "#{@url}#{@react}/ssr.jsx"
96 | }
97 | end
98 |
99 | def demo(:hybrid_form) do
100 | %{
101 | raw_view_url: "#{@raw_url}#{@live_views}/hybrid_form.ex",
102 | view_url: "#{@url}#{@live_views}/hybrid_form.ex",
103 | view_language: "elixir",
104 | raw_react_url: "#{@raw_url}#{@react}/delay-slider.tsx",
105 | react_url: "#{@url}#{@react}/delay-slider.tsx"
106 | }
107 | end
108 |
109 | def demo(:slot) do
110 | %{
111 | raw_view_url: "#{@raw_url}#{@live_views}/slot.ex",
112 | view_url: "#{@url}#{@live_views}/slot.ex",
113 | view_language: "elixir",
114 | raw_react_url: "#{@raw_url}#{@react}/slot.tsx",
115 | react_url: "#{@url}#{@react}/slot.tsx"
116 | }
117 | end
118 |
119 | def demo(:context) do
120 | %{
121 | raw_view_url: "#{@raw_url}#{@live_views}/context.ex",
122 | view_url: "#{@url}#{@live_views}/context.ex",
123 | view_language: "elixir",
124 | raw_react_url: "#{@raw_url}#{@react}/context.tsx",
125 | react_url: "#{@url}#{@react}/context.tsx"
126 | }
127 | end
128 |
129 | def demo(demo) do
130 | raise ArgumentError, "Unknown demo: #{inspect(demo)}"
131 | end
132 | end
133 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples/application.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamples.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | @impl true
9 | def start(_type, _args) do
10 | children = [
11 | {NodeJS.Supervisor, [path: LiveReact.SSR.NodeJS.server_path(), pool_size: 1]},
12 | LiveReactExamplesWeb.Telemetry,
13 | {DNSCluster,
14 | query: Application.get_env(:live_react_examples, :dns_cluster_query) || :ignore},
15 | {Phoenix.PubSub, name: LiveReactExamples.PubSub},
16 | # Start a worker by calling: LiveReactExamples.Worker.start_link(arg)
17 | # {LiveReactExamples.Worker, arg},
18 | # Start to serve requests, typically the last entry
19 | LiveReactExamplesWeb.Endpoint
20 | ]
21 |
22 | # Set up LiveReactExamples.Telemetry
23 | LiveReactExamples.Telemetry.setup()
24 |
25 | # See https://hexdocs.pm/elixir/Supervisor.html
26 | # for other strategies and supported options
27 | opts = [strategy: :one_for_one, name: LiveReactExamples.Supervisor]
28 | Supervisor.start_link(children, opts)
29 | end
30 |
31 | # Tell Phoenix to update the endpoint configuration
32 | # whenever the application is updated.
33 | @impl true
34 | def config_change(changed, _new, removed) do
35 | LiveReactExamplesWeb.Endpoint.config_change(changed, removed)
36 | :ok
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples/telemetry.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamples.Telemetry do
2 | require Logger
3 |
4 | def setup() do
5 | :ok =
6 | :telemetry.attach(
7 | "live-react-ssr-logger",
8 | [:live_react, :ssr, :stop],
9 | &LiveReactExamples.Telemetry.handle_event/4,
10 | nil
11 | )
12 | end
13 |
14 | def handle_event([:live_react, :ssr, :stop], %{duration: duration}, metadata, _config) do
15 | duration_ms = System.convert_time_unit(duration, :native, :microsecond)
16 | Logger.info("SSR completed for component: #{metadata.component} in #{duration_ms}µs")
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, components, channels, and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use LiveReactExamplesWeb, :controller
9 | use LiveReactExamplesWeb, :html
10 |
11 | The definitions below will be executed for every controller,
12 | component, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define additional modules and import
17 | those modules here.
18 | """
19 |
20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
21 |
22 | def router do
23 | quote do
24 | use Phoenix.Router, helpers: false
25 |
26 | # Import common connection and controller functions to use in pipelines
27 | import Plug.Conn
28 | import Phoenix.Controller
29 | import Phoenix.LiveView.Router
30 | end
31 | end
32 |
33 | def channel do
34 | quote do
35 | use Phoenix.Channel
36 | end
37 | end
38 |
39 | def controller do
40 | quote do
41 | use Phoenix.Controller,
42 | formats: [:html, :json],
43 | layouts: [html: LiveReactExamplesWeb.Layouts]
44 |
45 | import Plug.Conn
46 |
47 | unquote(verified_routes())
48 | end
49 | end
50 |
51 | def live_view do
52 | quote do
53 | use Phoenix.LiveView,
54 | layout: {LiveReactExamplesWeb.Layouts, :app}
55 |
56 | on_mount LiveReactExamplesWeb.LiveDemoAssigns
57 | unquote(html_helpers())
58 | end
59 | end
60 |
61 | def live_component do
62 | quote do
63 | use Phoenix.LiveComponent
64 |
65 | unquote(html_helpers())
66 | end
67 | end
68 |
69 | def html do
70 | quote do
71 | use Phoenix.Component
72 |
73 | # Import convenience functions from controllers
74 | import Phoenix.Controller,
75 | only: [get_csrf_token: 0, view_module: 1, view_template: 1]
76 |
77 | # Include general helpers for rendering HTML
78 | unquote(html_helpers())
79 | end
80 | end
81 |
82 | defp html_helpers do
83 | quote do
84 | # HTML escaping functionality
85 | import Phoenix.HTML
86 | # Core UI components and translation
87 | import LiveReactExamplesWeb.CoreComponents
88 |
89 | import LiveReact
90 |
91 | # Shortcut for generating JS commands
92 | alias Phoenix.LiveView.JS
93 |
94 | # Routes generation with the ~p sigil
95 | unquote(verified_routes())
96 | end
97 | end
98 |
99 | def verified_routes do
100 | quote do
101 | use Phoenix.VerifiedRoutes,
102 | endpoint: LiveReactExamplesWeb.Endpoint,
103 | router: LiveReactExamplesWeb.Router,
104 | statics: LiveReactExamplesWeb.static_paths()
105 | end
106 | end
107 |
108 | @doc """
109 | When used, dispatch to the appropriate controller/live_view/etc.
110 | """
111 | defmacro __using__(which) when is_atom(which) do
112 | apply(__MODULE__, which, [])
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/components/layouts.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.Layouts do
2 | @moduledoc """
3 | This module holds different layouts used by your application.
4 |
5 | See the `layouts` directory for all templates available.
6 | The "root" layout is a skeleton rendered as part of the
7 | application router. The "app" layout is set as the default
8 | layout on both `use LiveReactExamplesWeb, :controller` and
9 | `use LiveReactExamplesWeb, :live_view`.
10 | """
11 | use LiveReactExamplesWeb, :html
12 |
13 | embed_templates "layouts/*"
14 | end
15 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/components/layouts/app.html.heex:
--------------------------------------------------------------------------------
1 |
2 | <.border_beam size={100} duration={10} />
3 |
4 |
5 |
6 |
7 |
8 |
9 | LiveReact examples
10 |
11 |
12 |
13 | <.a class="flex space-x-2 items-center" href="https://github.com/mrdotb/live_react">
14 |
View on Github
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Dead Views 💀
31 |
32 | <.link
33 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
34 | href={~p"/simple"}
35 | >
36 | Simple
37 |
38 | <.link
39 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
40 | href={~p"/simple-props"}
41 | >
42 | Simple Props
43 |
44 | <.link
45 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
46 | href={~p"/typescript"}
47 | >
48 | TypeScript
49 |
50 | <.link
51 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
52 | href={~p"/lazy"}
53 | >
54 | Lazy
55 |
56 |
57 |
58 |
59 |
60 |
LiveViews 🔄
61 |
62 | <.link
63 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
64 | navigate={~p"/live-counter"}
65 | >
66 | Live Counter
67 |
68 | <.link
69 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
70 | navigate={~p"/log-list"}
71 | >
72 | Log List
73 |
74 | <.link
75 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
76 | navigate={~p"/flash-sonner"}
77 | >
78 | Flash with sonner
79 |
80 | <.link
81 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
82 | navigate={~p"/ssr"}
83 | >
84 | SSR
85 |
86 | <.link
87 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
88 | navigate={~p"/hybrid-form"}
89 | >
90 | Hybrid Form
91 |
92 | <.link
93 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
94 | navigate={~p"/slot"}
95 | >
96 | Slot
97 |
98 | <.link
99 | class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
100 | navigate={~p"/context"}
101 | >
102 | Context
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | <.demo {LiveReactExamples.demo(@demo)}>
113 | {@inner_content}
114 |
115 |
116 |
117 |
118 |
119 | <.react :if={@demo == :flash_sonner} name="FlashSonner" flash={@flash} socket={assigns[:socket]} />
120 | <.flash_group flash={@flash} />
121 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/components/layouts/root.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <.live_title suffix=" · Phoenix Framework">
8 | {assigns[:page_title] || "LiveReactExamples"}
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 | {@inner_content}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/controllers/error_html.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.ErrorHTML do
2 | @moduledoc """
3 | This module is invoked by your endpoint in case of errors on HTML requests.
4 |
5 | See config/config.exs.
6 | """
7 | use LiveReactExamplesWeb, :html
8 |
9 | # If you want to customize your error pages,
10 | # uncomment the embed_templates/1 call below
11 | # and add pages to the error directory:
12 | #
13 | # * lib/live_react_examples_web/controllers/error_html/404.html.heex
14 | # * lib/live_react_examples_web/controllers/error_html/500.html.heex
15 | #
16 | # embed_templates "error_html/*"
17 |
18 | # The default is to render a plain text page based on
19 | # the template name. For example, "404.html" becomes
20 | # "Not Found".
21 | def render(template, _assigns) do
22 | Phoenix.Controller.status_message_from_template(template)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/controllers/error_json.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.ErrorJSON do
2 | @moduledoc """
3 | This module is invoked by your endpoint in case of errors on JSON requests.
4 |
5 | See config/config.exs.
6 | """
7 |
8 | # If you want to customize a particular status code,
9 | # you may add your own clauses, such as:
10 | #
11 | # def render("500.json", _assigns) do
12 | # %{errors: %{detail: "Internal Server Error"}}
13 | # end
14 |
15 | # By default, Phoenix returns the status message from
16 | # the template name. For example, "404.json" becomes
17 | # "Not Found".
18 | def render(template, _assigns) do
19 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.PageController do
2 | use LiveReactExamplesWeb, :controller
3 |
4 | def home(conn, _params) do
5 | redirect(conn, to: ~p"/simple")
6 | end
7 |
8 | def simple(conn, _params) do
9 | render(conn, :simple, demo: :simple)
10 | end
11 |
12 | def simple_props(conn, _params) do
13 | render(conn, :simple_props, demo: :simple_props)
14 | end
15 |
16 | def typescript(conn, _params) do
17 | render(conn, :typescript, demo: :typescript)
18 | end
19 |
20 | def lazy(conn, _params) do
21 | render(conn, :lazy, demo: :lazy)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/controllers/page_html.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.PageHTML do
2 | @moduledoc """
3 | This module contains pages rendered by PageController.
4 |
5 | See the `page_html` directory for all templates available.
6 | """
7 | use LiveReactExamplesWeb, :html
8 |
9 | embed_templates "page_html/*"
10 | end
11 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/controllers/page_html/home.html.heex:
--------------------------------------------------------------------------------
1 | <.flash_group flash={@flash} />
2 |
3 |
10 |
11 |
15 |
19 |
24 |
29 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
48 |
49 |
50 | Phoenix Framework
51 |
52 | v{Application.spec(:phoenix, :vsn)}
53 |
54 |
55 |
56 | Peace of mind from prototype to production.
57 |
58 |
59 | Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
60 |
61 |
62 |
63 |
133 |
134 |
149 |
164 |
188 |
203 |
218 |
219 |
220 |
221 |
222 |
223 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/controllers/page_html/lazy.html.heex:
--------------------------------------------------------------------------------
1 | <.react name="Lazy" />
2 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/controllers/page_html/simple.html.heex:
--------------------------------------------------------------------------------
1 | <.react name="Simple" />
2 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/controllers/page_html/simple_props.html.heex:
--------------------------------------------------------------------------------
1 | <.react name="SimpleProps" user={%{name: "mrdotb", age: 30}} />
2 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/controllers/page_html/typescript.html.heex:
--------------------------------------------------------------------------------
1 | <.react name="Typescript" />
2 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :live_react_examples
3 |
4 | # The session will be stored in the cookie and signed,
5 | # this means its contents can be read but not tampered with.
6 | # Set :encryption_salt if you would also like to encrypt it.
7 | @session_options [
8 | store: :cookie,
9 | key: "_live_react_examples_key",
10 | signing_salt: "7e1J3j+/",
11 | same_site: "Lax"
12 | ]
13 |
14 | socket "/live", Phoenix.LiveView.Socket,
15 | websocket: [connect_info: [session: @session_options]],
16 | longpoll: [connect_info: [session: @session_options]]
17 |
18 | # Serve at "/" the static files from "priv/static" directory.
19 | #
20 | # You should set gzip to true if you are running phx.digest
21 | # when deploying your static files in production.
22 | plug Plug.Static,
23 | at: "/",
24 | from: :live_react_examples,
25 | gzip: false,
26 | only: LiveReactExamplesWeb.static_paths()
27 |
28 | # Code reloading can be explicitly enabled under the
29 | # :code_reloader configuration of your endpoint.
30 | if code_reloading? do
31 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
32 | plug Phoenix.LiveReloader
33 | plug Phoenix.CodeReloader
34 | end
35 |
36 | plug Plug.RequestId
37 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
38 |
39 | plug Plug.Parsers,
40 | parsers: [:urlencoded, :multipart, :json],
41 | pass: ["*/*"],
42 | json_decoder: Phoenix.json_library()
43 |
44 | plug Plug.MethodOverride
45 | plug Plug.Head
46 | plug Plug.Session, @session_options
47 | plug LiveReactExamplesWeb.Router
48 | end
49 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/live/context.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.LiveContext do
2 | use LiveReactExamplesWeb, :live_view
3 |
4 | def render(assigns) do
5 | ~H"""
6 | Hybrid: LiveView + React
7 | <.react name="Context" count={@count} socket={@socket} ssr={true} />
8 | """
9 | end
10 |
11 | def mount(_session, _params, socket) do
12 | {:ok, assign(socket, :count, 10)}
13 | end
14 |
15 | def handle_event("set_count", %{"value" => number}, socket) do
16 | {:noreply, assign(socket, :count, number)}
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/live/counter.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.LiveCounter do
2 | use LiveReactExamplesWeb, :live_view
3 |
4 | def render(assigns) do
5 | ~H"""
6 | Hybrid: LiveView + React
7 | <.react name="Counter" count={@count} socket={@socket} ssr={true} />
8 | """
9 | end
10 |
11 | def mount(_session, _params, socket) do
12 | {:ok, assign(socket, :count, 10)}
13 | end
14 |
15 | def handle_event("set_count", %{"value" => number}, socket) do
16 | {:noreply, assign(socket, :count, String.to_integer(number))}
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/live/demo_assigns.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.LiveDemoAssigns do
2 | @moduledoc """
3 | Assigns the current demo state.
4 | """
5 |
6 | import Phoenix.Component
7 | import Phoenix.LiveView
8 |
9 | def on_mount(:default, _params, _session, socket) do
10 | socket = attach_hook(socket, :active_tab, :handle_params, &set_active_demo/3)
11 | {:cont, socket}
12 | end
13 |
14 | defp set_active_demo(_params, _url, socket) do
15 | demo =
16 | case {socket.view, socket.assigns.live_action} do
17 | {LiveReactExamplesWeb.LiveCounter, _} ->
18 | :counter
19 |
20 | {LiveReactExamplesWeb.LiveLogList, _} ->
21 | :log_list
22 |
23 | {LiveReactExamplesWeb.LiveFlashSonner, _} ->
24 | :flash_sonner
25 |
26 | {LiveReactExamplesWeb.LiveSSR, _} ->
27 | :ssr
28 |
29 | {LiveReactExamplesWeb.LiveHybridForm, _} ->
30 | :hybrid_form
31 |
32 | {LiveReactExamplesWeb.LiveSlot, _} ->
33 | :slot
34 |
35 | {LiveReactExamplesWeb.LiveContext, _} ->
36 | :context
37 |
38 | {_view, _live_action} ->
39 | nil
40 | end
41 |
42 | {:cont, assign(socket, demo: demo)}
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/live/flash_sonner.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.LiveFlashSonner do
2 | use LiveReactExamplesWeb, :live_view
3 |
4 | def render(assigns) do
5 | ~H"""
6 | Flash sonner
7 | <.button phx-click="info" class="cursor-pointer">
8 | info
9 |
10 | <.button phx-click="error" class="cursor-pointer">
11 | error
12 |
13 | """
14 | end
15 |
16 | def mount(_session, _params, socket) do
17 | {:ok, socket}
18 | end
19 |
20 | def handle_event("info", _params, socket) do
21 | {:noreply, put_flash(socket, :info, "This is an info message")}
22 | end
23 |
24 | def handle_event("error", _params, socket) do
25 | {:noreply, put_flash(socket, :error, "This is an error message")}
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/live/hybrid_form.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.LiveHybridForm do
2 | use LiveReactExamplesWeb, :live_view
3 |
4 | def render(assigns) do
5 | ~H"""
6 | Hybrid form
7 |
8 | <.simple_form for={@form} phx-change="validate" phx-submit="submit">
9 | <.input field={@form[:email]} label="Email" />
10 | <.react
11 | label="Delay Between"
12 | name="DelaySlider"
13 | inputName="settings[delay_between]"
14 | value={@form[:delay_between].value}
15 | min={2_000}
16 | max={90_000}
17 | step={2_000}
18 | />
19 |
20 |
21 |
22 |
23 | <%= inspect(@form.params, pretty: true) %>
24 |
25 |
26 | """
27 | end
28 |
29 | def mount(_session, _params, socket) do
30 | form =
31 | to_form(
32 | %{
33 | "email" => "hello@mrdotb.com",
34 | "delay_between" => [4_000, 30_000]
35 | },
36 | as: :settings
37 | )
38 |
39 | socket = assign(socket, form: form)
40 | {:ok, socket}
41 | end
42 |
43 | def handle_event("validate", %{"settings" => settings}, socket) do
44 | form = to_form(settings, as: :settings, action: :validate)
45 | socket = assign(socket, form: form)
46 | {:noreply, socket}
47 | end
48 |
49 | def handle_event("submit", _, socket) do
50 | {:noreply, socket}
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/live/log_list.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.LiveLogList do
2 | use LiveReactExamplesWeb, :live_view
3 |
4 | def render(assigns) do
5 | ~H"""
6 | <.react name="LogList" socket={@socket} />
7 | """
8 | end
9 |
10 | def mount(_params, _session, socket) do
11 | if connected?(socket), do: :timer.send_interval(1000, self(), :tick)
12 | {:ok, socket}
13 | end
14 |
15 | def handle_event("add_item", %{"body" => body}, socket) do
16 | socket = push_event(socket, "new_item", create_log(body))
17 | {:noreply, socket}
18 | end
19 |
20 | def handle_info(:tick, socket) do
21 | datetime =
22 | DateTime.utc_now()
23 | |> DateTime.to_string()
24 |
25 | socket = push_event(socket, "new_item", create_log(datetime))
26 | {:noreply, socket}
27 | end
28 |
29 | defp create_log(body) do
30 | %{id: System.unique_integer([:positive]), body: body}
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/live/slot.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.LiveSlot do
2 | use LiveReactExamplesWeb, :live_view
3 |
4 | def render(assigns) do
5 | ~H"""
6 | Slot
7 | <.react name="Slot" socket={@socket} ssr={true}>
8 | button component passed as a slot and rendered
9 | <.button class="cursor-pointer">
10 | button
11 |
12 |
13 | """
14 | end
15 |
16 | def mount(_session, _params, socket) do
17 | {:ok, assign(socket, :count, 10)}
18 | end
19 |
20 | def handle_event("set_count", %{"value" => number}, socket) do
21 | {:noreply, assign(socket, :count, String.to_integer(number))}
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/live/ssr.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.LiveSSR do
2 | use LiveReactExamplesWeb, :live_view
3 |
4 | def render(assigns) do
5 | ~H"""
6 | SSR
7 |
11 | SSR guide
12 |
13 |
14 | <.react
15 | ssr={true}
16 | name="SSR"
17 | socket={@socket}
18 | text="I am rendered on Server"
19 | class="cursor-pointer"
20 | />
21 | <.react
22 | ssr={false}
23 | name="SSR"
24 | socket={@socket}
25 | text="I am rendered on Client"
26 | class="cursor-pointer"
27 | />
28 |
29 | """
30 | end
31 |
32 | def mount(_session, _params, socket) do
33 | {:ok, socket}
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.Router do
2 | use LiveReactExamplesWeb, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_live_flash
8 | plug :put_root_layout, html: {LiveReactExamplesWeb.Layouts, :root}
9 | plug :protect_from_forgery
10 | plug :put_secure_browser_headers
11 | end
12 |
13 | # pipeline :api do
14 | # plug :accepts, ["json"]
15 | # end
16 |
17 | scope "/", LiveReactExamplesWeb do
18 | pipe_through :browser
19 |
20 | get "/", PageController, :home
21 | get "/lazy", PageController, :lazy
22 | get "/simple", PageController, :simple
23 | get "/simple-props", PageController, :simple_props
24 | get "/typescript", PageController, :typescript
25 |
26 | live "/live-counter", LiveCounter
27 | live "/context", LiveContext
28 | live "/log-list", LiveLogList
29 | live "/flash-sonner", LiveFlashSonner
30 | live "/ssr", LiveSSR
31 | live "/hybrid-form", LiveHybridForm
32 | live "/slot", LiveSlot
33 | end
34 |
35 | # Other scopes may use custom stacks.
36 | # scope "/api", LiveReactExamplesWeb do
37 | # pipe_through :api
38 | # end
39 | end
40 |
--------------------------------------------------------------------------------
/live_react_examples/lib/live_react_examples_web/telemetry.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.Telemetry do
2 | use Supervisor
3 | import Telemetry.Metrics
4 |
5 | def start_link(arg) do
6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
7 | end
8 |
9 | @impl true
10 | def init(_arg) do
11 | children = [
12 | # Telemetry poller will execute the given period measurements
13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
15 | # Add reporters as children of your supervision tree.
16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
17 | ]
18 |
19 | Supervisor.init(children, strategy: :one_for_one)
20 | end
21 |
22 | def metrics do
23 | [
24 | # Phoenix Metrics
25 | summary("phoenix.endpoint.start.system_time",
26 | unit: {:native, :millisecond}
27 | ),
28 | summary("phoenix.endpoint.stop.duration",
29 | unit: {:native, :millisecond}
30 | ),
31 | summary("phoenix.router_dispatch.start.system_time",
32 | tags: [:route],
33 | unit: {:native, :millisecond}
34 | ),
35 | summary("phoenix.router_dispatch.exception.duration",
36 | tags: [:route],
37 | unit: {:native, :millisecond}
38 | ),
39 | summary("phoenix.router_dispatch.stop.duration",
40 | tags: [:route],
41 | unit: {:native, :millisecond}
42 | ),
43 | summary("phoenix.socket_connected.duration",
44 | unit: {:native, :millisecond}
45 | ),
46 | summary("phoenix.channel_joined.duration",
47 | unit: {:native, :millisecond}
48 | ),
49 | summary("phoenix.channel_handled_in.duration",
50 | tags: [:event],
51 | unit: {:native, :millisecond}
52 | ),
53 |
54 | # VM Metrics
55 | summary("vm.memory.total", unit: {:byte, :kilobyte}),
56 | summary("vm.total_run_queue_lengths.total"),
57 | summary("vm.total_run_queue_lengths.cpu"),
58 | summary("vm.total_run_queue_lengths.io")
59 | ]
60 | end
61 |
62 | defp periodic_measurements do
63 | [
64 | # A module, function and arguments to be invoked periodically.
65 | # This function must call :telemetry.execute/3 and a metric must be added above.
66 | # {LiveReactExamplesWeb, :count_users, []}
67 | ]
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/live_react_examples/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamples.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :live_react_examples,
7 | version: "0.1.0",
8 | elixir: "~> 1.14",
9 | elixirc_paths: elixirc_paths(Mix.env()),
10 | start_permanent: Mix.env() == :prod,
11 | aliases: aliases(),
12 | deps: deps()
13 | ]
14 | end
15 |
16 | # Configuration for the OTP application.
17 | #
18 | # Type `mix help compile.app` for more information.
19 | def application do
20 | [
21 | mod: {LiveReactExamples.Application, []},
22 | extra_applications: [:logger, :runtime_tools]
23 | ]
24 | end
25 |
26 | # Specifies which paths to compile per environment.
27 | defp elixirc_paths(:test), do: ["lib", "test/support"]
28 | defp elixirc_paths(_), do: ["lib"]
29 |
30 | # Specifies your project dependencies.
31 | #
32 | # Type `mix help deps` for examples and options.
33 | defp deps do
34 | [
35 | {:phoenix, "~> 1.7.14"},
36 | {:phoenix_html, "~> 4.1"},
37 | {:phoenix_live_reload, "~> 1.2", only: :dev},
38 | {:nodejs, "~> 3.1"},
39 | {:phoenix_live_view, "~> 1.0.0"},
40 | {:floki, ">= 0.30.0", only: :test},
41 | {:heroicons,
42 | github: "tailwindlabs/heroicons",
43 | tag: "v2.1.1",
44 | sparse: "optimized",
45 | app: false,
46 | compile: false,
47 | depth: 1},
48 | {:telemetry_metrics, "~> 1.0"},
49 | {:telemetry_poller, "~> 1.0"},
50 | {:jason, "~> 1.2"},
51 | {:dns_cluster, "~> 0.1.1"},
52 | {:bandit, "~> 1.5"},
53 | # For development
54 | {:live_react, path: ".."}
55 | # For deployment
56 | # {:live_react, "~> 1.0.0"}
57 | ]
58 | end
59 |
60 | # Aliases are shortcuts or tasks specific to the current project.
61 | # For example, to install project dependencies and perform other setup tasks, run:
62 | #
63 | # $ mix setup
64 | #
65 | # See the documentation for `Mix` for more info on aliases.
66 | defp aliases do
67 | [
68 | setup: ["deps.get", "assets.setup", "assets.build"],
69 | "assets.setup": ["cmd --cd assets npm install"],
70 | "assets.build": [
71 | "cmd --cd assets npm run build",
72 | "cmd --cd assets npm run build-server"
73 | ],
74 | "assets.deploy": [
75 | "cmd --cd assets npm run build",
76 | "cmd --cd assets npm run build-server",
77 | "phx.digest"
78 | ]
79 | ]
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/live_react_examples/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bandit": {:hex, :bandit, "1.6.11", "2fbadd60c95310eefb4ba7f1e58810aa8956e18c664a3b2029d57edb7d28d410", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "543f3f06b4721619a1220bed743aa77bf7ecc9c093ba9fab9229ff6b99eacc65"},
3 | "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
4 | "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
5 | "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
6 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
7 | "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"},
8 | "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
9 | "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
10 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
11 | "live_react": {:hex, :live_react, "1.0.0-rc.2", "487bde279fc1cf7f6bbd6a0ee7d20e9482c03f82ffc81ae7fdeb6b87fba4912a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:nodejs, "~> 3.1", [hex: :nodejs, repo: "hexpm", optional: true]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 3.3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c5a9138fdb62342804d2b76b4e2a70d0ba17e9b488aac899fb0fe7e72f164740"},
12 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
13 | "nodejs": {:hex, :nodejs, "3.1.3", "8693fae9fbefa14fb99329292c226df4d4711acfa5a3fa4182dd8d3f779b30bf", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1", [hex: :poolboy, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.7", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e7751aad77ac55f8e6c5c07617378afd88d2e0c349d9db2ebb5273aae46ef6a9"},
14 | "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"},
15 | "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
16 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"},
17 | "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.10", "d3d54f751ca538b17313541cabb1ab090a0d26e08ba914b49b6648022fa476f4", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "13f833a39b1368117e0529c0fe5029930a9bf11e2fb805c2263fcc32950f07a2"},
18 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
19 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
20 | "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
21 | "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
22 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
23 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
24 | "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"},
25 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
26 | "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
27 | "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"},
28 | "thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"},
29 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
30 | "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
31 | }
32 |
--------------------------------------------------------------------------------
/live_react_examples/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdotb/live_react/75c1c71b1a535bc5baceeeebf38e01e18cbd9c26/live_react_examples/priv/static/favicon.ico
--------------------------------------------------------------------------------
/live_react_examples/priv/static/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/live_react_examples/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/live_react_examples/rel/env.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # configure node for distributed erlang with IPV6 support
4 | export ERL_AFLAGS="-proto_dist inet6_tcp"
5 | export ECTO_IPV6="true"
6 | export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal"
7 | export RELEASE_DISTRIBUTION="name"
8 | export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"
9 |
10 | # Uncomment to send crash dumps to stderr
11 | # This can be useful for debugging, but may log sensitive information
12 | # export ERL_CRASH_DUMP=/dev/stderr
13 | # export ERL_CRASH_DUMP_BYTES=4096
14 |
--------------------------------------------------------------------------------
/live_react_examples/rel/overlays/bin/server:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | cd -P -- "$(dirname -- "$0")"
5 | PHX_SERVER=true exec ./live_react_examples start
6 |
--------------------------------------------------------------------------------
/live_react_examples/rel/overlays/bin/server.bat:
--------------------------------------------------------------------------------
1 | set PHX_SERVER=true
2 | call "%~dp0\live_react_examples" start
3 |
--------------------------------------------------------------------------------
/live_react_examples/test/live_react_examples_web/controllers/error_html_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.ErrorHTMLTest do
2 | use LiveReactExamplesWeb.ConnCase, async: true
3 |
4 | # Bring render_to_string/4 for testing custom views
5 | import Phoenix.Template
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(LiveReactExamplesWeb.ErrorHTML, "404", "html", []) == "Not Found"
9 | end
10 |
11 | test "renders 500.html" do
12 | assert render_to_string(LiveReactExamplesWeb.ErrorHTML, "500", "html", []) ==
13 | "Internal Server Error"
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/live_react_examples/test/live_react_examples_web/controllers/error_json_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.ErrorJSONTest do
2 | use LiveReactExamplesWeb.ConnCase, async: true
3 |
4 | test "renders 404" do
5 | assert LiveReactExamplesWeb.ErrorJSON.render("404.json", %{}) == %{
6 | errors: %{detail: "Not Found"}
7 | }
8 | end
9 |
10 | test "renders 500" do
11 | assert LiveReactExamplesWeb.ErrorJSON.render("500.json", %{}) ==
12 | %{errors: %{detail: "Internal Server Error"}}
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/live_react_examples/test/live_react_examples_web/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.PageControllerTest do
2 | use LiveReactExamplesWeb.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get(conn, ~p"/")
6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/live_react_examples/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveReactExamplesWeb.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common data structures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | we enable the SQL sandbox, so changes done to the database
12 | are reverted at the end of every test. If you are using
13 | PostgreSQL, you can even run database tests asynchronously
14 | by setting `use LiveReactExamplesWeb.ConnCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 |
20 | using do
21 | quote do
22 | # The default endpoint for testing
23 | @endpoint LiveReactExamplesWeb.Endpoint
24 |
25 | use LiveReactExamplesWeb, :verified_routes
26 |
27 | # Import conveniences for testing with connections
28 | import Plug.Conn
29 | import Phoenix.ConnTest
30 | import LiveReactExamplesWeb.ConnCase
31 | end
32 | end
33 |
34 | setup _tags do
35 | {:ok, conn: Phoenix.ConnTest.build_conn()}
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/live_react_examples/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveReact.MixProject do
2 | use Mix.Project
3 |
4 | @source_url "https://github.com/mrdotb/live_react"
5 | @version "1.0.1"
6 |
7 | def project do
8 | [
9 | app: :live_react,
10 | version: @version,
11 | elixir: "~> 1.16",
12 | start_permanent: Mix.env() == :prod,
13 | deps: deps(),
14 | description: "E2E reactivity for React and LiveView",
15 | package: package(),
16 | docs: docs(),
17 | source_url: @source_url
18 | ]
19 | end
20 |
21 | # Run "mix help compile.app" to learn about applications.
22 | def application do
23 | conditionals =
24 | case Application.get_env(:live_react, :ssr_module) do
25 | # Needed to use :httpc.request
26 | LiveReact.SSR.ViteJS -> [:inets]
27 | _ -> []
28 | end
29 |
30 | [
31 | extra_applications: [:logger] ++ conditionals
32 | ]
33 | end
34 |
35 | # Run "mix help deps" to learn about dependencies.
36 | defp deps do
37 | [
38 | {:jason, "~> 1.2"},
39 | {:nodejs, "~> 3.1", optional: true},
40 | {:floki, ">= 0.30.0", optional: true},
41 | {:phoenix, ">= 1.7.0"},
42 | {:phoenix_html, ">= 3.3.1"},
43 | {:phoenix_live_view, ">= 0.18.0"},
44 | {:telemetry, "~> 0.4 or ~> 1.0"},
45 | {:credo, "~> 1.7", only: [:dev, :test]},
46 | {:ex_doc, "~> 0.19", only: :dev, runtime: false},
47 | {:git_ops, "~> 2.7.2", only: [:dev]}
48 | ]
49 | end
50 |
51 | defp package do
52 | [
53 | maintainers: ["Baptiste Chaleil"],
54 | licenses: ["MIT"],
55 | links: %{
56 | Github: "https://github.com/mrdotb/live_react"
57 | },
58 | files:
59 | ~w(assets/copy assets/js lib)s ++
60 | ~w(CHANGELOG.md LICENSE.md mix.exs package.json README.md .formatter.exs)s
61 | ]
62 | end
63 |
64 | defp docs do
65 | [
66 | name: "LiveReact",
67 | source_ref: "v#{@version}",
68 | source_url: "https://github.com/mrdotb/live_react",
69 | homepage_url: "https://github.com/mrdotb/live_react",
70 | main: "readme",
71 | extras: [
72 | "README.md",
73 | "guides/installation.md",
74 | "guides/deployment.md",
75 | "guides/development.md",
76 | "guides/ssr.md",
77 | "CHANGELOG.md"
78 | ]
79 | ]
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
3 | "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
4 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
5 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"},
6 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
7 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
8 | "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"},
9 | "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
10 | "git_ops": {:hex, :git_ops, "2.7.2", "2d3c164a8bcaf13f129ab339e8e9f0a99c80ffa8f85dd0b344d7515275236dbc", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.27 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "1dcd68b3f5bcd0999d69274cd21e74e652a90452e683b54d490fa5b26152945f"},
11 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
12 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
13 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
14 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
15 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
16 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
17 | "nodejs": {:hex, :nodejs, "3.1.3", "8693fae9fbefa14fb99329292c226df4d4711acfa5a3fa4182dd8d3f779b30bf", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1", [hex: :poolboy, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.7", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e7751aad77ac55f8e6c5c07617378afd88d2e0c349d9db2ebb5273aae46ef6a9"},
18 | "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"},
19 | "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
20 | "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.10", "d3d54f751ca538b17313541cabb1ab090a0d26e08ba914b49b6648022fa476f4", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "13f833a39b1368117e0529c0fe5029930a9bf11e2fb805c2263fcc32950f07a2"},
21 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
22 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
23 | "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
24 | "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
25 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
26 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
27 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
28 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
29 | "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
30 | }
31 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "live_react",
3 | "version": "0.1.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "live_react",
9 | "version": "0.1.0",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "prettier": "^3.3.2"
13 | }
14 | },
15 | "node_modules/prettier": {
16 | "version": "3.4.2",
17 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
18 | "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
19 | "dev": true,
20 | "bin": {
21 | "prettier": "bin/prettier.cjs"
22 | },
23 | "engines": {
24 | "node": ">=14"
25 | },
26 | "funding": {
27 | "url": "https://github.com/prettier/prettier?sponsor=1"
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "live_react",
3 | "version": "0.1.0",
4 | "description": "E2E reactivity from React and LiveView",
5 | "license": "MIT",
6 | "module": "./assets/js/live_react/index.js",
7 | "exports": {
8 | ".": {
9 | "import": "./assets/js/live_react/index.mjs",
10 | "types": "./assets/js/live_react/index.d.mts"
11 | },
12 | "./server": "./assets/js/live_react/server.mjs",
13 | "./vite-plugin": "./assets/js/live_react/vite-plugin.js"
14 | },
15 | "author": "Baptiste Chaleil ",
16 | "repository": {
17 | "type": "git",
18 | "url": "git://github.com/mrdotb/live_react.git"
19 | },
20 | "files": [
21 | "README.MD",
22 | "LICENSE.md",
23 | "package.json",
24 | "assets/js/live_react/*"
25 | ],
26 | "devDependencies": {
27 | "prettier": "^3.3.2"
28 | },
29 | "scripts": {
30 | "format": "npx prettier --write ."
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/test/live_react_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveReactTest do
2 | use ExUnit.Case
3 |
4 | import LiveReact
5 | import Phoenix.Component
6 | import Phoenix.LiveViewTest
7 |
8 | alias LiveReact.Test
9 |
10 | doctest LiveReact
11 |
12 | describe "basic component rendering" do
13 | def simple_component(assigns) do
14 | ~H"""
15 | <.react name="MyComponent" firstName="john" lastName="doe" />
16 | """
17 | end
18 |
19 | test "renders component with correct props" do
20 | html = render_component(&simple_component/1)
21 | react = Test.get_react(html)
22 |
23 | assert react.component == "MyComponent"
24 | assert react.props == %{"firstName" => "john", "lastName" => "doe"}
25 | end
26 |
27 | test "generates consistent ID" do
28 | html = render_component(&simple_component/1)
29 | react = Test.get_react(html)
30 |
31 | assert react.id =~ ~r/MyComponent-\d+/
32 | end
33 | end
34 |
35 | describe "multiple components" do
36 | def multi_component(assigns) do
37 | ~H"""
38 |
39 | <.react id="profile-1" firstName="John" name="UserProfile" />
40 | <.react id="card-1" firstName="Jane" name="UserCard" />
41 |
42 | """
43 | end
44 |
45 | test "finds first component by default" do
46 | html = render_component(&multi_component/1)
47 | react = Test.get_react(html)
48 |
49 | assert react.component == "UserProfile"
50 | assert react.props == %{"firstName" => "John"}
51 | end
52 |
53 | test "finds specific component by name" do
54 | html = render_component(&multi_component/1)
55 | react = Test.get_react(html, name: "UserCard")
56 |
57 | assert react.component == "UserCard"
58 | assert react.props == %{"firstName" => "Jane"}
59 | end
60 |
61 | test "finds specific component by id" do
62 | html = render_component(&multi_component/1)
63 | react = Test.get_react(html, id: "card-1")
64 |
65 | assert react.component == "UserCard"
66 | assert react.id == "card-1"
67 | end
68 |
69 | test "raises error when component with name not found" do
70 | html = render_component(&multi_component/1)
71 |
72 | assert_raise RuntimeError,
73 | ~r/No React component found with name="Unknown".*Available components: UserProfile#profile-1, UserCard#card-1/,
74 | fn ->
75 | Test.get_react(html, name: "Unknown")
76 | end
77 | end
78 |
79 | test "raises error when component with id not found" do
80 | html = render_component(&multi_component/1)
81 |
82 | assert_raise RuntimeError,
83 | ~r/No React component found with id="unknown-id".*Available components: UserProfile#profile-1, UserCard#card-1/,
84 | fn ->
85 | Test.get_react(html, id: "unknown-id")
86 | end
87 | end
88 | end
89 |
90 | describe "styling" do
91 | def styled_component(assigns) do
92 | ~H"""
93 | <.react name="MyComponent" class="bg-blue-500 rounded-sm" />
94 | """
95 | end
96 |
97 | test "applies CSS classes" do
98 | html = render_component(&styled_component/1)
99 | react = Test.get_react(html)
100 |
101 | assert react.class == "bg-blue-500 rounded-sm"
102 | end
103 | end
104 |
105 | describe "SSR behavior" do
106 | def ssr_component(assigns) do
107 | ~H"""
108 | <.react name="MyComponent" ssr={false} />
109 | """
110 | end
111 |
112 | test "respects SSR flag" do
113 | html = render_component(&ssr_component/1)
114 | react = Test.get_react(html)
115 |
116 | assert react.ssr == false
117 | end
118 | end
119 |
120 | describe "slots" do
121 | def component_with_named_slot(assigns) do
122 | ~H"""
123 | <.react name="WithSlots">
124 | <:hello>Simple content
125 |
126 | """
127 | end
128 |
129 | def component_with_inner_block(assigns) do
130 | ~H"""
131 | <.react name="WithSlots">
132 | Simple content
133 |
134 | """
135 | end
136 |
137 | test "warns about usage of named slot" do
138 | assert_raise RuntimeError,
139 | "Unsupported slot: hello, only one default slot is supported, passed as React children.",
140 | fn -> render_component(&component_with_named_slot/1) end
141 | end
142 |
143 | test "renders default slot with inner_block" do
144 | html = render_component(&component_with_inner_block/1)
145 | react = Test.get_react(html)
146 |
147 | assert react.slots == %{"default" => "Simple content"}
148 | end
149 |
150 | test "encodes slot as base64" do
151 | html = render_component(&component_with_inner_block/1)
152 |
153 | # Get raw data-slots attribute to verify base64 encoding
154 | doc = Floki.parse_fragment!(html)
155 | slots_attr = Floki.attribute(doc, "data-slots")
156 |
157 | slots =
158 | slots_attr
159 | |> Jason.decode!()
160 | |> Enum.map(fn {key, value} -> {key, Base.decode64!(value)} end)
161 | |> Enum.into(%{})
162 |
163 | assert slots == %{"default" => "Simple content"}
164 | end
165 |
166 | test "handles empty slots" do
167 | html =
168 | render_component(fn assigns ->
169 | ~H"""
170 | <.react name="WithSlots" />
171 | """
172 | end)
173 |
174 | react = Test.get_react(html)
175 |
176 | assert react.slots == %{}
177 | end
178 | end
179 | end
180 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------