Utilize the EEx Engine instead of creating an EEx string (#177)
This commit is contained in:
parent
ece4cb8a26
commit
f942817994
147 changed files with 2752 additions and 4963 deletions
|
@ -25,7 +25,7 @@ locals_without_parens = ~w[
|
|||
marker mask mesh meshgradient meshpatch meshrow metadata mpath path pattern polygon
|
||||
polyline radialGradient rect set solidcolor stop svg switch symbol text
|
||||
textPath tspan unknown use view
|
||||
]a |> Enum.flat_map(fn e -> [{e, :*}, {:"#{e}!", :*}] end)
|
||||
]a |> Enum.map(fn e -> {e, :*} end)
|
||||
|
||||
[
|
||||
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
|
|
107
.github/workflows/ci.yml
vendored
107
.github/workflows/ci.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
otp: [23.x, 24.x]
|
||||
elixir: [1.10.x, 1.12.x]
|
||||
elixir: [1.13.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -20,14 +20,14 @@ jobs:
|
|||
with:
|
||||
otp-version: ${{matrix.otp}}
|
||||
elixir-version: ${{matrix.elixir}}
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
_build
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
||||
key: ${{ runner.os }}-mix-${{matrix.otp}}-${{matrix.elixir}}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-
|
||||
${{ runner.os }}-mix-${{matrix.otp}}-${{matrix.elixir}}-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
|
@ -36,77 +36,72 @@ jobs:
|
|||
- 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"
|
||||
# integration_tests:
|
||||
# runs-on: ubuntu-latest
|
||||
# name: Integration Test (${{matrix.elixir}}/${{matrix.otp}})
|
||||
# defaults:
|
||||
# run:
|
||||
# working-directory: "./integration_test/temple_demo"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
otp: [23.x]
|
||||
elixir: [1.9.x, 1.11.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
|
||||
|
||||
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: erlef/setup-beam@v1
|
||||
# with:
|
||||
# otp-version: 24.x
|
||||
# elixir-version: 1.13.x
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: ${{matrix.otp}}
|
||||
elixir-version: ${{matrix.elixir}}
|
||||
# - uses: actions/cache@v3
|
||||
# with:
|
||||
# path: |
|
||||
# deps
|
||||
# _build
|
||||
# key: ${{ runner.os }}-mix-24-1.13-${{ hashFiles('**/mix.lock') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-mix-24-1.13-
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
_build
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-
|
||||
# - name: Install Dependencies
|
||||
# if: steps.cache.outputs.cache-hit != 'true'
|
||||
# run: mix deps.get
|
||||
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: mix deps.get
|
||||
# - name: Run Tests
|
||||
# run: mix test || mix test --failed || mix test --failed
|
||||
# env:
|
||||
# MIX_ENV: test
|
||||
|
||||
- name: Run Tests
|
||||
run: mix test || mix test --failed || mix test --failed
|
||||
env:
|
||||
MIX_ENV: test
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: screenshots/
|
||||
# - uses: actions/upload-artifact@v2
|
||||
# if: failure()
|
||||
# with:
|
||||
# name: screenshots
|
||||
# path: screenshots/
|
||||
|
||||
formatter:
|
||||
runs-on: ubuntu-latest
|
||||
name: Formatter (1.11.x.x/23.x)
|
||||
name: Formatter (1.13.x.x/24.x)
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: 23.x
|
||||
elixir-version: 1.11.x
|
||||
- uses: actions/cache@v2
|
||||
otp-version: 24.x
|
||||
elixir-version: 1.13.x
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
_build
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
||||
key: ${{ runner.os }}-mix-24-1.13-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-
|
||||
${{ runner.os }}-mix-24-1.13-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
elixir 1.11.3
|
||||
erlang 23.2.6
|
||||
elixir ref:v1.13.4
|
||||
erlang 25.0-rc2
|
||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -2,6 +2,20 @@
|
|||
|
||||
## Main
|
||||
|
||||
### 0.9.0-rc.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Requires Elixir 1.13+
|
||||
- Whitespace control is now controlled by whether you use `do/end` or `:do` syntax. The `:do` syntax will render "tight" markup.
|
||||
- Components are no longer module based. Any function can now be a component. Now to render a component, you pass a function reference `c &my_component/1`.
|
||||
- Temple.Component has been removed, which removes the `render/1` macro for defining a component. Now all you need to do is define a function and have it take an `assigns` parameter and call the `temple/1` macro that is imported from `Temple`.
|
||||
- The `defcomp` macro has been removed, since now all you need is a function.
|
||||
- All Phoenix related things and dependencies have been removed. If you are going to use Temple with Phoenix, now use the [temple_phoenix](https://github.com/mhanberg/temple_phoenix) package instead.
|
||||
- Config options have changed. Now all you can configure are the aliases (unchanged from before) and now you can configure the EEx.Engine to use. By default it uses `EEx.SmartEngine`.
|
||||
|
||||
Please see the guides for more in depth migration information.
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Enhancements
|
||||
|
|
126
README.md
126
README.md
|
@ -1,13 +1,11 @@
|
|||
# ![](temple.png)
|
||||
# ![](temple-github-image.png)
|
||||
|
||||
[![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)
|
||||
|
||||
> You are looking at the README for the main branch. The README for the latest stable release is located [here](https://github.com/mhanberg/temple/tree/v0.8.0).
|
||||
> You are looking at the README for the main branch. The README for the latest stable release is located [here](https://github.com/mhanberg/temple/tree/v0.9.0).
|
||||
|
||||
Temple is a DSL for writing HTML and EEx 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).
|
||||
Temple is an Elixir DSL for writing HTML.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -16,8 +14,7 @@ Add `temple` to your list of dependencies in `mix.exs`:
|
|||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:temple, "~> 0.8.0"},
|
||||
{:phoenix_live_view, "~> 0.16"} # if you are using Phoenix LiveView
|
||||
{:temple, "~> 0.9.0-rc.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
@ -26,16 +23,16 @@ end
|
|||
Currently Temple has the following things on which it won't compromise.
|
||||
|
||||
- Will only work with valid Elixir syntax.
|
||||
- Should always work with normal EEx, as well as Phoenix and Phoenix LiveView.
|
||||
- Should work in all web environments such as Plug, Aino, Phoenix, and Phoenix LiveView.
|
||||
|
||||
## Usage
|
||||
|
||||
Using Temple is as simple as using the DSL inside of an `temple/1` block. This returns an EEx string at compile time.
|
||||
Using Temple is as simple as using the DSL inside of an `temple/1` block. The runtime result of the macro is your HTML.
|
||||
|
||||
See the [documentation](https://hexdocs.pm/temple/Temple.html) for more details.
|
||||
See the [guides](https://hexdocs.pm/temple/your-first-template.html) for more details.
|
||||
|
||||
```elixir
|
||||
use Temple
|
||||
import Temple
|
||||
|
||||
temple do
|
||||
h2 do: "todos"
|
||||
|
@ -66,15 +63,18 @@ end
|
|||
|
||||
### Components
|
||||
|
||||
Temple components provide an ergonomic API for creating flexible and reusable views. Unlike normal partials, Temple components can take slots, which are similar [Vue](https://v3.vuejs.org/guide/component-slots.html#named-slots).
|
||||
Temple components are simple to write and easy to use.
|
||||
|
||||
For example, if I were to define a `Card` component, I would create the following module.
|
||||
Unlike normal partials, Temple components have the concept of "slots", which are similar [Vue](https://v3.vuejs.org/guide/component-slots.html#named-slots). You can also refer to HEEx and Surface for examples of templates that have the "slot" concept.
|
||||
|
||||
Please see the [guides](https://hexdocs.pm/temple/components.html) for more details.
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.Components.Card do
|
||||
import Temple.Component
|
||||
defmodule MyAppWeb.Component do
|
||||
import Temple
|
||||
|
||||
render do
|
||||
def card(assigns) do
|
||||
temple do
|
||||
section do
|
||||
div do
|
||||
slot :header
|
||||
|
@ -89,17 +89,17 @@ defmodule MyAppWeb.Components.Card do
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
And we could use the component like so
|
||||
Using components is as simple as passing a reference to your component function to the `c` keyword.
|
||||
|
||||
```elixir
|
||||
# lib/my_app_web/views/page_view.ex
|
||||
alias MyAppWeb.Components.Card
|
||||
import MyAppWeb.Component
|
||||
|
||||
# lib/my_app_web/templates/page/index.html.exs
|
||||
c Card do
|
||||
c &card/1 do
|
||||
slot :header do
|
||||
@user.full_name
|
||||
end
|
||||
|
@ -117,87 +117,17 @@ c Card do
|
|||
end
|
||||
```
|
||||
|
||||
### Phoenix templates
|
||||
### Engine
|
||||
|
||||
To use temple as a Phoenix Template engine, you'll need to configure the right file extensions with the right Temple engine.
|
||||
By default, Temple will use the `EEx.SmartEngine` that is built into the Elixir standard library. If you are a web framework that uses it's own template engine (such as [Aino](https://github.com/oestrich/aino) and Phoenix/LiveView, you can configure Temple to it!
|
||||
|
||||
```elixir
|
||||
# config.exs
|
||||
config :phoenix, :template_engines,
|
||||
exs: Temple.Engine
|
||||
# or for LiveView support
|
||||
# this will work for files named like `index.html.lexs`
|
||||
# you can enable Elixir syntax highlighting in your editor
|
||||
lexs: Temple.LiveViewEngine
|
||||
# config/config.exs
|
||||
|
||||
# If you're going to be using live_view, make sure to set the `:mode` to `:live_view`.
|
||||
# This is necessary for Temple to emit markup that is compatible.
|
||||
config :temple, :mode, :live_view # defaults to normal
|
||||
|
||||
# config/dev.exs
|
||||
config :your_app, YourAppWeb.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r"lib/myapp_web/(live|views)/.*(ex|exs|lexs)$",
|
||||
~r"lib/myapp_web/templates/.*(eex|exs|lexs)$"
|
||||
]
|
||||
]
|
||||
config :temple,
|
||||
engine: Aino.View.Engine # or Phoenix.HTML.Engine or Phoenix.LiveView.Engine
|
||||
```
|
||||
|
||||
```elixir
|
||||
# app.html.exs
|
||||
|
||||
"<!DOCTYPE html>"
|
||||
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: "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 href: "https://hexdocs.pm/phoenix/overview.html"), do: "Get Started"
|
||||
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", do: get_flash(@conn, :info)
|
||||
p class: "alert alert-danger", role: "alert", do: get_flash(@conn, :error)
|
||||
|
||||
render @view_module, @view_template, assigns
|
||||
end
|
||||
|
||||
script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
#### temple.gen.layout
|
||||
|
||||
Generates the app layout.
|
||||
|
||||
#### temple.gen.html
|
||||
|
||||
Generates the templates for a resource.
|
||||
|
||||
### Formatter
|
||||
|
||||
To include Temple's formatter configuration, add `:temple` to your `.formatter.exs`.
|
||||
|
@ -209,7 +139,13 @@ To include Temple's formatter configuration, add `:temple` to your `.formatter.e
|
|||
]
|
||||
```
|
||||
|
||||
## Phoenix
|
||||
|
||||
To use with [Phoenix](https://github.com/phoenixframework/phoenix), please use the [temple_phoenix](https://github.com/mhanberg/temple_phoenix) package! This bundles up some useful helpers as well as the Phoenix Template engine.
|
||||
|
||||
## Related
|
||||
|
||||
- [Introducing Temple: An elegant HTML library for Elixir and Phoenix](https://www.mitchellhanberg.com/introducing-temple-an-elegant-html-library-for-elixir-and-phoenix/)
|
||||
- [Temple, AST, and Protocols](https://www.mitchellhanberg.com/temple-ast-and-protocols/)
|
||||
- [Thinking Elixir Episode 92: Temple with Mitchell Hanberg](https://podcast.thinkingelixir.com/92)
|
||||
- [How EEx Turns Your Template Into HTML](https://www.mitchellhanberg.com/how-eex-turns-your-template-into-html/)
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
use Mix.Config
|
||||
import Config
|
||||
|
||||
import_config "#{Mix.env()}.exs"
|
||||
import_config "#{config_env()}.exs"
|
||||
|
|
|
@ -1 +1 @@
|
|||
use Mix.Config
|
||||
import Config
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
use Mix.Config
|
||||
|
||||
# this is to make the warning go away,
|
||||
# Temple does not use a json_library
|
||||
config :phoenix, json_library: Temple
|
||||
import Config
|
||||
|
||||
config :temple,
|
||||
aliases: [
|
||||
|
|
240
guides/components.md
Normal file
240
guides/components.md
Normal file
|
@ -0,0 +1,240 @@
|
|||
# Components
|
||||
|
||||
Temple has the concept of components, which allow you an expressive and composable way to break up your templates into re-usable chunks.
|
||||
|
||||
A component is any arity-1 function that take an argument called `assigns` and returns the result of the `Temple.temple/1` macro.
|
||||
|
||||
## Definition
|
||||
|
||||
Here is an example of a simple Temple component. You can observe that it seems very similar to a regular Temple template, and that is because it is a regular template!
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Components do
|
||||
import Temple
|
||||
|
||||
def button(assigns) do
|
||||
temple do
|
||||
button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do
|
||||
@text
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To use a component, you will use the special `c` keyword. This is called a "keyword" because it is not a function or macro, but only exists inside of the `Temple.temple/1` block.
|
||||
|
||||
The first argument will be the function reference to your component function, followed by any assigns.
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.ConfirmDialog do
|
||||
import Temple
|
||||
import MyApp.Components
|
||||
|
||||
def render(assigns) do
|
||||
temple do
|
||||
dialog open: true do
|
||||
p do: "Are you sure?"
|
||||
form method: "dialog" do
|
||||
c &button/1, class: "border border-white", text: "Yes"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Slots
|
||||
|
||||
Temple components can take "slots" as well. This is the method for providing dynamic content from the call site into the component.
|
||||
|
||||
Slots are defined and rendered using the `slot` keyword. This is similar to the `c` keyword, in that it is not defined using a function or macro.
|
||||
|
||||
### Default Slot
|
||||
|
||||
The default slot can be rendered from within your component by passing the `slot` the atom `:default`. Let's redefine our button component using slots.
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Components do
|
||||
import Temple
|
||||
|
||||
def button(assigns) do
|
||||
temple do
|
||||
button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
You can pass content through the "default" slot of your component simply by passing a `do/end` block to your component at the call site. This is a special case for the default slot.
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.ConfirmDialog do
|
||||
import Temple
|
||||
import MyApp.Components
|
||||
|
||||
def render(assigns) do
|
||||
temple do
|
||||
dialog open: true do
|
||||
p do: "Are you sure?"
|
||||
form method: "dialog" do
|
||||
c &button/1, class: "border border-white" do
|
||||
"Yes"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Named Slots
|
||||
|
||||
You can also define a "named" slot, which allows you to pass more than one set of dynamic content to your component.
|
||||
|
||||
We'll use a "card" example to illustrate this. This example is adapted from the [Surface documentation](https://surface-ui.org/slots) on slots.
|
||||
|
||||
#### Definition
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Components do
|
||||
import Temple
|
||||
|
||||
def card(assigns) do
|
||||
temple do
|
||||
div class: "card" do
|
||||
header class: "card-header", style: "background-color: @f5f5f5" do
|
||||
p class: "card-header-title" do
|
||||
slot :header
|
||||
end
|
||||
end
|
||||
|
||||
div class: "card-content" do
|
||||
div class: "content" do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
|
||||
footer class: "card-footer", style: "background-color: #f5f5f5" do
|
||||
slot :footer
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
```elixir
|
||||
def MyApp.CardExample do
|
||||
import Temple
|
||||
import MyApp.Components
|
||||
|
||||
def render(assigns) do
|
||||
temple do
|
||||
c &card/1 do
|
||||
slot :header do
|
||||
"A simple card component"
|
||||
end
|
||||
|
||||
"This example demonstrates how to create components with multiple, named slots"
|
||||
|
||||
slot :footer do
|
||||
a href="#", class: "card-footer-item", do: "Footer Item 1"
|
||||
a href="#", class: "card-footer-item", do: "Footer Item 2"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Passing Data Through Slots
|
||||
|
||||
Sometimes it is necessary to pass data from a component definition back to the call site.
|
||||
|
||||
Let's look at what a `table` component could look like.
|
||||
|
||||
#### Definition
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Components do
|
||||
import Temple
|
||||
|
||||
def cols(items) do
|
||||
items
|
||||
|> List.first()
|
||||
|> Map.keys()
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
def table(assigns) do
|
||||
temple do
|
||||
table do
|
||||
thead do
|
||||
tr do
|
||||
for col <- cols(@entries) do
|
||||
tr do: String.upcase(to_string(col))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
tbody do
|
||||
for row <- @entries do
|
||||
tr do
|
||||
for col <- cols(@entries) do
|
||||
td do
|
||||
slot :cell, %{value: row[cell]}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
When we render the slot, we can pattern match on the data passed through the slot. If this seems familiar, it's because this is the same syntax you use when writing your tests using `ExUnit.Case.test/3`.
|
||||
|
||||
```elixir
|
||||
def MyApp.TableExample do
|
||||
import Temple
|
||||
import MyApp.Componens
|
||||
|
||||
def render(assigns) do
|
||||
temple do
|
||||
section do
|
||||
h2 do: "Inventory Levels"
|
||||
|
||||
c &table/1, entries: @item_inventories do
|
||||
slot :cell, %{value: value} do
|
||||
case value do
|
||||
0 ->
|
||||
span class: "font-bold" do
|
||||
"Out of stock!"
|
||||
end
|
||||
|
||||
level when is_number(level) ->
|
||||
span do
|
||||
"#{level} in stock"
|
||||
end
|
||||
|
||||
_ ->
|
||||
span do: value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
68
guides/getting-started.md
Normal file
68
guides/getting-started.md
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Getting Started
|
||||
|
||||
## Install
|
||||
|
||||
Welcome!
|
||||
|
||||
Temple is a HTML DSL for Elixir, let's get started!
|
||||
|
||||
|
||||
First, make sure you are using Elixir `V1.13` or higher.
|
||||
|
||||
Add `:temple` to your deps and run `mix deps.get`
|
||||
|
||||
```elixir
|
||||
{:temple, "~> 0.9.0-rc.0"}
|
||||
```
|
||||
|
||||
Now you must prepend the Temple compiler to your projects `:compilers` configuration in `mix.exs`. There is a chance that your project doesn't set this option at all, but don't worry, it's really easy to add!
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
# ...
|
||||
compilers: [:temple] ++ Mix.compilers(),
|
||||
# ...
|
||||
]
|
||||
end
|
||||
|
||||
# ...
|
||||
|
||||
end
|
||||
```
|
||||
|
||||
All done, Now let's start building our app!
|
||||
|
||||
## Configuration
|
||||
|
||||
Temple works out of the box without any configuration, but here are a couple of conifg options that you could need to use.
|
||||
|
||||
### Engine
|
||||
|
||||
By default, Temple uses the built in `EEx.SmartEngine`. If you want to use a different engine, this is as easy as setting the `:engine` configuration option.
|
||||
|
||||
```elixir
|
||||
# config/config.exs
|
||||
|
||||
config :temple,
|
||||
engine: Phoenix.HTML.Engine
|
||||
```
|
||||
|
||||
### Aliases
|
||||
|
||||
Temple code will reserve some local function calls for HTML tags. If you have a local function that you would like to use instead, you can create an alias for any tag.
|
||||
|
||||
Common aliases for Phoenix projects look like this:
|
||||
|
||||
```elixir
|
||||
config :temple,
|
||||
aliases: [
|
||||
label: :label_tag,
|
||||
link: :link_tag,
|
||||
select: :select_tag,
|
||||
textarea: :textarea_tag
|
||||
]
|
||||
```
|
3
guides/migrating/0.8-to-0.9.md
Normal file
3
guides/migrating/0.8-to-0.9.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Migrating from 0.8 to 0.9
|
||||
|
||||
TODO: explain it
|
236
guides/your-first-template.md
Normal file
236
guides/your-first-template.md
Normal file
|
@ -0,0 +1,236 @@
|
|||
# Your First Template
|
||||
|
||||
A Temple template is written inside of the `Temple.temple/1` macro. Code inside there will be compiled into efficient Elixir code by the configured EEx engine.
|
||||
|
||||
Local functions that have a corresponding HTML5 tag are reserved and will be used when generated your markup. Let's take a look at a basic form written with Temple.
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.FormExample do
|
||||
import Temple
|
||||
|
||||
def form_page() do
|
||||
assigns = %{title: "My Site | Sign Up", logged_in: false}
|
||||
|
||||
temple do
|
||||
"<!DOCTYPE html>"
|
||||
|
||||
html 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"
|
||||
link rel: "stylesheet", href: "/css/app.css"
|
||||
|
||||
title do: @title
|
||||
end
|
||||
|
||||
body do
|
||||
if @logged_in do
|
||||
header class: "header" do
|
||||
ul do
|
||||
li do
|
||||
a href: "/", do: "Home"
|
||||
end
|
||||
li do
|
||||
a href: "/logout", do: "Logout"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
form action: "", method: "get", class: "form-example" do
|
||||
div class: "form-example" do
|
||||
label for: "name", do: "Enter your name:"
|
||||
input type: "text", name: "name", id: "name", required: true
|
||||
end
|
||||
div class: "form-example" do
|
||||
label for: "email", do: "Enter your email:"
|
||||
input type: "email", name: "email", id: "email", required: true
|
||||
end
|
||||
div class: "form-example" do
|
||||
input type: "submit", value: "Subscribe!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This example showcases an entire HTML page made with Temple! Let's dive a little deeper everything we're seeing here.
|
||||
|
||||
Through out this guide, you will see code that includes features that are explained later on. Feel free to skip ahead to read on, or just keep reading. It will all make sense eventually!
|
||||
|
||||
## Text Nodes
|
||||
|
||||
The text node is a basic building block of any HTML document. In Temple, text nodes are represented by Elixir string literals.
|
||||
|
||||
The very first line of the previous example is our doc type, emitted into the final document with `"<!DOCTYPE html>"`. This is a text node and will be emitted into the document as-is.
|
||||
|
||||
Note: String _literals_ are emitted into text nodes. If you are using string interpolation with the `#{some_expression}` syntax, that is treated as an expression and will be evaluated in whichever way the configured engine evaluates expression. By default, the `EEx.SmartEngine` doesn't do any escaping of expressions, so that could still be emitted as-is, or even as HTML to be interpreted by your web browser.
|
||||
|
||||
## Void Tags
|
||||
|
||||
Void tags are HTML5 tags that do not have children, meaning they are "self closing".
|
||||
|
||||
We can observe these in the previous example as the `<input>` tag. You'll note that the tag does not have a `:do` key or a `do` block.
|
||||
|
||||
## Non-void Tags
|
||||
|
||||
Non-void tags are HTML5 tags that _do_ have children. You are probably most familiar with these type of tags, as they include the famous `<div></div>` and `<span></span>`.
|
||||
|
||||
These tags can enclose their children nodes with either a `do/end` block or the inline `:do` keyword.
|
||||
|
||||
### Whitespace
|
||||
|
||||
Nonvoid tags that use the `do/end` syntax will be emitted _with_ internal whitespace.
|
||||
|
||||
```elixir
|
||||
temple do
|
||||
div class: "foo" do
|
||||
# children
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
...will emit markup that looks like...
|
||||
|
||||
```html
|
||||
<div class="foo">
|
||||
<!-- children -->
|
||||
</div>
|
||||
```
|
||||
|
||||
Note: The Elixir comment _will not_ be rendered into an HTML comment. This is just used in the example. (This does sound like a good feature though...)
|
||||
|
||||
Nonvoid tags that use the `:do` keyword syntax will be emitted _without_ internal whitespace. This allows you to correctly use the `:empty` CSS psuedo-selector in your stylesheet.
|
||||
|
||||
|
||||
```elixir
|
||||
temple do
|
||||
p class: "alert alert-info", do: "Your account was recently updated!"
|
||||
end
|
||||
```
|
||||
|
||||
...will emit markup that looks like...
|
||||
|
||||
```html
|
||||
<p class="alert alert-info">Your account was recently updated!</p>
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
Attributes are declared as a keyword list.
|
||||
|
||||
- Keys with underscores are converted to the kebab syntax.
|
||||
- Values can be Elixir expressions.
|
||||
- Values that are compile time `true` will be emitted as a boolean attribute. `disabled` and `checked` are examples of boolean attributes.
|
||||
- Values that are compile time `false` will not be emitted into the document at all.
|
||||
- The class attribute has a special "object syntax" that allows you to specify classes as a keyword list, only emitting classes that evaluate to true into the final class.
|
||||
|
||||
Let's look at an example.
|
||||
|
||||
```elixir
|
||||
assigns = %{highlight?: false, user_name: "Mitch"}
|
||||
|
||||
temple do
|
||||
div id: "hero" do
|
||||
h2 class: "font-bold", do: "Profile"
|
||||
|
||||
section data_controller: "hero" do
|
||||
p class: ["border": @highlight?] do
|
||||
"Name: #{@user_name}"
|
||||
end
|
||||
|
||||
video autoplay: true, src: "https://example.com/rick-rolled.mp4"
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
...will emit markup that looks like...
|
||||
|
||||
```html
|
||||
<div id="hero">
|
||||
<h2 class="font-bold">Profile</h2>
|
||||
|
||||
<section data-controller="hero">
|
||||
<p class="">
|
||||
Name: Mitch
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<video autoplay src="https://example.com/rick-rolled.mp4"></video>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Elixir Expressions
|
||||
|
||||
### They Just Work
|
||||
|
||||
Any Elixir expression can be used anywhere inside of a Temple template. Here are a few examples.
|
||||
|
||||
```elixir
|
||||
temple do
|
||||
h2 do: "Members"
|
||||
|
||||
ul do
|
||||
for member <- @members do
|
||||
li do: member
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Match Expressions
|
||||
|
||||
Match expression are handled slightly differently. Generally if you are assigning an expression to a variable (a match), you are going to use that binding later and do _not_ want to emit it into the document.
|
||||
|
||||
So, match expressions are _not_ emitted into the document. They are functionally equivalent to the `<% .. %.` syntax of `EEx`. The expression is evaluated, but not included in the rendered document.
|
||||
|
||||
Typically you should not be writing this type of expression inside of your template, but if you wanted to declare an alias, you would need to write the following to not emit the alias into the document.
|
||||
|
||||
```elixir
|
||||
temple do
|
||||
_ = alias My.Deep.Module
|
||||
|
||||
div do
|
||||
Module.func()
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Assigns
|
||||
|
||||
Since Temple uses the `EEx.SmartEngine` by default, you are able to use the assigns feature.
|
||||
|
||||
The assigns feature allows you to ergonomically access the members of a `assigns` variable by the `@` macro.
|
||||
|
||||
The assign variable just needs to exist within the scope of the template (the same as a normal `EEx` template that uses `EEx.SmartEngine`), it can be a function parameter or created inside the function.
|
||||
|
||||
```elixir
|
||||
def card(assigns) do
|
||||
temple do
|
||||
div class: "card" do
|
||||
section class: "card-header" do
|
||||
@name
|
||||
end
|
||||
|
||||
section class: "card-body" do
|
||||
@bio
|
||||
end
|
||||
|
||||
if Enum.any?(@socials) do
|
||||
section class: "card-footer" do
|
||||
for social <- @socials do
|
||||
a href: social.link do
|
||||
social.name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
|
@ -1,5 +0,0 @@
|
|||
[
|
||||
import_deps: [:ecto, :phoenix, :temple],
|
||||
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
subdirectories: ["priv/*/migrations"]
|
||||
]
|
|
@ -1 +0,0 @@
|
|||
elixir 1.9.4
|
|
@ -1,18 +0,0 @@
|
|||
# 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
|
|
@ -1,43 +0,0 @@
|
|||
# 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,
|
||||
mode: :normal,
|
||||
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"
|
|
@ -1,68 +0,0 @@
|
|||
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|components)/.*(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
|
|
@ -1,55 +0,0 @@
|
|||
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"
|
|
@ -1,41 +0,0 @@
|
|||
# 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.
|
|
@ -1,33 +0,0 @@
|
|||
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,
|
||||
chromedriver: [
|
||||
# headless: false,
|
||||
binary: System.get_env("CHROME_BROWSER")
|
||||
],
|
||||
base_url: "http://localhost:4002",
|
||||
otp_app: :temple_demo,
|
||||
screenshot_on_failure: true
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
|
@ -1,9 +0,0 @@
|
|||
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
|
|
@ -1,34 +0,0 @@
|
|||
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
|
|
@ -1,104 +0,0 @@
|
|||
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
|
|
@ -1,20 +0,0 @@
|
|||
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
|
|
@ -1,5 +0,0 @@
|
|||
defmodule TempleDemo.Repo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :temple_demo,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
end
|
|
@ -1,85 +0,0 @@
|
|||
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]
|
||||
|
||||
import Temple.Component
|
||||
alias TempleDemoWeb.Component.Outer
|
||||
alias TempleDemoWeb.Component.Flash
|
||||
alias TempleDemoWeb.Component.Form
|
||||
|
||||
# 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
|
|
@ -1,35 +0,0 @@
|
|||
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
|
|
@ -1,9 +0,0 @@
|
|||
defmodule TempleDemoWeb.Component.Flash do
|
||||
import Temple.Component
|
||||
|
||||
render do
|
||||
div class: "alert alert-#{@type}", style: "border: solid 5px pink" do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
defmodule TempleDemoWeb.Component.Form do
|
||||
import Temple.Component
|
||||
|
||||
render do
|
||||
f = Phoenix.HTML.Form.form_for(@changeset, @action)
|
||||
|
||||
f
|
||||
|
||||
slot :f, f: f
|
||||
|
||||
"</form>"
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
defmodule TempleDemoWeb.Component.Inner do
|
||||
import Temple.Component
|
||||
|
||||
render do
|
||||
div id: "inner", outer_id: @outer_id do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
defmodule TempleDemoWeb.Component.Outer do
|
||||
import Temple.Component
|
||||
alias TempleDemoWeb.Component.Inner
|
||||
|
||||
render do
|
||||
c Inner, outer_id: "from-outer" do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
defmodule TempleDemoWeb.PageController do
|
||||
use TempleDemoWeb, :controller
|
||||
|
||||
def index(conn, params) do
|
||||
render(conn, "index.html", text: params["text"])
|
||||
end
|
||||
end
|
|
@ -1,63 +0,0 @@
|
|||
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
|
|
@ -1,58 +0,0 @@
|
|||
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
|
|
@ -1,24 +0,0 @@
|
|||
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
|
|
@ -1,43 +0,0 @@
|
|||
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
|
|
@ -1,53 +0,0 @@
|
|||
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
|
|
@ -1,44 +0,0 @@
|
|||
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
|
||||
for {type, message} <- get_flash(@conn) do
|
||||
p class: "alert alert-#{type}", role: "alert" do
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
@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
|
|
@ -1,77 +0,0 @@
|
|||
section class: "phx-hero" do
|
||||
h1 do
|
||||
gettext("Welcome to %{name}!", name: "Phoenix")
|
||||
end
|
||||
|
||||
c Outer, outer_id: "hello" do
|
||||
"inner content of outer"
|
||||
end
|
||||
|
||||
case @text do
|
||||
"staging" ->
|
||||
p do
|
||||
"Peace-of-mind from prototype to staging"
|
||||
end
|
||||
|
||||
_ ->
|
||||
p do
|
||||
"Peace-of-mind from prototype to production"
|
||||
end
|
||||
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 & 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
|
|
@ -1,7 +0,0 @@
|
|||
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
|
|
@ -1,31 +0,0 @@
|
|||
c Form, changeset: @changeset, action: @action do
|
||||
slot :f, %{f: f} do
|
||||
if @changeset.action do
|
||||
c Flash, type: :info 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)
|
||||
|
||||
input type: "text", disabled: true, id: "disabled-input"
|
||||
|
||||
div do
|
||||
submit "Save"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
h1 do: "Listing Posts"
|
||||
|
||||
table do
|
||||
c Headers do
|
||||
th do: "Title"
|
||||
th do: "Body"
|
||||
th do: "Published at"
|
||||
th do: "Author"
|
||||
th do: "BOB"
|
||||
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
|
||||
|
||||
span do
|
||||
link "New Post", to: Routes.post_path(@conn, :new)
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
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
|
|
@ -1,28 +0,0 @@
|
|||
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
|
|
@ -1,47 +0,0 @@
|
|||
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
|
|
@ -1,16 +0,0 @@
|
|||
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
|
|
@ -1,3 +0,0 @@
|
|||
defmodule TempleDemoWeb.LayoutView do
|
||||
use TempleDemoWeb, :view
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
defmodule TempleDemoWeb.PageView do
|
||||
use TempleDemoWeb, :view
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
defmodule TempleDemoWeb.PostView do
|
||||
use TempleDemoWeb, :view
|
||||
|
||||
def thing(), do: "foobar"
|
||||
|
||||
defcomp Headers do
|
||||
thead id: PostView.thing() do
|
||||
tr do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,68 +0,0 @@
|
|||
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.28.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
|
|
@ -1,44 +0,0 @@
|
|||
%{
|
||||
"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.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
|
||||
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
|
||||
"db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"},
|
||||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
||||
"ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [: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", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [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.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"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.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
|
||||
"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.8", "71cfa7a9bb9a37af4df98939790642f210e35f696b935ca6d9d9c55a884621a4", [: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", "35ded0a32f4836168c7ab6c33b88822eccd201bcd9492125a9bea4c54332d955"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
|
||||
"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"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"},
|
||||
"plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.5.0", "51c998f788c4e68fc9f947a5eba8c215fbb1d63a520f7604134cab0270ea6513", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5b2c8925a5e2587446f33810a58c01e66b3c345652eeec809b76ba007acde71a"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
||||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"postgrex": {:hex, :postgrex, "0.15.7", "724410acd48abac529d0faa6c2a379fb8ae2088e31247687b16cacc0e0883372", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "88310c010ff047cecd73d5ceca1d99205e4b1ab1b9abfdab7e00f5c9d20ef8f9"},
|
||||
"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.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
|
||||
"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.28.0", "2ff217c0f245cadb3e5d91748ebcf0102873ceb9ef8a3507717c8bdd73915668", [: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]}, {:web_driver_client, "~> 0.1.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "e58112650d0b51e81714a626eab7d486d7a77342c9bbc2ba262b6653f9b22558"},
|
||||
"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"},
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
## `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 ""
|
|
@ -1,95 +0,0 @@
|
|||
## 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 ""
|
|
@ -1,4 +0,0 @@
|
|||
[
|
||||
import_deps: [:ecto_sql],
|
||||
inputs: ["*.exs"]
|
||||
]
|
|
@ -1,14 +0,0 @@
|
|||
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
|
|
@ -1,11 +0,0 @@
|
|||
# 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.
|
|
@ -1,40 +0,0 @@
|
|||
/* 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-error {
|
||||
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.
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
|
@ -1,3 +0,0 @@
|
|||
// 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
|
@ -1,76 +0,0 @@
|
|||
"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);
|
||||
})();
|
|
@ -1,5 +0,0 @@
|
|||
# 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: /
|
|
@ -1,40 +0,0 @@
|
|||
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
|
|
@ -1,43 +0,0 @@
|
|||
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
|
|
@ -1,55 +0,0 @@
|
|||
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
|
|
@ -1,80 +0,0 @@
|
|||
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
|
|
@ -1,8 +0,0 @@
|
|||
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
|
|
@ -1,99 +0,0 @@
|
|||
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
|
|
@ -1,44 +0,0 @@
|
|||
defmodule TempleDemoWeb.TempleFeatureTest do
|
||||
use ExUnit.Case, async: false
|
||||
use Wallaby.Feature
|
||||
alias TempleDemoWeb.Router.Helpers, as: Routes
|
||||
@endpoint TempleDemoWeb.Endpoint
|
||||
|
||||
feature "renders the homepage", %{session: session} do
|
||||
session
|
||||
|> visit("/")
|
||||
|> assert_text("Welcome to Phoenix!")
|
||||
|> assert_text("inner content of outer")
|
||||
end
|
||||
|
||||
feature "case statements work", %{session: session} do
|
||||
session
|
||||
|> visit("/?text=staging")
|
||||
|> assert_text("Welcome to Phoenix!")
|
||||
|> assert_text("Peace-of-mind from prototype to staging")
|
||||
|> visit("/?text=foobar")
|
||||
|> assert_text("Welcome to Phoenix!")
|
||||
|> assert_text("Peace-of-mind from prototype to production")
|
||||
end
|
||||
|
||||
feature "can create a new post", %{session: session} do
|
||||
session
|
||||
|> visit(Routes.post_path(@endpoint, :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")
|
||||
|> assert_has(Query.css("#disabled-input[disabled]"))
|
||||
|> click(Query.button("Save"))
|
||||
|> assert_text("Post created successfully.")
|
||||
end
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
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
|
|
@ -1,8 +0,0 @@
|
|||
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
|
|
@ -1,3 +0,0 @@
|
|||
defmodule TempleDemoWeb.PageViewTest do
|
||||
use TempleDemoWeb.ConnCase, async: true
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, :manual)
|
||||
|
||||
ExUnit.start()
|
5
integration_test/temple_plug_demo/.formatter.exs
Normal file
5
integration_test/temple_plug_demo/.formatter.exs
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
import_deps: [:plug, :temple],
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
|
@ -7,7 +7,7 @@
|
|||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps/
|
||||
|
||||
# Where 3rd-party dependencies like ExDoc output generated docs.
|
||||
# Where third-party dependencies like ExDoc output generated docs.
|
||||
/doc/
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
|
@ -20,7 +20,7 @@ erl_crash.dump
|
|||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
temple_demo-*.tar
|
||||
|
||||
/screenshots/
|
||||
temple_plug_demo-*.tar
|
||||
|
||||
# Temporary files, for example, from tests.
|
||||
/tmp/
|
21
integration_test/temple_plug_demo/README.md
Normal file
21
integration_test/temple_plug_demo/README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# TemplePlugDemo
|
||||
|
||||
**TODO: Add description**
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||
by adding `temple_plug_demo` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:temple_plug_demo, "~> 0.1.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||
be found at <https://hexdocs.pm/temple_plug_demo>.
|
||||
|
5
integration_test/temple_plug_demo/assets/css/app.css
Normal file
5
integration_test/temple_plug_demo/assets/css/app.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
|
13
integration_test/temple_plug_demo/assets/tailwind.config.js
Normal file
13
integration_test/temple_plug_demo/assets/tailwind.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
// See the Tailwind configuration guide for advanced usage
|
||||
// https://tailwindcss.com/docs/configuration
|
||||
module.exports = {
|
||||
content: [
|
||||
'../lib/**/*.*ex'
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms')
|
||||
]
|
||||
}
|
15
integration_test/temple_plug_demo/config/config.exs
Normal file
15
integration_test/temple_plug_demo/config/config.exs
Normal file
|
@ -0,0 +1,15 @@
|
|||
import Config
|
||||
|
||||
config :tailwind,
|
||||
version: "3.0.24",
|
||||
default: [
|
||||
args: ~w(
|
||||
--config=tailwind.config.js
|
||||
--input=css/app.css
|
||||
--output=../priv/static/assets/app.css
|
||||
),
|
||||
cd: Path.expand("../assets", __DIR__)
|
||||
]
|
||||
|
||||
config :temple_plug_demo, :watchers,
|
||||
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
|
18
integration_test/temple_plug_demo/lib/temple_plug_demo.ex
Normal file
18
integration_test/temple_plug_demo/lib/temple_plug_demo.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule TemplePlugDemo do
|
||||
@moduledoc """
|
||||
Documentation for `TemplePlugDemo`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Hello world.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> TemplePlugDemo.hello()
|
||||
:world
|
||||
|
||||
"""
|
||||
def hello do
|
||||
:world
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
defmodule TemplePlugDemo.Application do
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
|
||||
def start(_type, _args) do
|
||||
children =
|
||||
[
|
||||
{Bandit, plug: TemplePlugDemo.Router, scheme: :http, options: [port: 4001]}
|
||||
] ++ watcher_children()
|
||||
|
||||
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
def watcher_children() do
|
||||
for args <- Application.get_env(:temple_plug_demo, :watchers, []) do
|
||||
{TemplePlugDemo.Watcher, args}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,47 @@
|
|||
defmodule TemplePlugDemo.Router do
|
||||
use Plug.Router
|
||||
|
||||
import Temple
|
||||
|
||||
plug Plug.Static, from: {:temple_plug_demo, "priv/static"}, at: "/static"
|
||||
|
||||
plug :match
|
||||
plug :dispatch
|
||||
|
||||
get "/" do
|
||||
assigns = %{title: "Motch App"}
|
||||
|
||||
response =
|
||||
temple do
|
||||
"<!DOCTYPE html>"
|
||||
|
||||
html do
|
||||
head do
|
||||
title do: @title
|
||||
|
||||
link rel: "stylesheet", href: "/static/assets/app.css"
|
||||
end
|
||||
|
||||
body class: "font-sans container mx-auto" do
|
||||
span do
|
||||
"loose"
|
||||
end
|
||||
|
||||
span do: "tight"
|
||||
|
||||
div do
|
||||
"world"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, response)
|
||||
end
|
||||
|
||||
match _ do
|
||||
send_resp(conn, 404, "oops")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,62 @@
|
|||
# module mostly taken from `Phoenix.Endpoint.Watcher`
|
||||
|
||||
defmodule TemplePlugDemo.Watcher do
|
||||
@moduledoc false
|
||||
require Logger
|
||||
|
||||
def child_spec(args) do
|
||||
%{
|
||||
id: make_ref(),
|
||||
start: {__MODULE__, :start_link, [args]},
|
||||
restart: :transient
|
||||
}
|
||||
end
|
||||
|
||||
def start_link({cmd, args}) do
|
||||
Task.start_link(__MODULE__, :watch, [to_string(cmd), args])
|
||||
end
|
||||
|
||||
def watch(_cmd, {mod, fun, args}) do
|
||||
try do
|
||||
apply(mod, fun, args)
|
||||
catch
|
||||
kind, reason ->
|
||||
# The function returned a non-zero exit code.
|
||||
# Sleep for a couple seconds before exiting to
|
||||
# ensure this doesn't hit the supervisor's
|
||||
# max_restarts/max_seconds limit.
|
||||
Process.sleep(2000)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
end
|
||||
end
|
||||
|
||||
def watch(cmd, args) when is_list(args) do
|
||||
{args, opts} = Enum.split_while(args, &is_binary(&1))
|
||||
opts = Keyword.merge([into: IO.stream(:stdio, :line), stderr_to_stdout: true], opts)
|
||||
|
||||
try do
|
||||
System.cmd(cmd, args, opts)
|
||||
catch
|
||||
:error, :enoent ->
|
||||
relative = Path.relative_to_cwd(cmd)
|
||||
|
||||
Logger.error(
|
||||
"Could not start watcher #{inspect(relative)} from #{inspect(cd(opts))}, executable does not exist"
|
||||
)
|
||||
|
||||
exit(:shutdown)
|
||||
else
|
||||
{_, 0} ->
|
||||
:ok
|
||||
|
||||
{_, _} ->
|
||||
# System.cmd returned a non-zero exit code
|
||||
# sleep for a couple seconds before exiting to ensure this doesn't
|
||||
# hit the supervisor's max_restarts / max_seconds limit
|
||||
Process.sleep(2000)
|
||||
exit(:watcher_command_error)
|
||||
end
|
||||
end
|
||||
|
||||
defp cd(opts), do: opts[:cd] || File.cwd!()
|
||||
end
|
33
integration_test/temple_plug_demo/mix.exs
Normal file
33
integration_test/temple_plug_demo/mix.exs
Normal file
|
@ -0,0 +1,33 @@
|
|||
defmodule TemplePlugDemo.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :temple_plug_demo,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.13",
|
||||
compilers: [:temple] ++ Mix.compilers(),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger],
|
||||
mod: {TemplePlugDemo.Application, []}
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:bandit, "~> 0.4.9"},
|
||||
{:tailwind, "~> 0.1", runtime: Mix.env() == :dev},
|
||||
{:temple, path: "../../"}
|
||||
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||
]
|
||||
end
|
||||
end
|
11
integration_test/temple_plug_demo/mix.lock
Normal file
11
integration_test/temple_plug_demo/mix.lock
Normal file
|
@ -0,0 +1,11 @@
|
|||
%{
|
||||
"bandit": {:hex, :bandit, "0.4.9", "8045b78a5087a51144a1a20bcbba879c989e659946e4ce27822726235b1d0d43", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.12", [hex: :plug, repo: "hexpm", optional: false]}, {:thousand_island, "~> 0.5.7", [hex: :thousand_island, repo: "hexpm", optional: false]}], "hexpm", "b04cba7e5530cc094d971c3664b0084c06e1f6d74518f4ddc92d0d00ebe6ca59"},
|
||||
"castore": {:hex, :castore, "0.1.16", "2675f717adc700475345c5512c381ef9273eb5df26bdd3f8c13e2636cf4cc175", [:mix], [], "hexpm", "28ed2c43d83b5c25d35c51bc0abf229ac51359c170cba76171a462ced2e4b651"},
|
||||
"hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
|
||||
"mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
|
||||
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
||||
"tailwind": {:hex, :tailwind, "0.1.5", "5561bed6c114434415077972f6d291e7d43b258ef0ee756bda1ead7293811f61", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "3be21a0ddec7fc29b323ee72bed7516078a2787f7b142e455698a2209296e2a5"},
|
||||
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
|
||||
"thousand_island": {:hex, :thousand_island, "0.5.7", "5066fb800287a9eb2699365746f51853ddc5ed0d4d39ea4c8b906f40b14a2499", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5d79e910a9d295e75ee4022df3a68173588dcc9a7e4dfcce4876325c8a9f51a7"},
|
||||
}
|
684
integration_test/temple_plug_demo/priv/static/assets/app.css
Normal file
684
integration_test/temple_plug_demo/priv/static/assets/app.css
Normal file
|
@ -0,0 +1,684 @@
|
|||
/*
|
||||
! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input:-ms-input-placeholder, textarea:-ms-input-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Ensure the default browser behavior of the `hidden` attribute.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: #fff;
|
||||
border-color: #6b7280;
|
||||
border-width: 1px;
|
||||
border-radius: 0px;
|
||||
padding-top: 0.5rem;
|
||||
padding-right: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
}
|
||||
|
||||
[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: #2563eb;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input:-ms-input-placeholder, textarea:-ms-input-placeholder {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input::placeholder,textarea::placeholder {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-date-and-time-value {
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
}
|
||||
|
||||
[multiple] {
|
||||
background-image: initial;
|
||||
background-position: initial;
|
||||
background-repeat: unset;
|
||||
background-size: initial;
|
||||
padding-right: 0.75rem;
|
||||
-webkit-print-color-adjust: unset;
|
||||
color-adjust: unset;
|
||||
}
|
||||
|
||||
[type='checkbox'],[type='radio'] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-origin: border-box;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
color: #2563eb;
|
||||
background-color: #fff;
|
||||
border-color: #6b7280;
|
||||
border-width: 1px;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
}
|
||||
|
||||
[type='checkbox'] {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
[type='radio'] {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
[type='checkbox']:focus,[type='radio']:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
|
||||
--tw-ring-offset-width: 2px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: #2563eb;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
[type='checkbox']:checked,[type='radio']:checked {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
[type='checkbox']:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
[type='radio']:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[type='checkbox']:indeterminate {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[type='file'] {
|
||||
background: unset;
|
||||
border-color: inherit;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-size: unset;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
[type='file']:focus {
|
||||
outline: 1px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1536px;
|
||||
}
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.font-sans {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
defmodule TemplePlugDemoTest do
|
||||
use ExUnit.Case
|
||||
doctest TemplePlugDemo
|
||||
|
||||
test "greets the world" do
|
||||
assert TemplePlugDemo.hello() == :world
|
||||
end
|
||||
end
|
1
integration_test/temple_plug_demo/test/test_helper.exs
Normal file
1
integration_test/temple_plug_demo/test/test_helper.exs
Normal file
|
@ -0,0 +1 @@
|
|||
ExUnit.start()
|
15
lib/mix/tasks/compile.temple.ex
Normal file
15
lib/mix/tasks/compile.temple.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule Mix.Tasks.Compile.Temple do
|
||||
use Mix.Task.Compiler
|
||||
|
||||
@recursive true
|
||||
|
||||
@impl Mix.Task.Compiler
|
||||
def run(_) do
|
||||
Code.put_compiler_option(
|
||||
:parser_options,
|
||||
Keyword.put(Code.get_compiler_option(:parser_options), :token_metadata, true)
|
||||
)
|
||||
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
|
@ -1,263 +0,0 @@
|
|||
if Code.ensure_loaded?(Mix.Phoenix) do
|
||||
defmodule Mix.Tasks.Temple.Gen.Html do
|
||||
@shortdoc "Generates controller, views, and context for an HTML resource in Temple"
|
||||
|
||||
@moduledoc """
|
||||
Generates controller, views, and context for an HTML resource in Temple.
|
||||
|
||||
mix temple.gen.html Accounts User users name:string age:integer
|
||||
|
||||
The first argument is the context module followed by the schema module
|
||||
and its plural name (used as the schema table name).
|
||||
|
||||
The context is an Elixir module that serves as an API boundary for
|
||||
the given resource. A context often holds many related resources.
|
||||
Therefore, if the context already exists, it will be augmented with
|
||||
functions for the given resource.
|
||||
|
||||
> Note: A resource may also be split
|
||||
> over distinct contexts (such as `Accounts.User` and `Payments.User`).
|
||||
|
||||
The schema is responsible for mapping the database fields into an
|
||||
Elixir struct.
|
||||
|
||||
Overall, this generator will add the following files to `lib/`:
|
||||
|
||||
* a context module in `lib/app/accounts.ex` for the accounts API
|
||||
* a schema in `lib/app/accounts/user.ex`, with an `users` table
|
||||
* a view in `lib/app_web/views/user_view.ex`
|
||||
* a controller in `lib/app_web/controllers/user_controller.ex`
|
||||
* default CRUD templates in `lib/app_web/templates/user`
|
||||
|
||||
A migration file for the repository and test files for the context and
|
||||
controller features will also be generated.
|
||||
|
||||
The location of the web files (controllers, views, templates, etc) in an
|
||||
umbrella application will vary based on the `:context_app` config located
|
||||
in your applications `:generators` configuration. When set, the Phoenix
|
||||
generators will generate web files directly in your lib and test folders
|
||||
since the application is assumed to be isolated to web specific functionality.
|
||||
If `:context_app` is not set, the generators will place web related lib
|
||||
and test files in a `web/` directory since the application is assumed
|
||||
to be handling both web and domain specific functionality.
|
||||
Example configuration:
|
||||
|
||||
config :my_app_web, :generators, context_app: :my_app
|
||||
|
||||
Alternatively, the `--context-app` option may be supplied to the generator:
|
||||
|
||||
mix phx.gen.html Sales User users --context-app warehouse
|
||||
|
||||
## Web namespace
|
||||
|
||||
By default, the controller and view will be namespaced by the schema name.
|
||||
You can customize the web module namespace by passing the `--web` flag with a
|
||||
module name, for example:
|
||||
|
||||
mix phx.gen.html.temple Sales User users --web Sales
|
||||
|
||||
Which would generate a `lib/app_web/controllers/sales/user_controller.ex` and
|
||||
`lib/app_web/views/sales/user_view.ex`.
|
||||
|
||||
## Generating without a schema or context file
|
||||
|
||||
In some cases, you may wish to bootstrap HTML templates, controllers, and
|
||||
controller tests, but leave internal implementation of the context or schema
|
||||
to yourself. You can use the `--no-context` and `--no-schema` flags for
|
||||
file generation control.
|
||||
|
||||
## table
|
||||
|
||||
By default, the table name for the migration and schema will be
|
||||
the plural name provided for the resource. To customize this value,
|
||||
a `--table` option may be provided. For example:
|
||||
|
||||
mix phx.gen.html.temple Accounts User users --table cms_users
|
||||
|
||||
## binary_id
|
||||
|
||||
Generated migration can use `binary_id` for schema's primary key
|
||||
and its references with option `--binary-id`.
|
||||
|
||||
## Default options
|
||||
|
||||
This generator uses default options provided in the `:generators`
|
||||
configuration of your application. These are the defaults:
|
||||
|
||||
config :your_app, :generators,
|
||||
migration: true,
|
||||
binary_id: false,
|
||||
sample_binary_id: "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
You can override those options per invocation by providing corresponding
|
||||
switches, e.g. `--no-binary-id` to use normal ids despite the default
|
||||
configuration or `--migration` to force generation of the migration.
|
||||
|
||||
Read the documentation for `phx.gen.schema` for more information on
|
||||
attributes.
|
||||
"""
|
||||
use Mix.Task
|
||||
|
||||
alias Mix.Phoenix.{Context, Schema}
|
||||
alias Mix.Tasks.Phx.Gen
|
||||
|
||||
@doc false
|
||||
def run(args) do
|
||||
if Mix.Project.umbrella?() do
|
||||
Mix.raise("mix temple.gen.html can only be run inside an application directory")
|
||||
end
|
||||
|
||||
{context, schema} = Gen.Context.build(args)
|
||||
Gen.Context.prompt_for_code_injection(context)
|
||||
|
||||
binding = [context: context, schema: schema, inputs: inputs(schema)]
|
||||
paths = [".", :temple]
|
||||
|
||||
prompt_for_conflicts(context)
|
||||
|
||||
context
|
||||
|> copy_new_files(paths, binding)
|
||||
|> print_shell_instructions()
|
||||
end
|
||||
|
||||
defp prompt_for_conflicts(context) do
|
||||
context
|
||||
|> files_to_be_generated()
|
||||
|> Kernel.++(context_files(context))
|
||||
|> Mix.Phoenix.prompt_for_conflicts()
|
||||
end
|
||||
|
||||
defp context_files(%Context{generate?: true} = context) do
|
||||
Gen.Context.files_to_be_generated(context)
|
||||
end
|
||||
|
||||
defp context_files(%Context{generate?: false}) do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc false
|
||||
def files_to_be_generated(%Context{schema: schema, context_app: context_app}) do
|
||||
web_prefix = Mix.Phoenix.web_path(context_app)
|
||||
test_prefix = Mix.Phoenix.web_test_path(context_app)
|
||||
web_path = to_string(schema.web_path)
|
||||
|
||||
[
|
||||
{:eex, "controller.ex",
|
||||
Path.join([web_prefix, "controllers", web_path, "#{schema.singular}_controller.ex"])},
|
||||
{:eex, "edit.html.exs",
|
||||
Path.join([web_prefix, "templates", web_path, schema.singular, "edit.html.exs"])},
|
||||
{:eex, "form.html.exs",
|
||||
Path.join([web_prefix, "templates", web_path, schema.singular, "form.html.exs"])},
|
||||
{:eex, "index.html.exs",
|
||||
Path.join([web_prefix, "templates", web_path, schema.singular, "index.html.exs"])},
|
||||
{:eex, "new.html.exs",
|
||||
Path.join([web_prefix, "templates", web_path, schema.singular, "new.html.exs"])},
|
||||
{:eex, "show.html.exs",
|
||||
Path.join([web_prefix, "templates", web_path, schema.singular, "show.html.exs"])},
|
||||
{:eex, "view.ex",
|
||||
Path.join([web_prefix, "views", web_path, "#{schema.singular}_view.ex"])},
|
||||
{:eex, "controller_test.exs",
|
||||
Path.join([
|
||||
test_prefix,
|
||||
"controllers",
|
||||
web_path,
|
||||
"#{schema.singular}_controller_test.exs"
|
||||
])}
|
||||
]
|
||||
end
|
||||
|
||||
@doc false
|
||||
def copy_new_files(%Context{} = context, paths, binding) do
|
||||
files = files_to_be_generated(context)
|
||||
|
||||
Mix.Phoenix.copy_from(paths, "priv/templates/temple.gen.html", binding, files)
|
||||
|
||||
if context.generate?,
|
||||
do: Gen.Context.copy_new_files(context, Mix.Phoenix.generator_paths(), binding)
|
||||
|
||||
context
|
||||
end
|
||||
|
||||
@doc false
|
||||
def print_shell_instructions(%Context{schema: schema, context_app: ctx_app} = context) do
|
||||
if schema.web_namespace do
|
||||
Mix.shell().info("""
|
||||
|
||||
Add the resource to your #{schema.web_namespace} :browser scope in #{
|
||||
Mix.Phoenix.web_path(ctx_app)
|
||||
}/router.ex:
|
||||
|
||||
scope "/#{schema.web_path}", #{
|
||||
inspect(Module.concat(context.web_module, schema.web_namespace))
|
||||
}, as: :#{schema.web_path} do
|
||||
pipe_through :browser
|
||||
...
|
||||
resources "/#{schema.plural}", #{inspect(schema.alias)}Controller
|
||||
end
|
||||
""")
|
||||
else
|
||||
Mix.shell().info("""
|
||||
|
||||
Add the resource to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
|
||||
|
||||
resources "/#{schema.plural}", #{inspect(schema.alias)}Controller
|
||||
""")
|
||||
end
|
||||
|
||||
if context.generate?, do: Gen.Context.print_shell_instructions(context)
|
||||
end
|
||||
|
||||
def inputs(%Schema{} = schema) do
|
||||
Enum.map(schema.attrs, fn
|
||||
{_, {:references, _}} ->
|
||||
{nil, nil, nil}
|
||||
|
||||
{key, :integer} ->
|
||||
{label(key), ~s(number_input f, #{inspect(key)}), error(key)}
|
||||
|
||||
{key, :float} ->
|
||||
{label(key), ~s(number_input f, #{inspect(key)}, step: "any"), error(key)}
|
||||
|
||||
{key, :decimal} ->
|
||||
{label(key), ~s(number_input f, #{inspect(key)}, step: "any"), error(key)}
|
||||
|
||||
{key, :boolean} ->
|
||||
{label(key), ~s(checkbox f, #{inspect(key)}), error(key)}
|
||||
|
||||
{key, :text} ->
|
||||
{label(key), ~s(textarea f, #{inspect(key)}), error(key)}
|
||||
|
||||
{key, :date} ->
|
||||
{label(key), ~s(date_select f, #{inspect(key)}), error(key)}
|
||||
|
||||
{key, :time} ->
|
||||
{label(key), ~s(time_select f, #{inspect(key)}), error(key)}
|
||||
|
||||
{key, :utc_datetime} ->
|
||||
{label(key), ~s(datetime_select f, #{inspect(key)}), error(key)}
|
||||
|
||||
{key, :naive_datetime} ->
|
||||
{label(key), ~s(datetime_select f, #{inspect(key)}), error(key)}
|
||||
|
||||
{key, {:array, :integer}} ->
|
||||
{label(key), ~s(multiple_select f, #{inspect(key)}, ["1": 1, "2": 2]), error(key)}
|
||||
|
||||
{key, {:array, _}} ->
|
||||
{label(key),
|
||||
~s(multiple_select f, #{inspect(key)}, ["Option 1": "option1", "Option 2": "option2"]),
|
||||
error(key)}
|
||||
|
||||
{key, _} ->
|
||||
{label(key), ~s(text_input f, #{inspect(key)}), error(key)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp label(key) do
|
||||
~s(label f, #{inspect(key)})
|
||||
end
|
||||
|
||||
defp error(field) do
|
||||
~s{error_tag(f, #{inspect(field)})}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
if Code.ensure_loaded?(Mix.Phoenix) do
|
||||
defmodule Mix.Tasks.Temple.Gen.Layout do
|
||||
use Mix.Task
|
||||
|
||||
@shortdoc "Generates a default Phoenix layout file in Temple"
|
||||
|
||||
@moduledoc """
|
||||
Generates a Phoenix layout file in Temple.
|
||||
mix temple.gen.layout
|
||||
"""
|
||||
def run(_args) do
|
||||
context_app = Mix.Phoenix.context_app()
|
||||
web_prefix = Mix.Phoenix.web_path(context_app)
|
||||
binding = [application_module: Mix.Phoenix.base()]
|
||||
|
||||
Mix.Phoenix.copy_from(temple_paths(), "priv/templates/temple.gen.layout", binding, [
|
||||
{:eex, "app.html.eex", "#{web_prefix}/templates/layout/app.html.exs"}
|
||||
])
|
||||
|
||||
instructions = """
|
||||
A new #{web_prefix}/templates/layout/app.html.exs file was generated.
|
||||
"""
|
||||
|
||||
Mix.shell().info(instructions)
|
||||
end
|
||||
|
||||
defp temple_paths do
|
||||
[".", :temple]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,254 +0,0 @@
|
|||
if Code.ensure_loaded?(Mix.Phoenix) do
|
||||
defmodule Mix.Tasks.Temple.Gen.Live do
|
||||
@shortdoc "Generates LiveView, templates, and context for a resource"
|
||||
|
||||
@moduledoc """
|
||||
Generates LiveView, templates, and context for a resource.
|
||||
|
||||
mix temple.gen.live Accounts User users name:string age:integer
|
||||
|
||||
The first argument is the context module followed by the schema module
|
||||
and its plural name (used as the schema table name).
|
||||
|
||||
The context is an Elixir module that serves as an API boundary for
|
||||
the given resource. A context often holds many related resources.
|
||||
Therefore, if the context already exists, it will be augmented with
|
||||
functions for the given resource.
|
||||
|
||||
When this command is run for the first time, a `ModalComponent` and
|
||||
`LiveHelpers` module will be created, along with the resource level
|
||||
LiveViews and components, including an `IndexLive`, `ShowLive`, `FormComponent`
|
||||
for the new resource.
|
||||
|
||||
> Note: A resource may also be split
|
||||
> over distinct contexts (such as `Accounts.User` and `Payments.User`).
|
||||
|
||||
The schema is responsible for mapping the database fields into an
|
||||
Elixir struct. It is followed by an optional list of attributes,
|
||||
with their respective names and types. See `mix phx.gen.schema`
|
||||
for more information on attributes.
|
||||
|
||||
Overall, this generator will add the following files to `lib/`:
|
||||
|
||||
* a context module in `lib/app/accounts.ex` for the accounts API
|
||||
* a schema in `lib/app/accounts/user.ex`, with an `users` table
|
||||
* a view in `lib/app_web/views/user_view.ex`
|
||||
* a LiveView in `lib/app_web/live/user_live/show_live.ex`
|
||||
* a LiveView in `lib/app_web/live/user_live/index_live.ex`
|
||||
* a LiveComponent in `lib/app_web/live/user_live/form_component.ex`
|
||||
* a LiveComponent in `lib/app_web/live/modal_component.ex`
|
||||
* a helpers modules in `lib/app_web/live/live_helpers.ex`
|
||||
|
||||
## The context app
|
||||
|
||||
A migration file for the repository and test files for the context and
|
||||
controller features will also be generated.
|
||||
|
||||
The location of the web files (LiveView's, views, templates, etc) in an
|
||||
umbrella application will vary based on the `:context_app` config located
|
||||
in your applications `:generators` configuration. When set, the Phoenix
|
||||
generators will generate web files directly in your lib and test folders
|
||||
since the application is assumed to be isolated to web specific functionality.
|
||||
If `:context_app` is not set, the generators will place web related lib
|
||||
and test files in a `web/` directory since the application is assumed
|
||||
to be handling both web and domain specific functionality.
|
||||
Example configuration:
|
||||
|
||||
config :my_app_web, :generators, context_app: :my_app
|
||||
|
||||
Alternatively, the `--context-app` option may be supplied to the generator:
|
||||
|
||||
mix temple.gen.live Sales User users --context-app warehouse
|
||||
|
||||
## Web namespace
|
||||
|
||||
By default, the controller and view will be namespaced by the schema name.
|
||||
You can customize the web module namespace by passing the `--web` flag with a
|
||||
module name, for example:
|
||||
|
||||
mix temple.gen.live Sales User users --web Sales
|
||||
|
||||
Which would generate a LiveViews inside `lib/app_web/live/sales/user_live/` and a
|
||||
view at `lib/app_web/views/sales/user_view.ex`.
|
||||
|
||||
## Customising the context, schema, tables and migrations
|
||||
|
||||
In some cases, you may wish to bootstrap HTML templates, LiveViews,
|
||||
and tests, but leave internal implementation of the context or schema
|
||||
to yourself. You can use the `--no-context` and `--no-schema` flags
|
||||
for file generation control.
|
||||
|
||||
You can also change the table name or configure the migrations to
|
||||
use binary ids for primary keys, see `mix phx.gen.schema` for more
|
||||
information.
|
||||
"""
|
||||
use Mix.Task
|
||||
|
||||
alias Mix.Phoenix.{Context}
|
||||
alias Mix.Tasks.Phx.Gen
|
||||
|
||||
@doc false
|
||||
def run(args) do
|
||||
if Mix.Project.umbrella?() do
|
||||
Mix.raise("mix temple.gen.live can only be run inside an application directory")
|
||||
end
|
||||
|
||||
{context, schema} = Gen.Context.build(args)
|
||||
Gen.Context.prompt_for_code_injection(context)
|
||||
|
||||
binding = [
|
||||
context: context,
|
||||
schema: schema,
|
||||
inputs: Mix.Tasks.Temple.Gen.Html.inputs(schema)
|
||||
]
|
||||
|
||||
paths = [".", :temple]
|
||||
|
||||
prompt_for_conflicts(context)
|
||||
|
||||
context
|
||||
|> copy_new_files(binding, paths)
|
||||
|> maybe_inject_helpers()
|
||||
|> print_shell_instructions()
|
||||
end
|
||||
|
||||
defp prompt_for_conflicts(context) do
|
||||
context
|
||||
|> files_to_be_generated()
|
||||
|> Kernel.++(context_files(context))
|
||||
|> Mix.Phoenix.prompt_for_conflicts()
|
||||
end
|
||||
|
||||
defp context_files(%Context{generate?: true} = context) do
|
||||
Gen.Context.files_to_be_generated(context)
|
||||
end
|
||||
|
||||
defp context_files(%Context{generate?: false}) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp files_to_be_generated(%Context{schema: schema, context_app: context_app}) do
|
||||
web_prefix = Mix.Phoenix.web_path(context_app)
|
||||
test_prefix = Mix.Phoenix.web_test_path(context_app)
|
||||
web_path = to_string(schema.web_path)
|
||||
live_subdir = "#{schema.singular}_live"
|
||||
|
||||
[
|
||||
{:eex, "show.ex", Path.join([web_prefix, "live", web_path, live_subdir, "show.ex"])},
|
||||
{:eex, "index.ex", Path.join([web_prefix, "live", web_path, live_subdir, "index.ex"])},
|
||||
{:eex, "form_component.ex",
|
||||
Path.join([web_prefix, "live", web_path, live_subdir, "form_component.ex"])},
|
||||
{:eex, "form_component.html.lexs",
|
||||
Path.join([web_prefix, "live", web_path, live_subdir, "form_component.html.lexs"])},
|
||||
{:eex, "index.html.lexs",
|
||||
Path.join([web_prefix, "live", web_path, live_subdir, "index.html.lexs"])},
|
||||
{:eex, "show.html.lexs",
|
||||
Path.join([web_prefix, "live", web_path, live_subdir, "show.html.lexs"])},
|
||||
{:eex, "live_test.exs",
|
||||
Path.join([test_prefix, "live", web_path, "#{schema.singular}_live_test.exs"])},
|
||||
{:new_eex, "modal_component.ex", Path.join([web_prefix, "live", "modal_component.ex"])},
|
||||
{:new_eex, "live_helpers.ex", Path.join([web_prefix, "live", "live_helpers.ex"])}
|
||||
]
|
||||
end
|
||||
|
||||
defp copy_new_files(%Context{} = context, binding, paths) do
|
||||
files = files_to_be_generated(context)
|
||||
|
||||
Mix.Phoenix.copy_from(
|
||||
paths,
|
||||
"priv/templates/temple.gen.live",
|
||||
binding,
|
||||
files
|
||||
)
|
||||
|
||||
if context.generate?,
|
||||
do: Gen.Context.copy_new_files(context, Mix.Phoenix.generator_paths(), binding)
|
||||
|
||||
context
|
||||
end
|
||||
|
||||
defp maybe_inject_helpers(%Context{context_app: ctx_app} = context) do
|
||||
web_prefix = Mix.Phoenix.web_path(ctx_app)
|
||||
[lib_prefix, web_dir] = Path.split(web_prefix)
|
||||
file_path = Path.join(lib_prefix, "#{web_dir}.ex")
|
||||
file = File.read!(file_path)
|
||||
inject = "import #{inspect(context.web_module)}.LiveHelpers"
|
||||
|
||||
if String.contains?(file, inject) do
|
||||
:ok
|
||||
else
|
||||
do_inject_helpers(context, file, file_path, inject)
|
||||
end
|
||||
|
||||
context
|
||||
end
|
||||
|
||||
defp do_inject_helpers(context, file, file_path, inject) do
|
||||
Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path)])
|
||||
|
||||
new_file =
|
||||
String.replace(
|
||||
file,
|
||||
"import Phoenix.LiveView.Helpers",
|
||||
"import Phoenix.LiveView.Helpers\n #{inject}"
|
||||
)
|
||||
|
||||
if file != new_file do
|
||||
File.write!(file_path, new_file)
|
||||
else
|
||||
Mix.shell().info("""
|
||||
|
||||
Could not find Phoenix.LiveView.Helpers imported in #{file_path}.
|
||||
|
||||
This typically happens because your application was not generated
|
||||
with the --live flag:
|
||||
|
||||
mix temple.new my_app --live
|
||||
|
||||
Please make sure LiveView is installed and that #{inspect(context.web_module)}
|
||||
defines both `live_view/0` and `live_component/0` functions,
|
||||
and that both functions import #{inspect(context.web_module)}.LiveHelpers.
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def print_shell_instructions(%Context{schema: schema, context_app: ctx_app} = context) do
|
||||
prefix = Module.concat(context.web_module, schema.web_namespace)
|
||||
web_path = Mix.Phoenix.web_path(ctx_app)
|
||||
|
||||
if schema.web_namespace do
|
||||
Mix.shell().info("""
|
||||
|
||||
Add the live routes to your #{schema.web_namespace} :browser scope in #{web_path}/router.ex:
|
||||
|
||||
scope "/#{schema.web_path}", #{inspect(prefix)}, as: :#{schema.web_path} do
|
||||
pipe_through :browser
|
||||
...
|
||||
|
||||
#{for line <- live_route_instructions(schema), do: " #{line}"}
|
||||
end
|
||||
""")
|
||||
else
|
||||
Mix.shell().info("""
|
||||
|
||||
Add the live routes to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
|
||||
|
||||
#{for line <- live_route_instructions(schema), do: " #{line}"}
|
||||
""")
|
||||
end
|
||||
|
||||
if context.generate?, do: Gen.Context.print_shell_instructions(context)
|
||||
end
|
||||
|
||||
defp live_route_instructions(schema) do
|
||||
[
|
||||
~s|live "/#{schema.plural}", #{inspect(schema.alias)}Live.Index, :index\n|,
|
||||
~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Index, :new\n|,
|
||||
~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Index, :edit\n\n|,
|
||||
~s|live "/#{schema.plural}/:id", #{inspect(schema.alias)}Live.Show, :show\n|,
|
||||
~s|live "/#{schema.plural}/:id/show/edit", #{inspect(schema.alias)}Live.Show, :edit|
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
216
lib/temple.ex
216
lib/temple.ex
|
@ -1,120 +1,67 @@
|
|||
defmodule Temple do
|
||||
alias Temple.Parser
|
||||
@engine Application.compile_env(:temple, :engine, EEx.SmartEngine)
|
||||
|
||||
@moduledoc """
|
||||
Temple syntax is available inside the `temple`, and is compiled into EEx at build time.
|
||||
Temple syntax is available inside the `temple`, and is compiled into efficient Elixir code at compile time using the configured `EEx.Engine`.
|
||||
|
||||
You should checkout the [guides](https://hexdocs.pm/temple/your-first-template.html) for a more in depth explanation.
|
||||
|
||||
## Usage
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.HomePage do
|
||||
import Temple
|
||||
|
||||
def render() do
|
||||
assigns = %{title: "My Site | Sign Up", logged_in: false}
|
||||
|
||||
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"
|
||||
"<!DOCTYPE html>"
|
||||
|
||||
div class: class, id: id do
|
||||
# Text nodes can be emitted as string literals or variables.
|
||||
"Bob"
|
||||
html 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"
|
||||
link rel: "stylesheet", href: "/css/app.css"
|
||||
|
||||
id
|
||||
title do: @title
|
||||
end
|
||||
|
||||
# Attributes that result in boolean values will be emitted as a boolean attribute. Examples of boolean attributes are `disabled` and `checked`.
|
||||
|
||||
input type: "text", disabled: true
|
||||
# <input type="text" disabled>
|
||||
|
||||
input type: "text", disabled: false
|
||||
# <input type="text">
|
||||
|
||||
# The class attribute also can take a keyword list of classes to conditionally render, based on the boolean result of the value.
|
||||
|
||||
div class: ["text-red-500": false, "text-green-500": true] do
|
||||
"Alert!"
|
||||
body do
|
||||
header class: "header" do
|
||||
ul do
|
||||
li do
|
||||
a href: "/", do: "Home"
|
||||
end
|
||||
li do
|
||||
if @logged_in do
|
||||
a href: "/logout", do: "Logout"
|
||||
else
|
||||
a href: "/login", do: "Login"
|
||||
end
|
||||
end
|
||||
|
||||
# <div class="text-green-500">Alert!</div>
|
||||
|
||||
# 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!"
|
||||
main do
|
||||
"Hi! Welcome to my website."
|
||||
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 emit 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
|
||||
```
|
||||
|
||||
## Whitespace Control
|
||||
|
||||
By default, Temple will emit internal whitespace into tags, something like this.
|
||||
|
||||
```elixir
|
||||
span do
|
||||
"Hello, world!"
|
||||
end
|
||||
```
|
||||
|
||||
```html
|
||||
<span>
|
||||
Hello, world!
|
||||
</span>
|
||||
```
|
||||
|
||||
If you need to create a "tight" tag, you can call the "bang" version of the desired tag.
|
||||
|
||||
```elixir
|
||||
span! do
|
||||
"Hello, world!"
|
||||
end
|
||||
```
|
||||
|
||||
```html
|
||||
<span>Hello, world!</span>
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Mode
|
||||
### Engine
|
||||
|
||||
There are two "modes", `:normal` (the default) and `:live_view`.
|
||||
|
||||
In `:live_view` mode, Temple emits markup that uses functions provided by Phoenix LiveView in order to be fully "diff trackable". These LiveView functions have not been released yet, so if you are going to combine Temple with LiveView, you need to use the latest unreleased default branch from GitHub.
|
||||
|
||||
You should use `:live_view` mode even if you only have a single LiveView.
|
||||
By default Temple wil use the `EEx.SmartEngine`, but you can configure it to use any other engine. Examples could be `Phoenix.HTML.Engine` or `Phoenix.LiveView.Engine`.
|
||||
|
||||
```elixir
|
||||
config :temple, :mode, :normal # default
|
||||
|
||||
# or
|
||||
|
||||
config :temple, :mode, :live_view
|
||||
config :temple, engine: Phoenix.HTML.Engine
|
||||
```
|
||||
|
||||
### Aliases
|
||||
|
@ -123,16 +70,16 @@ defmodule Temple do
|
|||
|
||||
```elixir
|
||||
config :temple, :aliases,
|
||||
label: :_label,
|
||||
link: :_link,
|
||||
select: :_select
|
||||
label: :label_tag,
|
||||
link: :link_tag,
|
||||
select: :select_tag
|
||||
|
||||
temple do
|
||||
_label do
|
||||
label_tag do
|
||||
"Email"
|
||||
end
|
||||
|
||||
_link href: "/css/site.css"
|
||||
link_tag href: "/css/site.css"
|
||||
end
|
||||
```
|
||||
|
||||
|
@ -147,83 +94,20 @@ defmodule Temple do
|
|||
```
|
||||
"""
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote location: :keep do
|
||||
import Temple
|
||||
require Temple.Component
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Context for temple markup.
|
||||
|
||||
Returns an EEx string that can be passed into an EEx template engine.
|
||||
|
||||
## Usage
|
||||
|
||||
```elixir
|
||||
import Temple
|
||||
|
||||
temple do
|
||||
div class: @class do
|
||||
"Hello, world!"
|
||||
end
|
||||
end
|
||||
|
||||
# <div class="<%= @class %>">
|
||||
# Hello, world!
|
||||
# </div>
|
||||
```
|
||||
"""
|
||||
defmacro temple([do: block] = _block) do
|
||||
markup =
|
||||
block
|
||||
|> Parser.parse()
|
||||
|> Enum.map(fn parsed -> Temple.Generator.to_eex(parsed, 0) end)
|
||||
|> Enum.intersperse("\n")
|
||||
|> :erlang.iolist_to_binary()
|
||||
|
||||
quote location: :keep do
|
||||
unquote(markup)
|
||||
end
|
||||
end
|
||||
|
||||
defmacro temple(block) do
|
||||
quote location: :keep do
|
||||
unquote(block)
|
||||
|> Parser.parse()
|
||||
|> Enum.map(fn parsed -> Temple.Generator.to_eex(parsed, 0) end)
|
||||
|> Enum.intersperse("\n")
|
||||
|> :erlang.iolist_to_binary()
|
||||
opts = [engine: engine()]
|
||||
|
||||
quote do
|
||||
require Temple.Renderer
|
||||
Temple.Renderer.compile(unquote(opts), unquote(block))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles temple markup into a quoted expression using the given EEx Engine.
|
||||
|
||||
Returns the same output that Phoenix templates output into the `render/1` function of their view modules.
|
||||
|
||||
## Usage
|
||||
|
||||
```elixir
|
||||
require Temple
|
||||
|
||||
Temple.compile Phoenix.HTML.Engine do
|
||||
div class: @class do
|
||||
"Hello, world!"
|
||||
end
|
||||
@doc false
|
||||
def component(func, assigns) do
|
||||
apply(func, [assigns])
|
||||
end
|
||||
|
||||
```
|
||||
"""
|
||||
defmacro compile(engine, [do: block] = _block) do
|
||||
markup =
|
||||
block
|
||||
|> Parser.parse()
|
||||
|> Enum.map(fn parsed -> Temple.Generator.to_eex(parsed, 0) end)
|
||||
|> Enum.intersperse("\n")
|
||||
|> :erlang.iolist_to_binary()
|
||||
|
||||
EEx.compile_string(markup, engine: engine, line: __CALLER__.line, file: __CALLER__.file)
|
||||
end
|
||||
@doc false
|
||||
def engine(), do: @engine
|
||||
end
|
||||
|
|
|
@ -1,274 +0,0 @@
|
|||
defmodule Temple.Component do
|
||||
@moduledoc """
|
||||
API for defining components.
|
||||
|
||||
Component modules are basically normal Phoenix View modules. The contents of the `render` macro are compiled into a `render/2` function. This means that you can define functions in your component module and use them in your component markup.
|
||||
|
||||
Since component modules are view modules, the assigns you pass to the component are accessible via the `@` macro and the `assigns` variable.
|
||||
|
||||
You must `require Temple.Component` in your views that use components, as the `c` and `slot` generate markup that uses macros provided by Temple.
|
||||
|
||||
## Components
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.Components.Flash do
|
||||
import Temple.Component
|
||||
|
||||
def border_class(:info), do: "border-blue-500"
|
||||
def border_class(:warning), do: "border-yellow-500"
|
||||
def border_class(:error), do: "border-red-500"
|
||||
def border_class(:success), do: "border-green-500"
|
||||
|
||||
render do
|
||||
div class: "border rounded p-2 #\{assigns[:class]} #\{border_class(@message_type)}" do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Components are used by calling the `c` keyword, followed by the component module and any assigns you need to pass to the template.
|
||||
|
||||
`c` is a _**compile time keyword**_, not a function or a macro, so you won't see it in the generated documention.
|
||||
|
||||
```
|
||||
c MyAppWeb.Components.Flash, class: "font-bold", message_type: :info do
|
||||
ul do
|
||||
for info <- infos do
|
||||
li class: "p-4" do
|
||||
info.message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Since components are just modules, if you alias your module, you can use them more ergonomically.
|
||||
|
||||
```
|
||||
# lib/my_app_web/views/page_view.ex
|
||||
alias MyAppWeb.Components.Flex
|
||||
|
||||
# lib/my_app_web/templates/page/index.html.exs
|
||||
c Flex, class: "justify-between items center" do
|
||||
for item <- items do
|
||||
div class: "p-4" do
|
||||
item.name
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Slots
|
||||
|
||||
Components can use slots, which are named placeholders for markup that can be passed to the component by the caller.
|
||||
|
||||
Slots are invoked by using the `slot` keyword, followed by the name of the slot and any assigns you'd like to pass into the slot.
|
||||
|
||||
`slot` is a _**compile time keyword**_, not a function or a macro, so you won't see it in the generated documention.
|
||||
|
||||
```elixir
|
||||
defmodule Flex do
|
||||
import Temple.Component
|
||||
|
||||
render do
|
||||
div class: "flex #\{@class}" do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
You can also use "named slots", which allow for data to be passed back into them. This is very useful
|
||||
when a component needs to pass data from the inside of the component back to the caller, like when rendering a form in LiveView.
|
||||
|
||||
```elixir
|
||||
defmodule Form do
|
||||
import Temple.Component
|
||||
|
||||
render do
|
||||
form = form_for(@changeset, @action, assigns)
|
||||
|
||||
form
|
||||
|
||||
slot :f, form: form
|
||||
|
||||
"</form>"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
By default, the body of a component fills the `:default` slot.
|
||||
|
||||
Named slots can be defined by invoking the `slot` keyword with the name of the slot and a do block.
|
||||
|
||||
You can also pattern match on any assigns that are being passed into the slot as if you were defining an anonymous function.
|
||||
|
||||
`slot` is a _**compile time keyword**_, not a function or a macro, so you won't see it in the generated documention.
|
||||
|
||||
```elixir
|
||||
# lib/my_app_web/templates/post/new.html.lexs
|
||||
|
||||
c Form, changeset: @changeset,
|
||||
action: @action,
|
||||
class: "form-control",
|
||||
phx_submit: :save,
|
||||
phx_change: :validate do
|
||||
slot :f, %{form: f} do
|
||||
label f do
|
||||
"Widget Name"
|
||||
text_input f, :name, class: "text-input"
|
||||
end
|
||||
|
||||
submit "Save!"
|
||||
end
|
||||
end
|
||||
```
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __component__(module, assigns \\ [], block \\ []) do
|
||||
{inner_block, assigns} =
|
||||
case {block, assigns} do
|
||||
{[do: do_block], _} -> {rewrite_do(do_block), assigns}
|
||||
{_, [do: do_block]} -> {rewrite_do(do_block), []}
|
||||
{_, _} -> {nil, assigns}
|
||||
end
|
||||
|
||||
if is_nil(inner_block) do
|
||||
quote do
|
||||
Phoenix.View.render(unquote(module), :self, unquote(assigns))
|
||||
end
|
||||
else
|
||||
quote do
|
||||
Phoenix.View.render(
|
||||
unquote(module),
|
||||
:self,
|
||||
Map.put(Map.new(unquote(assigns)), :inner_block, unquote(inner_block))
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __render_block__(inner_block, argument \\ []) do
|
||||
quote do
|
||||
unquote(inner_block).(unquote(argument))
|
||||
end
|
||||
end
|
||||
|
||||
defp rewrite_do([{:->, meta, _} | _] = do_block) do
|
||||
{:fn, meta, do_block}
|
||||
end
|
||||
|
||||
defp rewrite_do(do_block) do
|
||||
quote do
|
||||
fn _ ->
|
||||
unquote(do_block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a component template.
|
||||
|
||||
## Usage
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.Components.Flash do
|
||||
import Temple.Component
|
||||
|
||||
def border_class(:info), do: "border-blue-500"
|
||||
def border_class(:warning), do: "border-yellow-500"
|
||||
def border_class(:error), do: "border-red-500"
|
||||
def border_class(:success), do: "border-green-500"
|
||||
|
||||
render do
|
||||
div class: "border rounded p-2 #\{assigns[:class]} #\{border_class(@message_type)}" do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
"""
|
||||
defmacro render(block) do
|
||||
quote do
|
||||
def render(var!(assigns)) do
|
||||
require Temple
|
||||
|
||||
_ = var!(assigns)
|
||||
|
||||
Temple.compile(unquote(Temple.Component.__engine__()), unquote(block))
|
||||
end
|
||||
|
||||
def render(:self, var!(assigns)) do
|
||||
require Temple
|
||||
|
||||
_ = var!(assigns)
|
||||
|
||||
Temple.compile(unquote(Temple.Component.__engine__()), unquote(block))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a component module.
|
||||
|
||||
This macro makes it easy to define components without creating a separate file. It literally inlines a component module.
|
||||
|
||||
Since it defines a module inside of the current module, local function calls from the outer module won't be available. For convenience, the outer module is aliased for you, so you can call remote functions with a shorter module name.
|
||||
|
||||
## Usage
|
||||
|
||||
```elixir
|
||||
def MyAppWeb.SomeView do
|
||||
use MyAppWeb.SomeView, :view
|
||||
import Temple.Component, only: [defcomp: 2]
|
||||
|
||||
# define a function in outer module
|
||||
def foobar(), do: "foobar"
|
||||
|
||||
# define a component
|
||||
defcomp Button do
|
||||
button id: SomeView.foobar(), # `MyAppWeb.SomeView` is aliased for you.
|
||||
class: "text-sm px-3 py-2 rounded #\{assigns[:extra_classes]}",
|
||||
type: "submit" do
|
||||
slot :default
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# use the component in a SomeView template. Or else, you must alias `MyAppWeb.SomeView.Button`
|
||||
c Button, extra_classes: "border-2 border-red-500" do
|
||||
"Submit!"
|
||||
end
|
||||
```
|
||||
"""
|
||||
defmacro defcomp(module, [do: block] = _block) do
|
||||
quote location: :keep do
|
||||
defmodule unquote(module) do
|
||||
import Temple.Component
|
||||
alias unquote(__CALLER__.module)
|
||||
|
||||
render do
|
||||
unquote(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __engine__() do
|
||||
cond do
|
||||
Code.ensure_loaded?(Phoenix.LiveView.Engine) ->
|
||||
Phoenix.LiveView.Engine
|
||||
|
||||
Code.ensure_loaded?(Phoenix.HTML.Engine) ->
|
||||
Phoenix.HTML.Engine
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
defmodule Temple.Config do
|
||||
@moduledoc false
|
||||
|
||||
def mode do
|
||||
case Application.get_env(:temple, :mode, :normal) do
|
||||
:normal ->
|
||||
%{
|
||||
component_function: "Temple.Component.__component__",
|
||||
render_block_function: "Temple.Component.__render_block__",
|
||||
renderer: fn module -> Macro.to_string(module) end
|
||||
}
|
||||
|
||||
:live_view ->
|
||||
%{
|
||||
component_function: "component",
|
||||
render_block_function: "render_block",
|
||||
renderer: fn module -> "&" <> Macro.to_string(module) <> ".render/1" end
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,44 +0,0 @@
|
|||
defmodule Temple.Engine do
|
||||
@behaviour Phoenix.Template.Engine
|
||||
|
||||
@moduledoc """
|
||||
The Temple HTML engine makes it possible to use Temple with Phoenix controllers.
|
||||
|
||||
To get started, you will configure Phoenix to use this module for `.exs` files.
|
||||
|
||||
```elixir
|
||||
# config.exs
|
||||
config :phoenix, :template_engines,
|
||||
# this will work for files named like `index.html.exs`
|
||||
exs: Temple.Engine
|
||||
|
||||
# config/dev.exs
|
||||
config :your_app, YourAppWeb.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r"lib/myapp_web/(live|views)/.*(ex|exs|lexs)$",
|
||||
~r"lib/myapp_web/templates/.*(eex|exs|lexs)$"
|
||||
]
|
||||
]
|
||||
|
||||
# my_app/
|
||||
# lib/
|
||||
# my_app/
|
||||
# my_app_web/
|
||||
# templates/
|
||||
# posts/
|
||||
# show.html.exs
|
||||
```
|
||||
|
||||
Now you can get started by writing `exs` files in the templates directory and they will be compiled as you would expect.
|
||||
"""
|
||||
|
||||
def compile(path, _name) do
|
||||
require Temple
|
||||
|
||||
template = path |> File.read!() |> Code.string_to_quoted!(file: path)
|
||||
|
||||
Temple.temple(template)
|
||||
|> EEx.compile_string(engine: Phoenix.HTML.Engine, file: path, line: 1)
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
defprotocol Temple.Generator do
|
||||
@moduledoc false
|
||||
|
||||
def to_eex(ast, indent \\ 0)
|
||||
end
|
|
@ -1,46 +0,0 @@
|
|||
defmodule Temple.LiveViewEngine do
|
||||
@behaviour Phoenix.Template.Engine
|
||||
|
||||
@moduledoc """
|
||||
The Temple LiveView engine makes it possible to use Temple with Phoenix LiveView.
|
||||
|
||||
To get started, you will configure Phoenix to use this module for `.lexs` files.
|
||||
|
||||
```elixir
|
||||
# config.exs
|
||||
config :phoenix, :template_engines,
|
||||
# this will work for files named like `index.html.lexs`
|
||||
# you can enable Elixir syntax highlighting in your editor for this extension
|
||||
lexs: Temple.LiveViewEngine
|
||||
|
||||
# config/dev.exs
|
||||
config :your_app, YourAppWeb.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r"lib/myapp_web/(live|views)/.*(ex|exs|lexs)$",
|
||||
~r"lib/myapp_web/templates/.*(eex|exs|lexs)$"
|
||||
]
|
||||
]
|
||||
|
||||
# my_app/
|
||||
# lib/
|
||||
# my_app/
|
||||
# my_app_web/
|
||||
# live/
|
||||
# posts_live/
|
||||
# show.ex
|
||||
# show.html.lexs
|
||||
```
|
||||
|
||||
Now you can get started by writing `lexs` files co-located with your live views and they will be compiled as you would expect.
|
||||
"""
|
||||
|
||||
def compile(path, _name) do
|
||||
require Temple
|
||||
|
||||
template = path |> File.read!() |> Code.string_to_quoted!(file: path)
|
||||
|
||||
Temple.temple(template)
|
||||
|> EEx.compile_string(engine: Phoenix.LiveView.Engine, file: path, line: 1)
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue