Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
wintermeyer committed Jan 16, 2021
0 parents commit 7150e02
Show file tree
Hide file tree
Showing 19 changed files with 669 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
phx_tailwind_generators-*.tar


# Temporary files for e.g. tests
/tmp
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# PhxTailwindGenerators

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `phx_tailwind_generators` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:phx_tailwind_generators, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/phx_tailwind_generators](https://hexdocs.pm/phx_tailwind_generators).

203 changes: 203 additions & 0 deletions lib/mix/tasks/phx_gen_tailwind.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
defmodule Mix.Tasks.Phx.Gen.Tailwind do
@shortdoc "Generates controller, views, and context for an HTML resource"

@moduledoc """
Generates controller, views, and context for an HTML resource.
mix tailwind.gen.html 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.
> 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 tailwind.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 controller in `lib/app_web/controllers/user_controller.ex`
* default CRUD templates in `lib/app_web/templates/user`
## 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 (controllers, 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 tailwind.gen.html 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 tailwind.gen.html Sales User users --web Sales
Which would generate a `lib/app_web/controllers/sales/user_controller.ex` and
`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, controllers,
and controller 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 tailwind.gen.schema` for more
information.
"""
use Mix.Task

alias Mix.Phoenix.{Context, Schema}
alias Mix.Tasks.Phx.Gen

@doc false
def run(args) do
if Mix.Project.umbrella?() do
Mix.raise "mix tailwind.gen.html 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 = Mix.Phoenix.generator_paths()

prompt_for_conflicts(context)

context
|> copy_new_files(paths, binding)
|> 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

@doc false
def 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)

[
{:eex, "controller.ex", Path.join([web_prefix, "controllers", web_path, "#{schema.singular}_controller.ex"])},
{:eex, "edit.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "edit.html.eex"])},
{:eex, "form.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "form.html.eex"])},
{:eex, "index.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "index.html.eex"])},
{:eex, "new.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "new.html.eex"])},
{:eex, "show.html.eex", Path.join([web_prefix, "templates", web_path, schema.singular, "show.html.eex"])},
{:eex, "view.ex", Path.join([web_prefix, "views", web_path, "#{schema.singular}_view.ex"])},
{:eex, "controller_test.exs", Path.join([test_prefix, "controllers", web_path, "#{schema.singular}_controller_test.exs"])},
]
end

@doc false
def copy_new_files(%Context{} = context, paths, binding) do
files = files_to_be_generated(context)
# Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.tailwind", binding, files)
Mix.Phoenix.copy_from(paths, :code.priv_dir('templates/phx.gen.tailwind')|>to_string, binding, files)
if context.generate?, do: Gen.Context.copy_new_files(context, paths, binding)
context
end

@doc false
def print_shell_instructions(%Context{schema: schema, context_app: ctx_app} = context) do
if schema.web_namespace do
Mix.shell().info """
Add the resource to your #{schema.web_namespace} :browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
scope "/#{schema.web_path}", #{inspect Module.concat(context.web_module, schema.web_namespace)}, as: :#{schema.web_path} do
pipe_through :browser
...
resources "/#{schema.plural}", #{inspect schema.alias}Controller
end
"""
else
Mix.shell().info """
Add the resource to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
resources "/#{schema.plural}", #{inspect schema.alias}Controller
"""
end
if context.generate?, do: Gen.Context.print_shell_instructions(context)
end

@doc false
def inputs(%Schema{} = schema) do
Enum.map(schema.attrs, fn
{_, {:references, _}} ->
{nil, nil, nil}
{key, :integer} ->
{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)}
{key, :float} ->
{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)}
{key, :decimal} ->
{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)}
{key, :boolean} ->
{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)}
{key, :text} ->
{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)}
{key, :date} ->
{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)}
{key, :time} ->
{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)}
{key, :utc_datetime} ->
{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)}
{key, :naive_datetime} ->
{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)}
{key, {:array, :integer}} ->
{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)}
{key, {:array, _}} ->
{label(key), ~s(<%= multiple_select f, #{inspect(key)}, ["Option 1": "option1", "Option 2": "option2"] %>), error(key)}
{key, _} ->
{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)}
end)
end

defp label(key) do
~s(<%= label f, #{inspect(key)}, class: "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2" %>)
end

defp error(field) do
~s(<%= tailwind_error_tag f, #{inspect(field)} %>)
end
end
18 changes: 18 additions & 0 deletions lib/phx_tailwind_generators.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule PhxTailwindGenerators do
@moduledoc """
Documentation for `PhxTailwindGenerators`.
"""

@doc """
Hello world.
## Examples
iex> PhxTailwindGenerators.hello()
:world
"""
def hello do
:world
end
end
28 changes: 28 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule PhxTailwindGenerators.MixProject do
use Mix.Project

def project do
[
app: :phx_tailwind_generators,
version: "0.1.0",
elixir: "~> 1.11",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end
Binary file added priv/.DS_Store
Binary file not shown.
Binary file added priv/templates/.DS_Store
Binary file not shown.
62 changes: 62 additions & 0 deletions priv/templates/phx.gen.tailwind/controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Controller do
use <%= inspect context.web_module %>, :controller

alias <%= inspect context.module %>
alias <%= inspect schema.module %>

def index(conn, _params) do
<%= schema.plural %> = <%= inspect context.alias %>.list_<%= schema.plural %>()
render(conn, "index.html", <%= schema.plural %>: <%= schema.plural %>)
end

def new(conn, _params) do
changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(%<%= inspect schema.alias %>{})
render(conn, "new.html", changeset: changeset)
end

def create(conn, %{<%= inspect schema.singular %> => <%= schema.singular %>_params}) do
case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do
{:ok, <%= schema.singular %>} ->
conn
|> put_flash(:info, "<%= schema.human_singular %> created successfully.")
|> redirect(to: Routes.<%= schema.route_helper %>_path(conn, :show, <%= schema.singular %>))

{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end

def show(conn, %{"id" => id}) do
<%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
render(conn, "show.html", <%= schema.singular %>: <%= schema.singular %>)
end

def edit(conn, %{"id" => id}) do
<%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>)
render(conn, "edit.html", <%= schema.singular %>: <%= schema.singular %>, changeset: changeset)
end

def update(conn, %{"id" => id, <%= inspect schema.singular %> => <%= schema.singular %>_params}) do
<%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)

case <%= inspect context.alias %>.update_<%= schema.singular %>(<%= schema.singular %>, <%= schema.singular %>_params) do
{:ok, <%= schema.singular %>} ->
conn
|> put_flash(:info, "<%= schema.human_singular %> updated successfully.")
|> redirect(to: Routes.<%= schema.route_helper %>_path(conn, :show, <%= schema.singular %>))

{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", <%= schema.singular %>: <%= schema.singular %>, changeset: changeset)
end
end

def delete(conn, %{"id" => id}) do
<%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
{:ok, _<%= schema.singular %>} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>)

conn
|> put_flash(:info, "<%= schema.human_singular %> deleted successfully.")
|> redirect(to: Routes.<%= schema.route_helper %>_path(conn, :index))
end
end
Loading

0 comments on commit 7150e02

Please sign in to comment.