diff --git a/lib/mix/tasks/phx.gen.live.slime.ex b/lib/mix/tasks/phx.gen.live.slime.ex new file mode 100644 index 0000000..79e3412 --- /dev/null +++ b/lib/mix/tasks/phx.gen.live.slime.ex @@ -0,0 +1,249 @@ +defmodule Mix.Tasks.Phx.Gen.Live.Slime do + @shortdoc "Generates LiveView, templates, and context for a resource" + + @moduledoc """ + Generates LiveView, templates, and context for a resource. + + mix phx.gen.live Accounts User users name:string age:integer + + The first argument is the context module followed by the schema module + and its plural name (used as the schema table name). + + The context is an Elixir module that serves as an API boundary for + the given resource. A context often holds many related resources. + Therefore, if the context already exists, it will be augmented with + functions for the given resource. + + When this command is run for the first time, a `ModalComponent` and + `LiveHelpers` module will be created, along with the resource level + LiveViews and components, including an `IndexLive`, `ShowLive`, `FormComponent` + for the new resource. + + > Note: A resource may also be split + > over distinct contexts (such as `Accounts.User` and `Payments.User`). + + The schema is responsible for mapping the database fields into an + Elixir struct. It is followed by an optional list of attributes, + with their respective names and types. See `mix phx.gen.schema` + for more information on attributes. + + Overall, this generator will add the following files to `lib/`: + + * a context module in `lib/app/accounts.ex` for the accounts API + * a schema in `lib/app/accounts/user.ex`, with an `users` table + * a view in `lib/app_web/views/user_view.ex` + * a LiveView in `lib/app_web/live/user_live/show_live.ex` + * a LiveView in `lib/app_web/live/user_live/index_live.ex` + * a LiveComponent in `lib/app_web/live/user_live/form_component.ex` + * a LiveComponent in `lib/app_web/live/modal_component.ex` + * a helpers modules in `lib/app_web/live/live_helpers.ex` + + ## The context app + + A migration file for the repository and test files for the context and + controller features will also be generated. + + The location of the web files (LiveView's, views, templates, etc) in an + umbrella application will vary based on the `:context_app` config located + in your applications `:generators` configuration. When set, the Phoenix + generators will generate web files directly in your lib and test folders + since the application is assumed to be isolated to web specific functionality. + If `:context_app` is not set, the generators will place web related lib + and test files in a `web/` directory since the application is assumed + to be handling both web and domain specific functionality. + Example configuration: + + config :my_app_web, :generators, context_app: :my_app + + Alternatively, the `--context-app` option may be supplied to the generator: + + mix phx.gen.live Sales User users --context-app warehouse + + ## Web namespace + + By default, the controller and view will be namespaced by the schema name. + You can customize the web module namespace by passing the `--web` flag with a + module name, for example: + + mix phx.gen.live Sales User users --web Sales + + Which would generate a LiveViews inside `lib/app_web/live/sales/user_live/` and a + view at `lib/app_web/views/sales/user_view.ex`. + + ## Customising the context, schema, tables and migrations + + In some cases, you may wish to bootstrap HTML templates, LiveViews, + and tests, but leave internal implementation of the context or schema + to yourself. You can use the `--no-context` and `--no-schema` flags + for file generation control. + + You can also change the table name or configure the migrations to + use binary ids for primary keys, see `mix phx.gen.schema` for more + information. + """ + use Mix.Task + + alias Mix.Phoenix.{Context,Schema} + alias Mix.Tasks.Phx.Gen + + import Mix.Tasks.Phx.Gen.Live, only: [print_shell_instructions: 1] + + @doc false + def run(args) do + if Mix.Project.umbrella?() do + Mix.raise "mix phx.gen.live must be invoked from within your *_web application root directory" + end + + {context, schema} = Gen.Context.build(args) + Gen.Context.prompt_for_code_injection(context) + + binding = [context: context, schema: schema, inputs: inputs(schema)] + paths = [".", :phoenix_slime, :phoenix] + + prompt_for_conflicts(context) + + context + |> copy_new_files(binding, paths) + |> maybe_inject_helpers() + |> print_shell_instructions() + end + + defp prompt_for_conflicts(context) do + context + |> files_to_be_generated() + |> Kernel.++(context_files(context)) + |> Mix.Phoenix.prompt_for_conflicts() + end + defp context_files(%Context{generate?: true} = context) do + Gen.Context.files_to_be_generated(context) + end + defp context_files(%Context{generate?: false}) do + [] + end + + defp files_to_be_generated(%Context{schema: schema, context_app: context_app}) do + web_prefix = Mix.Phoenix.web_path(context_app) + test_prefix = Mix.Phoenix.web_test_path(context_app) + web_path = to_string(schema.web_path) + live_subdir = "#{schema.singular}_live" + + [ + {:eex, "show.ex", Path.join([web_prefix, "live", web_path, live_subdir, "show.ex"])}, + {:eex, "index.ex", Path.join([web_prefix, "live", web_path, live_subdir, "index.ex"])}, + {:eex, "form_component.ex", Path.join([web_prefix, "live", web_path, live_subdir, "form_component.ex"])}, + {:eex, "form_component.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "form_component.html.slimleex"])}, + {:eex, "index.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "index.html.slimleex"])}, + {:eex, "show.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "show.html.slimleex"])}, + {:eex, "live_test.exs", Path.join([test_prefix, "live", web_path, "#{schema.singular}_live_test.exs"])}, + {:new_eex, "modal_component.ex", Path.join([web_prefix, "live", "modal_component.ex"])}, + {:new_eex, "live_helpers.ex", Path.join([web_prefix, "live", "live_helpers.ex"])}, + ] + end + + defp copy_new_files(%Context{} = context, binding, paths) do + files = files_to_be_generated(context) + Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.live.slime", binding, files) + if context.generate?, do: Gen.Context.copy_new_files(context, paths, binding) + + context + end + + defp maybe_inject_helpers(%Context{context_app: ctx_app} = context) do + web_prefix = Mix.Phoenix.web_path(ctx_app) + [lib_prefix, web_dir] = Path.split(web_prefix) + file_path = Path.join(lib_prefix, "#{web_dir}.ex") + file = File.read!(file_path) + inject = "import #{inspect(context.web_module)}.LiveHelpers" + + if String.contains?(file, inject) do + :ok + else + do_inject_helpers(context, file, file_path, inject) + end + + context + end + + defp do_inject_helpers(context, file, file_path, inject) do + Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path)]) + + new_file = String.replace(file, "import Phoenix.LiveView.Helpers", "import Phoenix.LiveView.Helpers\n #{inject}") + if file != new_file do + File.write!(file_path, new_file) + else + Mix.shell().info """ + + Could not find Phoenix.LiveView.Helpers imported in #{file_path}. + + This typically happens because your application was not generated + with the --live flag: + + mix phx.new my_app --live + + Please make sure LiveView is installed and that #{inspect(context.web_module)} + defines both `live_view/0` and `live_component/0` functions, + and that both functions import #{inspect(context.web_module)}.LiveHelpers. + """ + end + end + + defp live_route_instructions(schema) do + [ + ~s|live "/#{schema.plural}", #{inspect(schema.alias)}Live.Index, :index\n|, + ~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Index, :new\n|, + ~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Index, :edit\n\n|, + ~s|live "/#{schema.plural}/:id", #{inspect(schema.alias)}Live.Show, :show\n|, + ~s|live "/#{schema.plural}/:id/show/edit", #{inspect(schema.alias)}Live.Show, :edit| + ] + end + + defp inputs(%Schema{attrs: attrs}) do + Enum.map(attrs, fn + {_, {:array, _}} -> + {nil, nil, nil} + + {_, {:references, _}} -> + {nil, nil, nil} + + {key, :integer} -> + {label(key), ~s(= number_input f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :float} -> + {label(key), ~s(= number_input f, #{inspect(key)}, step: "any", class: "form-control", autocomplete: "off"), + error(key)} + + {key, :decimal} -> + {label(key), ~s(= number_input f, #{inspect(key)}, step: "any", class: "form-control", autocomplete: "off"), + error(key)} + + {key, :boolean} -> + {label(key), ~s(= checkbox f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :text} -> + {label(key), ~s(= textarea f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :date} -> + {label(key), ~s(= date_select f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :time} -> + {label(key), ~s(= time_select f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :utc_datetime} -> + {label(key), ~s(= datetime_select f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :naive_datetime} -> + {label(key), ~s(= datetime_select f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, _} -> + {label(key), ~s(= text_input f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + end) + end + + defp label(key) do + ~s(= label f, #{inspect(key)}, class: "control-label") + end + + defp error(field) do + ~s(= error_tag f, #{inspect(field)}) + end +end diff --git a/mix.exs b/mix.exs index 7de4205..a88dc42 100644 --- a/mix.exs +++ b/mix.exs @@ -21,7 +21,7 @@ defmodule PhoenixSlime.Mixfile do def deps do [ - {:phoenix, "~> 1.4"}, + {:phoenix, "~> 1.5"}, {:phoenix_html, "~> 2.13"}, {:jason, "~> 1.0", optional: true}, {:slime, "~> 1.0"}, diff --git a/mix.lock b/mix.lock index 429216b..98f1b80 100644 --- a/mix.lock +++ b/mix.lock @@ -6,17 +6,17 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f1155337ae17ff7a1255217b4c1ceefcd1860b7ceb1a1874031e7a861b052e39"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, - "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, + "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "neotoma": {:hex, :neotoma, "1.7.3", "d8bd5404b73273989946e4f4f6d529e5c2088f5fa1ca790b4dbe81f4be408e61", [:rebar], [], "hexpm", "2da322b9b1567ffa0706a7f30f6bbbde70835ae44a1050615f4b4a3d436e0f28"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm", "51aa192e0941313c394956718bdb1e59325874f88f45871cff90345b97f60bba"}, - "phoenix": {:hex, :phoenix, "1.4.11", "d112c862f6959f98e6e915c3b76c7a87ca3efd075850c8daa7c3c7a609014b0d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef19d737ca23b66f7333eaa873cbfc5e6fa6427ef5a0ffd358de1ba8e1a4b2f4"}, + "phoenix": {:hex, :phoenix, "1.5.9", "a6368d36cfd59d917b37c44386e01315bc89f7609a10a45a22f47c007edf2597", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7e4bce20a67c012f1fbb0af90e5da49fa7bf0d34e3a067795703b74aef75427d"}, "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8b01b3d6d39731ab18aa548d928b5796166d2500755f553725cfe967bafba7d9"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"}, - "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "164baaeb382d19beee0ec484492aa82a9c8685770aee33b24ec727a0971b34d0"}, - "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, + "plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, "slime": {:hex, :slime, "1.2.1", "71e036056051f0a6fae136af34eaa1322e8e11cdd2da3a56196fd31bca34dd49", [:mix], [{:neotoma, "~> 1.7", [hex: :neotoma, repo: "hexpm", optional: false]}], "hexpm", "298568e64291fed4eb690be094f6c46400daa03b594bab34fcaa0167e139c263"}, - "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, + "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, } diff --git a/priv/templates/phx.gen.live.slime/form_component.ex b/priv/templates/phx.gen.live.slime/form_component.ex new file mode 100644 index 0000000..6957239 --- /dev/null +++ b/priv/templates/phx.gen.live.slime/form_component.ex @@ -0,0 +1,55 @@ +defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent do + use <%= inspect context.web_module %>, :live_component + + alias <%= inspect context.module %> + + @impl true + def update(%{<%= schema.singular %>: <%= schema.singular %>} = assigns, socket) do + changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>) + + {:ok, + socket + |> assign(assigns) + |> assign(:changeset, changeset)} + end + + @impl true + def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do + changeset = + socket.assigns.<%= schema.singular %> + |> <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>_params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("save", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do + save_<%= schema.singular %>(socket, socket.assigns.action, <%= schema.singular %>_params) + end + + defp save_<%= schema.singular %>(socket, :edit, <%= schema.singular %>_params) do + case <%= inspect context.alias %>.update_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params) do + {:ok, _<%= schema.singular %>} -> + {:noreply, + socket + |> put_flash(:info, "<%= schema.human_singular %> updated successfully") + |> push_redirect(to: socket.assigns.return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp save_<%= schema.singular %>(socket, :new, <%= schema.singular %>_params) do + case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do + {:ok, _<%= schema.singular %>} -> + {:noreply, + socket + |> put_flash(:info, "<%= schema.human_singular %> created successfully") + |> push_redirect(to: socket.assigns.return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end + end +end diff --git a/priv/templates/phx.gen.live.slime/form_component.html.leex b/priv/templates/phx.gen.live.slime/form_component.html.leex new file mode 100644 index 0000000..76a69a0 --- /dev/null +++ b/priv/templates/phx.gen.live.slime/form_component.html.leex @@ -0,0 +1,15 @@ +h2 = @title + += f = form_for @changeset, "#", + id: "<%= schema.singular %>-form", + phx_target: @myself, + phx_change: "validate", + phx_submit: "save" + + <%= for {label, input, error} <- inputs, input do %> + <%= label %> + <%= input %> + <%= error %> + <% end %> + + = submit "Save", phx_disable_with: "Saving..." diff --git a/priv/templates/phx.gen.live.slime/index.ex b/priv/templates/phx.gen.live.slime/index.ex new file mode 100644 index 0000000..f46c53b --- /dev/null +++ b/priv/templates/phx.gen.live.slime/index.ex @@ -0,0 +1,46 @@ +defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Index do + use <%= inspect context.web_module %>, :live_view + + alias <%= inspect context.module %> + alias <%= inspect schema.module %> + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :<%= schema.collection %>, list_<%= schema.plural %>())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit <%= schema.human_singular %>") + |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New <%= schema.human_singular %>") + |> assign(:<%= schema.singular %>, %<%= inspect schema.alias %>{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing <%= schema.human_plural %>") + |> assign(:<%= schema.singular %>, nil) + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id) + {:ok, _} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>) + + {:noreply, assign(socket, :<%= schema.collection %>, list_<%=schema.plural %>())} + end + + defp list_<%= schema.plural %> do + <%= inspect context.alias %>.list_<%= schema.plural %>() + end +end diff --git a/priv/templates/phx.gen.live.slime/index.html.leex b/priv/templates/phx.gen.live.slime/index.html.leex new file mode 100644 index 0000000..ee30474 --- /dev/null +++ b/priv/templates/phx.gen.live.slime/index.html.leex @@ -0,0 +1,30 @@ +h1 Listing <%= schema.human_plural %> + += if @live_action in [:new, :edit] do + = live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent, + id: @<%= schema.singular %>.id || :new, + title: @page_title, + action: @live_action, + <%= schema.singular %>: @<%= schema.singular %>, + return_to: Routes.<%= schema.route_helper %>_index_path(@socket, :index) + +table + thead + tr + <%= for {k, _} <- schema.attrs do %> + th <%= Phoenix.Naming.humanize(Atom.to_string(k)) %> + <% end %> + th + tbody id="<%= schema.plural %>" + = for <%= schema.singular %> <- @<%= schema.collection %> do + tr id="<%= schema.singular %>-#{<%= schema.singular %>.id}" + <%= for {k, _} <- schema.attrs do %> + td = <%= schema.singular %>.<%= k %> + <% end %> + td + span = live_redirect "Show", to: Routes.<%= schema.route_helper %>_show_path(@socket, :show, <%= schema.singular %>) + span = live_patch "Edit", to: Routes.<%= schema.route_helper %>_index_path(@socket, :edit, <%= schema.singular %>) + span = link "Delete", to: "#", phx_click: "delete", phx_value_id: <%= schema.singular %>.id, data: [confirm: "Are you sure?"] + + +span = live_patch "New <%= schema.human_singular %>", to: Routes.<%= schema.route_helper %>_index_path(@socket, :new) diff --git a/priv/templates/phx.gen.live.slime/live_helpers.ex b/priv/templates/phx.gen.live.slime/live_helpers.ex new file mode 100644 index 0000000..91f0a5d --- /dev/null +++ b/priv/templates/phx.gen.live.slime/live_helpers.ex @@ -0,0 +1,23 @@ +defmodule <%= inspect context.web_module %>.LiveHelpers do + import Phoenix.LiveView.Helpers + + @doc """ + Renders a component inside the `<%= inspect context.web_module %>.ModalComponent` component. + + The rendered modal receives a `:return_to` option to properly update + the URL when the modal is closed. + + ## Examples + + <%%= live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent, + id: @<%= schema.singular %>.id || :new, + action: @live_action, + <%= schema.singular %>: @<%= schema.singular %>, + return_to: Routes.<%= schema.singular %>_index_path(@socket, :index) %> + """ + def live_modal(socket, component, opts) do + path = Keyword.fetch!(opts, :return_to) + modal_opts = [id: :modal, return_to: path, component: component, opts: opts] + live_component(socket, <%= inspect context.web_module %>.ModalComponent, modal_opts) + end +end diff --git a/priv/templates/phx.gen.live.slime/live_test.exs b/priv/templates/phx.gen.live.slime/live_test.exs new file mode 100644 index 0000000..9fccb0c --- /dev/null +++ b/priv/templates/phx.gen.live.slime/live_test.exs @@ -0,0 +1,116 @@ +defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>LiveTest do + use <%= inspect context.web_module %>.ConnCase + + import Phoenix.LiveViewTest + + alias <%= inspect context.module %> + + @create_attrs <%= inspect schema.params.create %> + @update_attrs <%= inspect schema.params.update %> + @invalid_attrs <%= inspect for {key, _} <- schema.params.create, into: %{}, do: {key, nil} %> + + defp fixture(:<%= schema.singular %>) do + {:ok, <%= schema.singular %>} = <%= inspect context.alias %>.create_<%= schema.singular %>(@create_attrs) + <%= schema.singular %> + end + + defp create_<%= schema.singular %>(_) do + <%= schema.singular %> = fixture(:<%= schema.singular %>) + %{<%= schema.singular %>: <%= schema.singular %>} + end + + describe "Index" do + setup [:create_<%= schema.singular %>] + + test "lists all <%= schema.plural %>", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do + {:ok, _index_live, html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert html =~ "Listing <%= schema.human_plural %>"<%= if schema.string_attr do %> + assert html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %> + end + + test "saves new <%= schema.singular %>", %{conn: conn} do + {:ok, index_live, _html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert index_live |> element("a", "New <%= schema.human_singular %>") |> render_click() =~ + "New <%= schema.human_singular %>" + + assert_patch(index_live, Routes.<%= schema.route_helper %>_index_path(conn, :new)) + + assert index_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + index_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @create_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert html =~ "<%= schema.human_singular %> created successfully"<%= if schema.string_attr do %> + assert html =~ "some <%= schema.string_attr %>"<% end %> + end + + test "updates <%= schema.singular %> in listing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do + {:ok, index_live, _html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert index_live |> element("#<%= schema.singular %>-#{<%= schema.singular %>.id} a", "Edit") |> render_click() =~ + "Edit <%= schema.human_singular %>" + + assert_patch(index_live, Routes.<%= schema.route_helper %>_index_path(conn, :edit, <%= schema.singular %>)) + + assert index_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + index_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert html =~ "<%= schema.human_singular %> updated successfully"<%= if schema.string_attr do %> + assert html =~ "some updated <%= schema.string_attr %>"<% end %> + end + + test "deletes <%= schema.singular %> in listing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do + {:ok, index_live, _html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert index_live |> element("#<%= schema.singular %>-#{<%= schema.singular %>.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#<%= schema.singular %>-#{<%= schema.singular %>.id}") + end + end + + describe "Show" do + setup [:create_<%= schema.singular %>] + + test "displays <%= schema.singular %>", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do + {:ok, _show_live, html} = live(conn, Routes.<%= schema.route_helper %>_show_path(conn, :show, <%= schema.singular %>)) + + assert html =~ "Show <%= schema.human_singular %>"<%= if schema.string_attr do %> + assert html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %> + end + + test "updates <%= schema.singular %> within modal", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do + {:ok, show_live, _html} = live(conn, Routes.<%= schema.route_helper %>_show_path(conn, :show, <%= schema.singular %>)) + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit <%= schema.human_singular %>" + + assert_patch(show_live, Routes.<%= schema.route_helper %>_show_path(conn, :edit, <%= schema.singular %>)) + + assert show_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + show_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.<%= schema.route_helper %>_show_path(conn, :show, <%= schema.singular %>)) + + assert html =~ "<%= schema.human_singular %> updated successfully"<%= if schema.string_attr do %> + assert html =~ "some updated <%= schema.string_attr %>"<% end %> + end + end +end diff --git a/priv/templates/phx.gen.live.slime/modal_component.ex b/priv/templates/phx.gen.live.slime/modal_component.ex new file mode 100644 index 0000000..ab4301d --- /dev/null +++ b/priv/templates/phx.gen.live.slime/modal_component.ex @@ -0,0 +1,26 @@ +defmodule <%= inspect context.web_module %>.ModalComponent do + use <%= inspect context.web_module %>, :live_component + + @impl true + def render(assigns) do + ~L""" +