Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/Markdown/StripEnvTrailingNewlines.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

class StripEnvTrailingNewlines implements Preprocessor
{
public function supports(?string $grammarName): bool
{
return $grammarName === 'env';
}

/**
* Torchlight's env grammar uses `([^#]*)` for unquoted values, which
* swallows the newline Phiki appends to every line. That leaves
Expand Down
18 changes: 9 additions & 9 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use League\CommonMark\Extension\Attributes\AttributesExtension;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Extension\DescriptionList\DescriptionListExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use Statamic\Facades\Collection;
use Statamic\Facades\Markdown;
use Stillat\DocumentationSearch\Events\SearchEntriesCreated;
use Torchlight\Engine\CommonMark\Extension as TorchlightExtension;
use Torchlight\Engine\CommonMark\CodeBlockRenderer;
use Torchlight\Engine\Engine;
use Torchlight\Engine\Options as TorchlightOptions;

class AppServiceProvider extends ServiceProvider
Expand All @@ -42,16 +44,14 @@ public function boot(): void
if (! app()->runningConsoleCommand('search:update')) {
TorchlightOptions::setDefaultOptionsBuilder(fn () => TorchlightOptions::fromArray(config('torchlight.options')));

$extension = new TorchlightExtension(
config('torchlight.theme'),
true,
['env' => new StripEnvTrailingNewlines],
);
$extension
->renderer()
$engine = new Engine;
$engine->registerPreprocessor(new StripEnvTrailingNewlines, 'env');
$engine->getEnvironment()->grammar('antlers', resource_path('syntaxes/antlers.json'));

$renderer = (new CodeBlockRenderer(config('torchlight.theme'), $engine))
->setDefaultGrammar(config('torchlight.options.defaultLanguage'));

Markdown::addExtension(fn () => $extension);
Markdown::addRenderer(fn () => [FencedCode::class, $renderer, 10]);
}

Event::listen(SearchEntriesCreated::class, SearchEntriesCreatedListener::class);
Expand Down
4 changes: 3 additions & 1 deletion content/collections/pages/antlers-cheat-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ The one-liners:
| Include a view | `{{ partial:footer }}` |
| Include one that might not exist | `{{ partial:if_exists src="blog/card" }}` |
| Pass data to a partial | `{{ partial:blog/card mode="stacked" }}` |
| Render a component | `<x-alert type="warning" />` |
| Pass a variable to a component | `<x-alert :title="page_title" />` |
| Render everything pushed onto a stack | `{{ stack:scripts }}` |

Some helpful things you may need when building your frontend:
Expand Down Expand Up @@ -295,7 +297,7 @@ Some helpful things you may need when building your frontend:
{{ /section:footer }}
```

More: [partials](/frontend/antlers#partials), [slots](/frontend/antlers#slots), [stacks](/frontend/antlers#stacks), [section & yield](/frontend/antlers#section--yield).
More: [partials](/frontend/antlers#partials), [partial slots](/frontend/antlers#slots), [components](/frontend/antlers-components), [stacks](/frontend/antlers#stacks), [section & yield](/frontend/antlers#section--yield).

## Escaping and preventing parsing

Expand Down
245 changes: 245 additions & 0 deletions content/collections/pages/antlers-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
---
id: 083c717b-545c-45be-a65b-67faf1aa886f
blueprint: page
title: 'Antlers Components'
intro: 'Build reusable components with Antlers, render existing Laravel Blade components, and use HTML-style syntax for Statamic Tags.'
related_entries:
- d37b2af2-f2bf-493a-9345-7087fb5929ce
- 0c54fe7c-c87a-4812-b76e-48f16cf08e0d
- c7816387-ebc4-4204-b5f2-8e7073a4db8b
---
## Overview

Components are reusable, self-contained chunks of interface, such as callouts, cards, buttons, layouts, or whatever else you keep copying and pasting around your site. Antlers can render [Laravel Blade components](https://laravel.com/docs/13.x/blade#components), and anonymous components can be written with Antlers itself.

That gives you two closely related flavors of angle-bracket syntax:

| Syntax | What it renders |
| --- | --- |
| `<x-callout />` | A Laravel component class or anonymous component view. The view may use Blade or Antlers. |
| `<s:collection:blog>...</s:collection:blog>` | A Statamic [Tag](/tags) using component-style syntax. |
| `{{ collection:blog }}...{{ /collection:blog }}` | The same Statamic Tag using classic Antlers syntax. |

:::tip
[Partials](/frontend/antlers#partials) inherit the current Antlers scope and are perfect for straightforward includes. Components have their own scope and explicitly receive data through props, the Cascade, and slots.
:::

## Creating a component

Anonymous components live in `resources/views/components`. Give the view an `.antlers.html` extension to build it with Antlers:

```antlers
{{# resources/views/components/callout.antlers.html #}}
@props([
'type' => 'info',
'title' => 'Heads up!' | upper,
])

<aside {{ attributes.merge([
'class' => 'callout callout--' + type,
]) }}>
<h2>{{ title }}</h2>

{{ if slot | has_actual_content }}
<div>{{ slot }}</div>
{{ /if }}
</aside>
```

Render it from any Antlers template with an `x-` tag. Paired components receive everything between their tags as the default `slot`:

```antlers
<x-callout class="mt-8">
Save your work before continuing.
</x-callout>
```

Components without slot content may self-close:

```antlers
<x-callout type="warning" title="Back up first" />
```

Components in subdirectories use dot notation. For example, `resources/views/components/menu/item.antlers.html` becomes `<x-menu.item />`.

:::tip
**[Blade components](https://laravel.com/docs/blade#components) work, too!** Use them from Antlers without rewriting anything. Blade and Antlers components may even nest inside each other like one big, happy template-language family.
:::

## Props

The `@props` directive defines the data your component expects. Values with named keys provide defaults, and those defaults are Antlers expressions, not PHP. That means variables, operators, and modifiers like `upper` are all fair game, if that's your style.

```antlers
@props([
'type' => 'info',
'title' => 'Heads up!' | upper,
])
```

Literal attributes pass strings. Prefix an attribute with `:` to resolve its value from the Antlers scope, or use `:$variable` when the prop and variable share a name:

```antlers
{{ page_title = 'Back up first' }}
{{ type = 'warning' }}

<x-callout :title="page_title" :$type />
```

These are Antlers' [usual parameter rules](/frontend/antlers#tag-parameters), even when the component itself is written in Blade.

## Attributes

Attributes not declared as props are collected in the `attributes` bag. Render the bag directly, or call its Laravel methods with Antlers' dot syntax:

```antlers
<aside {{ attributes.merge([
'class' => 'callout callout--' + type,
]) }}>
...
</aside>
```

The `merge` method adds the component's default attributes while preserving those passed by the caller. Class names are combined, so our earlier `class="mt-8"` joins the callout classes instead of booting them out of the club.

## Slots

The `slot` variable contains the component's unnamed content. Use the `has_actual_content` modifier when whitespace and HTML comments alone should count as empty:

```antlers
{{ if slot | has_actual_content }}
<div>{{ slot }}</div>
{{ /if }}
```

The closely related `is_string` modifier is available when you need to distinguish an ordinary string from a slot object or another value:

```antlers
{{ if value | is_string }}
{{ value }}
{{ /if }}
```

### Named slots

Use `<x-slot:name>` to send content to a named slot:

```antlers
<x-panel>
<x-slot:heading class="text-xl">
Fresh from the blog
</x-slot:heading>

The latest dispatches from our crew.
</x-panel>
```

The component receives the slot as a variable. Any attributes on the slot are available through its own `attributes` bag:

```antlers
{{# resources/views/components/panel.antlers.html #}}
<section>
<header {{ heading.attributes }}>{{ heading }}</header>
<div>{{ slot }}</div>
</section>
```

## Scope

A component does not inherit the Antlers variables or Cascade data around it. Variables created inside the component do not leak out, either. Pass values as props when they are part of the component's public API.

Slot content is the intentional exception. It is evaluated in the caller's scope, so variables available where you invoke the component remain available inside its default and named slots.

### Cascade data

Use `@cascade` when a component needs data from Statamic's [Cascade](/data-inheritance). Pass a list to import only the values you need. Values listed without defaults are required; a named key may provide a fallback:

```antlers
@cascade([
'title',
'eyebrow' => 'Latest',
])

<h2>{{ title }}</h2>
<p>{{ eyebrow }}</p>
```

Omit the arguments to import the entire Cascade:

```antlers
@cascade
```

Pulling in everything is convenient, but selecting values keeps the component's dependencies much easier to spot six months from now.

### Sharing parent props

The `@aware` directive lets a nested component consume props explicitly passed to an ancestor component. Here, the menu item picks up the menu's `tone`:

```antlers
{{# resources/views/components/menu.antlers.html #}}
@props([
'tone' => 'light'
])

<nav class="menu menu--{{ tone }}">
{{ slot }}
</nav>
```

```antlers
{{# resources/views/components/menu/item.antlers.html #}}
@aware([
'tone' => 'light'
])

<a class="menu__item menu__item--{{ tone }}">{{ slot }}</a>
```

```antlers
<x-menu tone="dark">
<x-menu.item>Docs</x-menu.item>
</x-menu>
```

Like `@props`, the values passed to `@aware` are Antlers expressions. Providing a fallback keeps the nested component useful when it appears outside its usual parent.

### Escaping directives

If you need any of these directives to appear as literal text, add another `@`:

```antlers
@@props(['example'])
@@aware(['example'])
@@cascade
```

This renders `@props(['example'])`, `@aware(['example'])`, and `@cascade` without evaluating them.

## Component-style Statamic Tags

Statamic Tags may also use HTML-like syntax in Antlers. Prefix the Tag with either `s:` or `statamic:` and otherwise use it as normal:

```antlers
<s:collection:pages limit="3">
<a href="{{ url }}">{{ title }}</a>
</s:collection:pages>
```

This is equivalent to classic Antlers syntax:

```antlers
{{ collection:pages limit="3" }}
<a href="{{ url }}">{{ title }}</a>
{{ /collection:pages }}
```

Parameters still follow Antlers rules, including dynamic values and shorthand:

```antlers
<s:collection :from="collection_handle" :$limit>
<a href="{{ url }}">{{ title }}</a>
</s:collection>
```

You may use `s-` and `statamic-` prefixes instead if dashes feel more HTML-ish, and Tags without enclosed content may self-close. This syntax still invokes a Statamic Tag; it does not look for a component view in `resources/views/components`.
5 changes: 5 additions & 0 deletions content/collections/pages/antlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,11 @@ Now you can define the context of the named slot using the `slot:name` tag forma
</a>
{{ /partial:modal }}
```

### Components

Components are reusable, isolated chunks of UI with props, attribute bags, and slots. Antlers can render existing Laravel Blade components, and you can author anonymous components with Antlers itself. Head over to [Antlers Components](/frontend/antlers-components) to learn more.

### Stacks

Antlers allows you to push template code to a "stack" which can be rendered somewhere else in your layout (most commonly) or another view. This can be particularly useful for specifying any JavaScript libraries required by your child views:
Expand Down
2 changes: 1 addition & 1 deletion content/collections/pages/blade.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Under the hood, this is syntactic sugar for creating an Antlers partial and does

## Using Antlers Blade components

Despite the name, Antlers Blade Components are a Blade-only feature that allows you to use existing tags inside your Blade templates using a custom tag syntax. For example, you can gather all entries from a "pages" collection using the [collection](/tags/collection) tag like so:
Antlers Blade Components allow you to use existing tags inside your Blade templates with a custom tag syntax. The same component-style syntax also works [inside Antlers templates](/frontend/antlers-components#component-style-statamic-tags). For example, you can gather all entries from a "pages" collection using the [collection](/tags/collection) tag like so:

```blade
<s:collection:pages>
Expand Down
2 changes: 2 additions & 0 deletions content/trees/collections/pages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ tree:
entry: d37b2af2-f2bf-493a-9345-7087fb5929ce
-
entry: 0c54fe7c-c87a-4812-b76e-48f16cf08e0d
-
entry: 083c717b-545c-45be-a65b-67faf1aa886f
-
entry: c7816387-ebc4-4204-b5f2-8e7073a4db8b
-
Expand Down
3 changes: 3 additions & 0 deletions content/trees/navigation/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ tree:
id: 9d43191e-652b-48e8-8ec5-5cd6266d793d
entry: d37b2af2-f2bf-493a-9345-7087fb5929ce
title: 'Antlers Templates'
-
id: ff6cfc92-2cb1-49c8-911a-678a58d02e04
entry: 083c717b-545c-45be-a65b-67faf1aa886f
-
id: 40ef0b25-0f5e-4523-a527-52a099838d09
entry: c7816387-ebc4-4204-b5f2-8e7073a4db8b
Expand Down
Loading