Compile to EEx (#80)

Code is gross
This commit is contained in:
Mitchell Hanberg 2020-06-16 15:28:21 -04:00 committed by GitHub
parent 43bd75056f
commit 33c95186fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 2875 additions and 3024 deletions

View File

@ -16,7 +16,7 @@ locals_without_parens = ~w[
details summary menuitem menu
meta link base
area br col embed hr img input keygen param source track wbr
text partial
txt partial
animate animateMotion animateTransform circle clipPath
color-profile defs desc discard ellipse feBlend
@ -24,7 +24,7 @@ locals_without_parens = ~w[
feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset
fePointLight feSpecularLighting feSpotLight feTile feTurbulence filter foreignObject g hatch hatchpath image line linearGradient
marker mask mesh meshgradient meshpatch meshrow metadata mpath path pattern polygon
polyline radialGradient rect set solidcolor stop svg switch symbol text_
polyline radialGradient rect set solidcolor stop svg switch symbol text
textPath tspan unknown use view
form_for inputs_for

View File

@ -24,9 +24,49 @@ jobs:
id: cache
with:
path: deps
key: ${{ runner.os }}-${{ matrix.elixir }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
restore-keys: |
${{ runner.os }}-mix-
key: ${{ runner.os }}-test-${{ matrix.elixir }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
restore-keys:
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: mix deps.get
- name: Run Tests
run: mix test
integration_tests:
runs-on: ubuntu-latest
name: Integration Test (${{matrix.elixir}}/${{matrix.otp}})
defaults:
run:
working-directory: "./integration_test/temple_demo"
strategy:
matrix:
otp: [21.x]
elixir: [1.7.x, 1.8.x, 1.9.x, 1.10.x]
services:
db:
image: postgres:12
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: temple_demo_test
ports: ['5432:5432']
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v2
- uses: actions/setup-elixir@v1.2.0
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
- uses: actions/cache@v1
id: cache
with:
path: deps
key: ${{ runner.os }}-integration-test-${{ matrix.elixir }}-${{ hashFiles(format('{0}{1}', github.workspace, '/integration_test/temple_demo/mix.lock')) }}
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
@ -34,6 +74,14 @@ jobs:
- name: Run Tests
run: mix test
env:
MIX_ENV: test
- uses: actions/upload-artifact@v2
if: failure()
with:
name: screenshots
path: screenshots/
formatter:
runs-on: ubuntu-latest

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
# The directory Mix downloads your dependencies sources to.
/deps/
/integration_test/temple_demo/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/

View File

@ -1,2 +1,2 @@
elixir 1.10.2
erlang 22.3.1
erlang 23.0.1

View File

@ -2,6 +2,17 @@
## Master
## 0.6.0-alpha.0
### Breaking!
This version is the start of a complete rewrite of Temple.
- Compiles to EEx at build time.
- Compatible with `Phoenix.LiveView`
- All modules other than `Temple` are removed
- `mix temple.convert` Mix task removed
## 0.5.0
- Introduce `@assigns` assign

View File

@ -1,12 +1,6 @@
# ![](temple.png)
> Temple is now undergoing a rewrite. The goal is to compile to EEx at compile time, so that it can then be fed straight into the Phoenix HTML and LiveView engines. This way, Temple becomes compatible with LiveView as well as gaining all the same optimizations as normal Phoenix templates.
>
> There is no guarantee that the rewrite will maintain the current feature set of Temple, mainly the Component API.
>
> To follow along, please checkout out the [rewrite](https://github.com/mhanberg/temple/tree/rewrite) branch. The code will likely be in a "spike" state until further notice, so don't let the WIP commits scare you off 😄
---
> You are looking at the README for the master branch. The README for the latest stable release is located [here](https://github.com/mhanberg/temple/tree/v0.5.0).
[![Actions Status](https://github.com/mhanberg/temple/workflows/CI/badge.svg)](https://github.com/mhanberg/temple/actions)
[![Hex.pm](https://img.shields.io/hexpm/v/temple.svg)](https://hex.pm/packages/temple)
@ -14,7 +8,7 @@
Temple is a DSL for writing HTML using Elixir.
You're probably here because you want to use Temple to write Phoenix templates, which is why Temple includes a [Phoenix template engine](#phoenix-templates) and Temple-compatible [Phoenix form helpers](#phoenixhtml).
You're probably here because you want to use Temple to write Phoenix templates, which is why Temple includes a [Phoenix template engine](#phoenix-templates).
## Installation
@ -22,13 +16,13 @@ Add `temple` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:temple, "~> 0.5.0"}]
[{:temple, "~> 0.6.0-alpha.0"}]
end
```
## Usage
Using Temple is a as simple as using the DSL inside of an `temple/1` block. This returns a safe result of the form `{:safe, html_string}`.
Using Temple is a as simple as using the DSL inside of an `temple/1` block. This returns an EEx string at compile time.
See the [documentation](https://hexdocs.pm/temple/Temple.Html.html) for more details.
@ -36,7 +30,7 @@ See the [documentation](https://hexdocs.pm/temple/Temple.Html.html) for more det
use Temple
temple do
h2 "todos"
h2 do: "todos"
ul class: "list" do
for item <- @items do
@ -45,12 +39,12 @@ temple do
div class: "bullet hidden"
end
div item
div do: item
end
end
end
script """
script do: """
function toggleCheck({currentTarget}) {
currentTarget.children[0].children[0].classList.toggle("hidden");
}
@ -62,81 +56,37 @@ temple do
end
```
### Components
Temple provides an API for creating custom components that act as custom HTML elements.
These components can be given `assigns` that are available inside the component definition as module attributes. The contents of a components `do` block are available as a special `@children` assign.
See the [documentation](https://hexdocs.pm/temple/Temple.html#defcomponent/2) for more details.
```elixir
defcomponent :flex do
div id: @id, class: "flex" do
@children
end
end
temple do
flex id: "my-flex" do
div "Item 1"
div "Item 2"
div "Item 3"
end
end
```
### Phoenix.HTML
Temple provides macros for working with the helpers provided by the [Phoenix.HTML](https://www.github.com/phoenixframework/phoenix_html) package.
Most of the macros are purely wrappers, while the semantics of some are changed to work with Temple.
See the [documentation](https://hexdocs.pm/temple/Temple.Form.html#content) for more details.
```elixir
temple do
form_for @conn, Routes.some_path(@conn, :create) do
text_input form, :name
end
end
```
### Phoenix templates
Add the templating engine to your Phoenix configuration.
See the [documentation](https://hexdocs.pm/temple/Temple.Engine.html#content) for more details.
See the [Temple.Engine](https://hexdocs.pm/temple/Temple.Engine.html#content) and [Temple.LiveEngine](https://hexdocs.pm/temple/Temple.LiveEngine.html#content) for more details.
```elixir
# config.exs
config :phoenix, :template_engines, exs: Temple.Engine
config :phoenix, :template_engines,
exs: Temple.Engine
exs: Temple.LiveEngine
# config/dev.exs
config :your_app, YourAppWeb.Endpoint,
live_reload: [
patterns: [
~r"lib/your_app_web/templates/.*(exs)$"
~r"lib/myapp_web/(live|views)/.*(ex|exs)$",
~r"lib/myapp_web/templates/.*(eex|exs)$"
]
]
# your_app_web.ex
def view do
quote do
# ...
use Temple # Replaces the call to import Phoenix.HTML
end
end
```
```elixir
# app.html.exs
html lang: "en" do
head do
meta charset: "utf-8"
meta http_equiv: "X-UA-Compatible", content: "IE=edge"
meta name: "viewport", content: "width=device-width, initial-scale=1.0"
title "YourApp · Phoenix Framework"
title do: "YourApp · Phoenix Framework"
link rel: "stylesheet", href: Routes.static_path(@conn, "/css/app.css")
end
@ -158,10 +108,15 @@ html lang: "en" do
end
main role: "main", class: "container" do
p get_flash(@conn, :info), class: "alert alert-info", role: "alert"
p get_flash(@conn, :error), class: "alert alert-danger", role: "alert"
p class: "alert alert-info", role: "alert" do
get_flash(@conn, :info)
end
partial render(@view_module, @view_template, assigns)
p class: "alert alert-danger", role: "alert" do
get_flash(@conn, :error)
end
render @view_module, @view_template, assigns
end
script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js")
@ -171,11 +126,13 @@ end
### Tasks
#### temple.convert
#### temple.gen.layout
This task can be used to convert plain HTML and SVG into Temple syntax. Input is taken from stdin or from a file and the output is sent to stdout.
Generates the app layout.
`cat index.html | mix temple.convert > index.html.exs`
#### temple.gen.html
Generates the templates for a resource.
### Formatter

5
bin/integration-test Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
(cd integration_test/temple_demo && mix test)

5
bin/test Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
mix test

7
bin/test-all Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
mix test
(cd integration_test/temple_demo && mix test)

View File

@ -0,0 +1,5 @@
[
import_deps: [:ecto, :phoenix, :temple],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
]

26
integration_test/temple_demo/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# 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 3rd-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").
temple_demo-*.tar
/screenshots/

View File

@ -0,0 +1,18 @@
# TempleDemo
To start your Phoenix server:
* Setup the project with `mix setup`
* Start Phoenix endpoint with `mix phx.server`
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
## Learn more
* Official website: https://www.phoenixframework.org/
* Guides: https://hexdocs.pm/phoenix/overview.html
* Docs: https://hexdocs.pm/phoenix
* Forum: https://elixirforum.com/c/phoenix-forum
* Source: https://github.com/phoenixframework/phoenix

View File

@ -0,0 +1,42 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
# General application configuration
use Mix.Config
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
config :temple_demo,
ecto_repos: [TempleDemo.Repo]
config :phoenix, :template_engines,
exs: Temple.Engine
# Configures the endpoint
config :temple_demo, TempleDemoWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "ww1nKdikInNFHHUfSdCE1wiTcOmQq/KLvOxG7CY1TlKLDTmLW5yheCCYpfoxmZAW",
render_errors: [view: TempleDemoWeb.ErrorView, accepts: ~w(html json), layout: false],
pubsub_server: TempleDemo.PubSub,
live_view: [signing_salt: "KCU/YIG0"]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason
config :temple, :aliases,
label: :_label,
link: :_link,
textarea: :_textarea
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View File

@ -0,0 +1,68 @@
use Mix.Config
# Configure your database
config :temple_demo, TempleDemo.Repo,
username: "postgres",
password: "postgres",
database: "temple_demo_dev",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with webpack to recompile .js and .css sources.
config :temple_demo, TempleDemoWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: []
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# Mix task:
#
# mix phx.gen.cert
#
# Note that this task requires Erlang/OTP 20 or later.
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
#
# https: [
# port: 4001,
# cipher_suite: :strong,
# keyfile: "priv/cert/selfsigned_key.pem",
# certfile: "priv/cert/selfsigned.pem"
# ],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.
# Watch static and templates for browser reloading.
config :temple_demo, TempleDemoWeb.Endpoint,
live_reload: [
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/temple_demo_web/(live|views)/.*(ex)$",
~r"lib/temple_demo_web/templates/.*(eex|exs)$"
]
]
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime

View File

@ -0,0 +1,55 @@
use Mix.Config
# For production, don't forget to configure the url host
# to something meaningful, Phoenix uses this information
# when generating URLs.
#
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
config :temple_demo, TempleDemoWeb.Endpoint,
url: [host: "example.com", port: 80],
cache_static_manifest: "priv/static/cache_manifest.json"
# Do not print debug messages in production
config :logger, level: :info
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
# config :temple_demo, TempleDemoWeb.Endpoint,
# ...
# url: [host: "example.com", port: 443],
# https: [
# port: 443,
# cipher_suite: :strong,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
# transport_options: [socket_opts: [:inet6]]
# ]
#
# The `cipher_suite` is set to `:strong` to support only the
# latest and more secure SSL ciphers. This means old browsers
# and clients may not be supported. You can set it to
# `:compatible` for wider support.
#
# `:keyfile` and `:certfile` expect an absolute path to the key
# and cert in disk or a relative path inside priv, for example
# "priv/ssl/server.key". For all supported SSL configuration
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
#
# We also recommend setting `force_ssl` in your endpoint, ensuring
# no data is ever sent via http, always redirecting to https:
#
# config :temple_demo, TempleDemoWeb.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
# Finally import the config/prod.secret.exs which loads secrets
# and configuration from environment variables.
import_config "prod.secret.exs"

View File

@ -0,0 +1,41 @@
# In this file, we load production configuration and secrets
# from environment variables. You can also hardcode secrets,
# although such is generally not recommended and you have to
# remember to add this file to your .gitignore.
use Mix.Config
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
config :temple_demo, TempleDemo.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
"""
config :temple_demo, TempleDemoWeb.Endpoint,
http: [
port: String.to_integer(System.get_env("PORT") || "4000"),
transport_options: [socket_opts: [:inet6]]
],
secret_key_base: secret_key_base
# ## Using releases (Elixir v1.9+)
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start each relevant endpoint:
#
# config :temple_demo, TempleDemoWeb.Endpoint, server: true
#
# Then you can assemble a release by calling `mix release`.
# See `mix help release` for more information.

View File

@ -0,0 +1,30 @@
use Mix.Config
# Configure your database
#
# The MIX_TEST_PARTITION environment variable can be used
# to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information.
config :temple_demo, TempleDemo.Repo,
username: "postgres",
password: "postgres",
database: "temple_demo_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :temple_demo, TempleDemoWeb.Endpoint,
http: [port: 4002],
server: true
config :temple_demo, :sql_sandbox, true
config :wallaby,
base_url: "http://localhost:4002",
otp_app: :temple_demo,
screenshot_on_failure: true
# Print only warnings and errors during test
config :logger, level: :warn

View File

@ -0,0 +1,9 @@
defmodule TempleDemo do
@moduledoc """
TempleDemo keeps the contexts that define your domain
and business logic.
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
end

View File

@ -0,0 +1,34 @@
defmodule TempleDemo.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
def start(_type, _args) do
children = [
# Start the Ecto repository
TempleDemo.Repo,
# Start the Telemetry supervisor
TempleDemoWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: TempleDemo.PubSub},
# Start the Endpoint (http/https)
TempleDemoWeb.Endpoint
# Start a worker by calling: TempleDemo.Worker.start_link(arg)
# {TempleDemo.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: TempleDemo.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
TempleDemoWeb.Endpoint.config_change(changed, removed)
:ok
end
end

View File

@ -0,0 +1,104 @@
defmodule TempleDemo.Blog do
@moduledoc """
The Blog context.
"""
import Ecto.Query, warn: false
alias TempleDemo.Repo
alias TempleDemo.Blog.Post
@doc """
Returns the list of posts.
## Examples
iex> list_posts()
[%Post{}, ...]
"""
def list_posts do
Repo.all(Post)
end
@doc """
Gets a single post.
Raises `Ecto.NoResultsError` if the Post does not exist.
## Examples
iex> get_post!(123)
%Post{}
iex> get_post!(456)
** (Ecto.NoResultsError)
"""
def get_post!(id), do: Repo.get!(Post, id)
@doc """
Creates a post.
## Examples
iex> create_post(%{field: value})
{:ok, %Post{}}
iex> create_post(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_post(attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a post.
## Examples
iex> update_post(post, %{field: new_value})
{:ok, %Post{}}
iex> update_post(post, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_post(%Post{} = post, attrs) do
post
|> Post.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a post.
## Examples
iex> delete_post(post)
{:ok, %Post{}}
iex> delete_post(post)
{:error, %Ecto.Changeset{}}
"""
def delete_post(%Post{} = post) do
Repo.delete(post)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking post changes.
## Examples
iex> change_post(post)
%Ecto.Changeset{data: %Post{}}
"""
def change_post(%Post{} = post, attrs \\ %{}) do
Post.changeset(post, attrs)
end
end

View File

@ -0,0 +1,20 @@
defmodule TempleDemo.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :author, :string
field :body, :string
field :published_at, :naive_datetime
field :title, :string
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body, :published_at, :author])
|> validate_required([:title, :body, :published_at, :author])
end
end

View File

@ -0,0 +1,5 @@
defmodule TempleDemo.Repo do
use Ecto.Repo,
otp_app: :temple_demo,
adapter: Ecto.Adapters.Postgres
end

View File

@ -0,0 +1,80 @@
defmodule TempleDemoWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, views, channels and so on.
This can be used in your application as:
use TempleDemoWeb, :controller
use TempleDemoWeb, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define any helper function in modules
and import those modules here.
"""
def controller do
quote do
use Phoenix.Controller, namespace: TempleDemoWeb
import Plug.Conn
import TempleDemoWeb.Gettext
alias TempleDemoWeb.Router.Helpers, as: Routes
end
end
def view do
quote do
use Phoenix.View,
root: "lib/temple_demo_web/templates",
namespace: TempleDemoWeb
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
# Include shared imports and aliases for views
unquote(view_helpers())
end
end
def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
end
end
def channel do
quote do
use Phoenix.Channel
import TempleDemoWeb.Gettext
end
end
defp view_helpers do
quote do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
import TempleDemoWeb.ErrorHelpers
import TempleDemoWeb.Gettext
alias TempleDemoWeb.Router.Helpers, as: Routes
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

View File

@ -0,0 +1,35 @@
defmodule TempleDemoWeb.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", TempleDemoWeb.RoomChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# TempleDemoWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
@impl true
def id(_socket), do: nil
end

View File

@ -0,0 +1,7 @@
defmodule TempleDemoWeb.PageController do
use TempleDemoWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end

View File

@ -0,0 +1,63 @@
defmodule TempleDemoWeb.PostController do
use TempleDemoWeb, :controller
alias TempleDemo.Blog
alias TempleDemo.Blog.Post
def index(conn, _params) do
posts = Blog.list_posts()
render(conn, "index.html", posts: posts)
end
def new(conn, _params) do
changeset = Blog.change_post(%Post{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"post" => post_params}) do
case Blog.create_post(post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Post created successfully.")
|> redirect(to: Routes.post_path(conn, :show, post))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
post = Blog.get_post!(id)
render(conn, "show.html", post: post)
end
def edit(conn, %{"id" => id}) do
post = Blog.get_post!(id)
changeset = Blog.change_post(post)
render(conn, "edit.html", post: post, changeset: changeset)
end
def update(conn, %{"id" => id, "post" => post_params}) do
post = Blog.get_post!(id)
case Blog.update_post(post, post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Post updated successfully.")
|> redirect(to: Routes.post_path(conn, :show, post))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", post: post, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
post = Blog.get_post!(id)
{:ok, _post} = Blog.delete_post(post)
conn
|> put_flash(:info, "Post deleted successfully.")
|> redirect(to: Routes.post_path(conn, :index))
end
end

View File

@ -0,0 +1,59 @@
defmodule TempleDemoWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :temple_demo
if Application.get_env(:temple_demo, :sql_sandbox) do
plug Phoenix.Ecto.SQL.Sandbox
end
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@session_options [
store: :cookie,
key: "_temple_demo_key",
signing_salt: "p72rbvlQ"
]
socket "/socket", TempleDemoWeb.UserSocket,
websocket: true,
longpoll: false
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/",
from: :temple_demo,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :temple_demo
end
plug Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug TempleDemoWeb.Router
end

View File

@ -0,0 +1,24 @@
defmodule TempleDemoWeb.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
import TempleDemoWeb.Gettext
# Simple translation
gettext("Here is the string to translate")
# Plural translation
ngettext("Here is the string to translate",
"Here are the strings to translate",
3)
# Domain-based translation
dgettext("errors", "Here is the error message to translate")
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
use Gettext, otp_app: :temple_demo
end

View File

@ -0,0 +1,43 @@
defmodule TempleDemoWeb.Router do
use TempleDemoWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", TempleDemoWeb do
pipe_through :browser
get "/", PageController, :index
resources "/posts", PostController
end
# Other scopes may use custom stacks.
# scope "/api", TempleDemoWeb do
# pipe_through :api
# end
# Enables LiveDashboard only for development
#
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
if Mix.env() in [:dev, :test] do
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through :browser
live_dashboard "/dashboard", metrics: TempleDemoWeb.Telemetry
end
end
end

View File

@ -0,0 +1,53 @@
defmodule TempleDemoWeb.Telemetry do
use Supervisor
import Telemetry.Metrics
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
# Add reporters as children of your supervision tree.
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
# Database Metrics
summary("temple_demo.repo.query.total_time", unit: {:native, :millisecond}),
summary("temple_demo.repo.query.decode_time", unit: {:native, :millisecond}),
summary("temple_demo.repo.query.query_time", unit: {:native, :millisecond}),
summary("temple_demo.repo.query.queue_time", unit: {:native, :millisecond}),
summary("temple_demo.repo.query.idle_time", unit: {:native, :millisecond}),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
]
end
defp periodic_measurements do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
# {TempleDemoWeb, :count_users, []}
]
end
end

View File

@ -0,0 +1,41 @@
html lang: "en" do
head do
meta charset: "utf-8"
meta http_equiv: "X-UA-Compatible", content: "IE=edge"
meta name: "viewport", content: "width=device-width, initial-scale=1.0"
title do: "TempleDemo · Phoenix Framework"
_link(rel: "stylesheet", href: Routes.static_path(@conn, "/css/app.css"))
end
body do
header do
section class: "container" do
nav role: "navigation" do
ul do
li do
a href: "https://hexdocs.pm/phoenix/overview.html" do
"Get Started"
end
end
end
end
a href: "http://phoenixframework.org/", class: "phx-logo" do
img src: Routes.static_path(@conn, "/images/phoenix.png"),
alt: "Phoenix Framework Logo"
end
end
end
main role: "main", class: "container" do
p class: "alert alert-info", role: "alert", compact: true, do: get_flash(@conn, :info)
p class: "alert alert-danger", role: "alert", compact: true, do: get_flash(@conn, :error)
@inner_content
end
script type: "text/javascript", src: Routes.static_path(@conn, "/js/phoenix_html.js")
script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js")
end
end

View File

@ -0,0 +1,65 @@
section class: "phx-hero" do
h1 do
gettext("Welcome to %{name}!", name: "Phoenix")
end
p do
"Peace-of-mind from prototype to production"
end
end
section class: "row" do
article class: "column" do
h2 do: "Resources"
ul do
li do
a href: "https://hexdocs.pm/phoenix/overview.html" do
"Guides &amp; Docs"
end
end
li do
a href: "https://github.com/phoenixframework/phoenix" do
"Source"
end
end
li do
a href: "https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md" do
"v1.5 Changelog"
end
end
end
end
article class: "column" do
h2 do: "Help"
ul do
li do
a href: "https://elixirforum.com/c/phoenix-forum" do
"Forum"
end
end
li do
a href: "https://webchat.freenode.net/?channels=elixir-lang" do
"#elixir-lang on Freenode IRC"
end
end
li do
a href: "https://twitter.com/elixirphoenix" do
"Twitter @elixirphoenix"
end
end
li do
a href: "https://elixir-slackin.herokuapp.com/" do
"Elixir on Slack"
end
end
end
end
end

View File

@ -0,0 +1,7 @@
h1 do: "Edit Post"
render("form.html", Map.put(assigns, :action, Routes.post_path(@conn, :update, @post)))
span do
link "Back", to: Routes.post_path(@conn, :index)
end

View File

@ -0,0 +1,27 @@
form_for @changeset, @action, fn f ->
if @changeset.action do
div class: "alert alert-danger" do
p do: "Oops, something went wrong! Please check the errors below."
end
end
label f, :title
text_input f, :title
error_tag(f, :title)
label f, :body
textarea f, :body
error_tag(f, :body)
label f, :published_at
datetime_select f, :published_at
error_tag(f, :published_at)
label f, :author
text_input f, :author
error_tag(f, :author)
div do
submit "Save"
end
end

View File

@ -0,0 +1,33 @@
h1 do: "Listing Posts"
table do
thead do
tr do
th do: "Title"
th do: "Body"
th do: "Published at"
th do: "Author"
th()
end
tbody do
for post <- @posts do
tr do
td do: post.title
td do: post.body
td do: post.published_at
td do: post.author
td do
link "Show", to: Routes.post_path(@conn, :show, post)
link "Edit", to: Routes.post_path(@conn, :edit, post)
link "Delete", to: Routes.post_path(@conn, :delete, post),
method: :delete, data: [confirm: "Are you sure?"]
end
end
end
end
end
end
span do
link "New Post", to: Routes.post_path(@conn, :new)
end

View File

@ -0,0 +1,7 @@
h1 do: "New Post"
render("form.html", Map.put(assigns, :action, Routes.post_path(@conn, :create)))
span do
link "Back", to: Routes.post_path(@conn, :index)
end

View File

@ -0,0 +1,25 @@
h1 do: "Show Post"
ul do
li do: [strong(do: "Title"), @post.title]
li do
strong do: "Body"
Phoenix.HTML.Format.text_to_html @post.body, attributes: [class: "whitespace-pre"]
end
li do
strong do: "Published at"
@post.published_at
end
li do
strong do: "Author"
@post.author
end
span do
link "Edit", to: Routes.post_path(@conn, :edit, @post)
end
span do
link "Back", to: Routes.post_path(@conn, :index)
end
end

View File

@ -0,0 +1,47 @@
defmodule TempleDemoWeb.ErrorHelpers do
@moduledoc """
Conveniences for translating and building error messages.
"""
use Phoenix.HTML
@doc """
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error),
class: "invalid-feedback",
phx_feedback_for: input_id(form, field)
)
end)
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate "is invalid" in the "errors" domain
# dgettext("errors", "is invalid")
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# Because the error messages we show in our forms and APIs
# are defined inside Ecto, we need to translate them dynamically.
# This requires us to call the Gettext module passing our gettext
# backend as first argument.
#
# Note we use the "errors" domain, which means translations
# should be written to the errors.po file. The :count option is
# set by Ecto and indicates we should also apply plural rules.
if count = opts[:count] do
Gettext.dngettext(TempleDemoWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(TempleDemoWeb.Gettext, "errors", msg, opts)
end
end
end

View File

@ -0,0 +1,16 @@
defmodule TempleDemoWeb.ErrorView do
use TempleDemoWeb, :view
# If you want to customize a particular status code
# for a certain format, you may uncomment below.
# def render("500.html", _assigns) do
# "Internal Server Error"
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.html" becomes
# "Not Found".
def template_not_found(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end

View File

@ -0,0 +1,3 @@
defmodule TempleDemoWeb.LayoutView do
use TempleDemoWeb, :view
end

View File

@ -0,0 +1,3 @@
defmodule TempleDemoWeb.PageView do
use TempleDemoWeb, :view
end

View File

@ -0,0 +1,3 @@
defmodule TempleDemoWeb.PostView do
use TempleDemoWeb, :view
end

View File

@ -0,0 +1,68 @@
defmodule TempleDemo.MixProject do
use Mix.Project
def project do
[
app: :temple_demo,
version: "0.1.0",
elixir: "~> 1.7",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {TempleDemo.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.5.1"},
{:phoenix_ecto, "~> 4.1"},
{:ecto_sql, "~> 3.4"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_dashboard, "~> 0.2.0"},
{:telemetry_metrics, "~> 0.4"},
{:telemetry_poller, "~> 0.4"},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:wallaby, "~> 0.26.0", only: :test},
{:tzdata, "~> 1.0.3"},
{:temple, path: "../../"}
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to install project dependencies and perform other setup tasks, run:
#
# $ mix setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
setup: ["deps.get", "ecto.setup"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"]
]
end
end

View File

@ -0,0 +1,42 @@
%{
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"},
"ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.5.2", "7ba05d6cb0024eefd3cb08b176e6f041a9edff094912de2f6a49e3ba67140fb3", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [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", "3047022367d415a935dceda1176e67d9c7f2d41cd52a0419b53cfca66fc4c64e"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.4", "3080e8a89bab3ec08d4dd9a6858dfa24af9334464aae78c83e58a2db37c6f983", [:mix], [{:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.12.0 or ~> 0.13.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1c89595ef60f1b76ac07705e73f001823af451491792a4b0d5b2b2a3789b0a00"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.2", "38d94c30df5e2ef11000697a4fbe2b38d0fbf79239d492ff1be87bbc33bc3a84", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "a3dec3d28ddb5476c96a7c8a38ea8437923408bc88da43e5c45d97037b396280"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.13.0", "dec006b3da4ab164283d5bebe960724eb4d19cd0ed553e05fb99b260233e200f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.17 or ~> 1.5.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "bd6f13b666fa9bfeca88b013db20414c693d5a5e6d19b1fc2602c282d626ed8e"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [: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", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},
"plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.5.0", "1b796e74add83abf844e808564275dfb342bcc930b04c7577ab780e262b0d998", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31225e6ce7a37a421a0a96ec55244386aec1c190b22578bd245188a4a33298fd"},
"telemetry_poller": {:hex, :telemetry_poller, "0.5.0", "4770888ef85599ead39c7f51d6b4b62306e602d96c69b2625d54dea3d9a5204b", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69e4e8e65b0ae077c9e14cd5f42c7cc486de0e07ac6e3409e6f0e52699a7872c"},
"tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"},
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
"wallaby": {:hex, :wallaby, "0.26.0", "170b05b2fe572ec38071dbe45a908123959d5245f389f657e9a79eb463dc0431", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:web_driver_client, "~> 0.1.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "07a437e75c9276900288e4fe5c1814a5486f10a54940aa524ea65ce22b40c182"},
"web_driver_client": {:hex, :web_driver_client, "0.1.0", "19466a989c76b7ec803c796cec0fec4611a64f445fd5120ce50c9e3817e09c2c", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "c9c031ca915e8fc75b5e24ac93503244f3cc406dd7f53047087a45aa62d60e9e"},
}

View File

@ -0,0 +1,97 @@
## `msgid`s in this file come from POT (.pot) files.
##
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""

View File

@ -0,0 +1,95 @@
## This is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here has no
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""

View File

@ -0,0 +1,4 @@
[
import_deps: [:ecto_sql],
inputs: ["*.exs"]
]

View File

@ -0,0 +1,15 @@
defmodule TempleDemo.Repo.Migrations.CreatePosts do
use Ecto.Migration
def change do
create table(:posts) do
add :title, :string
add :body, :text
add :published_at, :naive_datetime
add :author, :string
timestamps()
end
end
end

View File

@ -0,0 +1,11 @@
# Script for populating the database. You can run it as:
#
# mix run priv/repo/seeds.exs
#
# Inside the script, you can read and write to any of your
# repositories directly:
#
# TempleDemo.Repo.insert!(%TempleDemo.SomeSchema{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.

View File

@ -0,0 +1,40 @@
/* This file is for your main application css. */
@import "./phoenix.css";
.invalid-feedback {
color: red;
}
.alert:empty {
display: none;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.alert p {
margin-bottom: 0;
}
strong {
font-weight: 700;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,3 @@
// for phoenix_html support, including form and button helpers
// copy the following scripts into your javascript bundle:
// * deps/phoenix_html/priv/static/phoenix_html.js

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,76 @@
"use strict";
(function() {
var PolyfillEvent = eventConstructor();
function eventConstructor() {
if (typeof window.CustomEvent === "function") return window.CustomEvent;
// IE<=9 Support
function CustomEvent(event, params) {
params = params || {bubbles: false, cancelable: false, detail: undefined};
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
}
CustomEvent.prototype = window.Event.prototype;
return CustomEvent;
}
function buildHiddenInput(name, value) {
var input = document.createElement("input");
input.type = "hidden";
input.name = name;
input.value = value;
return input;
}
function handleClick(element) {
var to = element.getAttribute("data-to"),
method = buildHiddenInput("_method", element.getAttribute("data-method")),
csrf = buildHiddenInput("_csrf_token", element.getAttribute("data-csrf")),
form = document.createElement("form"),
target = element.getAttribute("target");
form.method = (element.getAttribute("data-method") === "get") ? "get" : "post";
form.action = to;
form.style.display = "hidden";
if (target) form.target = target;
form.appendChild(csrf);
form.appendChild(method);
document.body.appendChild(form);
form.submit();
}
window.addEventListener("click", function(e) {
var element = e.target;
while (element && element.getAttribute) {
var phoenixLinkEvent = new PolyfillEvent('phoenix.link.click', {
"bubbles": true, "cancelable": true
});
if (!element.dispatchEvent(phoenixLinkEvent)) {
e.preventDefault();
e.stopImmediatePropagation();
return false;
}
if (element.getAttribute("data-method")) {
handleClick(element);
e.preventDefault();
return false;
} else {
element = element.parentNode;
}
}
}, false);
window.addEventListener('phoenix.link.click', function (e) {
var message = e.target.getAttribute("data-confirm");
if(message && !window.confirm(message)) {
e.preventDefault();
}
}, false);
})();

View File

@ -0,0 +1,5 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /

View File

@ -0,0 +1,40 @@
defmodule TempleDemoWeb.ChannelCase do
@moduledoc """
This module defines the test case to be used by
channel tests.
Such tests rely on `Phoenix.ChannelTest` and also
import other functionality to make it easier
to build common data structures and query the data layer.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use TempleDemoWeb.ChannelCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with channels
import Phoenix.ChannelTest
import TempleDemoWeb.ChannelCase
# The default endpoint for testing
@endpoint TempleDemoWeb.Endpoint
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(TempleDemo.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, {:shared, self()})
end
:ok
end
end

View File

@ -0,0 +1,43 @@
defmodule TempleDemoWeb.ConnCase do
@moduledoc """
This module defines the test case to be used by
tests that require setting up a connection.
Such tests rely on `Phoenix.ConnTest` and also
import other functionality to make it easier
to build common data structures and query the data layer.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use TempleDemoWeb.ConnCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
import TempleDemoWeb.ConnCase
alias TempleDemoWeb.Router.Helpers, as: Routes
# The default endpoint for testing
@endpoint TempleDemoWeb.Endpoint
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(TempleDemo.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, {:shared, self()})
end
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end

View File

@ -0,0 +1,55 @@
defmodule TempleDemo.DataCase do
@moduledoc """
This module defines the setup for tests requiring
access to the application's data layer.
You may define functions here to be used as helpers in
your tests.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use TempleDemo.DataCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
alias TempleDemo.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import TempleDemo.DataCase
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(TempleDemo.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, {:shared, self()})
end
:ok
end
@doc """
A helper that transforms changeset errors into a map of messages.
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
assert "password is too short" in errors_on(changeset).password
assert %{password: ["password is too short"]} = errors_on(changeset)
"""
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
end

View File

@ -0,0 +1,70 @@
defmodule TempleDemo.BlogTest do
use TempleDemo.DataCase
alias TempleDemo.Blog
describe "posts" do
alias TempleDemo.Blog.Post
@valid_attrs %{author: "some author", body: "some body", published_at: ~N[2010-04-17 14:00:00], title: "some title"}
@update_attrs %{author: "some updated author", body: "some updated body", published_at: ~N[2011-05-18 15:01:01], title: "some updated title"}
@invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil}
def post_fixture(attrs \\ %{}) do
{:ok, post} =
attrs
|> Enum.into(@valid_attrs)
|> Blog.create_post()
post
end
test "list_posts/0 returns all posts" do
post = post_fixture()
assert Blog.list_posts() == [post]
end
test "get_post!/1 returns the post with given id" do
post = post_fixture()
assert Blog.get_post!(post.id) == post
end
test "create_post/1 with valid data creates a post" do
assert {:ok, %Post{} = post} = Blog.create_post(@valid_attrs)
assert post.author == "some author"
assert post.body == "some body"
assert post.published_at == ~N[2010-04-17 14:00:00]
assert post.title == "some title"
end
test "create_post/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Blog.create_post(@invalid_attrs)
end
test "update_post/2 with valid data updates the post" do
post = post_fixture()
assert {:ok, %Post{} = post} = Blog.update_post(post, @update_attrs)
assert post.author == "some updated author"
assert post.body == "some updated body"
assert post.published_at == ~N[2011-05-18 15:01:01]
assert post.title == "some updated title"
end
test "update_post/2 with invalid data returns error changeset" do
post = post_fixture()
assert {:error, %Ecto.Changeset{}} = Blog.update_post(post, @invalid_attrs)
assert post == Blog.get_post!(post.id)
end
test "delete_post/1 deletes the post" do
post = post_fixture()
assert {:ok, %Post{}} = Blog.delete_post(post)
assert_raise Ecto.NoResultsError, fn -> Blog.get_post!(post.id) end
end
test "change_post/1 returns a post changeset" do
post = post_fixture()
assert %Ecto.Changeset{} = Blog.change_post(post)
end
end
end

View File

@ -0,0 +1,8 @@
defmodule TempleDemoWeb.PageControllerTest do
use TempleDemoWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "Welcome to Phoenix!"
end
end

View File

@ -0,0 +1,88 @@
defmodule TempleDemoWeb.PostControllerTest do
use TempleDemoWeb.ConnCase
alias TempleDemo.Blog
@create_attrs %{author: "some author", body: "some body", published_at: ~N[2010-04-17 14:00:00], title: "some title"}
@update_attrs %{author: "some updated author", body: "some updated body", published_at: ~N[2011-05-18 15:01:01], title: "some updated title"}
@invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil}
def fixture(:post) do
{:ok, post} = Blog.create_post(@create_attrs)
post
end
describe "index" do
test "lists all posts", %{conn: conn} do
conn = get(conn, Routes.post_path(conn, :index))
assert html_response(conn, 200) =~ "Listing Posts"
end
end
describe "new post" do
test "renders form", %{conn: conn} do
conn = get(conn, Routes.post_path(conn, :new))
assert html_response(conn, 200) =~ "New Post"
end
end
describe "create post" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, Routes.post_path(conn, :create), post: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == Routes.post_path(conn, :show, id)
conn = get(conn, Routes.post_path(conn, :show, id))
assert html_response(conn, 200) =~ "Show Post"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, Routes.post_path(conn, :create), post: @invalid_attrs)
assert html_response(conn, 200) =~ "New Post"
end
end
describe "edit post" do
setup [:create_post]
test "renders form for editing chosen post", %{conn: conn, post: post} do
conn = get(conn, Routes.post_path(conn, :edit, post))
assert html_response(conn, 200) =~ "Edit Post"
end
end
describe "update post" do
setup [:create_post]
test "redirects when data is valid", %{conn: conn, post: post} do
conn = put(conn, Routes.post_path(conn, :update, post), post: @update_attrs)
assert redirected_to(conn) == Routes.post_path(conn, :show, post)
conn = get(conn, Routes.post_path(conn, :show, post))
assert html_response(conn, 200) =~ "some updated author"
end
test "renders errors when data is invalid", %{conn: conn, post: post} do
conn = put(conn, Routes.post_path(conn, :update, post), post: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Post"
end
end
describe "delete post" do
setup [:create_post]
test "deletes chosen post", %{conn: conn, post: post} do
conn = delete(conn, Routes.post_path(conn, :delete, post))
assert redirected_to(conn) == Routes.post_path(conn, :index)
assert_error_sent 404, fn ->
get(conn, Routes.post_path(conn, :show, post))
end
end
end
defp create_post(_) do
post = fixture(:post)
{:ok, post: post}
end
end

View File

@ -0,0 +1,32 @@
defmodule TempleDemoWeb.TempleFeatureTest do
use ExUnit.Case, async: false
use Wallaby.Feature
alias TempleDemoWeb.Router.Helpers, as: Routes
alias TempleDemoWeb.Endpoint, as: E
feature "renders the homepage", %{session: session} do
session
|> visit("/")
|> assert_text("Welcome to Phoenix!")
end
feature "can create a new post", %{session: session} do
session
|> visit(Routes.post_path(E, :index))
|> click(Query.link("New Post"))
|> fill_in(Query.text_field("Title"), with: "Temple is awesome!")
|> fill_in(Query.text_field("Body"), with: "In this post I will show you how to use Temple")
|> find(Query.select("post_published_at_year"), fn s ->
s |> click(Query.option("2020"))
end)
|> find(Query.select("post_published_at_month"), fn s ->
s |> click(Query.option("May"))
end)
|> find(Query.select("post_published_at_day"), fn s ->
s |> click(Query.option("21"))
end)
|> fill_in(Query.text_field("Author"), with: "Mitchelob Ultra")
|> click(Query.button("Save"))
|> assert_text("Post created successfully.")
end
end

View File

@ -0,0 +1,14 @@
defmodule TempleDemoWeb.ErrorViewTest do
use TempleDemoWeb.ConnCase, async: true
# Bring render/3 and render_to_string/3 for testing custom views
import Phoenix.View
test "renders 404.html" do
assert render_to_string(TempleDemoWeb.ErrorView, "404.html", []) == "Not Found"
end
test "renders 500.html" do
assert render_to_string(TempleDemoWeb.ErrorView, "500.html", []) == "Internal Server Error"
end
end

View File

@ -0,0 +1,8 @@
defmodule TempleDemoWeb.LayoutViewTest do
use TempleDemoWeb.ConnCase, async: true
# When testing helpers, you may want to import Phoenix.HTML and
# use functions such as safe_to_string() to convert the helper
# result into an HTML string.
# import Phoenix.HTML
end

View File

@ -0,0 +1,3 @@
defmodule TempleDemoWeb.PageViewTest do
use TempleDemoWeb.ConnCase, async: true
end

View File

@ -0,0 +1,3 @@
Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, :manual)
ExUnit.start()

32
lib/buffer.ex Normal file
View File

@ -0,0 +1,32 @@
defmodule Temple.Buffer do
@moduledoc false
def start_link(state \\ []) do
Agent.start_link(fn -> state end)
end
def put(buffer, value) do
Agent.update(buffer, fn b -> [value | b] end)
end
def remove_new_line(buffer) do
Agent.update(buffer, fn
["\n" | rest] ->
rest
rest ->
rest
end)
end
def get(buffer) do
buffer
|> Agent.get(& &1)
|> Enum.reverse()
|> Enum.join()
|> String.trim()
end
def stop(buffer) do
Agent.stop(buffer)
end
end

View File

@ -1,23 +0,0 @@
defmodule Mix.Tasks.Temple.Convert do
use Mix.Task
@preferred_cli_env :dev
@shortdoc "Converts HTML to Temple syntax"
@moduledoc """
Converts HTML to Temple syntax
Takes HTML from a file or from stdin and outputs temple syntax to stdout.
"""
def run(args) do
html =
if Enum.count(args) > 0 do
args |> List.first() |> File.read!()
else
IO.read(:stdio, :all)
end
{:ok, result} = Temple.HtmlToTemple.parse(html)
IO.write(result)
end
end

View File

@ -213,51 +213,51 @@ if Code.ensure_loaded?(Mix.Phoenix) do
{nil, nil, nil}
{key, :integer} ->
{label(key), ~s(number_input form, #{inspect(key)}), error(key)}
{label(key), ~s(number_input f, #{inspect(key)}), error(key)}
{key, :float} ->
{label(key), ~s(number_input form, #{inspect(key)}, step: "any"), error(key)}
{label(key), ~s(number_input f, #{inspect(key)}, step: "any"), error(key)}
{key, :decimal} ->
{label(key), ~s(number_input form, #{inspect(key)}, step: "any"), error(key)}
{label(key), ~s(number_input f, #{inspect(key)}, step: "any"), error(key)}
{key, :boolean} ->
{label(key), ~s(checkbox form, #{inspect(key)}), error(key)}
{label(key), ~s(checkbox f, #{inspect(key)}), error(key)}
{key, :text} ->
{label(key), ~s(textarea form, #{inspect(key)}), error(key)}
{label(key), ~s(textarea f, #{inspect(key)}), error(key)}
{key, :date} ->
{label(key), ~s(date_select form, #{inspect(key)}), error(key)}
{label(key), ~s(date_select f, #{inspect(key)}), error(key)}
{key, :time} ->
{label(key), ~s(time_select form, #{inspect(key)}), error(key)}
{label(key), ~s(time_select f, #{inspect(key)}), error(key)}
{key, :utc_datetime} ->
{label(key), ~s(datetime_select form, #{inspect(key)}), error(key)}
{label(key), ~s(datetime_select f, #{inspect(key)}), error(key)}
{key, :naive_datetime} ->
{label(key), ~s(datetime_select form, #{inspect(key)}), error(key)}
{label(key), ~s(datetime_select f, #{inspect(key)}), error(key)}
{key, {:array, :integer}} ->
{label(key), ~s(multiple_select form, #{inspect(key)}, ["1": 1, "2": 2]), error(key)}
{label(key), ~s(multiple_select f, #{inspect(key)}, ["1": 1, "2": 2]), error(key)}
{key, {:array, _}} ->
{label(key),
~s(multiple_select form, #{inspect(key)}, ["Option 1": "option1", "Option 2": "option2"]),
~s(multiple_select f, #{inspect(key)}, ["Option 1": "option1", "Option 2": "option2"]),
error(key)}
{key, _} ->
{label(key), ~s(text_input form, #{inspect(key)}), error(key)}
{label(key), ~s(text_input f, #{inspect(key)}), error(key)}
end)
end
defp label(key) do
~s(phx_label form, #{inspect(key)})
~s(label f, #{inspect(key)})
end
defp error(field) do
~s{partial error_tag(form, #{inspect(field)})}
~s{error_tag(f, #{inspect(field)})}
end
end
end

View File

@ -1,182 +1,423 @@
defmodule Temple do
alias Temple.Buffer
@moduledoc """
> Warning: Docs are WIP
Temple syntax is available inside the `temple` and `live_temple` macros, and is compiled into EEx at build time.
### Usage
```elixir
temple do
# You can define attributes by passing a keyword list to the element, the values can be literals or variables.
class = "text-blue"
id = "jumbotron"
div class: class, id: id do
# Text nodes can be emitted as string literals or variables.
"Bob"
id
end
# if and unless expressions can be used to conditionally render content
if 5 > 0 do
p do
"Greater than 0!"
end
end
unless 5 > 0 do
p do
"Less than 0!"
end
end
# You can loop over items using for comprehensions
for x <- 0..5 do
div do
x
end
end
# You can use multiline anonymous functions, like if you're building a form in Phoenix
form_for @changeset, Routes.user_path(@conn, :create), fn f ->
"Name: "
text_input f, :name
end
# You can explicitly call a tag by prefixing with the Temple module
Temple.div do
"Foo"
end
# You can also pass children as a do key instead of a block
div do: "Alice", class: "text-yellow"
end
```
### Reserved keywords
You can pass a keyword list to an element as element attributes, but there are several reserved keywords.
#### Compact
Passing `compact: true` will not rendering new lines from within the element. This is useful if you are trying to use the `:empty` psuedo selector.
```elixir
temple do
p compact: true do
"Foo"
end
p do
"Bar"
end
end
```
would evaluate to
```html
<p>Foo</p>
<p>
Bar
</p>
```
### Configuration
#### Aliases
You can add an alias for an element if there is a namespace collision with a function. If you are using `Phoenix.HTML`, there will be namespace collisions with the `<link>` and `<label>` elements.
```elixir
config :temple, :aliases,
label: :_label,
link: :_link
temple do
_label do
"Email"
end
_link href: "/css/site.css"
end
```
This will result in:
```html
<label>
Email
</label>
<link href="/css/site.css">
```
"""
defmacro __using__(_) do
quote location: :keep do
import Temple
end
end
@doc """
Creates a markup context.
defmodule Private do
@moduledoc false
@aliases Application.get_env(:temple, :aliases, [])
All tags must be called inside of a `Temple.temple/1` block.
@nonvoid_elements ~w[
head title style script
noscript template
body section nav article aside h1 h2 h3 h4 h5 h6
header footer address main
p pre blockquote ol ul li dl dt dd figure figcaption div
a em strong small s cite q dfn abbr data time code var samp kbd
sub sup i b u mark ruby rt rp bdi bdo span
ins del
iframe object video audio canvas
map
table caption colgroup tbody thead tfoot tr td th
form fieldset legend label button select datalist optgroup
option textarea output progress meter
details summary menuitem menu
html
]a
Returns a safe result of the form `{:safe, result}`
@nonvoid_elements_aliases Enum.map(@nonvoid_elements, fn el ->
Keyword.get(@aliases, el, el)
end)
@nonvoid_elements_lookup Enum.map(@nonvoid_elements, fn el ->
{Keyword.get(@aliases, el, el), el}
end)
## Example
@void_elements ~w[
meta link base
area br col embed hr img input keygen param source track wbr
]a
```
team = ["Alice", "Bob", "Carol"]
@void_elements_aliases Enum.map(@void_elements, fn el -> Keyword.get(@aliases, el, el) end)
@void_elements_lookup Enum.map(@void_elements, fn el ->
{Keyword.get(@aliases, el, el), el}
end)
temple do
for name <- team do
div class: "text-bold" do
text name
end
def snake_to_kebab(stringable),
do:
stringable |> to_string() |> String.replace_trailing("_", "") |> String.replace("_", "-")
def kebab_to_snake(stringable),
do: stringable |> to_string() |> String.replace("-", "_")
def compile_attrs([]), do: ""
def compile_attrs([attrs]) when is_list(attrs) do
compile_attrs(attrs)
end
end
# {:safe, "<div class=\"text-bold\">Alice</div><div class=\"text-bold\">Bob</div><div class=\"text-bold\">Carol</div>"}
```
"""
defmacro temple([do: block] = _block) do
quote location: :keep do
import Kernel, except: [div: 2, use: 1, use: 2]
import Temple.Html
import Temple.Svg
import Temple.Form
import Temple.Link
def compile_attrs(attrs) do
for {name, value} <- attrs, into: "" do
name = snake_to_kebab(name)
with {:ok, var!(buff, Temple.Html)} <- Temple.Utils.start_buffer([]) do
unquote(block)
case value do
{_, _, _} = macro ->
" " <> name <> "=\"<%= " <> Macro.to_string(macro) <> " %>\""
markup = Temple.Utils.get_buffer(var!(buff, Temple.Html))
:ok = Temple.Utils.stop_buffer(var!(buff, Temple.Html))
Temple.Utils.join_and_escape(markup)
end
end
end
@doc """
Emits a text node into the markup.
```
temple do
div do
text "Hello, world!"
end
end
# {:safe, "<div>Hello, world!</div>"}
```
"""
defmacro text(text) do
quote location: :keep do
Temple.Utils.put_buffer(
var!(buff, Temple.Html),
unquote(text) |> Temple.Utils.escape_content()
)
end
end
@doc """
Emits a Phoenix partial into the markup.
```
temple do
html lang: "en" do
head do
title "MyApp"
link rel: "stylesheet", href: Routes.static_path(@conn, "/css/app.css")
end
body do
main role: "main", class: "container" do
p get_flash(@conn, :info), class: "alert alert-info", role: "alert"
p get_flash(@conn, :error), class: "alert alert-danger", role: "alert"
partial render(@view_module, @view_template, assigns)
value ->
" " <> name <> "=\"" <> to_string(value) <> "\""
end
script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js")
end
end
def split_args(nil), do: {[], []}
def split_args(args) do
{do_and_else, args} =
args
|> Enum.split_with(fn
arg when is_list(arg) ->
(Keyword.keys(arg) -- [:do, :else]) |> Enum.count() == 0
_ ->
false
end)
{List.flatten(do_and_else), args}
end
def split_on_fn([{:fn, _, _} = func | rest], {args, _, args2}) do
split_on_fn(rest, {args, func, args2})
end
def split_on_fn([arg | rest], {args, nil, args2}) do
split_on_fn(rest, {[arg | args], nil, args2})
end
def split_on_fn([arg | rest], {args, func, args2}) do
split_on_fn(rest, {args, func, [arg | args2]})
end
def split_on_fn([], {args, func, args2}) do
{Enum.reverse(args), func, Enum.reverse(args2)}
end
def pop_compact?([]), do: {false, []}
def pop_compact?([args]) when is_list(args), do: pop_compact?(args)
def pop_compact?(args) do
Keyword.pop(args, :compact, false)
end
def traverse(buffer, {:__block__, _meta, block}) do
traverse(buffer, block)
end
def traverse(buffer, {name, meta, args} = macro) do
{do_and_else, args} =
args
|> split_args()
includes_fn? = args |> Enum.any?(fn x -> match?({:fn, _, _}, x) end)
case name do
{:., _, [{:__aliases__, _, [:Temple]}, name]} when name in @nonvoid_elements_aliases ->
{do_and_else, args} =
case args do
[args] ->
{do_value, args} = Keyword.pop(args, :do)
do_and_else = Keyword.put_new(do_and_else, :do, do_value)
{do_and_else, args}
_ ->
{do_and_else, args}
end
name = @nonvoid_elements_lookup[name]
{compact?, args} = pop_compact?(args)
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
unless compact?, do: Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
if compact?, do: Buffer.remove_new_line(buffer)
Buffer.put(buffer, "</#{name}>")
Buffer.put(buffer, "\n")
{:., _, [{:__aliases__, _, [:Temple]}, name]} when name in @void_elements_aliases ->
name = @void_elements_lookup[name]
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
Buffer.put(buffer, "\n")
name when name in @nonvoid_elements_aliases ->
{do_and_else, args} =
case args do
[args] ->
{do_value, args} = Keyword.pop(args, :do)
do_and_else = Keyword.put_new(do_and_else, :do, do_value)
{do_and_else, args}
_ ->
{do_and_else, args}
end
name = @nonvoid_elements_lookup[name]
{compact?, args} = pop_compact?(args)
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
unless compact?, do: Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
if compact?, do: Buffer.remove_new_line(buffer)
Buffer.put(buffer, "</#{name}>")
Buffer.put(buffer, "\n")
name when name in @void_elements_aliases ->
name = @void_elements_lookup[name]
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
Buffer.put(buffer, "\n")
name when includes_fn? ->
{args, func_arg, args2} = split_on_fn(args, {[], nil, []})
{func, _, [{arrow, _, [[{arg, _, _}], block]}]} = func_arg
Buffer.put(
buffer,
"<%= " <>
to_string(name) <>
" " <>
(Enum.map(args, &Macro.to_string(&1)) |> Enum.join(", ")) <>
", " <>
to_string(func) <> " " <> to_string(arg) <> " " <> to_string(arrow) <> " %>"
)
Buffer.put(buffer, "\n")
traverse(buffer, block)
if Enum.any?(args2) do
Buffer.put(
buffer,
"<% end, " <>
(Enum.map(args2, fn arg -> Macro.to_string(arg) end)
|> Enum.join(", ")) <> " %>"
)
Buffer.put(buffer, "\n")
else
Buffer.put(buffer, "<% end %>")
Buffer.put(buffer, "\n")
end
name when name in [:for, :if, :unless] ->
Buffer.put(buffer, "<%= " <> Macro.to_string({name, meta, args}) <> " do %>")
Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
if Keyword.has_key?(do_and_else, :else) do
Buffer.put(buffer, "<% else %>")
Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:else])
end
Buffer.put(buffer, "<% end %>")
Buffer.put(buffer, "\n")
name when name in [:=] ->
Buffer.put(buffer, "<% " <> Macro.to_string(macro) <> " %>")
Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
_ ->
Buffer.put(buffer, "<%= " <> Macro.to_string(macro) <> " %>")
Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
end
end
def traverse(buffer, [first | rest]) do
traverse(buffer, first)
traverse(buffer, rest)
end
def traverse(buffer, text) when is_binary(text) do
Buffer.put(buffer, text)
Buffer.put(buffer, "\n")
end
def traverse(_buffer, arg) when arg in [nil, []] do
nil
end
end
```
"""
defmacro partial(partial) do
defmacro temple([do: block] = _block) do
{:ok, buffer} = Buffer.start_link()
buffer
|> Temple.Private.traverse(block)
markup = Buffer.get(buffer)
Buffer.stop(buffer)
quote location: :keep do
Temple.Utils.put_buffer(
var!(buff, Temple.Html),
unquote(partial) |> Temple.Utils.from_safe()
)
unquote(markup)
end
end
@doc """
Defines a custom component.
Components are the primary way to extract partials and markup helpers.
## Assigns
Components accept a keyword list or a map of assigns and can be referenced in the body of the component by a module attribute of the same name.
This works exactly the same as EEx templates. The whole list or map of assigns can be accessed by a special assign called `@assigns`.
## Children
If a block is passed to the component, it can be referenced by a special assign called `@children`.
## Example
```
defcomponent :flex do
div id: @id, class: "flex" do
@children
end
end
temple do
flex id: "my-flex" do
div "Item 1"
div "Item 2"
div "Item 3"
end
end
# {:safe, "<div id=\"my-flex\" class=\"flex\">
# <div>Item 1</div>
# <div>Item 2</div>
# <div>Item 3</div>
# </div>"}
```
"""
defmacro defcomponent(name, [do: _] = block) do
defmacro temple(block) do
quote location: :keep do
defmacro unquote(name)() do
outer = unquote(Macro.escape(block))
import Temple
Temple.Utils.__quote__(outer)
end
{:ok, buffer} = Buffer.start_link()
defmacro unquote(name)(assigns_or_block)
buffer
|> Temple.Private.traverse(unquote(block))
defmacro unquote(name)([{:do, inner}]) do
outer =
unquote(Macro.escape(block))
|> Temple.Utils.__insert_assigns__([], inner)
markup = Buffer.get(buffer)
Temple.Utils.__quote__(outer)
end
Buffer.stop(buffer)
defmacro unquote(name)(assigns) do
outer =
unquote(Macro.escape(block))
|> Temple.Utils.__insert_assigns__(assigns, nil)
Temple.Utils.__quote__(outer)
end
defmacro unquote(name)(assigns, inner) do
outer =
unquote(Macro.escape(block))
|> Temple.Utils.__insert_assigns__(assigns, inner)
Temple.Utils.__quote__(outer)
end
markup
end
end
defmacro live_temple([do: block] = _block) do
{:ok, buffer} = Buffer.start_link()
buffer
|> Temple.Private.traverse(block)
markup = Buffer.get(buffer)
Buffer.stop(buffer)
EEx.compile_string(markup, engine: Phoenix.LiveView.Engine)
end
end

View File

@ -1,112 +0,0 @@
defmodule Temple.Elements do
@moduledoc """
This module contains the primitives used to generate the macros in the `Temple.Html` and `Temple.Svg` modules.
"""
@doc """
Defines an element.
*Note*: Underscores are converted to dashes.
```elixir
defmodule MyElements do
import Temple.Elements
defelement :super_select, :nonvoid # <super-select></super-select>
defelement :super_input, :void # <super-input>
end
```
"""
defmacro defelement(name, type)
defmacro defelement(name, :nonvoid) do
quote location: :keep do
defmacro unquote(name)() do
Temple.Elements.nonvoid_element(unquote(name))
end
@doc false
defmacro unquote(name)(attrs_or_content_or_block)
defmacro unquote(name)([{:do, _inner}] = block) do
Temple.Elements.nonvoid_element(unquote(name), block)
end
defmacro unquote(name)(attrs_or_content) do
Temple.Elements.nonvoid_element(unquote(name), attrs_or_content)
end
@doc false
defmacro unquote(name)(attrs_or_content, block_or_attrs)
defmacro unquote(name)(attrs, [{:do, _inner}] = block) do
Temple.Elements.nonvoid_element(unquote(name), attrs, block)
end
defmacro unquote(name)(content, attrs) do
Temple.Elements.nonvoid_element(unquote(name), content, attrs)
end
end
end
defmacro defelement(name, :void) do
quote location: :keep do
defmacro unquote(name)(attrs \\ []) do
Temple.Elements.void_element(unquote(name), attrs)
end
end
end
@doc false
def nonvoid_element(el) do
quote location: :keep do
Temple.Utils.put_open_tag(var!(buff, Temple.Html), unquote(el), [])
Temple.Utils.put_close_tag(var!(buff, Temple.Html), unquote(el))
end
end
@doc false
def nonvoid_element(el, attrs_or_content_or_block)
def nonvoid_element(el, [{:do, inner}]) do
quote location: :keep do
Temple.Utils.put_open_tag(var!(buff, Temple.Html), unquote(el), [])
_ = unquote(inner)
Temple.Utils.put_close_tag(var!(buff, Temple.Html), unquote(el))
end
end
def nonvoid_element(el, attrs_or_content) do
quote location: :keep do
Temple.Utils.put_open_tag(var!(buff, Temple.Html), unquote(el), unquote(attrs_or_content))
Temple.Utils.put_close_tag(var!(buff, Temple.Html), unquote(el))
end
end
@doc false
def nonvoid_element(el, attrs_or_content, block_or_attrs)
def nonvoid_element(el, attrs, [{:do, inner}] = _block) do
quote location: :keep do
Temple.Utils.put_open_tag(var!(buff, Temple.Html), unquote_splicing([el, attrs]))
_ = unquote(inner)
Temple.Utils.put_close_tag(var!(buff, Temple.Html), unquote(el))
end
end
def nonvoid_element(el, content, attrs) do
quote location: :keep do
Temple.Utils.put_open_tag(var!(buff, Temple.Html), unquote_splicing([el, attrs]))
text unquote(content)
Temple.Utils.put_close_tag(var!(buff, Temple.Html), unquote(el))
end
end
@doc false
def void_element(el, attrs \\ []) do
quote location: :keep do
Temple.Utils.put_void_tag(var!(buff, Temple.Html), unquote_splicing([el, attrs]))
end
end
end

View File

@ -1,117 +1,19 @@
defmodule Temple.Engine do
@behaviour Phoenix.Template.Engine
@moduledoc """
Temple provides a templating engine for use in Phoenix web applications.
You can configure your application to use Temple templates by adding the following configuration.
```elixir
# config.exs
config :phoenix, :template_engines, exs: Temple.Engine
# config/dev.exs
config :your_app, YourAppWeb.Endpoint,
live_reload: [
patterns: [
~r"lib/your_app_web/templates/.*(exs)$"
]
]
# your_app_web.ex
def view do
quote location: :keep do
# ...
use Temple # Replaces the call to import Phoenix.HTML
end
end
```
## Usage
Temple templates use the `.exs` extension, because they are written with pure Elixir!
`assigns` (@conn, etc) are handled the same as normal `Phoenix.HTML.Engine` templates.
Note: The `Temple.temple/1` macro is _not_ needed for Temple templates due to the engine taking care of that for you.
```
# app.html.exs
html lang: "en" do
head do
meta charset: "utf-8"
meta http_equiv: "X-UA-Compatible", content: "IE=edge"
meta name: "viewport", content: "width=device-width, initial-scale=1.0"
title "YourApp · Phoenix Framework"
link rel: "stylesheet", href: Routes.static_path(@conn, "/css/app.css")
end
body do
header do
section class: "container" do
nav role: "navigation" do
ul do
li do: a("Get Started", href: "https://hexdocs.pm/phoenix/overview.html")
end
end
a href: "http://phoenixframework.org/", class: "phx-logo" do
img src: Routes.static_path(@conn, "/images/phoenix.png"),
alt: "Phoenix Framework Logo"
end
end
end
main role: "main", class: "container" do
p get_flash(@conn, :info), class: "alert alert-info", role: "alert"
p get_flash(@conn, :error), class: "alert alert-danger", role: "alert"
partial render(@view_module, @view_template, assigns)
end
script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js")
end
end
```
"""
@moduledoc false
def compile(path, _name) do
template =
path
|> File.read!()
|> Code.string_to_quoted!(file: path)
|> handle_assigns()
require Temple
quote location: :keep do
use Temple
template = path |> File.read!() |> Code.string_to_quoted!(file: path)
temple do: unquote(template)
end
end
ast =
quote do
unquote(template)
end
defp handle_assigns(quoted) do
quoted
|> Macro.prewalk(fn
{:@, _, [{key, _, _}]} ->
quote location: :keep do
case Access.fetch(var!(assigns), unquote(key)) do
{:ok, val} ->
val
:error ->
raise ArgumentError, """
assign @#{unquote(key)} not available in Temple template.
Please make sure all proper assigns have been set. If this
is a child template, ensure assigns are given explicitly by
the parent template as they are not automatically forwarded.
Available assigns: #{inspect(Enum.map(var!(assigns), &elem(&1, 0)))}
"""
end
end
ast ->
ast
end)
Temple.temple(ast)
|> EEx.compile_string(engine: Phoenix.HTML.Engine, file: path, line: 1)
end
end

View File

@ -1,304 +0,0 @@
defmodule Temple.Form do
@moduledoc """
This modules wraps all of the functions from the `Phoenix.HTML.Form` module to make them compatible with with Temple.
"""
alias Phoenix.HTML
alias Temple.Utils
@doc """
Generates an empty form tag.
See `Temple.Form.form_for/4` for more details
"""
defmacro form_for(form_data, action) do
quote location: :keep do
form_for(unquote_splicing([form_data, action]), [])
end
end
@doc """
Generates a form tag with a form builder and a block.
The form builder will be available inside the block through the `form` variable.
This is a wrapper around the `Phoenix.HTML.Form.form_for/4` function and accepts all of the same options.
## Example
```
temple do
form_for @conn, Routes.some_path(@conn, :create) do
text_input form, :name
end
end
# {:safe,
# "<form accept-charset=\"UTF-8\" action=\"/\" method=\"post\">
# <input name=\"_csrf_token\" type=\"hidden\" value=\"AS5qfX1gcns6eU56BlQgBlwCDgMlNgAAiJ0MR91Kh3v3bbCS5SKjuw==\">
# <input name=\"_utf8\" type=\"hidden\" value=\"✓\">
# <input id=\"name\" name=\"name\" type=\"text\">
# </form>"}
```
"""
defmacro form_for(form_data, action, opts \\ [], block) do
quote location: :keep do
var!(form) = HTML.Form.form_for(unquote_splicing([form_data, action, opts]))
Utils.put_buffer(var!(buff, Temple.Html), var!(form) |> HTML.Safe.to_iodata())
_ = unquote(block)
Utils.put_buffer(var!(buff, Temple.Html), "</form>")
end
end
@helpers [
:checkbox,
:color_input,
:date_input,
:date_select,
:datetime_local_input,
:datetime_select,
:email_input,
:file_input,
:hidden_input,
:number_input,
:password_input,
:range_input,
:search_input,
:telephone_input,
:text_input,
:time_input,
:time_select,
:url_input
]
for helper <- @helpers do
@doc """
Please see `Phoenix.HTML.Form.#{helper}/3` for details.
"""
defmacro unquote(helper)(form, field, opts \\ []) do
helper = unquote(helper)
quote location: :keep do
{:safe, input} =
apply(Phoenix.HTML.Form, unquote(helper), [unquote_splicing([form, field, opts])])
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
end
@doc """
Please see `Phoenix.HTML.Form.textarea/3` for details.
Note: Temple defines this function as `text_area` with an underscore, whereas Phoenix.HTML defines it as `textarea` without an underscore.
"""
defmacro text_area(form, field, opts \\ []) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.textarea(unquote_splicing([form, field, opts]))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Please see `Phoenix.HTML.Form.reset/2` for details.
"""
defmacro reset(value, opts \\ []) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.reset(unquote_splicing([value, opts]))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Please see `Phoenix.HTML.Form.submit/1` for details.
"""
defmacro submit(do: block) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.submit(do: temple(do: unquote(block)))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
defmacro submit(value) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.submit(unquote(value))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Please see `Phoenix.HTML.Form.submit/1` for details.
"""
defmacro submit(opts, do: block) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.submit(unquote(opts), do: temple(do: unquote(block)))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
defmacro submit(value, opts) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.submit(unquote_splicing([value, opts]))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Please see `Phoenix.HTML.Form.label/2` for details.
"""
defmacro phx_label(form, field) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.label(unquote_splicing([form, field]))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Please see `Phoenix.HTML.Form.label/3` for details.
"""
defmacro phx_label(form, field, do: block) do
quote location: :keep do
{:safe, input} =
Phoenix.HTML.Form.label(unquote_splicing([form, field]), do: temple(do: unquote(block)))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
defmacro phx_label(form, field, text_or_opts) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.label(unquote_splicing([form, field, text_or_opts]))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Please see `Phoenix.HTML.Form.label/4` for details.
"""
defmacro phx_label(form, field, opts, do: block) do
quote location: :keep do
{:safe, input} =
Phoenix.HTML.Form.label(unquote_splicing([form, field, opts]),
do: temple(do: unquote(block))
)
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
defmacro phx_label(form, field, text, opts) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.label(unquote_splicing([form, field, text, opts]))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Please see `Phoenix.HTML.Form.radio_button/4` for details.
"""
defmacro radio_button(form, field, value, attrs \\ []) do
quote location: :keep do
{:safe, input} =
Phoenix.HTML.Form.radio_button(unquote_splicing([form, field, value, attrs]))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Please see `Phoenix.HTML.Form.multiple_select/4` for details.
"""
defmacro multiple_select(form, field, options, attrs \\ []) do
quote location: :keep do
{:safe, input} =
Phoenix.HTML.Form.multiple_select(unquote_splicing([form, field, options, attrs]))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Please see `Phoenix.HTML.Form.select/4` for details.
"""
defmacro select(form, field, options, attrs \\ []) do
quote location: :keep do
{:safe, input} = Phoenix.HTML.Form.select(unquote_splicing([form, field, options, attrs]))
Utils.put_buffer(var!(buff, Temple.Html), input)
end
end
@doc """
Generate a new form builder for the given parameter in form.
The form builder will be available inside the block through the `inner_form` variable.
This is a wrapper around the `Phoenix.HTML.Form.inputs_for/4` function and accepts all of the same options.
## Example
```
temple do
form_for @parent, Routes.some_path(@conn, :create) do
text_input form, :name
inputs_for form, :job do
text_input inner_form, :description
end
inputs_for form, :children do
text_input inner_form, :name
end
end
end
# {:safe,
# "<form accept-charset=\"UTF-8\" action=\"/\" method=\"post\">
# <input name=\"_csrf_token\" type=\"hidden\" value=\"AS5qfX1gcns6eU56BlQgBlwCDgMlNgAAiJ0MR91Kh3v3bbCS5SKjuw==\">
# <input name=\"_utf8\" type=\"hidden\" value=\"✓\">
# <input id=\"name\" name=\"parent[name]\" type=\"text\">
#
# <input id=\"name\" name=\"parent[job][description]\" type=\"text\">
#
# <input id=\"name\" name=\"parent[children][1][name]\" type=\"text\">
# <input id=\"name\" name=\"parent[children][2][name]\" type=\"text\">
# </form>"}
```
"""
defmacro inputs_for(form, field, options \\ [], do: block) do
quote location: :keep do
form = unquote(form)
field = unquote(field)
options = unquote(options)
options =
form.options
|> Keyword.take([:multipart])
|> Keyword.merge(options)
form.impl.to_form(form.source, form, field, options)
|> Enum.each(fn form ->
Enum.map(form.hidden, fn {k, v} ->
{:safe, hidden_input} = Phoenix.HTML.Form.hidden_input(form, k, value: v)
hidden_input
end)
|> Enum.each(&Utils.put_buffer(var!(buff, Temple.Html), &1))
var!(inner_form) = form
_ = unquote(block)
end)
end
end
end

View File

@ -1,108 +0,0 @@
defmodule Temple.Html do
require Temple.Elements
@moduledoc """
The `Temple.Html` module defines macros for all HTML5 compliant elements.
`Temple.Html` macros must be called inside of a `Temple.temple/1` block.
*Note*: Only the lowest arity macros are documented. Void elements are defined as a 1-arity macro and non-void elements are defined as 0, 1, and 2-arity macros.
## Attributes
Html accept a keyword list or a map of attributes to be emitted into the element's opening tag. Multi-word attribute keys written in snake_case (`data_url`) will be transformed into kebab-case (`data-url`).
## Children
Non-void elements (such as `div`) accept a block that can be used to nest other tags or text nodes. These blocks can contain arbitrary Elixir code such as variables and for comprehensions.
If you are only emitting a text node within a block, you can use the shortened syntax by passing the text in as the first parameter of the tag.
## Example
```
temple do
# empty non-void element
div()
# non-void element with keyword list attributes
div class: "text-red", id: "my-el"
# non-void element with map attributes
div %{:class => "text-red", "id" => "my-el"}
# non-void element with children
div do
text "Hello, world!"
for name <- @names do
div data_name: name
end
end
# non-void element with a single text node
div "Hello, world!", class: "text-green"
# void elements
input name: "comments", placeholder: "Enter a comment..."
end
# {:safe,
# "<div></div>
# <div class=\"text-red\" id=\"my-el\"></div>
# <div>
# Hello, world!
# <div data-name=\"Alice\"></div>
# <div data-name=\"Bob\"></div>
# <div data-name=\"Carol\"></div>
# </div>
# <div class=\"text-green\">Hello, world!</div>
# <input name=\"comments\" placeholder=\"Enter a comment...\">"
# }
```
"""
@nonvoid_elements ~w[
head title style script
noscript template
body section nav article aside h1 h2 h3 h4 h5 h6
header footer address main
p pre blockquote ol ul li dl dt dd figure figcaption div
a em strong small s cite q dfn abbr data time code var samp kbd
sub sup i b u mark ruby rt rp bdi bdo span
ins del
iframe object video audio canvas
map
table caption colgroup tbody thead tfoot tr td th
form fieldset legend label button select datalist optgroup
option textarea output progress meter
details summary menuitem menu
]a
@void_elements ~w[
meta link base
area br col embed hr img input keygen param source track wbr
]a
@doc false
def nonvoid_elements, do: @nonvoid_elements
@doc false
def void_elements, do: @void_elements
for el <- @nonvoid_elements do
Temple.Elements.defelement(unquote(el), :nonvoid)
end
for el <- @void_elements do
Temple.Elements.defelement(unquote(el), :void)
end
defmacro html(attrs \\ [], [{:do, _inner}] = block) do
doc_type =
quote location: :keep do
Temple.Utils.put_buffer(var!(buff, Temple.Html), "<!DOCTYPE html>")
end
[doc_type, Temple.Elements.nonvoid_element(:html, attrs, block)]
end
end

View File

@ -1,76 +0,0 @@
defmodule Temple.HtmlToTemple do
@moduledoc false
@tags Temple.Html.void_elements() ++
Temple.Html.nonvoid_elements() ++ Temple.Svg.elements() ++ [:html]
def parse(doc) do
result =
doc
|> Floki.parse()
|> List.wrap()
|> Enum.map(&do_parse(&1, 0))
|> Enum.join("\n")
{:ok, result}
end
def do_parse({tag, [], []}, indent) do
tag = tag |> find_tag
(Temple.Utils.kebab_to_snake(tag) <> "()\n") |> pad_indent(indent)
end
def do_parse({tag, attrs, []}, indent) do
tag = tag |> find_tag
(Temple.Utils.kebab_to_snake(tag) <> build_attrs(attrs) <> "\n") |> pad_indent(indent)
end
def do_parse({tag, attrs, [""]}, indent), do: do_parse({tag, attrs, []}, indent)
def do_parse({tag, attrs, children}, indent) do
tag = tag |> find_tag
head =
(Temple.Utils.kebab_to_snake(tag) <> build_attrs(attrs) <> " do\n")
|> pad_indent(indent)
parsed_childs =
for child <- children do
do_parse(child, indent + 1)
end
|> Enum.join("\n")
head <> parsed_childs <> pad_indent("end\n", indent)
end
def do_parse(text, indent) when is_binary(text) do
(~s|text "| <> text <> ~s|"\n|) |> pad_indent(indent)
end
defp build_attrs([]), do: ""
defp build_attrs(attrs) do
attrs =
for {key, value} <- attrs do
wrap_in_quotes(key) <> ~s|: "| <> value <> ~s|"|
end
|> Enum.join(", ")
" " <> attrs
end
defp wrap_in_quotes(key) do
if Regex.match?(~r/[^a-zA-Z_]/, key) do
~s|"| <> key <> ~s|"|
else
key
end
end
defp pad_indent(paddable, indent) do
String.pad_leading(paddable, 2 * indent + String.length(paddable))
end
defp find_tag(tag),
do: @tags |> Enum.find(fn x -> String.downcase(to_string(x)) == tag end)
end

View File

@ -1,50 +0,0 @@
defmodule Temple.Link do
alias Phoenix.HTML
alias Temple.Utils
@moduledoc """
This modules wraps all of the functions from the `Phoenix.HTML.Link` module to make them compatible with with Temple.
"""
@doc """
Please see `Phoenix.HTML.Link.link/2` for details.
"""
defmacro phx_link(opts, do: block) do
quote location: :keep do
{:safe, link} =
temple(do: unquote(block))
|> HTML.Link.link(unquote(opts))
Utils.put_buffer(var!(buff, Temple.Html), link)
end
end
defmacro phx_link(content, opts) do
quote location: :keep do
{:safe, link} = HTML.Link.link(unquote_splicing([content, opts]))
Utils.put_buffer(var!(buff, Temple.Html), link)
end
end
@doc """
Please see `Phoenix.HTML.Link.button/2` for details.
"""
defmacro phx_button(opts, do: block) do
quote location: :keep do
{:safe, link} =
temple(do: unquote(block))
|> HTML.Link.button(unquote(opts))
Utils.put_buffer(var!(buff, Temple.Html), link)
end
end
defmacro phx_button(content, opts) do
quote location: :keep do
{:safe, link} = HTML.Link.button(unquote_splicing([content, opts]))
Utils.put_buffer(var!(buff, Temple.Html), link)
end
end
end

View File

@ -0,0 +1,14 @@
defmodule Temple.LiveViewEngine do
@behaviour Phoenix.Template.Engine
@moduledoc false
def compile(path, _name) do
require Temple
ast = path |> File.read!() |> Code.string_to_quoted!(file: path)
Temple.temple(ast)
|> EEx.compile_string(engine: Phoenix.LiveView.Engine, file: path, line: 1)
end
end

View File

@ -1,27 +0,0 @@
defmodule Temple.Svg do
require Temple.Elements
@moduledoc """
The `Temple.Svg` module defines macros for all SVG elements.
Usage is the same as `Temple.Html`.
"""
@elements ~w[
animate animateMotion animateTransform circle clipPath
color_profile defs desc discard ellipse feBlend
feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow
feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset
fePointLight feSpecularLighting feSpotLight feTile feTurbulence filter foreignObject g hatch hatchpath image line linearGradient
marker mask metadata mpath path pattern polygon
polyline radialGradient rect set solidcolor stop svg switch symbol text_
textPath tspan use view
]a
@doc false
def elements(), do: @elements
for el <- @elements do
Temple.Elements.defelement(unquote(el), :nonvoid)
end
end

View File

@ -1,100 +1,8 @@
defmodule Temple.Utils do
@moduledoc false
def puts(binary) do
IO.puts(binary)
def put_open_tag(buff, el, attrs) when is_list(attrs) or is_map(attrs) do
el = el |> snake_to_kebab
put_buffer(buff, "<#{el}#{compile_attrs(attrs)}>")
binary
end
def put_open_tag(buff, el, content)
when is_binary(content) or is_number(content) or is_atom(content) do
el = el |> snake_to_kebab
put_buffer(buff, "<#{el}>")
put_buffer(buff, escape_content(content))
end
def put_close_tag(buff, el) do
el = el |> snake_to_kebab
put_buffer(buff, "</#{el}>")
end
def put_void_tag(buff, el, attrs) do
el = el |> snake_to_kebab
put_buffer(buff, "<#{el}#{Temple.Utils.compile_attrs(attrs)}>")
end
def from_safe({:safe, partial}) do
partial
end
def from_safe(partial) do
partial |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string()
end
def insert_assigns({:@, _, [{:children, _, _}]}, _, inner) do
inner
end
def insert_assigns({:@, _, [{:assigns, _, _}]}, assigns, _) do
assigns
end
def insert_assigns({:@, _, [{name, _, _}]}, assigns, _) when is_atom(name) do
quote location: :keep do
Access.get(unquote_splicing([assigns, name]))
end
end
def insert_assigns(ast, _, _), do: ast
def compile_attrs([]), do: ""
def compile_attrs(attrs) do
for {name, value} <- attrs, into: "" do
name = snake_to_kebab(name)
" " <> name <> "=\"" <> to_string(value) <> "\""
end
end
def join_and_escape(markup) do
markup |> Enum.reverse() |> Enum.join("\n") |> Phoenix.HTML.raw()
end
def start_buffer(initial_buffer), do: Agent.start(fn -> initial_buffer end)
def put_buffer(buff, content), do: Agent.update(buff, &[content | &1])
def get_buffer(buff), do: Agent.get(buff, & &1)
def stop_buffer(buff), do: Agent.stop(buff)
def escape_content(content) do
content
|> to_string
|> Phoenix.HTML.html_escape()
|> Phoenix.HTML.safe_to_string()
end
defp snake_to_kebab(stringable),
do: stringable |> to_string() |> String.replace_trailing("_", "") |> String.replace("_", "-")
def kebab_to_snake(stringable),
do: stringable |> to_string() |> String.replace("-", "_")
def __quote__(outer) do
quote [location: :keep], do: unquote(outer)
end
def __insert_assigns__(block, assigns, inner) do
block
|> Macro.prewalk(&Temple.Utils.insert_assigns(&1, assigns, inner))
end
def doc_path(:html, el), do: "./tmp/docs/html/#{el}.txt"
def doc_path(:svg, el), do: "./tmp/docs/svg/#{el}.txt"
def to_valid_tag(tag),
do: tag |> to_string |> String.replace_trailing("_", "") |> String.replace("_", "-")
end

11
mix.exs
View File

@ -6,7 +6,7 @@ defmodule Temple.MixProject do
app: :temple,
name: "Temple",
description: "An HTML DSL for Elixir and Phoenix",
version: "0.5.0",
version: "0.6.0-alpha.0",
package: package(),
elixirc_paths: elixirc_paths(Mix.env()),
elixir: "~> 1.7",
@ -46,13 +46,8 @@ defmodule Temple.MixProject do
defp deps do
[
{:phoenix_html, "~> 2.13"},
{:ecto, "~> 3.0", optional: true},
{:phoenix_ecto, "~> 4.0", optional: true},
{:ex_doc, "~> 0.0", only: [:dev], runtime: false},
{:html_sanitize_ex, "~> 1.3", only: [:dev, :test], runtime: false},
{:phoenix, "~> 1.4", optional: true},
{:floki, "~> 0.26.0", only: [:dev, :test], runtime: false}
{:ex_doc, "~> 0.22.0", only: [:dev], runtime: false},
{:phoenix, ">= 0.0.0", optional: true}
]
end
end

View File

@ -1,21 +1,14 @@
%{
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
"ecto": {:hex, :ecto, "3.4.1", "ca5b5f6314eebd7fa2e52c6d78abb1ef955005dd60cc7a047b963ee23ee14a6c", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "748a317a2eacac0b7b6540cb7d2198b79457ede9cec2b4d1582117f90ac309d5"},
"ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"},
"floki": {:hex, :floki, "0.26.0", "4df88977e2e357c6720e1b650f613444bfb48c5acfc6a0c646ab007d08ad13bf", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e7b66ce7feef5518a9cd9fc7b52dd62a64028bd9cb6d6ad282a0f0fc90a4ae52"},
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.0", "0310d27d7bafb662f30bff22ec732a72414799c83eaf44239781fd23b96216c0", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "c5d79626be0b6e50c19ecdfb783ee26e85bd3a77436b488379ce6dc104ec4593"},
"makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
"earmark": {:hex, :earmark, "1.4.5", "62ffd3bd7722fb7a7b1ecd2419ea0b458c356e7168c1f5d65caf09b4fbdd13c8", [:mix], [], "hexpm", "b7d0e6263d83dc27141a523467799a685965bf8b13b6743413f19a7079843f4f"},
"ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"},
"makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm", "b93e2b1e564bdbadfecc297277f9e6d0902da645b417d6c9210f6038ac63489a"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
"phoenix": {:hex, :phoenix, "1.4.16", "2cbbe0c81e6601567c44cc380c33aa42a1372ac1426e3de3d93ac448a7ec4308", [: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", "856cc1a032fa53822737413cf51aa60e750525d7ece7d1c0576d90d7c0f05c24"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.14.1", "7dabafadedb552db142aacbd1f11de1c0bbaa247f90c449ca549d5e30bbc66b4", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "536d5200ad37fecfe55b3241d90b7a8c3a2ca60cd012fc065f776324fa9ab0a9"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"},
"plug": {:hex, :plug, "1.10.0", "6508295cbeb4c654860845fb95260737e4a8838d34d115ad76cd487584e2fc4d", [: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: true]}], "hexpm", "422a9727e667be1bf5ab1de03be6fa0ad67b775b2d84ed908f3264415ef29d4a"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [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", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [: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", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"poison": {:hex, :poison, "1.5.2", "560bdfb7449e3ddd23a096929fb9fc2122f709bcc758b2d5d5a5c7d0ea848910", [:mix], [], "hexpm", "4afc59dcadf71be7edc8b934b39f554ec7b31e2b1b1a4767383a663f86958ce3"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
}

View File

@ -1,7 +1,7 @@
h1 "Edit <%= schema.human_singular %>"
h1 do: "Edit <%= schema.human_singular %>"
partial render("form.html", Map.put(assigns, :action, Routes.<%= schema.route_helper %>_path(@conn, :update, @<%= schema.singular %>)))
render("form.html", Map.put(assigns, :action, Routes.<%= schema.route_helper %>_path(@conn, :update, @<%= schema.singular %>)))
span do
phx_link "Back", to: Routes.<%= schema.route_helper %>_path(@conn, :index)
link "Back", to: Routes.<%= schema.route_helper %>_path(@conn, :index)
end

View File

@ -1,12 +1,14 @@
form_for @changeset, @action do
form_for @changeset, @action, fn f ->
if @changeset.action do
div class: "alert alert-danger" do
p "Oops, something went wrong! Please check the errors below."
p do: "Oops, something went wrong! Please check the errors below."
end
end <%= for {label, input, error} <- inputs, input do %>
<%= label %>
<%= input %>
<%= error %> <% end %>
<%= error %>
<% end %>
div do
submit "Save"
end

View File

@ -1,19 +1,19 @@
h1 "Listing <%= schema.human_plural %>"
h1 do: "Listing <%= schema.human_plural %>"
table do
thead do
tr do <%= for {k, _} <- schema.attrs do %>
th "<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"<% end %>
th do: "<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"<% end %>
th()
end
tbody do
for <%= schema.singular %> <- @<%= schema.plural %> do
tr do <%= for {k, _} <- schema.attrs do %>
td <%= schema.singular %>.<%= k %> <% end %>
td do: <%= schema.singular %>.<%= k %> <% end %>
td do
phx_link "Show", to: Routes.<%= schema.route_helper %>_path(@conn, :show, <%= schema.singular %>)
phx_link "Edit", to: Routes.<%= schema.route_helper %>_path(@conn, :edit, <%= schema.singular %>)
phx_link "Delete", to: Routes.<%= schema.route_helper %>_path(@conn, :delete, <%= schema.singular %>),
link "Show", to: Routes.<%= schema.route_helper %>_path(@conn, :show, <%= schema.singular %>)
link "Edit", to: Routes.<%= schema.route_helper %>_path(@conn, :edit, <%= schema.singular %>)
link "Delete", to: Routes.<%= schema.route_helper %>_path(@conn, :delete, <%= schema.singular %>),
method: :delete, data: [confirm: "Are you sure?"]
end
end
@ -23,5 +23,5 @@ table do
end
span do
phx_link "New <%= schema.human_singular %>", to: Routes.<%= schema.route_helper %>_path(@conn, :new)
link "New <%= schema.human_singular %>", to: Routes.<%= schema.route_helper %>_path(@conn, :new)
end

View File

@ -1,7 +1,7 @@
h1 "New <%= schema.human_singular %>"
h1 do: "New <%= schema.human_singular %>"
partial render("form.html", Map.put(assigns, :action, Routes.<%= schema.route_helper %>_path(@conn, :create)))
render("form.html", Map.put(assigns, :action, Routes.<%= schema.route_helper %>_path(@conn, :create)))
span do
phx_link "Back", to: Routes.<%= schema.route_helper %>_path(@conn, :index)
link "Back", to: Routes.<%= schema.route_helper %>_path(@conn, :index)
end

View File

@ -1,16 +1,16 @@
h1 "Show <%= schema.human_singular %>"
h1 do: "Show <%= schema.human_singular %>"
ul do <%= for {k, _} <- schema.attrs do %>
li do
strong "<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"
text @<%= schema.singular %>.<%= k %>
strong do: "<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"
@<%= schema.singular %>.<%= k %>
end <% end %>
span do
phx_link "Edit", to: Routes.<%= schema.route_helper %>_path(@conn, :edit, @<%= schema.singular %>)
link "Edit", to: Routes.<%= schema.route_helper %>_path(@conn, :edit, @<%= schema.singular %>)
end
span do
phx_link "Back", to: Routes.<%= schema.route_helper %>_path(@conn, :index)
link "Back", to: Routes.<%= schema.route_helper %>_path(@conn, :index)
end
end

View File

@ -28,7 +28,7 @@ html lang: "en" do
p get_flash(@conn, :info), class: "alert alert-info", role: "alert"
p get_flash(@conn, :error), class: "alert alert-danger", role: "alert"
partial render(@view_module, @view_template, assigns)
@inner_content
end
script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js")

View File

@ -1,144 +0,0 @@
defmodule Mix.Tasks.HtmlToTempleTest do
use ExUnit.Case, async: true
test "converts html to temple syntax" do
html = """
<html lang="en">
<head>
<meta>
<script></script>
<link>
</head>
<body>
<header class="header" data-action="do a thing">
<nav role="navigation">
<ul>
<li><a href="/home">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/profile">Profile</a></li>
</ul>
</nav>
</header>
<main role="main">
<svg>
<path d="alksdjfalksdjfslkadfj"/>
<linearGradient>
<stop></stop>
</linearGradient
</svg>
</main>
<footer></footer>
</body>
</html>
"""
{:ok, result} = Temple.HtmlToTemple.parse(html)
assert result === """
html lang: "en" do
head do
meta()
script()
link()
end
body do
header class: "header", "data-action": "do a thing" do
nav role: "navigation" do
ul do
li do
a href: "/home" do
text "Home"
end
end
li do
a href: "/about" do
text "About"
end
end
li do
a href: "/profile" do
text "Profile"
end
end
end
end
end
main role: "main" do
svg do
path d: "alksdjfalksdjfslkadfj"
linearGradient do
stop()
end
end
end
footer()
end
end
"""
end
test "parses HTML fragments" do
html = """
<section class="phx-hero">
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
<p>A productive web framework that<br/>
does not compromise speed or maintainability.</p>
</section>
<section class="row">
<article class="column">
<h2>Resources</h2>
<ul>
<li>
<a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
</li>
</ul>
</article>
</section>
"""
{:ok, result} = Temple.HtmlToTemple.parse(html)
assert result === """
section class: "phx-hero" do
h1 do
text "<%= gettext \"Welcome to %{name}!\", name: \"Phoenix\" %>"
end
p do
text "A productive web framework that"
br()
text "
does not compromise speed or maintainability."
end
end
section class: "row" do
article class: "column" do
h2 do
text "Resources"
end
ul do
li do
a href: "https://hexdocs.pm/phoenix/overview.html" do
text "Guides & Docs"
end
end
end
end
end
"""
end
end

View File

@ -1,71 +0,0 @@
defmodule Temple.Support.Component do
import Temple
defcomponent :flex do
div(class: "flex")
end
defcomponent :takes_children do
div do
div(id: "static-child-1")
@children
div(id: "static-child-2")
end
end
defcomponent :lists_assigns do
partial inspect(@assigns) |> Phoenix.HTML.raw()
end
defcomponent :arbitrary_code do
num = 1..10 |> Enum.reduce(0, fn x, sum -> x + sum end)
div do
text(num)
end
end
defcomponent :uses_conditionals do
if @condition do
div()
else
span()
end
end
defcomponent :arbitrary_data do
for item <- @lists do
div do
text(inspect(item))
end
end
end
defcomponent :safe do
div()
end
defcomponent :safe_with_prop do
div id: "safe-with-prop" do
text(@prop)
div do
span do
for x <- @lists do
div(do: text(x))
end
end
end
end
end
defcomponent :variable_as_prop do
div id: @bob
end
defcomponent :variable_as_prop_with_block do
div id: @bob do
@children
end
end
end

View File

@ -7,16 +7,16 @@ defmodule Temple.Support.Utils do
end
def a == b when is_binary(a) and is_binary(b) do
Kernel.==(
String.replace(a, ~r/\n/, ""),
String.replace(b, ~r/\n/, "")
)
a = String.replace(a, "\n", "")
b = String.replace(b, "\n", "")
Kernel.==(a, b)
end
def a =~ b when is_binary(a) and is_binary(b) do
Kernel.=~(
String.replace(a, ~r/\n/, ""),
String.replace(b, ~r/\n/, "")
)
a = String.replace(a, "\n", "")
b = String.replace(b, "\n", "")
Kernel.=~(a, b)
end
end

View File

@ -1,32 +0,0 @@
defmodule Temple.ElementsTest do
use ExUnit.Case, async: true
import Temple.Elements, only: [defelement: 2]
import Temple, only: [temple: 1, text: 1]
import Temple.Html, only: [option: 2]
use Temple.Support.Utils
defelement(:my_select, :nonvoid)
defelement(:my_input, :void)
test "defines a nonvoid element" do
{:safe, result} =
temple do
my_select class: "hello" do
option "A", value: "A"
option "B", value: "B"
end
end
assert result ==
~s{<my-select class="hello"><option value="A">A</option><option value="B">B</option></my-select>}
end
test "defines a void element" do
{:safe, result} =
temple do
my_input(class: "hello")
end
assert result == ~s{<my-input class="hello">}
end
end

View File

@ -1,714 +0,0 @@
defmodule Temple.FormTest do
use ExUnit.Case, async: true
use Temple
use Temple.Support.Utils
describe "form_for" do
test "returns a form tag" do
conn = %Plug.Conn{}
action = "/"
{:safe, result} =
temple do
form_for(conn, action, [])
end
assert result =~ ~s{<form}
assert result =~ ~s{</form>}
end
test "can take a block" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
div()
end
end
assert result =~ ~s{<form}
assert result =~ ~s{<div></div>}
assert result =~ ~s{</form>}
end
test "can take a block that references the form" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
text_input(form, :bob)
end
end
assert result =~ ~s{<form}
assert result =~ ~s{<input}
assert result =~ ~s{type="text"}
assert result =~ ~s{name="bob"}
assert result =~ ~s{</form>}
end
end
defmodule Person do
use Ecto.Schema
schema "persons" do
field(:name)
belongs_to(:company, Company)
has_many(:responsibilities, Reponsibility)
end
end
defmodule Company do
use Ecto.Schema
schema "companies" do
field(:name)
field(:field)
end
end
defmodule Responsibility do
use Ecto.Schema
schema "responsibilities" do
field(:description)
belongs_to(:person, Person)
end
end
describe "inputs_for" do
test "generates inputs for belongs_to" do
person = %Person{company: %Company{}}
changeset = Ecto.Changeset.change(person)
action = "/"
opts = []
{:safe, result} =
temple do
form_for changeset, action, opts do
text_input(form, :name)
inputs_for form, :company do
text_input(inner_form, :name)
_ = "Bob"
text_input(inner_form, :field)
end
end
end
assert result =~ ~s{<form}
assert result =~ ~s{<input}
assert result =~ ~s{type="text"}
assert result =~ ~s{name="person[company][name]"}
assert result =~ ~s{name="person[company][field]"}
assert result =~ ~s{</form>}
refute result =~ ~s{Bob}
end
test "generates inputs for has_many" do
person = %Person{
id: 1,
responsibilities: [
%Responsibility{id: 1, person_id: 1},
%Responsibility{id: 2, person_id: 1}
]
}
changeset = Ecto.Changeset.change(person)
action = "/"
opts = []
{:safe, result} =
temple do
form_for changeset, action, opts do
text_input(form, :name)
inputs_for form, :responsibilities do
phx_label(inner_form, :description)
text_area(inner_form, :description)
_ = "Bob"
end
end
end
assert result =~ ~s{<form}
assert result =~ ~s{<input}
assert result =~ ~s{type="text"}
assert result =~ ~s{name="person[responsibilities][0][description]"}
assert result =~ ~s{name="person[responsibilities][1][description]"}
assert result =~ ~s{</form>}
refute result =~ ~s{Bob}
end
end
describe "helpers" do
test "generates a checkbox input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
checkbox(form, :bob, class: "styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="checkbox"}
assert result =~ ~s{class="styles"}
assert result =~ ~s{name="bob"}
end
test "generates a color input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
color_input(form, :bob, class: "styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="color"}
assert result =~ ~s{class="styles"}
assert result =~ ~s{name="bob"}
end
test "generates a date input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
date_input(form, :bob, class: "date-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="date"}
assert result =~ ~s{class="date-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a date select input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
date_select(form, :bob, class: "date-styles")
end
end
assert result =~ ~s{<select}
end
test "generates a datetime_local_input input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
datetime_local_input(form, :bob, class: "date-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="datetime-local"}
assert result =~ ~s{class="date-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a datetime_select input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
datetime_select(form, :bob, class: "datetime-select-styles")
end
end
assert result =~ ~s{<select}
end
test "generates a email input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
email_input(form, :bob, class: "email-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="email"}
assert result =~ ~s{class="email-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a file input" do
conn = %Plug.Conn{}
action = "/"
opts = [multipart: true]
{:safe, result} =
temple do
form_for conn, action, opts do
file_input(form, :bob, class: "file-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="file"}
assert result =~ ~s{class="file-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a hidden input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
hidden_input(form, :bob, class: "hidden-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="hidden"}
assert result =~ ~s{class="hidden-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a number input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
number_input(form, :bob, class: "number-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="number"}
assert result =~ ~s{class="number-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a password input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
password_input(form, :bob, class: "password-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="password"}
assert result =~ ~s{class="password-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a range input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
range_input(form, :bob, class: "range-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="range"}
assert result =~ ~s{class="range-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a search input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
search_input(form, :bob, class: "search-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="search"}
assert result =~ ~s{class="search-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a telephone input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
telephone_input(form, :bob, class: "telephone-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="tel"}
assert result =~ ~s{class="telephone-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a radio button" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
radio_button(form, :bob, "1", class: "radio-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="radio"}
assert result =~ ~s{class="radio-styles"}
assert result =~ ~s{name="bob"}
assert result =~ ~s{value="1"}
end
test "generates a text_area/2" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
text_area(form, :bob)
end
end
assert result =~ ~s{<textarea}
assert result =~ ~s{name="bob"}
end
test "generates a text_area/3" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
text_area(form, :bob, class: "textarea-styles")
end
end
assert result =~ ~s{<textarea}
assert result =~ ~s{class="textarea-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a time input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
time_input(form, :bob, class: "time-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="time"}
assert result =~ ~s{class="time-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a time_select input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
time_select(form, :bob)
end
end
assert result =~ ~s{<select}
end
test "generates a url input" do
conn = %Plug.Conn{}
action = "/"
opts = []
{:safe, result} =
temple do
form_for conn, action, opts do
url_input(form, :bob, class: "url-styles")
end
end
assert result =~ ~s{<input}
assert result =~ ~s{type="url"}
assert result =~ ~s{class="url-styles"}
assert result =~ ~s{name="bob"}
end
test "generates a reset input" do
{:safe, result} =
temple do
reset("Reset", class: "reset-styles")
end
assert result =~ ~s{<input}
assert result =~ ~s{type="reset}
assert result =~ ~s{class="reset-styles"}
end
test "generates a submit/1 input" do
{:safe, result} =
temple do
submit("Submit")
end
assert String.starts_with?(result, ~s{<button})
assert result =~ ~s{type="submit}
assert result =~ ~s{Submit}
assert String.ends_with?(result, ~s{</button>})
end
test "generates a submit/1 input that takes a block" do
{:safe, result} =
temple do
submit do
div do
text "Submit"
end
end
end
assert String.starts_with?(result, ~s{<button})
assert result =~ ~s{type="submit}
assert result =~ ~s{<div>}
assert result =~ ~s{Submit}
assert result =~ ~s{</div>}
assert String.ends_with?(result, ~s{</button>})
end
test "generates a submit/2 input that takes text and opts" do
{:safe, result} =
temple do
submit("Submit", class: "btn")
end
assert String.starts_with?(result, ~s{<button})
assert result =~ ~s{type="submit}
assert result =~ ~s{class="btn"}
assert result =~ ~s{Submit}
assert String.ends_with?(result, ~s{</button>})
end
test "generates a submit/2 input that takes opts and a block" do
{:safe, result} =
temple do
submit class: "btn" do
div do
text "Submit"
end
end
end
assert String.starts_with?(result, ~s{<button})
assert result =~ ~s{type="submit}
assert result =~ ~s{class="btn"}
assert result =~ ~s{<div>}
assert result =~ ~s{Submit}
assert result =~ ~s{</div>}
assert String.ends_with?(result, ~s{</button>})
end
test "generates a phx_label/2 tag" do
{:safe, result} =
temple do
phx_label(:user, :name)
end
assert result =~ ~s{<label}
assert result =~ ~s{for="user_name"}
assert result =~ ~s{Name}
assert result =~ ~s{</label>}
end
test "generates a phx_label/3 with attrs" do
{:safe, result} =
temple do
phx_label(:user, :name, class: "label-style")
end
assert result =~ ~s{<label}
assert result =~ ~s{for="user_name"}
assert result =~ ~s{class="label-style"}
assert result =~ ~s{Name}
assert result =~ ~s{</label>}
end
test "generates a phx_label/3 with text" do
{:safe, result} =
temple do
phx_label(:user, :name, "Name")
end
assert result =~ ~s{<label}
assert result =~ ~s{for="user_name"}
assert result =~ ~s{Name}
assert result =~ ~s{</label>}
end
test "generates a phx_label/3 with block" do
{:safe, result} =
temple do
phx_label :user, :name do
div do
text "Name"
end
end
end
assert String.starts_with?(result, ~s{<label})
assert result =~ ~s{for="user_name"}
assert result =~ ~s{<div>}
assert result =~ ~s{Name}
assert result =~ ~s{</div>}
assert String.ends_with?(result, ~s{</label>})
end
test "generates a phx_label/4 with text and opts" do
{:safe, result} =
temple do
phx_label(:user, :name, "Name", class: "label-style")
end
assert result =~ ~s{<label}
assert result =~ ~s{for="user_name"}
assert result =~ ~s{class="label-style"}
assert result =~ ~s{Name}
assert result =~ ~s{</label>}
end
test "generates a phx_label/4 with block" do
{:safe, result} =
temple do
phx_label :user, :name, class: "label-style" do
div do
text "Name"
end
end
end
assert String.starts_with?(result, ~s{<label})
assert result =~ ~s{for="user_name"}
assert result =~ ~s{class="label-style"}
assert result =~ ~s{<div>}
assert result =~ ~s{Name}
assert result =~ ~s{</div>}
assert String.ends_with?(result, ~s{</label>})
end
test "generates a multiple_select tag" do
options = [
Alice: 1,
Bob: 2,
Carol: 3
]
{:safe, result} =
temple do
multiple_select(:user, :name, options, class: "label-style")
end
assert result =~ ~s{<select}
assert result =~ ~s{name="user[name][]"}
assert result =~ ~s{class="label-style"}
assert result =~ ~s{multiple=""}
assert result =~ ~s{<option}
assert result =~ ~s{value="1"}
assert result =~ ~s{Alice}
assert result =~ ~s{value="2"}
assert result =~ ~s{Bob}
assert result =~ ~s{value="3"}
assert result =~ ~s{Carol}
assert result =~ ~s{</select>}
end
test "generates a select tag" do
options = [
Alice: 1,
Bob: 2,
Carol: 3
]
{:safe, result} =
temple do
select :user, :name, options, class: "label-style"
end
assert result =~ ~s{<select}
assert result =~ ~s{name="user[name]"}
assert result =~ ~s{class="label-style"}
assert result =~ ~s{<option}
assert result =~ ~s{value="1"}
assert result =~ ~s{Alice}
assert result =~ ~s{value="2"}
assert result =~ ~s{Bob}
assert result =~ ~s{value="3"}
assert result =~ ~s{Carol}
assert result =~ ~s{</select>}
refute result =~ ~s{multiple=""}
end
end
end

View File

@ -1,335 +0,0 @@
defmodule Temple.HtmlTest do
use ExUnit.Case, async: true
use Temple
use Temple.Support.Utils
test "renders a html with a block" do
{:safe, result} =
temple do
html(do: div())
end
assert result == ~s{<!DOCTYPE html><html><div></div></html>}
end
test "renders a html with attrs and a block" do
{:safe, result} =
temple do
html(class: "hello") do
div()
end
end
assert result == ~s{<!DOCTYPE html><html class="hello"><div></div></html>}
end
for tag <- Temple.Html.nonvoid_elements() do
test "renders a #{tag}" do
{:safe, result} =
temple do
unquote(tag)()
end
assert result == ~s{<#{unquote(tag)}></#{unquote(tag)}>}
end
test "renders a #{tag} with attrs" do
{:safe, result} =
temple do
unquote(tag)(class: "hello")
end
assert result == ~s{<#{unquote(tag)} class="hello"></#{unquote(tag)}>}
end
test "renders a #{tag} with content" do
{:safe, result} =
temple do
unquote(tag)("Hi")
end
assert result == "<#{unquote(tag)}>Hi</#{unquote(tag)}>"
end
test "renders a #{tag} with escaped content" do
{:safe, result} =
temple do
unquote(tag)("<div>1</div>")
end
assert result == "<#{unquote(tag)}>&lt;div&gt;1&lt;/div&gt;</#{unquote(tag)}>"
end
test "renders a #{tag} with attrs and content" do
{:safe, result} =
temple do
unquote(tag)("Hi", class: "hello")
end
assert result == ~s{<#{unquote(tag)} class="hello">Hi</#{unquote(tag)}>}
end
test "renders a #{tag} with a block" do
{:safe, result} =
temple do
unquote(tag)(do: unquote(tag)())
end
assert result == ~s{<#{unquote(tag)}><#{unquote(tag)}></#{unquote(tag)}></#{unquote(tag)}>}
end
test "renders a #{tag} with attrs and a block" do
{:safe, result} =
temple do
unquote(tag)(class: "hello") do
unquote(tag)()
end
end
assert result ==
~s{<#{unquote(tag)} class="hello"><#{unquote(tag)}></#{unquote(tag)}></#{
unquote(tag)
}>}
end
end
for tag <- Temple.Html.void_elements() do
test "renders a #{tag}" do
{:safe, result} =
temple do
unquote(tag)()
end
assert result == ~s{<#{unquote(tag)}>}
end
test "renders a #{tag} with attrs" do
{:safe, result} =
temple do
unquote(tag)(class: "hello")
end
assert result == ~s{<#{unquote(tag)} class="hello">}
end
end
describe "non-void elements" do
test "renders two divs" do
{:safe, result} =
temple do
div()
div()
end
assert result == "<div></div><div></div>"
end
test "renders two els in the right order" do
{:safe, result} =
temple do
div()
span()
end
assert result == "<div></div><span></span>"
end
test "renders an el that taks attrs and a block" do
{:safe, result} =
temple do
div class: "bob" do
span()
span()
end
end
assert result == ~s{<div class="bob"><span></span><span></span></div>}
end
test "renders one els nested inside an el" do
{:safe, result} =
temple do
div do
span()
end
end
assert result == "<div><span></span></div>"
end
test "renders two els nested inside an el" do
{:safe, result} =
temple do
div do
span()
span()
end
end
assert result == "<div><span></span><span></span></div>"
end
test "renders two divs that are rendered by a loop" do
{:safe, result} =
temple do
for _ <- 1..2 do
div()
end
end
assert result == "<div></div><div></div>"
end
test "renders two spans" do
{:safe, result} =
temple do
span()
span()
end
assert result == "<span></span><span></span>"
end
test "renders a div within a div" do
{:safe, result} =
temple do
div do
div()
end
end
assert result == "<div><div></div></div>"
end
test "renders an attribute on a div" do
{:safe, result} =
temple do
div class: "hello" do
div class: "hi"
end
end
assert result == ~s{<div class="hello"><div class="hi"></div></div>}
end
test "renders an attribute passed in as a map on a div" do
{:safe, result} =
temple do
div %{class: "hello"} do
div %{"class" => "hi"}
end
end
assert result == ~s{<div class="hello"><div class="hi"></div></div>}
end
test "renders an attribute on a div passed as a variable" do
attrs1 = [class: "hello"]
attrs2 = [class: "hi"]
{:safe, result} =
temple do
div attrs1 do
div attrs2
end
end
assert result == ~s{<div class="hello"><div class="hi"></div></div>}
end
test "renders multiple attributes on a div without block" do
{:safe, result} =
temple do
div class: "hello", id: "12"
end
assert result == ~s{<div class="hello" id="12"></div>}
end
test "can accept content as the first argument" do
{:safe, result} =
temple do
div "CONTENT"
div "MORE", class: "hi"
end
assert result == ~s{<div>CONTENT</div><div class="hi">MORE</div>}
end
test "can accept content as first argument passed as a variable" do
content = "CONTENT"
more = "MORE"
{:safe, result} =
temple do
div content
div more, class: "hi"
end
assert result == ~s{<div>CONTENT</div><div class="hi">MORE</div>}
end
end
describe "void elements" do
test "renders an input" do
{:safe, result} =
temple do
input()
end
assert result == ~s{<input>}
end
test "renders an input with an attribute" do
{:safe, result} =
temple do
input type: "number"
end
assert result == ~s{<input type="number">}
end
test "can use string interpolation in an attribute" do
interop = "hi"
{:safe, result} =
temple do
div class: "#{interop} world"
end
assert result == ~s{<div class="hi world"></div>}
end
end
describe "escaping" do
test "text is excaped" do
{:safe, result} =
temple do
text "<div>Text</div>"
end
assert result == ~s{&lt;div&gt;Text&lt;/div&gt;}
end
end
describe "data attributes" do
test "can have one data attributes" do
{:safe, result} =
temple do
div data_controller: "stimulus-controller"
end
assert result == ~s{<div data-controller="stimulus-controller"></div>}
end
test "can have multiple data attributes" do
{:safe, result} =
temple do
div data_controller: "stimulus-controller", data_target: "stimulus-target"
end
assert result ==
~s{<div data-controller="stimulus-controller" data-target="stimulus-target"></div>}
end
end
end

View File

@ -1,171 +0,0 @@
defmodule Temple.LinkTest do
use ExUnit.Case, async: true
use Temple
use Temple.Support.Utils
describe "phx_link" do
test "emits a link" do
{:safe, actual} =
temple do
phx_link("hi", to: "/hello")
end
assert actual =~ ~s{<a}
assert actual =~ ~s{href="/hello"}
assert actual =~ ~s{hi}
end
test "emits a link when passed block that has text" do
{:safe, actual} =
temple do
phx_link to: "/hello" do
text "hi"
end
end
assert String.starts_with?(actual, ~s{<a})
assert actual =~ ~s{href="/hello"}
assert actual =~ ~s{hi}
assert String.ends_with?(actual, ~s{</a>})
end
test "emits a link when passed block that has more markup" do
{:safe, actual} =
temple do
phx_link to: "/hello" do
div do
div "hi"
end
end
end
assert String.starts_with?(actual, ~s{<a})
assert actual =~ ~s{href="/hello"}
assert actual =~ ~s{<div><div>}
assert actual =~ ~s{hi}
assert actual =~ ~s{</div></div>}
assert String.ends_with?(actual, ~s{</a>})
end
test "emits a link with additional html attributes" do
{:safe, actual} =
temple do
phx_link("hi",
to: "/hello",
class: "phoenix",
id: "legendary",
data: [confirm: "Really?"],
method: :delete
)
end
assert actual =~ ~s{<a}
assert actual =~ ~s{href="/hello"}
assert actual =~ ~s{class="phoenix"}
assert actual =~ ~s{id="legendary"}
assert actual =~ ~s{data-confirm="Really?"}
assert actual =~ ~s{hi}
end
test "emits a link with a non GET method" do
{:safe, actual} =
temple do
phx_link("hi",
to: "/hello",
method: :delete
)
end
assert actual =~ ~s{<a}
assert actual =~ ~s{data-csrf="}
assert actual =~ ~s{data-method="delete"}
assert actual =~ ~s{data-to="/hello"}
assert actual =~ ~s{hi}
end
end
describe "phx_button" do
test "emits a button" do
{:safe, actual} =
temple do
phx_button("hi", to: "/hello")
end
assert actual =~ ~s{<button}
assert actual =~ ~s{data-to="/hello"}
assert actual =~ ~s{data-method="post"}
assert actual =~ ~s{hi}
end
test "emits a button when passed block that has text" do
{:safe, actual} =
temple do
phx_button to: "/hello" do
text "hi"
end
end
assert String.starts_with?(actual, ~s{<button})
assert actual =~ ~s{hi}
assert actual =~ ~s{data-to="/hello"}
assert actual =~ ~s{data-method="post"}
assert String.ends_with?(actual, ~s{</button>})
end
test "emits a button when passed block that has more markup" do
{:safe, actual} =
temple do
phx_button to: "/hello" do
div do
div "hi"
end
end
end
assert String.starts_with?(actual, ~s{<button})
assert actual =~ ~s{data-to="/hello"}
assert actual =~ ~s{data-method="post"}
assert actual =~ ~s{<div><div>}
assert actual =~ ~s{hi}
assert actual =~ ~s{</div></div>}
assert String.ends_with?(actual, ~s{</button>})
end
test "emits a button with additional html attributes" do
{:safe, actual} =
temple do
phx_button("hi",
to: "/hello",
class: "phoenix",
id: "legendary",
data: [confirm: "Really?"],
method: :delete
)
end
assert String.starts_with?(actual, ~s{<button})
assert actual =~ ~s{class="phoenix"}
assert actual =~ ~s{id="legendary"}
assert actual =~ ~s{data-confirm="Really?"}
assert actual =~ ~s{hi}
assert String.ends_with?(actual, ~s{</button>})
end
test "emits a button with a non GET method" do
{:safe, actual} =
temple do
phx_button("hi",
to: "/hello",
method: :delete
)
end
assert String.starts_with?(actual, ~s{<button})
assert actual =~ ~s{data-csrf="}
assert actual =~ ~s{data-method="delete"}
assert actual =~ ~s{data-to="/hello"}
assert actual =~ ~s{hi}
assert String.ends_with?(actual, ~s{</button>})
end
end
end

View File

@ -1,150 +0,0 @@
defmodule Temple.SvgTest do
use ExUnit.Case, async: true
import Temple
import Temple.Utils, only: [to_valid_tag: 1]
use Temple.Support.Utils
for tag <- Temple.Svg.elements() -- [:text_] do
test "renders a #{tag}" do
{:safe, result} =
temple do
unquote(tag)()
end
assert result == ~s{<#{to_valid_tag(unquote(tag))}></#{to_valid_tag(unquote(tag))}>}
end
test "renders a #{tag} with attrs" do
{:safe, result} =
temple do
unquote(tag)(class: "hello")
end
assert result ==
~s{<#{to_valid_tag(unquote(tag))} class="hello"></#{to_valid_tag(unquote(tag))}>}
end
test "renders a #{tag} with content" do
{:safe, result} =
temple do
unquote(tag)("Hi")
end
assert result == "<#{to_valid_tag(unquote(tag))}>Hi</#{to_valid_tag(unquote(tag))}>"
end
test "renders a #{tag} with escaped content" do
{:safe, result} =
temple do
unquote(tag)("<div>1</div>")
end
assert result ==
"<#{to_valid_tag(unquote(tag))}>&lt;div&gt;1&lt;/div&gt;</#{
to_valid_tag(unquote(tag))
}>"
end
test "renders a #{tag} with attrs and content" do
{:safe, result} =
temple do
unquote(tag)("Hi", class: "hello")
end
assert result ==
~s{<#{to_valid_tag(unquote(tag))} class="hello">Hi</#{to_valid_tag(unquote(tag))}>}
end
test "renders a #{tag} with a block" do
{:safe, result} =
temple do
unquote(tag)(do: unquote(tag)())
end
assert result ==
~s{<#{to_valid_tag(unquote(tag))}><#{to_valid_tag(unquote(tag))}></#{
to_valid_tag(unquote(tag))
}></#{to_valid_tag(unquote(tag))}>}
end
test "renders a #{tag} with attrs and a block" do
{:safe, result} =
temple do
unquote(tag)(class: "hello") do
unquote(tag)()
end
end
assert result ==
~s{<#{to_valid_tag(unquote(tag))} class="hello"><#{to_valid_tag(unquote(tag))}></#{
to_valid_tag(unquote(tag))
}></#{to_valid_tag(unquote(tag))}>}
end
end
test "renders a text" do
{:safe, result} =
temple do
text_()
end
assert result == ~s{<text></text>}
end
test "renders a text with attrs" do
{:safe, result} =
temple do
text_(class: "hello")
end
assert result == ~s{<text class="hello"></text>}
end
test "renders a text with content" do
{:safe, result} =
temple do
text_("Hi")
end
assert result == "<text>Hi</text>"
end
test "renders a text with escaped content" do
{:safe, result} =
temple do
text_("<div>1</div>")
end
assert result == "<text>&lt;div&gt;1&lt;/div&gt;</text>"
end
test "renders a text with attrs and content" do
{:safe, result} =
temple do
text_("Hi", class: "hello")
end
assert result == ~s{<text class="hello">Hi</text>}
end
test "renders a text with a block" do
{:safe, result} =
temple do
text_(do: text_())
end
assert result == ~s{<text><text></text></text>}
end
test "renders a text with attrs and a block" do
{:safe, result} =
temple do
text_(class: "hello") do
text_()
end
end
assert result ==
~s{<text class="hello"><text></text></text>}
end
end

View File

@ -1,23 +0,0 @@
defmodule Temple.UtilsTest do
use ExUnit.Case, async: true
describe "from_safe/1" do
test "returns a the text from a safe partial" do
expected = "I am safe!"
partial = {:safe, expected}
result = Temple.Utils.from_safe(partial)
assert result == expected
end
test "escapes an unsafe partial and returns the text" do
expected = "I am &lt;safe&gt;!"
partial = "I am <safe>!"
result = Temple.Utils.from_safe(partial)
assert result == expected
end
end
end

View File

@ -3,216 +3,266 @@ defmodule TempleTest do
use Temple
use Temple.Support.Utils
describe "custom component" do
test "defcomponent works when requiring the module" do
require Temple.Support.Component, as: C
test "renders an attribute on a div passed as a variable" do
result =
temple do
div class: "hello" do
div class: "hi"
end
end
{:safe, result} =
temple do
C.flex()
assert result == ~s{<div class="hello"><div class="hi"></div></div>}
end
C.flex([])
C.flex([], [])
test "renders void element" do
result =
temple do
input name: "password"
end
C.flex do
text "hi"
assert result == ~s{<input name="password">}
end
test "renders a text node from the text keyword with siblings" do
result =
temple do
div class: "hello" do
"hi"
"foo"
end
end
assert result == ~s{<div class="hello">hifoo</div>}
end
test "renders a variable text node as eex" do
result =
temple do
div class: "hello" do
foo
end
end
assert result == ~s{<div class="hello"><%= foo %></div>}
end
test "renders an assign text node as eex" do
result =
temple do
div class: "hello" do
@foo
end
end
assert result == ~s{<div class="hello"><%= @foo %></div>}
end
test "renders a match expression" do
result =
temple do
x = 420
div do
"blaze it"
end
end
assert result == ~s{<% x = 420 %><div>blaze it</div>}
end
test "renders a non-match expression" do
result =
temple do
IO.inspect(:foo)
div do
"bar"
end
end
assert result == ~s{<%= IO.inspect(:foo) %><div>bar</div>}
end
test "renders an expression in attr as eex" do
result =
temple do
div class: foo <> " bar"
end
assert result == ~s{<div class="<%= foo <> " bar" %>"></div>}
end
test "renders an attribute on a div passed as a variable as eex" do
result =
temple do
div class: Enum.map([:one, :two], fn x -> x end) do
div class: "hi"
end
end
assert result ==
~s{<div class="<%= Enum.map([:one, :two], fn x -> x end) %>"><div class="hi"></div></div>}
end
test "renders a for comprehension as eex" do
result =
temple do
for x <- 1..5 do
div class: "hi"
end
end
assert result == ~s{<%= for(x <- 1..5) do %><div class="hi"></div><% end %>}
end
test "renders an if expression as eex" do
result =
temple do
if true == false do
div class: "hi"
end
end
assert result == ~s{<%= if(true == false) do %><div class="hi"></div><% end %>}
end
test "renders an if/else expression as eex" do
result =
temple do
if true == false do
div class: "hi"
else
div class: "haha"
end
end
assert result ==
~s{<%= if(true == false) do %><div class="hi"></div><% else %><div class="haha"></div><% end %>}
end
test "renders an unless expression as eex" do
result =
temple do
unless true == false do
div class: "hi"
end
end
assert result == ~s{<%= unless(true == false) do %><div class="hi"></div><% end %>}
end
test "renders multiline anonymous function with 1 arg before the function" do
result =
temple do
form_for Routes.user_path(@conn, :create), fn f ->
"Name: "
text_input f, :name
end
end
assert result ==
~s{<%= form_for Routes.user_path(@conn, :create), fn f -> %>Name: <%= text_input(f, :name) %><% end %>}
end
test "renders multiline anonymous functions with 2 args before the function" do
result =
temple do
form_for @changeset, Routes.user_path(@conn, :create), fn f ->
"Name: "
text_input f, :name
end
end
assert result ==
~s{<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>Name: <%= text_input(f, :name) %><% end %>}
end
test "renders multiline anonymous functions with complex nested children" do
result =
temple do
form_for @changeset, Routes.user_path(@conn, :create), fn f ->
div do
"Name: "
text_input f, :name
end
end
end
assert result ==
~s{<div class="flex"></div><div class="flex"></div><div class="flex"></div><div class="flex"></div>}
end
assert result ==
~s{<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %><div>Name: <%= text_input(f, :name) %></div><% end %>}
end
test "defines a basic component" do
import Temple.Support.Component
{:safe, result} =
temple do
flex()
test "renders multiline anonymous function with 3 arg before the function" do
result =
temple do
form_for @changeset, Routes.user_path(@conn, :create), [foo: :bar], fn f ->
"Name: "
text_input f, :name
end
end
assert result == ~s{<div class="flex"></div>}
end
assert result ==
~s{<%= form_for @changeset, Routes.user_path(@conn, :create), [foo: :bar], fn f -> %>Name: <%= text_input(f, :name) %><% end %>}
end
test "defines a component that takes 1 child" do
import Temple.Support.Component
test "renders multiline anonymous function with 1 arg before the function and 1 arg after" do
result =
temple do
form_for @changeset,
fn f ->
"Name: "
text_input f, :name
end,
foo: :bar
end
{:safe, result} =
temple do
takes_children do
div id: "dynamic-child"
assert result ==
~s{<%= form_for @changeset, fn f -> %>Name: <%= text_input(f, :name) %><% end, [foo: :bar] %>}
end
test "tags prefixed with Temple. should be interpreted as temple tags" do
result =
temple do
div do
Temple.span do
"bob"
end
end
end
assert result ==
~s{<div><div id="static-child-1"></div><div id="dynamic-child"></div><div id="static-child-2"></div></div>}
end
assert result == ~s{<div><span>bob</span></div>}
end
test "defines a component that takes multiple children" do
import Temple.Support.Component
{:safe, result} =
temple do
takes_children do
div id: "dynamic-child-1"
div id: "dynamic-child-2"
end
test "can pass do as an arg instead of a block" do
result =
temple do
div class: "font-bold" do
"Hello, world"
end
assert result ==
~s{<div><div id="static-child-1"></div><div id="dynamic-child-1"></div><div id="dynamic-child-2"></div><div id="static-child-2"></div></div>}
end
div class: "font-bold", do: "Hello, world"
div do: "Hello, world"
end
test "can access a prop" do
import Temple.Support.Component
assert result ==
~s{<div class="font-bold">Hello, world</div><div class="font-bold">Hello, world</div><div>Hello, world</div>}
end
{:safe, result} =
temple do
takes_children name: "mitch" do
text @name
end
test "passing 'compact: true' will not insert new lines" do
import Temple.Support.Utils, only: []
import Kernel
result =
temple do
p compact: true do
"Bob"
end
assert result ==
~s{<div><div id="static-child-1"></div>mitch<div id="static-child-2"></div></div>}
end
test "can access assigns list" do
import Temple.Support.Component
assigns = [foo: "bar", hello: "world"]
{:safe, result} =
temple do
lists_assigns(assigns)
p compact: true do
foo
end
end
assert result == inspect(assigns)
end
test "can access assigns map" do
import Temple.Support.Component
assigns = %{foo: "bar", hello: "world"}
{:safe, result} =
temple do
lists_assigns(assigns)
end
assert result == inspect(assigns)
end
test "can have arbitrary code inside the definition" do
import Temple.Support.Component
{:safe, result} =
temple do
arbitrary_code()
end
assert result == ~s{<div>55</div>}
end
test "can use conditionals to render different markup" do
import Temple.Support.Component
{:safe, result} =
temple do
uses_conditionals(condition: true)
uses_conditionals(condition: false)
end
assert result == ~s{<div></div><span></span>}
end
test "can pass arbitrary data as assigns" do
import Temple.Support.Component
{:safe, result} =
temple do
arbitrary_data(
lists: [:atom, %{key: "value"}, {:status, :tuple}, "string", 1, [1, 2, 3]]
)
end
assert result ==
~s|<div>:atom</div><div>%{key: &quot;value&quot;}</div><div>{:status, :tuple}</div><div>&quot;string&quot;</div><div>1</div><div>[1, 2, 3]</div>|
end
test "can pass a variable as a prop" do
import Temple.Support.Component
bob = "hi"
{:safe, result} =
temple do
variable_as_prop(bob: bob)
end
assert result == ~s|<div id="hi"></div>|
end
test "can pass a variable as a prop to a component with a block" do
import Temple.Support.Component
bob = "hi"
{:safe, result} =
temple do
variable_as_prop_with_block bob: bob do
div()
end
end
assert result == ~s|<div id="hi"><div></div></div>|
end
test "can pass all of the assigns as a variable" do
import Temple.Support.Component
assigns = [bob: "hi"]
{:safe, result} =
temple do
variable_as_prop(assigns)
end
assert result == ~s|<div id="hi"></div>|
end
test "can pass all of the assigns as a variable with a block" do
import Temple.Support.Component
assigns = [bob: "hi"]
{:safe, result} =
temple do
variable_as_prop_with_block assigns do
div()
end
end
assert result == ~s|<div id="hi"><div></div></div>|
end
test "can pass a map as assigns with a block" do
import Temple.Support.Component
assigns = %{bob: "hi"}
{:safe, result} =
temple do
variable_as_prop_with_block assigns do
div()
end
variable_as_prop_with_block %{bob: "hi"} do
div()
end
end
assert result == ~s|<div id="hi"><div></div></div><div id="hi"><div></div></div>|
end
assert result == ~s{<p>Bob</p>\n<p><%= foo %></p>}
end
end