` element. Add `class="text-red-500"` to it and watch it turn red in your browser.
113 |
114 | Of course you have to customize `lib/example_shop_web/templates/layout/app.html.eex` to get a nice layout. You find some good examples at https://tailwindui.com
115 |
116 | ### Add CSS for the forms
117 |
118 | We are not 100% there yet because we need some extra CSS for forms. But that is done in two steps:
119 |
120 | ```bash
121 | $ cd assets
122 | $ npm install @tailwindcss/forms
123 | $ cd ..
124 | ```
125 |
126 | Change **assets/tailwind.config.js** according to this diff.
127 |
128 | ````
129 | 16,17c16,19
130 | < plugins: [],
131 | < }
132 | ---
133 | > plugins: [
134 | > require('@tailwindcss/forms'),
135 | > ],
136 | > };
137 | ````
138 |
139 | Now you can install the generator.
140 |
141 | ## Installation of the generator
142 |
143 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed
144 | by adding `phx_tailwind_generators` to your list of dependencies in `mix.exs`:
145 |
146 | ```elixir
147 | def deps do
148 | [
149 | {:phx_tailwind_generators, "~> 0.1.6"}
150 | ]
151 | end
152 | ```
153 |
154 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
155 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
156 | be found at [https://hexdocs.pm/phx_tailwind_generators](https://hexdocs.pm/phx_tailwind_generators).
157 |
158 | ## Use of the generator
159 |
160 | The generator works like the default scaffold generator. Only the name is different:
161 |
162 | ```bash
163 | mix phx.gen.tailwind Blog Post posts title body:text
164 | ```
165 |
166 | This will create templates which use Tailwind CSS. Have fun with it.
167 |
168 | Please do submit bugs or better create pull requests!
169 |
170 | ## Bonus: Install Alpine.js
171 |
172 | Since you are now using Phoenix with Tailwind the chances are high that
173 | you want to use [Alpine.js](https://github.com/alpinejs/alpine) too. Follow me.
174 |
175 | ```bash
176 | $ cd assets
177 | $ npm install alpinejs
178 | $ cd ..
179 | ```
180 |
181 | Open **assets/js/app.js** in your editor and add these lines to the bottom:
182 |
183 | ```javascript
184 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
185 |
186 | // Alpinejs
187 | import Alpine from "alpinejs"
188 | let liveSocket = new LiveSocket("/live", Socket, {
189 | params: { _csrf_token: csrfToken },
190 | dom: {
191 | onBeforeElUpdated(from, to) {
192 | if (from.__x) { Alpine.clone(from.__x, to) }
193 | }
194 | }
195 | })
196 | ```
197 |
198 | You can test your new setup with this example code. Just place it in `lib/example_shop_web/templates/page/index.html.eex` and click on the blue button.
199 |
200 | ```html
201 |
202 |
203 |
204 |
208 | Dropdown Body
209 |
210 |
211 | ```
212 |
213 | ## Acknowledgments
214 |
215 | Very little in this repository was created by me. Most of this is copy and pasted from other resources. And the whole mix package wouldn't exist without the help of [James Edward Gray II](https://twitter.com/jeg2) who walked me through the steps of creating it.
216 |
217 | Resources I used:
218 |
219 | - [Adding Tailwind CSS to Phoenix 1.4 and 1.5](https://pragmaticstudio.com/tutorials/adding-tailwind-css-to-phoenix)
220 | - [Phoenix 1.5 with Tailwind](https://sisccr.medium.com/phoenix-1-5-with-tailwind-4030198bf7c7)
221 | - [Combine Phoenix LiveView with Alpine.js](https://fullstackphoenix.com/tutorials/combine-phoenix-liveview-with-alpine-js)
222 | - [Optimizing User Experience with LiveView](https://dockyard.com/blog/2020/12/21/optimizing-user-experience-with-liveview)
223 | - [Alpine.js](https://github.com/alpinejs/alpine)
224 |
--------------------------------------------------------------------------------
/lib/mix/tasks/phx_gen_tailwind.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.Gen.Tailwind do
2 | @shortdoc "Generates controller, views, and context for an HTML resource with Tailwind CSS"
3 |
4 | @moduledoc """
5 | Generates controller, views, and context for an HTML resource with
6 | Tailwind CSS.
7 |
8 | mix phx.gen.tailwind Accounts User users name:string age:integer
9 |
10 | The first argument is the context module followed by the schema module
11 | and its plural name (used as the schema table name).
12 |
13 | The context is an Elixir module that serves as an API boundary for
14 | the given resource. A context often holds many related resources.
15 | Therefore, if the context already exists, it will be augmented with
16 | functions for the given resource.
17 |
18 | > Note: A resource may also be split
19 | > over distinct contexts (such as `Accounts.User` and `Payments.User`).
20 |
21 | The schema is responsible for mapping the database fields into an
22 | Elixir struct. It is followed by an optional list of attributes,
23 | with their respective names and types. See `mix tailwind.gen.schema`
24 | for more information on attributes.
25 |
26 | Overall, this generator will add the following files to `lib/`:
27 |
28 | * a context module in `lib/app/accounts.ex` for the accounts API
29 | * a schema in `lib/app/accounts/user.ex`, with an `users` table
30 | * a view in `lib/app_web/views/user_view.ex`
31 | * a controller in `lib/app_web/controllers/user_controller.ex`
32 | * default CRUD templates in `lib/app_web/templates/user`
33 |
34 | ## The context app
35 |
36 | A migration file for the repository and test files for the context and
37 | controller features will also be generated.
38 |
39 | The location of the web files (controllers, views, templates, etc) in an
40 | umbrella application will vary based on the `:context_app` config located
41 | in your applications `:generators` configuration. When set, the Phoenix
42 | generators will generate web files directly in your lib and test folders
43 | since the application is assumed to be isolated to web specific functionality.
44 | If `:context_app` is not set, the generators will place web related lib
45 | and test files in a `web/` directory since the application is assumed
46 | to be handling both web and domain specific functionality.
47 | Example configuration:
48 |
49 | config :my_app_web, :generators, context_app: :my_app
50 |
51 | Alternatively, the `--context-app` option may be supplied to the generator:
52 |
53 | mix phx.gen.tailwind Sales User users --context-app warehouse
54 |
55 | ## Web namespace
56 |
57 | By default, the controller and view will be namespaced by the schema name.
58 | You can customize the web module namespace by passing the `--web` flag with a
59 | module name, for example:
60 |
61 | mix phx.gen.tailwind Sales User users --web Sales
62 |
63 | Which would generate a `lib/app_web/controllers/sales/user_controller.ex` and
64 | `lib/app_web/views/sales/user_view.ex`.
65 |
66 | ## Customising the context, schema, tables and migrations
67 |
68 | In some cases, you may wish to bootstrap HTML templates, controllers,
69 | and controller tests, but leave internal implementation of the context
70 | or schema to yourself. You can use the `--no-context` and `--no-schema`
71 | flags for file generation control.
72 |
73 | You can also change the table name or configure the migrations to
74 | use binary ids for primary keys, see `mix tailwind.gen.schema` for more
75 | information.
76 | """
77 | use Mix.Task
78 |
79 | alias Mix.Phoenix.{Context, Schema}
80 | alias Mix.Tasks.Phx.Gen
81 |
82 | @doc false
83 | def run(args) do
84 | if Mix.Project.umbrella?() do
85 | Mix.raise "mix phx.gen.tailwind must be invoked from within your *_web application root directory"
86 | end
87 |
88 | {context, schema} = Gen.Context.build(args)
89 | Gen.Context.prompt_for_code_injection(context)
90 |
91 | binding = [context: context, schema: schema, inputs: inputs(schema)]
92 | paths = [:phx_tailwind_generators, :phoenix]
93 |
94 | prompt_for_conflicts(context)
95 |
96 | context
97 | |> copy_new_files(paths, binding)
98 | |> inject_error_helper(paths, binding)
99 | |> print_shell_instructions()
100 | end
101 |
102 | defp prompt_for_conflicts(context) do
103 | context
104 | |> files_to_be_generated()
105 | |> Kernel.++(context_files(context))
106 | |> Mix.Phoenix.prompt_for_conflicts()
107 | end
108 | defp context_files(%Context{generate?: true} = context) do
109 | Gen.Context.files_to_be_generated(context)
110 | end
111 | defp context_files(%Context{generate?: false}) do
112 | []
113 | end
114 |
115 | @doc false
116 | def files_to_be_generated(%Context{schema: schema, context_app: context_app}) do
117 | web_prefix = Mix.Phoenix.web_path(context_app)
118 | test_prefix = Mix.Phoenix.web_test_path(context_app)
119 | web_path = to_string(schema.web_path)
120 |
121 | [
122 | {:eex, "controller.ex", Path.join([web_prefix, "controllers", web_path, "#{schema.singular}_controller.ex"])},
123 | {:eex, "edit.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "edit.html.eex"])},
124 | {:eex, "form.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "form.html.eex"])},
125 | {:eex, "index.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "index.html.eex"])},
126 | {:eex, "new.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "new.html.eex"])},
127 | {:eex, "show.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "show.html.eex"])},
128 | {:eex, "view.ex", Path.join([web_prefix, "views", web_path, "#{schema.singular}_view.ex"])},
129 | {:eex, "controller_test.exs", Path.join([test_prefix, "controllers", web_path, "#{schema.singular}_controller_test.exs"])},
130 | ]
131 | end
132 |
133 | @doc false
134 | def copy_new_files(%Context{} = context, paths, binding) do
135 | files = files_to_be_generated(context)
136 | Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.tailwind", binding, files)
137 | if context.generate?, do: Gen.Context.copy_new_files(context, paths, binding)
138 | context
139 | end
140 |
141 | @doc false
142 | def print_shell_instructions(%Context{schema: schema, context_app: ctx_app} = context) do
143 | if schema.web_namespace do
144 | Mix.shell().info """
145 |
146 | Add the resource to your #{schema.web_namespace} :browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
147 |
148 | scope "/#{schema.web_path}", #{inspect Module.concat(context.web_module, schema.web_namespace)}, as: :#{schema.web_path} do
149 | pipe_through :browser
150 | ...
151 | resources "/#{schema.plural}", #{inspect schema.alias}Controller
152 | end
153 | """
154 | else
155 | Mix.shell().info """
156 |
157 | Add the resource to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
158 |
159 | resources "/#{schema.plural}", #{inspect schema.alias}Controller
160 | """
161 | end
162 | if context.generate?, do: Gen.Context.print_shell_instructions(context)
163 | end
164 |
165 | @doc false
166 | def inputs(%Schema{} = schema) do
167 | Enum.map(schema.attrs, fn
168 | {_, {:references, _}} ->
169 | {nil, nil, nil}
170 | {key, :integer} ->
171 | {label(key), ~s(<%= number_input f, #{inspect(key)}, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
172 | {key, :float} ->
173 | {label(key), ~s(<%= number_input f, #{inspect(key)}, step: "any", class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
174 | {key, :decimal} ->
175 | {label(key), ~s(<%= number_input f, #{inspect(key)}, step: "any", class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
176 | {key, :boolean} ->
177 | {label(key), ~s(<%= checkbox f, #{inspect(key)}, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
178 | {key, :text} ->
179 | {label(key), ~s(<%= textarea f, #{inspect(key)}, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
180 | {key, :date} ->
181 | {label(key), ~s(<%= date_select f, #{inspect(key)}, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
182 | {key, :time} ->
183 | {label(key), ~s(<%= time_select f, #{inspect(key)}, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
184 | {key, :utc_datetime} ->
185 | {label(key), ~s(<%= datetime_select f, #{inspect(key)}, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
186 | {key, :naive_datetime} ->
187 | {label(key), ~s(<%= datetime_select f, #{inspect(key)}, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
188 | {key, {:array, :integer}} ->
189 | {label(key), ~s(<%= multiple_select f, #{inspect(key)}, ["1": 1, "2": 2], class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
190 | {key, {:array, _}} ->
191 | {label(key), ~s(<%= multiple_select f, #{inspect(key)}, ["Option 1": "option1", "Option 2": "option2"] %>), error(key)}
192 | {key, _} ->
193 | {label(key), ~s(<%= text_input f, #{inspect(key)}, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>), error(key)}
194 | end)
195 | end
196 |
197 | defp label(key) do
198 | ~s(<%= label f, #{inspect(key)}, class: "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2" %>)
199 | end
200 |
201 | defp error(field) do
202 | ~s(<%= tailwind_error_tag f, #{inspect(field)} %>)
203 | end
204 |
205 | defp inject_error_helper(%Context{context_app: ctx_app} = context, _paths, _binding) do
206 | web_prefix = Mix.Phoenix.web_path(ctx_app)
207 | file_path = Path.join(web_prefix, "views/error_helpers.ex")
208 |
209 | """
210 |
211 | def tailwind_error_tag(form, field) do
212 | Enum.map(Keyword.get_values(form.errors, field), fn error ->
213 | content_tag(:p, translate_error(error),
214 | class: "mt-2 text-sm text-red-500",
215 | phx_feedback_for: input_name(form, field)
216 | )
217 | end)
218 | end
219 | """
220 | |> inject_before_final_end(file_path)
221 |
222 | context
223 | end
224 |
225 | defp inject_before_final_end(content_to_inject, file_path) do
226 | with {:ok, file} <- read_file(file_path),
227 | {:ok, new_file} <- PhxTailwindGenerators.inject_before_final_end(file, content_to_inject) do
228 | print_injecting(file_path)
229 | File.write!(file_path, new_file)
230 | else
231 | :already_injected ->
232 | :ok
233 |
234 | {:error, {:file_read_error, _}} ->
235 | :ok
236 | # print_injecting(file_path)
237 | #
238 | # print_unable_to_read_file_error(
239 | # file_path,
240 | # """
241 | # Please add the following to the end of your equivalent
242 | # #{Path.relative_to_cwd(file_path)} module:
243 | # #{indent_spaces(content_to_inject, 2)}
244 | # """
245 | # )
246 | end
247 | end
248 |
249 | defp read_file(file_path) do
250 | case File.read(file_path) do
251 | {:ok, file} -> {:ok, file}
252 | {:error, reason} -> {:error, {:file_read_error, reason}}
253 | end
254 | end
255 |
256 | defp print_injecting(file_path, suffix \\ []) do
257 | Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path), suffix])
258 | end
259 |
260 | end
261 |
--------------------------------------------------------------------------------
/lib/phx_tailwind_generators.ex:
--------------------------------------------------------------------------------
1 | defmodule PhxTailwindGenerators do
2 | @moduledoc """
3 | Documentation for `PhxTailwindGenerators`.
4 | """
5 |
6 | @doc """
7 | Injects snippet before the final end in a file
8 | """
9 | @spec inject_before_final_end(String.t(), String.t()) :: {:ok, String.t()} | :already_injected
10 | def inject_before_final_end(code, code_to_inject) when is_binary(code) and is_binary(code_to_inject) do
11 | if String.contains?(code, code_to_inject) do
12 | :already_injected
13 | else
14 | new_code =
15 | code
16 | |> String.trim_trailing()
17 | |> String.trim_trailing("end")
18 | |> Kernel.<>(code_to_inject)
19 | |> Kernel.<>("end\n")
20 |
21 | {:ok, new_code}
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule PhxTailwindGenerators.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :phx_tailwind_generators,
7 | version: "0.1.7",
8 | elixir: "~> 1.11",
9 | start_permanent: Mix.env() == :prod,
10 | deps: deps(),
11 | description: "Generators to create templates with Tailwind CSS.",
12 | name: "PhxTailwindGenerators",
13 | package: %{
14 | maintainers: ["Stefan Wintermeyer"],
15 | licenses: ["MIT"],
16 | links: %{
17 | github: "https://github.com/wintermeyer/phx_tailwind_generators"
18 | }
19 | },
20 | source_url: "https://github.com/wintermeyer/phx_tailwind_generators",
21 | homepage_url: "https://github.com/wintermeyer/phx_tailwind_generators"
22 | ]
23 | end
24 |
25 | # Run "mix help compile.app" to learn about applications.
26 | def application do
27 | [
28 | extra_applications: [:logger]
29 | ]
30 | end
31 |
32 | # Run "mix help deps" to learn about dependencies.
33 | defp deps do
34 | [
35 | {:phoenix, ">= 1.5.7"},
36 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
37 | ]
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "earmark_parser": {:hex, :earmark_parser, "1.4.16", "607709303e1d4e3e02f1444df0c821529af1c03b8578dfc81bb9cf64553d02b9", [:mix], [], "hexpm", "69fcf696168f5a274dd012e3e305027010658b2d1630cef68421d6baaeaccead"},
3 | "ex_doc": {:hex, :ex_doc, "0.25.3", "3edf6a0d70a39d2eafde030b8895501b1c93692effcbd21347296c18e47618ce", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "9ebebc2169ec732a38e9e779fd0418c9189b3ca93f4a676c961be6c1527913f5"},
4 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
5 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"},
6 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
7 | "mime": {:hex, :mime, "2.0.1", "0de4c81303fe07806ebc2494d5321ce8fb4df106e34dd5f9d787b637ebadc256", [:mix], [], "hexpm", "7a86b920d2aedce5fb6280ac8261ac1a739ae6c1a1ad38f5eadf910063008942"},
8 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
9 | "phoenix": {:hex, :phoenix, "1.6.2", "6cbd5c8ed7a797f25a919a37fafbc2fb1634c9cdb12a4448d7a5d0b26926f005", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7bbee475acae0c3abc229b7f189e210ea788e63bd168e585f60c299a4b2f9133"},
10 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
11 | "phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"},
12 | "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.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.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
13 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
14 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
15 | }
16 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.tailwind/controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Controller do
2 | use <%= inspect context.web_module %>, :controller
3 |
4 | alias <%= inspect context.module %>
5 | alias <%= inspect schema.module %>
6 |
7 | def index(conn, _params) do
8 | <%= schema.plural %> = <%= inspect context.alias %>.list_<%= schema.plural %>()
9 | render(conn, "index.html", <%= schema.plural %>: <%= schema.plural %>)
10 | end
11 |
12 | def new(conn, _params) do
13 | changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(%<%= inspect schema.alias %>{})
14 | render(conn, "new.html", changeset: changeset)
15 | end
16 |
17 | def create(conn, %{<%= inspect schema.singular %> => <%= schema.singular %>_params}) do
18 | case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do
19 | {:ok, <%= schema.singular %>} ->
20 | conn
21 | |> put_flash(:info, "<%= schema.human_singular %> created successfully.")
22 | |> redirect(to: Routes.<%= schema.route_helper %>_path(conn, :show, <%= schema.singular %>))
23 |
24 | {:error, %Ecto.Changeset{} = changeset} ->
25 | render(conn, "new.html", changeset: changeset)
26 | end
27 | end
28 |
29 | def show(conn, %{"id" => id}) do
30 | <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
31 | render(conn, "show.html", <%= schema.singular %>: <%= schema.singular %>)
32 | end
33 |
34 | def edit(conn, %{"id" => id}) do
35 | <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
36 | changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>)
37 | render(conn, "edit.html", <%= schema.singular %>: <%= schema.singular %>, changeset: changeset)
38 | end
39 |
40 | def update(conn, %{"id" => id, <%= inspect schema.singular %> => <%= schema.singular %>_params}) do
41 | <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
42 |
43 | case <%= inspect context.alias %>.update_<%= schema.singular %>(<%= schema.singular %>, <%= schema.singular %>_params) do
44 | {:ok, <%= schema.singular %>} ->
45 | conn
46 | |> put_flash(:info, "<%= schema.human_singular %> updated successfully.")
47 | |> redirect(to: Routes.<%= schema.route_helper %>_path(conn, :show, <%= schema.singular %>))
48 |
49 | {:error, %Ecto.Changeset{} = changeset} ->
50 | render(conn, "edit.html", <%= schema.singular %>: <%= schema.singular %>, changeset: changeset)
51 | end
52 | end
53 |
54 | def delete(conn, %{"id" => id}) do
55 | <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
56 | {:ok, _<%= schema.singular %>} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>)
57 |
58 | conn
59 | |> put_flash(:info, "<%= schema.human_singular %> deleted successfully.")
60 | |> redirect(to: Routes.<%= schema.route_helper %>_path(conn, :index))
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.tailwind/controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ControllerTest do
2 | use <%= inspect context.web_module %>.ConnCase
3 |
4 | alias <%= inspect context.module %>
5 |
6 | @create_attrs <%= inspect schema.params.create %>
7 | @update_attrs <%= inspect schema.params.update %>
8 | @invalid_attrs <%= inspect for {key, _} <- schema.params.create, into: %{}, do: {key, nil} %>
9 |
10 | def fixture(:<%= schema.singular %>) do
11 | {:ok, <%= schema.singular %>} = <%= inspect context.alias %>.create_<%= schema.singular %>(@create_attrs)
12 | <%= schema.singular %>
13 | end
14 |
15 | describe "index" do
16 | test "lists all <%= schema.plural %>", %{conn: conn} do
17 | conn = get(conn, Routes.<%= schema.route_helper %>_path(conn, :index))
18 | assert html_response(conn, 200) =~ "Listing <%= schema.human_plural %>"
19 | end
20 | end
21 |
22 | describe "new <%= schema.singular %>" do
23 | test "renders form", %{conn: conn} do
24 | conn = get(conn, Routes.<%= schema.route_helper %>_path(conn, :new))
25 | assert html_response(conn, 200) =~ "New <%= schema.human_singular %>"
26 | end
27 | end
28 |
29 | describe "create <%= schema.singular %>" do
30 | test "redirects to show when data is valid", %{conn: conn} do
31 | conn = post(conn, Routes.<%= schema.route_helper %>_path(conn, :create), <%= schema.singular %>: @create_attrs)
32 |
33 | assert %{id: id} = redirected_params(conn)
34 | assert redirected_to(conn) == Routes.<%= schema.route_helper %>_path(conn, :show, id)
35 |
36 | conn = get(conn, Routes.<%= schema.route_helper %>_path(conn, :show, id))
37 | assert html_response(conn, 200) =~ "Show <%= schema.human_singular %>"
38 | end
39 |
40 | test "renders errors when data is invalid", %{conn: conn} do
41 | conn = post(conn, Routes.<%= schema.route_helper %>_path(conn, :create), <%= schema.singular %>: @invalid_attrs)
42 | assert html_response(conn, 200) =~ "New <%= schema.human_singular %>"
43 | end
44 | end
45 |
46 | describe "edit <%= schema.singular %>" do
47 | setup [:create_<%= schema.singular %>]
48 |
49 | test "renders form for editing chosen <%= schema.singular %>", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
50 | conn = get(conn, Routes.<%= schema.route_helper %>_path(conn, :edit, <%= schema.singular %>))
51 | assert html_response(conn, 200) =~ "Edit <%= schema.human_singular %>"
52 | end
53 | end
54 |
55 | describe "update <%= schema.singular %>" do
56 | setup [:create_<%= schema.singular %>]
57 |
58 | test "redirects when data is valid", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
59 | conn = put(conn, Routes.<%= schema.route_helper %>_path(conn, :update, <%= schema.singular %>), <%= schema.singular %>: @update_attrs)
60 | assert redirected_to(conn) == Routes.<%= schema.route_helper %>_path(conn, :show, <%= schema.singular %>)
61 |
62 | conn = get(conn, Routes.<%= schema.route_helper %>_path(conn, :show, <%= schema.singular %>))<%= if schema.string_attr do %>
63 | assert html_response(conn, 200) =~ <%= inspect Mix.Phoenix.Schema.default_param(schema, :update) %><% else %>
64 | assert html_response(conn, 200)<% end %>
65 | end
66 |
67 | test "renders errors when data is invalid", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
68 | conn = put(conn, Routes.<%= schema.route_helper %>_path(conn, :update, <%= schema.singular %>), <%= schema.singular %>: @invalid_attrs)
69 | assert html_response(conn, 200) =~ "Edit <%= schema.human_singular %>"
70 | end
71 | end
72 |
73 | describe "delete <%= schema.singular %>" do
74 | setup [:create_<%= schema.singular %>]
75 |
76 | test "deletes chosen <%= schema.singular %>", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
77 | conn = delete(conn, Routes.<%= schema.route_helper %>_path(conn, :delete, <%= schema.singular %>))
78 | assert redirected_to(conn) == Routes.<%= schema.route_helper %>_path(conn, :index)
79 | assert_error_sent 404, fn ->
80 | get(conn, Routes.<%= schema.route_helper %>_path(conn, :show, <%= schema.singular %>))
81 | end
82 | end
83 | end
84 |
85 | defp create_<%= schema.singular %>(_) do
86 | <%= schema.singular %> = fixture(:<%= schema.singular %>)
87 | %{<%= schema.singular %>: <%= schema.singular %>}
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.tailwind/edit.html.eex:
--------------------------------------------------------------------------------
1 | <%%= render "form.html", Map.put(assigns, :action, Routes.<%= schema.route_helper %>_path(@conn, :update, @<%= schema.singular %>)) |> Map.put(:title, gettext("Edit %{name}", name: "<%= schema.human_singular %>")) %>
2 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.tailwind/form.html.eex:
--------------------------------------------------------------------------------
1 | <%%= form_for @changeset, @action, [class: ""], fn f -> %>
2 |
3 |
4 |
5 |
6 |
7 | <%%= assigns[:title] %>
8 |
9 |
10 | <%%= assigns[:subtitle] %>
11 |
12 |
13 |
14 | <%%= if @changeset.action do %>
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 | <%%= gettext "Oops, something went wrong! Please check the errors below." %>
25 |
26 |
27 |
28 |
29 | <%% end %>
30 |
31 |
32 | <%= for {label, input, error} <- inputs, input do %>
33 |