├── .formatter.exs ├── .gitignore ├── LICENSE.md ├── README.md ├── lib ├── mix │ └── tasks │ │ └── phx_gen_tailwind.ex └── phx_tailwind_generators.ex ├── mix.exs ├── mix.lock ├── priv └── templates │ └── phx.gen.tailwind │ ├── controller.ex │ ├── controller_test.exs │ ├── edit.html.eex │ ├── form.html.eex │ ├── index.html.eex │ ├── new.html.eex │ ├── show.html.eex │ └── view.ex ├── screencast.gif └── test ├── phx_tailwind_generators_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | phx_tailwind_generators-*.tar 24 | 25 | 26 | # Temporary files for e.g. tests 27 | /tmp 28 | 29 | # macOS 30 | .DS_Store 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright © 2021 Stefan Wintermeyer and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhxTailwindGenerators 2 | 3 | A scaffold generator for new resources which uses [Tailwind CSS](https://tailwindui.com) and not the default [Milligram](https://milligram.io). 4 | 5 | If you already have a Tailwind system up and running within your Phoenix application than you just have to add `{:phx_tailwind_generators, "~> 0.1.6"}` to your `mix.exs` and run a `mix deps.get` to get access to the `mix phx.gen.tailwind Blog Post posts title body:text` generator. 6 | 7 | ![Screencast](screencast.gif) 8 | 9 | In case you don't have a Phoenix with Tailwind setup yet you have two choices: 10 | 11 | - https://fullstackphoenix.com/boilerplates will do the heavy lifting for you. 12 | - Read the following small Howto. 13 | 14 | ## Setup Phoenix with Tailwind 15 | 16 | A step by step howto to setup a Phoenix system with [Tailwind CSS](https://tailwindui.com). When useful I use the output of [diff](https://en.wikipedia.org/wiki/Diff) to describe where to include/change code (the first number is the line number). 17 | 18 | I promise to not use "this is easy". Nothing is easy if you don't know how to do it. 19 | 20 | ### A green field 21 | 22 | We start with a fresh Phoenix application named `example_shop`: 23 | 24 | ```bash 25 | $ mix phx.new example_shop 26 | $ cd example_shop 27 | $ mix ecto.create 28 | ``` 29 | 30 | ### Add PostCSS 31 | 32 | [PostCSS](https://postcss.org) is a tool for transforming CSS with JavaScript. 33 | 34 | ```bash 35 | $ cd assets 36 | $ npm install tailwindcss postcss autoprefixer postcss-loader@4.2 --save-dev 37 | $ cd .. 38 | ``` 39 | 40 | Create the following file with this content: 41 | 42 | **assets/postcss.config.js** 43 | ```javascript 44 | module.exports = { 45 | plugins: { 46 | tailwindcss: {}, 47 | autoprefixer: {}, 48 | } 49 | } 50 | ``` 51 | 52 | Open the file **assets/webpack.config.js** in the 53 | editor of your choice. Search for `sass-loader` and add `'postcss-loader',` before it. 54 | 55 | The diff: 56 | ```` 57 | 41a42 58 | > 'postcss-loader', 59 | ```` 60 | 61 | ### Purge unused styles in production 62 | 63 | This makes for a minimal CSS file in your production environment. This results in a better WebPerformance. 64 | 65 | ```bash 66 | $ cd assets 67 | $ npx tailwind init 68 | $ cd .. 69 | ``` 70 | 71 | Open the file **assets/tailwind.config.js** with an editor and change it according this diff: 72 | 73 | ```` 74 | 2c2,8 75 | < purge: [], 76 | --- 77 | > purge: [ 78 | > "../**/*.html.eex", 79 | > "../**/*.html.leex", 80 | > "../**/views/**/*.ex", 81 | > "../**/live/**/*.ex", 82 | > "./js/**/*.js", 83 | > ], 84 | ```` 85 | 86 | Do this change in **assets/package.json** 87 | 88 | ```` 89 | 6c6 90 | < "deploy": "webpack --mode production", 91 | --- 92 | > "deploy": "NODE_ENV=production webpack --mode production", 93 | ```` 94 | 95 | Replace **assets/css/app.scss** with this code: 96 | 97 | ```css 98 | /* This file is for your main application css. */ 99 | @tailwind base; 100 | @tailwind components; 101 | @tailwind utilities; 102 | ``` 103 | 104 | Remove the not needed default Phoenix CSS: 105 | 106 | ```bash 107 | $ rm assets/css/phoenix.css 108 | ``` 109 | 110 | **Now you have a running Tailwind CSS system within your Phoenix application!** 111 | 112 | The test: Fire up your Phoenix application with `mix phx.server` and open http://localhost:4000 with your browser. Open `lib/example_shop_web/templates/page/index.html.eex` in your editor and search for a `

` or `

