Utilize the EEx Engine instead of creating an EEx string (#177)

This commit is contained in:
Mitchell Hanberg 2022-04-19 23:56:46 -04:00 committed by GitHub
parent ece4cb8a26
commit f942817994
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
147 changed files with 2752 additions and 4963 deletions

View File

@ -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}"],

View File

@ -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'

View File

@ -1,2 +1,2 @@
elixir 1.11.3
erlang 23.2.6
elixir ref:v1.13.4
erlang 25.0-rc2

View File

@ -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

144
README.md
View File

@ -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,40 +63,43 @@ 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
section do
div do
slot :header
end
def card(assigns) do
temple do
section do
div do
slot :header
end
div do
slot :default
end
div do
slot :default
end
div do
slot :footer
div do
slot :footer
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/)

View File

@ -1,3 +1,3 @@
use Mix.Config
import Config
import_config "#{Mix.env()}.exs"
import_config "#{config_env()}.exs"

View File

@ -1 +1 @@
use Mix.Config
import Config

View File

@ -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
View 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
View 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
]
```

View File

@ -0,0 +1,3 @@
# Migrating from 0.8 to 0.9
TODO: explain it

View 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
```

View File

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

View File

@ -1 +0,0 @@
elixir 1.9.4

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 &amp; Docs"
end
end
li do
a href: "https://github.com/phoenixframework/phoenix" do
"Source"
end
end
li do
a href: "https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md" do
"v1.5 Changelog"
end
end
end
end
article class: "column" do
h2 do: "Help"
ul do
li do
a href: "https://elixirforum.com/c/phoenix-forum" do
"Forum"
end
end
li do
a href: "https://webchat.freenode.net/?channels=elixir-lang" do
"#elixir-lang on Freenode IRC"
end
end
li do
a href: "https://twitter.com/elixirphoenix" do
"Twitter @elixirphoenix"
end
end
li do
a href: "https://elixir-slackin.herokuapp.com/" do
"Elixir on Slack"
end
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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"},
}

View File

@ -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 ""

View File

@ -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 ""

View File

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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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);
})();

View File

@ -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: /

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
# Used by "mix format"
[
import_deps: [:plug, :temple],
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

View File

@ -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/

View 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>.

View File

@ -0,0 +1,5 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View 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')
]
}

View 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)]}

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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"},
}

View 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";
}

View File

@ -0,0 +1,8 @@
defmodule TemplePlugDemoTest do
use ExUnit.Case
doctest TemplePlugDemo
test "greets the world" do
assert TemplePlugDemo.hello() == :world
end
end

View File

@ -0,0 +1 @@
ExUnit.start()

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
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"
defmodule MyApp.HomePage do
import Temple
div class: class, id: id do
# Text nodes can be emitted as string literals or variables.
"Bob"
def render() do
assigns = %{title: "My Site | Sign Up", logged_in: false}
id
end
temple do
"<!DOCTYPE html>"
# Attributes that result in boolean values will be emitted as a boolean attribute. Examples of boolean attributes are `disabled` and `checked`.
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"
input type: "text", disabled: true
# <input type="text" disabled>
title do: @title
end
input type: "text", disabled: false
# <input type="text">
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
end
end
# 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!"
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!"
main do
"Hi! Welcome to my website."
end
end
end
end
end
unless 5 > 0 do
p do
"Less than 0!"
end
end
# You can loop over items using for comprehensions
for x <- 0..5 do
div do
x
end
end
# You can use multiline anonymous functions, like if you're building a form in Phoenix
form_for @changeset, Routes.user_path(@conn, :create), fn f ->
"Name: "
text_input f, :name
end
# You can explicitly 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,5 +0,0 @@
defprotocol Temple.Generator do
@moduledoc false
def to_eex(ast, indent \\ 0)
end

View File

@ -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