├── .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 | ![Temple](temple-github-image.png) 2 | 3 | [![Actions Status](https://github.com/mhanberg/temple/workflows/CI/badge.svg)](https://github.com/mhanberg/temple/actions) 4 | [![Hex.pm](https://img.shields.io/hexpm/v/temple.svg)](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 `