├── .formatter.exs
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── release.yaml
├── .gitignore
├── .mise.toml
├── .release-please-manifest.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config
├── config.exs
├── dev.exs
└── test.exs
├── guides
├── components.md
├── converting-html.md
├── getting-started.md
├── migrating
│ ├── 0.10-to-0.11.md
│ └── 0.8-to-0.9.md
└── your-first-template.md
├── lib
├── mix
│ └── tasks
│ │ ├── compile.temple.ex
│ │ └── temple.convert.ex
├── temple.ex
└── temple
│ ├── ast.ex
│ ├── ast
│ ├── anonymous_functions.ex
│ ├── components.ex
│ ├── default.ex
│ ├── do_expressions.ex
│ ├── element_list.ex
│ ├── empty.ex
│ ├── match.ex
│ ├── nonvoid_elements_aliases.ex
│ ├── right_arrow.ex
│ ├── slot.ex
│ ├── slottable.ex
│ ├── temple_namespace_nonvoid.ex
│ ├── temple_namespace_void.ex
│ ├── text.ex
│ ├── utils.ex
│ └── void_elements_aliases.ex
│ ├── component.ex
│ ├── converter.ex
│ ├── parser.ex
│ └── renderer.ex
├── mix.exs
├── mix.lock
├── release-please-config.json
├── temple-github-image.png
├── temple.png
└── test
├── support
├── component.ex
├── components.ex
└── helpers.ex
├── temple
├── ast
│ ├── anonymous_functions_test.exs
│ ├── components_test.exs
│ ├── default_test.exs
│ ├── do_expressions_test.exs
│ ├── empty_test.exs
│ ├── match_test.exs
│ ├── nonvoid_elements_aliases_test.exs
│ ├── right_arrow_test.exs
│ ├── slot_test.exs
│ ├── temple_namespace_nonvoid_test.exs
│ ├── temple_namespace_void_test.exs
│ ├── text_test.exs
│ ├── utils_test.exs
│ └── void_elements_aliases_test.exs
├── converter_test.exs
└── renderer_test.exs
├── temple_test.exs
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | temple = ~w[temple c slot]a
2 |
3 | html = ~w[
4 | html head title style script
5 | noscript template
6 | body section nav article aside h1 h2 h3 h4 h5 h6
7 | header footer address main
8 | p pre blockquote ol ul li dl dt dd figure figcaption div
9 | a em strong small s cite q dfn abbr data time code var samp kbd
10 | sub sup i b u mark ruby rt rp bdi bdo span
11 | ins del
12 | iframe object video audio canvas
13 | map svg math
14 | table caption colgroup tbody thead tfoot tr td th
15 | form fieldset legend label button select datalist optgroup
16 | option textarea output progress meter
17 | details summary menuitem menu
18 | meta link base
19 | area br col embed hr img input keygen param source track wbr
20 | ]a
21 |
22 | svg = ~w[
23 | circle ellipse line path polygon polyline rect stop use a
24 | altGlyph altGlyphDef altGlyphItem animate animateColor animateMotion
25 | animateTransform animation audio canvas clipPath cursor defs desc
26 | discard feBlend feColorMatrix feComponentTransfer feComposite
27 | feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight
28 | feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
29 | feImage feMerge feMergeNode feMorphology feOffset fePointLight
30 | feSpecularLighting feSpotLight feTile feTurbulence filter font
31 | foreignObject g glyph glyphRef handler hatch hatchpath hkern iframe
32 | image linearGradient listener marker mask mesh meshgradient meshpatch
33 | meshrow metadata mpath pattern prefetch radialGradient script set
34 | solidColor solidcolor style svg switch symbol tbreak text textArea
35 | textPath title tref tspan unknown video view vkern
36 | ]a
37 |
38 | mathml = ~w[
39 | math mi mn mo ms mspace mtext
40 | merror mfrac mpadded mphantom mroot mrow msqrt mstyle
41 | mmultiscripts mover msub msubsup msup munder munderover
42 | mtable mtd mtr annotation semantics mprescripts
43 | ]a
44 |
45 | locals_without_parens = Enum.map(temple ++ html ++ svg ++ mathml, &{&1, :*})
46 |
47 | [
48 | import_deps: [:typed_struct],
49 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"],
50 | locals_without_parens: locals_without_parens ++ [assert_html: 2],
51 | export: [locals_without_parens: locals_without_parens]
52 | ]
53 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS:
28 | - Temple Version
29 | - Elixir Version
30 | - Erlang Version
31 |
32 | **Additional context**
33 | Add any other context about the problem here.
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: mix
4 | directory: "/"
5 | schedule:
6 | interval: monthly
7 | time: "10:00"
8 | open-pull-requests-limit: 10
9 |
10 | - package-ecosystem: github-actions
11 | directory: "/"
12 | schedule:
13 | interval: monthly
14 | time: "10:00"
15 | open-pull-requests-limit: 10
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | pull_request:
4 | push:
5 | branches: main
6 |
7 | jobs:
8 | tests:
9 | runs-on: ubuntu-latest
10 | name: Test (${{matrix.elixir}}/${{matrix.otp}})
11 |
12 | strategy:
13 | matrix:
14 | otp: [27.x]
15 | elixir: [1.17.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: erlef/setup-beam@v1
20 | with:
21 | otp-version: ${{matrix.otp}}
22 | elixir-version: ${{matrix.elixir}}
23 | - uses: actions/cache@v4
24 | id: cache
25 | with:
26 | path: |
27 | deps
28 | _build
29 | key: ${{ runner.os }}-mix-${{matrix.otp}}-${{matrix.elixir}}-${{ hashFiles('**/mix.lock') }}
30 | restore-keys: |
31 | ${{ runner.os }}-mix-${{matrix.otp}}-${{matrix.elixir}}-
32 |
33 | - name: Install Dependencies
34 | if: steps.cache.outputs.cache-hit != 'true'
35 | run: mix deps.get
36 |
37 | - name: Run Tests
38 | run: mix test
39 |
40 | # integration_tests:
41 | # runs-on: ubuntu-latest
42 | # name: Integration Test (${{matrix.elixir}}/${{matrix.otp}})
43 | # defaults:
44 | # run:
45 | # working-directory: "./integration_test/temple_demo"
46 |
47 | # services:
48 | # db:
49 | # image: postgres:12
50 | # env:
51 | # POSTGRES_USER: postgres
52 | # POSTGRES_PASSWORD: postgres
53 | # POSTGRES_DB: temple_demo_test
54 | # ports: ['5432:5432']
55 | # options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
56 |
57 | # steps:
58 | # - uses: actions/checkout@v4
59 | # - uses: erlef/setup-beam@v1
60 | # with:
61 | # otp-version: 24.x
62 | # elixir-version: 1.13.x
63 |
64 | # - uses: actions/cache@v4
65 | # with:
66 | # path: |
67 | # deps
68 | # _build
69 | # key: ${{ runner.os }}-mix-24-1.13-${{ hashFiles('**/mix.lock') }}
70 | # restore-keys: |
71 | # ${{ runner.os }}-mix-24-1.13-
72 |
73 | # - name: Install Dependencies
74 | # if: steps.cache.outputs.cache-hit != 'true'
75 | # run: mix deps.get
76 |
77 | # - name: Run Tests
78 | # run: mix test || mix test --failed || mix test --failed
79 | # env:
80 | # MIX_ENV: test
81 |
82 | # - uses: actions/upload-artifact@v2
83 | # if: failure()
84 | # with:
85 | # name: screenshots
86 | # path: screenshots/
87 |
88 | formatter:
89 | runs-on: ubuntu-latest
90 | name: Formatter (1.17.x.x/27.x)
91 |
92 | steps:
93 | - uses: actions/checkout@v4
94 | - uses: erlef/setup-beam@v1
95 | with:
96 | otp-version: 27.x
97 | elixir-version: 1.17.x
98 | - uses: actions/cache@v4
99 | id: cache
100 | with:
101 | path: |
102 | deps
103 | _build
104 | key: ${{ runner.os }}-mix-27-1.17-${{ hashFiles('**/mix.lock') }}
105 | restore-keys: |
106 | ${{ runner.os }}-mix-27-1.17-
107 |
108 | - name: Install Dependencies
109 | if: steps.cache.outputs.cache-hit != 'true'
110 | run: mix deps.get
111 |
112 | - name: Run Formatter
113 | run: mix format --check-formatted
114 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 |
11 | jobs:
12 | release:
13 | name: release
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | otp: [25.3]
18 | elixir: [1.14.x]
19 | steps:
20 | - uses: googleapis/release-please-action@v4
21 | id: release
22 |
23 | - uses: actions/checkout@v4
24 | if: ${{ steps.release.outputs.release_created }}
25 |
26 | - uses: erlef/setup-beam@v1
27 | with:
28 | otp-version: ${{matrix.otp}}
29 | elixir-version: ${{matrix.elixir}}
30 | if: ${{ steps.release.outputs.release_created }}
31 |
32 | - uses: actions/cache@v4
33 | id: cache
34 | if: ${{ steps.release.outputs.release_created }}
35 | with:
36 | path: |
37 | deps
38 | _build
39 | key: ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}
40 | restore-keys: |
41 | ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-
42 |
43 | - name: Install Dependencies
44 | if: steps.release.outputs.release_created && steps.cache.outputs.cache-hit != 'true'
45 | run: mix deps.get
46 |
47 | - name: publish to hex
48 | if: ${{ steps.release.outputs.release_created }}
49 | env:
50 | HEX_API_KEY: ${{secrets.HEX_API_KEY}}
51 | run: |
52 | mix hex.publish --yes
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 | /integration_test/temple_demo/deps/
10 |
11 | # Where third-party dependencies like ExDoc output generated docs.
12 | /doc/
13 |
14 | # Ignore .fetch files in case you like to edit your project deps locally.
15 | /.fetch
16 |
17 | # If the VM crashes, it generates a dump, let's ignore it too.
18 | erl_crash.dump
19 |
20 | # Also ignore archive artifacts (built via "mix archive.build").
21 | *.ez
22 |
23 | # Ignore package tarball (built via "mix hex.build").
24 | temple-*.tar
25 |
26 | /tmp/
27 |
28 |
--------------------------------------------------------------------------------
/.mise.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | erlang = "27.1.1"
3 | elixir = "1.17.3-otp-27"
4 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "0.14.1"
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## Main
4 |
5 | ## [0.14.1](https://github.com/mhanberg/temple/compare/v0.14.0...v0.14.1) (2025-03-04)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * correctly use rest in components ([#266](https://github.com/mhanberg/temple/issues/266)) ([c772e2d](https://github.com/mhanberg/temple/commit/c772e2d37e4499c2816a5c88e3b98836fb218162))
11 |
12 |
13 | ### Miscellaneous Chores
14 |
15 | * release 0.14.1 ([d16b312](https://github.com/mhanberg/temple/commit/d16b312978909ed97dd6bd795ecf9115714def1c))
16 |
17 | ## [0.14.0](https://github.com/mhanberg/temple/compare/v0.13.1...v0.14.0) (2024-10-20)
18 |
19 |
20 | ### Features
21 |
22 | * allow component to declare let! parameter ([#245](https://github.com/mhanberg/temple/issues/245)) ([a18e6fe](https://github.com/mhanberg/temple/commit/a18e6fea31a70c91cf1a9ebd5548763487e0cc4d)), closes [#239](https://github.com/mhanberg/temple/issues/239)
23 |
24 |
25 | ### Bug Fixes
26 |
27 | * allow normal list elements in class attr ([#246](https://github.com/mhanberg/temple/issues/246)) ([e79e6c7](https://github.com/mhanberg/temple/commit/e79e6c7564666a98804182d1373701adaf931434)), closes [#238](https://github.com/mhanberg/temple/issues/238)
28 |
29 | ## [0.13.1](https://github.com/mhanberg/temple/compare/v0.13.0...v0.13.1) (2024-10-20)
30 |
31 |
32 | ### Bug Fixes
33 |
34 | * add mathml tags to formatter ([4e8de14](https://github.com/mhanberg/temple/commit/4e8de1404a390d2ddbb419ff6af8784a7a9f316c))
35 | * void foreign elements get self closing tags ([#243](https://github.com/mhanberg/temple/issues/243)) ([4e8de14](https://github.com/mhanberg/temple/commit/4e8de1404a390d2ddbb419ff6af8784a7a9f316c)), closes [#242](https://github.com/mhanberg/temple/issues/242)
36 |
37 | ## [0.13.0](https://github.com/mhanberg/temple/compare/v0.12.1...v0.13.0) (2024-10-17)
38 |
39 |
40 | ### Features
41 |
42 | * add missing and new HTML elements & MathML ([#240](https://github.com/mhanberg/temple/issues/240)) ([209589e](https://github.com/mhanberg/temple/commit/209589e319d06b6aae4d32824370ce6254fd6193))
43 |
44 | ## [0.12.1](https://github.com/mhanberg/temple/compare/v0.12.0...v0.12.1) (2024-06-20)
45 |
46 |
47 | ### Bug Fixes
48 |
49 | * relax constraint on phoenix_html ([f28468e](https://github.com/mhanberg/temple/commit/f28468e7f877a39759696333c4135fb1aa877d11))
50 |
51 | ## [0.12.0](https://github.com/mhanberg/temple/compare/v0.11.0...v0.12.0) (2023-06-13)
52 |
53 |
54 | ### ⚠ BREAKING CHANGES
55 |
56 | * configure runtime attributes function ([#202](https://github.com/mhanberg/temple/issues/202))
57 |
58 | ### Features
59 |
60 | * configure runtime attributes function ([#202](https://github.com/mhanberg/temple/issues/202)) ([dc57221](https://github.com/mhanberg/temple/commit/dc57221bc99e165530134559097b27b1dfe95dbe))
61 |
62 |
63 | ### Bug Fixes
64 |
65 | * **docs:** typos ([7a50587](https://github.com/mhanberg/temple/commit/7a505875af6a1cee1536e516528f5be914df1f3f))
66 |
67 | ## v0.11.0
68 |
69 | ### Breaking Changes
70 |
71 | - Rendering slots is now done by passing the assign with the slot name to the `slot` keyword instead of name as an atom. If this slot has multiple definitions, you can loop through them and render each one individually, or render them all at once. Please see the migration guide for more information.
72 | - The `:default` slot has been renamed to `:inner_block`. This is to be easily compatible with HEEx/Surface. Please see the migration guide for more information.
73 | - Capturing the data being passed into a slot is now defined using the `:let!` attribute. Please see the migration guide for more information.
74 |
75 | ### Enhancements
76 |
77 | - Temple components are now compatible with HEEx/Surface components! Some small tweaks to the component implementation has made this possible. Please see the guides for more information.
78 | - Multiple instances of the same slot name can now be declared and then rendered inside the component (similar to HEEx and Surface).
79 | - You can now pass arbitrary data to slots, and it does not need to be a map or a keyword list. I don't think this is a breaking change, but please submit an issue if you notice it is.
80 | - Slot attributes. You can now pass data into a slot from the definition site and use it at the call site (inside the component).
81 | - Dynamic attributes/assigns. You can now pass dynamic attributes to the `:rest!` attribute in a tag, component, or slot.
82 |
83 | ### Fixes
84 |
85 | - Attributes with runtime values that evaluate to true or false will be rendered correctly as boolean attributes.
86 |
87 | ### 0.10.0
88 |
89 | ### Enhancements
90 |
91 | - mix temple.convert task to convert HTML into Temple syntax.
92 | - Temple now works with SVG elements.
93 |
94 | ### 0.9.0
95 |
96 | ### Breaking Changes
97 |
98 | - Requires Elixir 1.13+
99 | - Whitespace control is now controlled by whether you use `do/end` or `:do` syntax. The `:do` syntax will render "tight" markup.
100 | - 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`.
101 | - 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`.
102 | - The `defcomp` macro has been removed, since now all you need is a function.
103 | - 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.
104 | - 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`.
105 |
106 | Please see the guides for more in depth migration information.
107 |
108 | ## 0.8.0
109 |
110 | ### Enhancements
111 |
112 | - Better whitespace control
113 |
114 | You can now use a "bang" version of any nonvoid tag to forgo the internal whitespace.
115 |
116 | ```elixir
117 | span do
118 | "So much room for activities!"
119 | end
120 |
121 | #
122 | # So much room for activities!
123 | #
124 |
125 | span! do
126 | "It's a little cramped in here!"
127 | end
128 |
129 | # It's a little cramped in here!
130 | ```
131 |
132 | ## 0.7.0
133 |
134 | ### Enhancements
135 |
136 | - [breaking] Attributes who values are boolean expressions will be emitted as boolean attributes.
137 | - Class "object" syntax. Conditionally add classes by passing a keyword list to the `class` attribute.
138 |
139 | ## 0.6.2
140 |
141 | ### Bug fixes
142 |
143 | - Compile void elements with zero attrs #135
144 |
145 | ## 0.6.1
146 |
147 | ### Bug fixes
148 |
149 | - Only collect slots in the root of a component instance #127
150 |
151 | ## 0.6.0 The LiveView compatibility release!
152 |
153 | Temple now is written to be fully compatible with Phoenix LiveView! This comes with substantial internal changes as well as a better component API.
154 |
155 | ### Phoenix LiveView
156 |
157 | Temple now outputs LiveView compatible EEx at compile time, which is fed right into the normal LiveView EEx engine (or the traditional HTML Engine if you are not using LiveView).
158 |
159 | ### Components
160 |
161 | Temple now has a more complete component API.
162 |
163 | Components work with anywhere, whether you are writing a little plug app, a vanilla Phoenix app, or a Phoenix LiveView app!
164 |
165 | Please see the [documenation](https://hexdocs.pm/temple/Temple.html) for more information.
166 |
167 | To migrate component from the 0.5.0 syntax to the 0.6.0 syntax, you can use the following as a guide
168 |
169 | ```elixir
170 | # 0.5.0
171 |
172 | # definition
173 | defmodule PageView do
174 | defcomponent :flex do
175 | div id: @id, class: "flex" do
176 | @children
177 | end
178 | end
179 | end
180 |
181 | # usage
182 |
183 | require PageView
184 | # or
185 |
186 | import PageView
187 |
188 | temple do
189 | PageView.flex id: "my-flex" do
190 | div "Item 1"
191 | div "Item 2"
192 | div "Item 3"
193 | end
194 |
195 | # with import
196 | flex id: "my-flex" do
197 | div "Item 1"
198 | div "Item 2"
199 | div "Item 3"
200 | end
201 | end
202 | ```
203 |
204 | to
205 |
206 | ```elixir
207 | # 0.6.0
208 |
209 | # definition
210 |
211 | defmodule Flex do
212 | import Temple.Component
213 |
214 | render do
215 | div id: @id, class: "flex" do
216 | slot :default
217 | end
218 | end
219 | end
220 |
221 | # usage
222 |
223 | temple do
224 | c Flex id: "my-flex" do
225 | div do: "Item 1"
226 | div do: "Item 2"
227 | div do: "Item 3"
228 | end
229 | end
230 | ```
231 |
232 | ### Other breaking changes
233 |
234 | 0.6.0 has been a year in the making and a lot has changed in that time (in many cases, several times over), and I honestly can't really remember everything that is different now, but I will list some things here that I think you'll need to change or look out for.
235 |
236 | - The `partial` macro is removed.
237 | - You can now just call the `render` function like you normally would to render a phoenix partial.
238 | - The `defcomponent` macro is removed.
239 | - You now define components using the API described above.
240 | - The `text` macro is now removed.
241 | - You can just use a string literal or a variable to emit a text node.
242 | - Elements and components no longer can take "content" as the first argument. A do block is now required, but you can still use the keyword list style for a concise style, e.g., `span do: "foobar"` instead of `span "foobar"`.
243 | - The `:compact` reserved keyword option was removed.
244 | - The macros that wrapped `Phoenix.HTML` are removed as they are no longer needed.
245 | - The `temple.convert` task has been removed, but I am working to bring it back.
246 |
247 | There might be some more, so if you run into any problems, please open a [GitHub Discussion](https://github.com/mhanberg/temple/discussions/new).
248 |
249 | ## 0.6.0-rc.1
250 |
251 | ### Enhancements
252 |
253 | - Components can now use slots.
254 | - Markup is 100% live view compliant.
255 |
256 | ### Breaking
257 |
258 | - `@inner_content` is removed in favor of invoking the default slot.
259 | - The `compact` reserved keyword for elements has been removed. This is not really intentional, just a side effect of getting slots to a usable place. I expect to add it back, or at least similar functionality in the future.
260 |
261 | ## 0.6.0-rc.0
262 |
263 | - Can pass a keyword list to be evaluated at runtime as attrs/assigns to an element.
264 |
265 | ```elixir
266 | # compile time
267 |
268 | div class: "foo", id: bar do
269 | # something
270 | end
271 |
272 | #
273 | #
274 | #
275 |
276 | # runtime
277 |
278 | div some_var do
279 | # something
280 | end
281 |
282 | #
>
283 | #
284 | #
285 | ```
286 |
287 | - it now parses `case` expressions
288 |
289 | ### Breaking
290 |
291 | #### Components
292 |
293 | Components are now a thin layer over template partials, compiling to calls to `render/3` and `render_layout/4` under the hood.
294 |
295 | To upgrade your components the new syntax, you can copy your component markup and paste it into the `render/1` macro inside the component module and references to `@children` can be updated to `@inner_content`.
296 |
297 | Components can are also referenced differently than before when using them. Before, one would simply call `flex` to render a component named `Flex`. Now, one must use the keyword `c` to render a component, passing the keyword the component module along with any assigns.
298 |
299 | ##### Before
300 |
301 | ```elixir
302 | # definition
303 | div class: "flex #{@class}" do
304 | @children
305 | end
306 |
307 | # usage
308 |
309 | flex class: "justify-between" do
310 | for item <- @items do
311 | div do
312 | item.name
313 | end
314 | end
315 | end
316 | ```
317 |
318 | ##### After
319 |
320 | ```elixir
321 | # definition
322 | defmodule MyAppWeb.Component.Flex do
323 | use Temple.Component
324 |
325 | render do
326 | div class: "flex #{@class}" do
327 | @inner_content
328 | end
329 | end
330 | end
331 |
332 | # usage
333 | alias MyApp.Component.Flex # probably located in my_app_web.ex
334 |
335 | c Flex, class: "justify-between" do
336 | for item <- @items do
337 | div do
338 | item.name
339 | end
340 | end
341 | end
342 | ```
343 |
344 | ### Bugs
345 |
346 | - Did not correctly parse expressions with do blocks where the expression had two or more arguments before the block
347 |
348 | ## 0.6.0-alpha.4
349 |
350 | - Fix a bug where lists would not properly compile
351 |
352 | ## 0.6.0-alpha.3
353 |
354 | - Compile functions/macros that take blocks that are not if/unless/for
355 |
356 | ## 0.6.0-alpha.2
357 |
358 | ### Component API
359 |
360 | Please see the README for more details regarding the Component API
361 |
362 | ## 0.6.0-alpha.1
363 |
364 | ### Generators
365 |
366 | You can now use `mix temple.gen.live Context Schema table_name col:type` in the same way you can with Phoenix.
367 |
368 | ### Other
369 |
370 | - Make a note in the README to set the filetype for Live temple templates to `lexs`. You should be able to set this extension to use Elixir for syntax highlighting in your editor. In vim, you can add the following to your `.vimrc`
371 |
372 | ```vim
373 | augroup elixir
374 | autocmd!
375 |
376 | autocmd BufRead,BufNewFile *.lexs set filetype=elixir
377 | augroup END
378 | ```
379 |
380 | ## 0.6.0-alpha.0
381 |
382 | ### Breaking!
383 |
384 | This version is the start of a complete rewrite of Temple.
385 |
386 | - Compiles to EEx at build time.
387 | - Compatible with `Phoenix.LiveView`
388 | - All modules other than `Temple` are removed
389 | - `mix temple.convert` Mix task removed
390 |
391 | ## 0.5.0
392 |
393 | - Introduce `@assigns` assign
394 | - Join markup with a newline instead of empty string
395 |
396 | ## 0.4.4
397 |
398 | - Removes unnecessary plug dependency.
399 | - Bumps some other dependencies.
400 |
401 | ## 0.4.3
402 |
403 | - Compiles when Phoenix is not included in the host application.
404 |
405 | ## 0.4.2
406 |
407 | - temple.convert task no longer fails when parsing HTML fragments.
408 |
409 | ## 0.4.1
410 |
411 | - Only use Floki in dev and test environments
412 |
413 | ## 0.4.0
414 |
415 | - `Temple.Svg` module
416 | - `mix temple.convert` Mix task
417 | - (dev) rename `mix update_mdn_docs` to `mix temple.update_mdn_docs` and don't ship it to hex
418 |
419 | ### Breaking
420 |
421 | - Rename `Temple.Tags` to `Temple.Html`
422 |
423 | ## v0.3.1
424 |
425 | - `Temple.Form.phx_label`, `Temple.Form.submit`, `Temple.Link.phx_button`, `Temple.Link.phx_link` now correctly parse blocks. Before this, they would escape anything passed to the block instead of accepting it as raw HTML.
426 |
427 | ## v0.3.0
428 |
429 | - `Temple.Tags.html` now prepends the doctype, making it valid HTML
430 | - `Temple.Elements` module
431 |
432 | ### Breaking
433 |
434 | - `Temple.Tags.html` no longer accepts content as the first argument. A legal `html` tag must contain only a single `head` and a single `body`.
435 |
436 | ## 0.2.0
437 |
438 | - Wrap `radio_buttton/4` from Phoenix.HTML.Form
439 |
440 | ## 0.1.2
441 |
442 | ### Bugfixes
443 |
444 | - Allow components to be used correctly when their module was `require`d instead of `import`ed
445 |
446 | ## 0.1.1
447 |
448 | ### Bugfixes
449 |
450 | - Escape content passed to 1-arity tag macros
451 |
452 | ### Development
453 |
454 | - Upgrade various optional development packages
455 |
456 | ## 0.1.0
457 |
458 | - Initial Release
459 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Mitchell Hanberg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://github.com/mhanberg/temple/actions)
4 | [](https://hex.pm/packages/temple)
5 |
6 | > 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.11.0).
7 |
8 | # Temple
9 |
10 | Temple is an Elixir DSL for writing HTML and SVG.
11 |
12 | ## Installation
13 |
14 | Add `temple` to your list of dependencies in `mix.exs`:
15 |
16 |
17 | ```elixir
18 | def deps do
19 | [
20 | {:temple, "~> 0.14.0"}
21 | ]
22 | end
23 | ```
24 |
25 |
26 | ## Goals
27 |
28 | Currently Temple has the following things on which it won't compromise.
29 |
30 | - Will only work with valid Elixir syntax.
31 | - Should work in all web environments such as Plug, Aino, Phoenix, and Phoenix LiveView.
32 |
33 | ## Usage
34 |
35 | Using Temple is as simple as using the DSL inside of an `temple/1` block. The runtime result of the macro is your HTML.
36 |
37 | See the [guides](https://hexdocs.pm/temple/your-first-template.html) for more details.
38 |
39 | ```elixir
40 | import Temple
41 |
42 | temple do
43 | h2 do: "todos"
44 |
45 | ul class: "list" do
46 | for item <- @items do
47 | li class: "item" do
48 | div class: "checkbox" do
49 | div class: "bullet hidden"
50 | end
51 |
52 | div do: item
53 | end
54 | end
55 | end
56 |
57 | script do: """
58 | function toggleCheck({currentTarget}) {
59 | currentTarget.children[0].children[0].classList.toggle("hidden");
60 | }
61 |
62 | let items = document.querySelectorAll("li");
63 |
64 | Array.from(items).forEach(checkbox => checkbox.addEventListener("click", toggleCheck));
65 | """
66 | end
67 | ```
68 |
69 | ### Components
70 |
71 | Temple components are simple to write and easy to use.
72 |
73 | 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.
74 |
75 | Temple components are compatible with HEEx and Surface components and can be shared.
76 |
77 | Please see the [guides](https://hexdocs.pm/temple/components.html) for more details.
78 |
79 | ```elixir
80 | defmodule MyAppWeb.Component do
81 | import Temple
82 |
83 | def card(assigns) do
84 | temple do
85 | section do
86 | div do
87 | slot @header
88 | end
89 |
90 | div do
91 | slot @inner_block
92 | end
93 |
94 | div do
95 | slot @footer
96 | end
97 | end
98 | end
99 | end
100 | end
101 | ```
102 |
103 | Using components is as simple as passing a reference to your component function to the `c` keyword.
104 |
105 | ```elixir
106 | import MyAppWeb.Component
107 |
108 | c &card/1 do
109 | slot :header do
110 | @user.full_name
111 | end
112 |
113 | @user.bio
114 |
115 | slot :footer do
116 | a href: "https://twitter.com/#{@user.twitter}" do
117 | "@#{@user.twitter}"
118 | end
119 | a href: "https://github.com/#{@user.github}" do
120 | "@#{@user.github}"
121 | end
122 | end
123 | end
124 | ```
125 |
126 | ### Engine
127 |
128 | By default, Temple will use [Phoenix.HTML.Engine](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Engine.html) from [phoenix_html](https://github.com/phoenixframework/phoenix_html), which provides HTML escaping.
129 | You can use any other engine that implements the `EEx.Engine` behaviour,
130 | such as `EEx.SmartEngine` or [Aino.View.Engine](https://github.com/oestrich/aino).
131 |
132 | ```elixir
133 | # config/config.exs
134 |
135 | config :temple,
136 | engine: Aino.View.Engine # or EEx.SmartEngine or Phoenix.LiveView.Engine
137 | ```
138 |
139 | ### Formatter
140 |
141 | To include Temple's formatter configuration, add `:temple` to your `.formatter.exs`.
142 |
143 | ```elixir
144 | [
145 | import_deps: [:temple],
146 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs,lexs}"],
147 | ]
148 | ```
149 |
150 | ## Phoenix
151 |
152 | When using Phoenix ~> 1.7, all you need to do is include `:temple` in your mix.exs.
153 |
154 | If you plan on using the template structure that < 1.6 Phoenix applications use, you can use `:temple_phoenix` as described below.
155 |
156 | 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.
157 |
158 | ## Related
159 |
160 | - [Introducing Temple: An elegant HTML library for Elixir and Phoenix](https://www.mitchellhanberg.com/introducing-temple-an-elegant-html-library-for-elixir-and-phoenix/)
161 | - [Temple, AST, and Protocols](https://www.mitchellhanberg.com/temple-ast-and-protocols/)
162 | - [Thinking Elixir Episode 92: Temple with Mitchell Hanberg](https://podcast.thinkingelixir.com/92)
163 | - [How EEx Turns Your Template Into HTML](https://www.mitchellhanberg.com/how-eex-turns-your-template-into-html/)
164 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | import_config "#{config_env()}.exs"
4 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :temple,
4 | aliases: [
5 | select: :select__,
6 | link: :link__
7 | ]
8 |
--------------------------------------------------------------------------------
/guides/components.md:
--------------------------------------------------------------------------------
1 | # Components
2 |
3 | Temple has the concept of components, which allow you an expressive and composable way to break up your templates into reusable chunks.
4 |
5 | A component is any arity-1 function that takes an argument called `assigns` and returns the result of the `Temple.temple/1` macro.
6 |
7 | ## Definition
8 |
9 | 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!
10 |
11 | ```elixir
12 | defmodule MyApp.Components do
13 | import Temple
14 |
15 | def button(assigns) do
16 | temple do
17 | button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do
18 | @text
19 | end
20 | end
21 | end
22 | end
23 | ```
24 |
25 | ## Usage
26 |
27 | 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 the `Temple.temple/1` block.
28 |
29 | The first argument will be the function reference to your component function, followed by any assigns. You can pass dynamic assigns using the `:rest!` keyword the same way you would with a normal tag.
30 |
31 | ```elixir
32 | defmodule MyApp.ConfirmDialog do
33 | import Temple
34 | import MyApp.Components
35 |
36 | def render(assigns) do
37 | temple do
38 | dialog open: true do
39 | p do: "Are you sure?"
40 | form method: "dialog" do
41 | c &button/1, class: "border border-white", text: "Yes"
42 | end
43 | end
44 | end
45 | end
46 | end
47 | ```
48 |
49 | ## Slots
50 |
51 | Temple components can take "slots" as well. This is the method for providing dynamic content from the call site into the component.
52 |
53 | 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.
54 |
55 | ### Default Slot
56 |
57 | The default slot can be rendered from within your component by passing the `slot` the `@inner_block` assign. Let's redefine our button component using slots.
58 |
59 | ```elixir
60 | defmodule MyApp.Components do
61 | import Temple
62 |
63 | def button(assigns) do
64 | temple do
65 | button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do
66 | slot @inner_block
67 | end
68 | end
69 | end
70 | end
71 | ```
72 |
73 | 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.
74 |
75 | ```elixir
76 | defmodule MyApp.ConfirmDialog do
77 | import Temple
78 | import MyApp.Components
79 |
80 | def render(assigns) do
81 | temple do
82 | dialog open: true do
83 | p do: "Are you sure?"
84 | form method: "dialog" do
85 | c &button/1, class: "border border-white" do
86 | "Yes"
87 | end
88 | end
89 | end
90 | end
91 | end
92 | end
93 | ```
94 |
95 | ### Named Slots
96 |
97 | You can also define a "named" slot, which allows you to pass more than one set of dynamic content to your component.
98 |
99 | We'll use a "card" example to illustrate this. This example is adapted from the [Surface documentation](https://surface-ui.org/slots) on slots.
100 |
101 | #### Definition
102 |
103 | ```elixir
104 | defmodule MyApp.Components do
105 | import Temple
106 |
107 | def card(assigns) do
108 | temple do
109 | div class: "card" do
110 | header class: "card-header", style: "background-color: @f5f5f5" do
111 | p class: "card-header-title" do
112 | slot @header
113 | end
114 | end
115 |
116 | div class: "card-content" do
117 | div class: "content" do
118 | slot @inner_block
119 | end
120 | end
121 |
122 | footer class: "card-footer", style: "background-color: #f5f5f5" do
123 | slot @footer
124 | end
125 | end
126 | end
127 | end
128 | end
129 | ```
130 |
131 | #### Usage
132 |
133 | ```elixir
134 | def MyApp.CardExample do
135 | import Temple
136 | import MyApp.Components
137 |
138 | def render(assigns) do
139 | temple do
140 | c &card/1 do
141 | slot :header do
142 | "A simple card component"
143 | end
144 |
145 | "This example demonstrates how to create components with multiple, named slots"
146 |
147 | slot :footer do
148 | a href: "#", class: "card-footer-item", do: "Footer Item 1"
149 | a href: "#", class: "card-footer-item", do: "Footer Item 2"
150 | end
151 | end
152 | end
153 | end
154 | end
155 | ```
156 |
157 | ## Passing data to and through Slots
158 |
159 | Sometimes it is necessary to pass data _into_ a slot (hereby known as *slot attributes*) from the call site and _from_ a component definition (hereby known as *slot arguments*) back to the call site. Dynamic slot attributes can be passed using the `:rest!` attribute in the same way you can with tag attributes.
160 |
161 | Let's look at what a `table` component could look like. Here we observe we access an attribute in the slot in the header with `col.label`.
162 |
163 | This example is taken from the HEEx documentation to demonstrate how you can build the same thing with Temple.
164 |
165 | Note: Slot attributes can only be accessed on an individual slot, so if you define a single slot definition, you still need to loop through it to access it, as they are stored as a list.
166 |
167 | #### Definition
168 |
169 | ```elixir
170 | defmodule MyApp.Components do
171 | import Temple
172 |
173 | def table(assigns) do
174 | temple do
175 | table do
176 | thead do
177 | tr do
178 | for col <- @col do
179 | th do: col.label # 👈 accessing a slot attribute
180 | end
181 | end
182 | end
183 |
184 | tbody do
185 | for row <- @rows do
186 | tr do
187 | for col <- @col do
188 | td do
189 | slot col, row
190 | end
191 | end
192 | end
193 | end
194 | end
195 | end
196 | end
197 | end
198 | end
199 | ```
200 |
201 | #### Usage
202 |
203 | When we render the slot, we can pattern match on the data passed through the slot via the `:let` attribute.
204 |
205 | ```elixir
206 | def MyApp.TableExample do
207 | import Temple
208 | import MyApp.Componens
209 |
210 | def render(assigns) do
211 | temple do
212 | section do
213 | h2 do: "Users"
214 |
215 | c &table/1, rows: @users do
216 | # 👇 defining the parameter for the slot argument
217 | slot :col, let!: user, label: "Name" do # 👈 passing a slot attribute
218 | user.name
219 | end
220 |
221 | slot :col, let!: user, label: "Address" do
222 | user.address
223 | end
224 | end
225 | end
226 | end
227 | end
228 | end
229 | ```
230 |
--------------------------------------------------------------------------------
/guides/converting-html.md:
--------------------------------------------------------------------------------
1 | # Converting HTML
2 |
3 | If you want to use something like [TailwindUI](https://tailwindui.com) with Temple, you're going to have to convert a ton of vanilla HTML into Temple syntax.
4 |
5 | Luckily, Temple provides a mix task for converting an HTML file into Temple syntax and writes it to stdout.
6 |
7 | ## Usage
8 |
9 | First, we would want to create a temporary HTML file with the HTML we'd like to convert.
10 |
11 | > #### Hint {: .tip}
12 | >
13 | > The following examples use the `pbpaste` and `pbcopy` utilities found on macOS. These are used to send your clipboard contents into stdout and put stdout into your clipboard.
14 |
15 | ```shell
16 | $ pbpaste > temp.html
17 | ```
18 |
19 | Then, we can convert that file and copy the output into our clipboard.
20 |
21 | ```shell
22 | $ mix temple.convert temp.html | pbcopy
23 | ```
24 |
25 | Now, you are free to paste the new temple syntax into your project!
26 |
--------------------------------------------------------------------------------
/guides/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Install
4 |
5 | Welcome!
6 |
7 | Temple is an HTML DSL for Elixir; let's get started!
8 |
9 |
10 | First, make sure you are using Elixir `V1.13` or higher.
11 |
12 | Add `:temple` to your deps and run `mix deps.get`
13 |
14 | ```elixir
15 | {:temple, "~> 0.14.0"}
16 | ```
17 |
18 | To handle [whitespace](your-first-template.html#whitespace) correctly, 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!
19 |
20 | ```elixir
21 | defmodule MyApp.MixProject do
22 | use Mix.Project
23 |
24 | def project do
25 | [
26 | # ...
27 | compilers: [:temple] ++ Mix.compilers(),
28 | # ...
29 | ]
30 | end
31 |
32 | # ...
33 |
34 | end
35 | ```
36 |
37 | All done, Now let's start building our app!
38 |
39 | ## Configuration
40 |
41 | Temple works out of the box without any configuration, but here are a couple of config options that you could need to use.
42 |
43 | ### Engine
44 |
45 | By default, Temple uses the built in `Phoenix.HTML.Engine`. If you want to use a different engine, this is as easy as setting the `:engine` configuration option.
46 |
47 | You can also configure the function that is used for runtime attributes. By default, Temple uses `Phoenix.HTML.attributes_escape/1`.
48 |
49 | ```elixir
50 | # config/config.exs
51 |
52 | config :temple,
53 | engine: EEx.SmartEngine,
54 | attributes: {Temple, :attributes}
55 | ```
56 |
57 | ### Aliases
58 |
59 | 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.
60 |
61 | Common aliases for Phoenix projects look like this:
62 |
63 | ```elixir
64 | config :temple,
65 | aliases: [
66 | label: :label_tag,
67 | link: :link_tag,
68 | select: :select_tag,
69 | textarea: :textarea_tag
70 | ]
71 | ```
72 |
--------------------------------------------------------------------------------
/guides/migrating/0.10-to-0.11.md:
--------------------------------------------------------------------------------
1 | # Migrating from 0.10 to 0.11
2 |
3 | Most of the changes in this release are related to tweaking Temple's component model to align with HEEx & Surface.
4 |
5 | ## Rendering Slots
6 |
7 | Slots are now available as assigns in the component and are rendered as such.
8 |
9 | ### Before
10 |
11 | ```elixir
12 | def my_component(assign) do
13 | temple do
14 | span do
15 | slot :a_slot
16 | end
17 | end
18 | end
19 | ```
20 |
21 | ### After
22 |
23 | ```elixir
24 | def my_component(assign) do
25 | temple do
26 | span do
27 | slot @a_slot
28 | end
29 | end
30 | end
31 | ```
32 |
33 | ## :default slot has been renamed to :inner_block
34 |
35 | The main body of a component has been renamed from `:default` to `:inner_block`.
36 |
37 | Note: The "after" example also includes the necessary change specified above.
38 |
39 | ### Before
40 |
41 | ```elixir
42 | def my_component(assign) do
43 | temple do
44 | span do
45 | slot :default
46 | end
47 | end
48 | end
49 | ```
50 |
51 | ### After
52 |
53 | ```elixir
54 | def my_component(assign) do
55 | temple do
56 | span do
57 | slot @inner_block
58 | end
59 | end
60 | end
61 | ```
62 |
63 | ## Passing data into slots
64 |
65 | The syntax for capturing data being passed from the call site of a slot to the definition of a slot (or put another way, from the definition of a component to the call site of the component) has changed.
66 |
67 | You now capture it as the value of the `:let!` attribute on the slot definition.
68 |
69 | ### Before
70 |
71 | ```elixir
72 | def my_component(assign) do
73 | temple do
74 | c &my_component/1 do
75 | slot :a_slot, %{some: value} do
76 | "I'm using some #{value}"
77 | end
78 | end
79 | end
80 | end
81 | ```
82 |
83 | ### After
84 |
85 | ```elixir
86 | def my_component(assign) do
87 | temple do
88 | c &my_component/1 do
89 | slot :a_slot, let!: %{some: value} do
90 | "I'm using some #{value}"
91 | end
92 | end
93 | end
94 | end
95 | ```
96 |
--------------------------------------------------------------------------------
/guides/migrating/0.8-to-0.9.md:
--------------------------------------------------------------------------------
1 | # Migrating from 0.8 to 0.9
2 |
3 | First off, Temple now requires Elixir 1.13 or higher. This is because of some changes that were brought to the Elixir parser.
4 |
5 | ## Whitespace Control
6 |
7 | To control whitespace in an element, Temple will now control this based on whether the `do` was used in the keyword list syntax or the do/end syntax.
8 |
9 | In 0.8, you would do:
10 |
11 | ```elixir
12 | span do
13 | "hello!"
14 | end
15 |
16 | #
17 | # hello!
18 | #
19 |
20 | # The ! version of the element would render it as "tight"
21 | span! do
22 | "hello!"
23 | end
24 |
25 | # hello!
26 | ```
27 |
28 | In 0.9, you would do:
29 |
30 | ```elixir
31 | span do
32 | "hello!"
33 | end
34 |
35 | #
36 | # hello!
37 | #
38 |
39 | span do: "hello!"
40 |
41 | # hello!
42 | ```
43 |
44 | ## Components
45 |
46 | Components are no longer module based. To render a component, you can pass a function reference to the `c` keyword. You also no longer need to define a component in a module, using the `Temple.Component` module and its `render` macro.
47 |
48 | In 0.8, you would define a component like:
49 |
50 | ```elixir
51 | defmodule MyAppWeb.Component.Card do
52 | import Temple.Component
53 |
54 | render do
55 | div class: "border p-4 rounded" do
56 | slot :default
57 | end
58 | end
59 | end
60 | ```
61 |
62 | And you would use the component like:
63 |
64 | ```elixir
65 | div do
66 | c MyAppWeb.Component.Card do
67 | "Welcome to my app!"
68 | end
69 | end
70 | ```
71 |
72 | In 0.9, you would define a component like:
73 |
74 | ```elixir
75 | defmodule MyAppWeb.Components do
76 | import Temple
77 |
78 | def card(assigns) do
79 | temple do
80 | div class: "border p-4 rounded" do
81 | slot :default
82 | end
83 | end
84 | end
85 | end
86 | ```
87 |
88 | And you would use the component like:
89 |
90 | ```elixir
91 | div do
92 | c &MyAppWeb.Components.card/1 do
93 | "Welcome to my app!"
94 | end
95 | end
96 | ```
97 |
98 | We can observe here that in 0.9 the component is just any 1-arity function, so you can define them anywhere and you can have more than 1 in a single module.
99 |
100 | ### defcomp
101 |
102 | Now that components are just functions, you no longer need this special macro to define a component in the middle of the module.
103 |
104 | This can simply be converted to a function.
105 |
106 | ## Phoenix
107 |
108 | All Phoenix related items have moved to the [temple_phoenix](https://github.com/mhanberg/temple_phoenix) package. Please see those library docs for more details.
109 |
--------------------------------------------------------------------------------
/guides/your-first-template.md:
--------------------------------------------------------------------------------
1 | # Your First Template
2 |
3 | A Temple template is written inside the `Temple.temple/1` macro. Code inside there will be compiled into efficient Elixir code by the configured EEx engine.
4 |
5 | Local functions that have a corresponding HTML5 tag are reserved and will be used when generating your markup. Let's take a look at a basic form written with Temple.
6 |
7 | ```elixir
8 | defmodule MyApp.FormExample do
9 | import Temple
10 |
11 | def form_page() do
12 | assigns = %{title: "My Site | Sign Up", logged_in: false}
13 |
14 | temple do
15 | ""
16 |
17 | html do
18 | head do
19 | meta charset: "utf-8"
20 | meta http_equiv: "X-UA-Compatible", content: "IE=edge"
21 | meta name: "viewport", content: "width=device-width, initial-scale=1.0"
22 | link rel: "stylesheet", href: "/css/app.css"
23 |
24 | title do: @title
25 | end
26 |
27 | body do
28 | if @logged_in do
29 | header class: "header" do
30 | ul do
31 | li do
32 | a href: "/", do: "Home"
33 | end
34 | li do
35 | a href: "/logout", do: "Logout"
36 | end
37 | end
38 | end
39 | end
40 |
41 | form action: "", method: "get", class: "form-example" do
42 | div class: "form-example" do
43 | label for: "name", do: "Enter your name:"
44 | input type: "text", name: "name", id: "name", required: true
45 | end
46 | div class: "form-example" do
47 | label for: "email", do: "Enter your email:"
48 | input type: "email", name: "email", id: "email", required: true
49 | end
50 | div class: "form-example" do
51 | input type: "submit", value: "Subscribe!"
52 | end
53 | end
54 | end
55 | end
56 | end
57 | end
58 | end
59 | ```
60 |
61 | This example showcases an entire HTML page made with Temple! Let's dive a little deeper into everything we're seeing here.
62 |
63 | Throughout 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!
64 |
65 | ## Text Nodes
66 |
67 | The text node is a basic building block of any HTML document. In Temple, text nodes are represented by Elixir string literals.
68 |
69 | The very first line of the previous example is our doc type, emitted into the final document with `""`. This is a text node that will be emitted into the document as-is.
70 |
71 | 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. Some engines like `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.
72 |
73 | ## Void Tags
74 |
75 | Void tags are HTML5 tags that do not have children, meaning they are "self closing".
76 |
77 | We can observe these in the previous example as the `` tag. You'll note that the tag does not have a `:do` key or a `do` block.
78 |
79 | ## Non-void Tags
80 |
81 | Non-void tags are HTML5 tags that _do_ have children. You are probably most familiar with these types of tags, as they include the famous `` and ``.
82 |
83 | These tags can enclose their children nodes with either a `do/end` block or the inline `:do` keyword.
84 |
85 | ### Whitespace
86 |
87 | Nonvoid tags that use the `do/end` syntax will be emitted _with_ internal whitespace.
88 |
89 | ```elixir
90 | temple do
91 | div class: "foo" do
92 | # children
93 | end
94 | end
95 | ```
96 |
97 | ...will emit markup that looks like...
98 |
99 | ```html
100 |
101 |
102 |
103 | ```
104 |
105 | 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...)
106 |
107 | Nonvoid tags that use the `:do` keyword syntax will be emitted _without_ internal whitespace. This allows you to correctly use the `:empty` CSS pseudo-selector in your stylesheet.
108 |
109 |
110 | ```elixir
111 | temple do
112 | p class: "alert alert-info", do: "Your account was recently updated!"
113 | end
114 | ```
115 |
116 | ...will emit markup that looks like...
117 |
118 | ```html
119 |
Your account was recently updated!
120 | ```
121 |
122 | ## Attributes
123 |
124 | Temple leverages `Phoenix.HTML.attributes_escape/1` internally, so you can refer to its documentation for all the details.
125 |
126 | ### Dynamic Attributes
127 |
128 | To render dynamic attributes into a tag, you can pass them with the reserved attribute `:rest!`.
129 |
130 | ```elixir
131 | assigns = %{
132 | data: [data_foo: "hi"]
133 | }
134 |
135 | temple do
136 | div id: "foo", rest!: @data do
137 | "Hello, world!"
138 | end
139 | end
140 | ```
141 |
142 | will render to
143 |
144 | ```html
145 |
146 | Hello, world!
147 |
148 | ```
149 |
150 | ## Elixir Expressions
151 |
152 | Any Elixir expression can be used anywhere inside a Temple template. Here are a few examples.
153 |
154 | ```elixir
155 | temple do
156 | h2 do: "Members"
157 |
158 | ul do
159 | for member <- @members do
160 | li do: member
161 | end
162 | end
163 | end
164 | ```
165 |
166 | ### Match Expressions
167 |
168 | Match expressions 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.
169 |
170 | 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.
171 |
172 | Typically, you should not be writing this type of expression inside 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.
173 |
174 | ```elixir
175 | temple do
176 | _ = alias My.Deep.Module
177 |
178 | div do
179 | Module.func()
180 | end
181 | end
182 | ```
183 |
184 | ## Assigns
185 |
186 | Since Temple uses the `Phoenix.HTML.Engine` by default, you can use the assigns feature.
187 |
188 | The assigns feature allows you to ergonomically access the members of a `assigns` variable by the `@` macro.
189 |
190 | 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.
191 |
192 | ```elixir
193 | def card(assigns) do
194 | temple do
195 | div class: "card" do
196 | section class: "card-header" do
197 | @name
198 | end
199 |
200 | section class: "card-body" do
201 | @bio
202 | end
203 |
204 | if Enum.any?(@socials) do
205 | section class: "card-footer" do
206 | for social <- @socials do
207 | a href: social.link do
208 | social.name
209 | end
210 | end
211 | end
212 | end
213 | end
214 | end
215 | end
216 | ```
217 |
--------------------------------------------------------------------------------
/lib/mix/tasks/compile.temple.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Compile.Temple do
2 | use Mix.Task.Compiler
3 |
4 | @recursive true
5 |
6 | @impl Mix.Task.Compiler
7 | def run(_) do
8 | Code.put_compiler_option(
9 | :parser_options,
10 | Keyword.put(Code.get_compiler_option(:parser_options), :token_metadata, true)
11 | )
12 |
13 | {:ok, []}
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/mix/tasks/temple.convert.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Temple.Convert do
2 | use Mix.Task
3 |
4 | @shortdoc "A task to convert vanilla HTML into Temple syntax"
5 | @moduledoc """
6 | This task is useful for converting a ton of HTML into Temple syntax.
7 |
8 | > #### Note about EEx and HEEx {: .tip}
9 | >
10 | > In the future, this should be able to convert EEx and HEEx as well, but that would involve invoking or forking their parsers. That is certainly doable, but is out of scope for what I needed right now. Contributions are welcome!
11 |
12 | ## Usage
13 |
14 | ```shell
15 | $ mix temple.convert some_file.html
16 | ```
17 | """
18 |
19 | @doc false
20 | def run(argv) do
21 | case argv do
22 | [] ->
23 | Mix.raise(
24 | "You need to provide the path to an HTML file you would like to convert to Temple syntax"
25 | )
26 |
27 | [file] ->
28 | file
29 | |> File.read!()
30 | |> Temple.Converter.convert()
31 | |> IO.puts()
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/temple.ex:
--------------------------------------------------------------------------------
1 | defmodule Temple do
2 | @moduledoc """
3 | Temple syntax is available inside the `temple`, and is compiled into efficient Elixir code at compile time using the configured `EEx.Engine`.
4 |
5 | You should checkout the [guides](https://hexdocs.pm/temple/your-first-template.html) for a more in depth explanation.
6 |
7 | ## Usage
8 |
9 | ```elixir
10 | defmodule MyApp.HomePage do
11 | import Temple
12 |
13 | def render() do
14 | assigns = %{title: "My Site | Sign Up", logged_in: false}
15 |
16 | temple do
17 | ""
18 |
19 | html do
20 | head do
21 | meta charset: "utf-8"
22 | meta http_equiv: "X-UA-Compatible", content: "IE=edge"
23 | meta name: "viewport", content: "width=device-width, initial-scale=1.0"
24 | link rel: "stylesheet", href: "/css/app.css"
25 |
26 | title do: @title
27 | end
28 |
29 | body do
30 | header class: "header" do
31 | ul do
32 | li do
33 | a href: "/", do: "Home"
34 | end
35 | li do
36 | if @logged_in do
37 | a href: "/logout", do: "Logout"
38 | else
39 | a href: "/login", do: "Login"
40 | end
41 | end
42 | end
43 | end
44 |
45 | main do
46 | "Hi! Welcome to my website."
47 | end
48 | end
49 | end
50 | end
51 | end
52 | end
53 | ```
54 |
55 | ## Configuration
56 |
57 | ### Engine
58 |
59 | By default Temple wil use the `Phoenix.HTML.Engine`, but you can configure it to use any other engine. Examples could be `EEx.SmartEngine` or `Phoenix.LiveView.Engine`.
60 |
61 | ```elixir
62 | config :temple, engine: EEx.SmartEngine
63 | ```
64 |
65 | ### Aliases
66 |
67 | You can add an alias for an element if there is a namespace collision with a function. If you are using `Phoenix.HTML`, there will be namespace collisions with the `` and `