` 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 | 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 |
34 | <%= label %> 35 |
36 | <%= input %> 37 | <%= error %> 38 |
39 |
40 | <% end %> 41 |
42 |
43 | 44 |
45 | 46 |
47 |
48 |
49 | <%%= link to: Routes.<%= schema.route_helper %>_path(@conn, :index), class: "inline-flex items-center px-5 mr-2 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %> 50 | 51 | 54 | 55 | <%%= gettext "Back" %> 56 | <%% end %> 57 |
58 |
59 | <%%= submit class: "ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" do %> 60 | 61 | 64 | 65 | <%%= gettext "Save" %> 66 | <%% end %> 67 |
68 |
69 |
70 | 71 | <%% end %> 72 | -------------------------------------------------------------------------------- /priv/templates/phx.gen.tailwind/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%%= gettext "Listing %{name}", name: "<%= schema.human_plural %>" %> 4 |

5 |
6 | 7 |
8 |
9 |
10 |
11 | 12 | 13 | 14 | <%= for {k, _} <- schema.attrs do %> 15 | <% end %> 16 | 19 | 20 | 21 | 22 | <%%= for {<%= schema.singular %>, counter} <- Enum.with_index(@<%= schema.plural %>) do %> 23 | "> 24 | <%= for {k, _} <- schema.attrs do %> 25 | <% end %> 26 | 55 | 56 | <%% end %> 57 | 58 | 59 |
<%= Phoenix.Naming.humanize(Atom.to_string(k)) %> 17 | Show/Edit/Delete 18 |
<%%= <%= schema.singular %>.<%= k %> %> 27 | 28 | <%%= link to: Routes.<%= schema.route_helper %>_path(@conn, :show, <%= schema.singular %>), class: "inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" do %> 29 | 30 | 34 | 35 | <%%= gettext "Show" %> 36 | <%% end %> 37 | <%%= link to: Routes.<%= schema.route_helper %>_path(@conn, :edit, <%= schema.singular %>), class: "inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500" do %> 38 | 39 | 43 | 44 | <%%= gettext "Edit" %> 45 | <%% end %> 46 | <%%= link to: Routes.<%= schema.route_helper %>_path(@conn, :delete, <%= schema.singular %>), method: :delete, data: [confirm: "Are you sure?"], class: "inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" do %> 47 | 48 | 51 | 52 | <%%= gettext "Delete" %> 53 | <%% end %> 54 |
60 |
61 |
62 |
63 |
64 | 65 |
66 | <%%= link to: Routes.<%= schema.route_helper %>_path(@conn, :new), class: "inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" do %> 67 | 68 | 71 | 72 | <%%= gettext "New %{name}", name: "<%= schema.human_singular %>" %> 73 | <%% end %> 74 |
75 | -------------------------------------------------------------------------------- /priv/templates/phx.gen.tailwind/new.html.eex: -------------------------------------------------------------------------------- 1 | <%%= render "form.html", Map.put(assigns, :action, Routes.<%= schema.route_helper %>_path(@conn, :create)) |> Map.put(:title, gettext("New %{name}", name: "<%= schema.human_singular %>")) %> 2 | -------------------------------------------------------------------------------- /priv/templates/phx.gen.tailwind/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%%= gettext "Show %{name}", name: "<%= schema.human_singular %>" %> 5 |

6 |
7 |
8 |
9 | <%= for {{k, _}, counter} <- Enum.with_index(schema.attrs) do %> 10 |
px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> 11 |
12 | <%= Phoenix.Naming.humanize(Atom.to_string(k)) %> 13 |
14 |
15 | <%%= @<%= schema.singular %>.<%= k %> %> 16 |
17 |
18 | <% end %> 19 |
20 |
21 |
22 | 23 |
24 | <%%= link to: Routes.<%= schema.route_helper %>_path(@conn, :edit, @<%= schema.singular %>), class: "inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500" do %> 25 | 26 | 30 | 31 | <%%= gettext "Edit" %> 32 | <%% end %> 33 | 34 | <%%= link to: Routes.<%= schema.route_helper %>_path(@conn, :index), class: "inline-flex items-center px-5 mr-2 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %> 35 | 36 | 39 | 40 | <%%= gettext "Back" %> 41 | <%% end %> 42 |
43 | -------------------------------------------------------------------------------- /priv/templates/phx.gen.tailwind/view.ex: -------------------------------------------------------------------------------- 1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>View do 2 | use <%= inspect context.web_module %>, :view 3 | end 4 | -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wintermeyer/phx_tailwind_generators/c3a56fcb099cbd288d9fed575e2e43f15e2b03c8/screencast.gif -------------------------------------------------------------------------------- /test/phx_tailwind_generators_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhxTailwindGeneratorsTest do 2 | use ExUnit.Case 3 | doctest PhxTailwindGenerators 4 | 5 | test "greets the world" do 6 | assert PhxTailwindGenerators.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------