Components

The Edge components system is highly inspired by the frontend frameworks like Vue or Svelte. It borrows the concept of reusability, isolated state, props and slots from them.

However, do note that Edge is a backend template engine and hence some of the principles cannot be replicated because of the server side environment and the design of Edge. It includes

Creating a component

Components are represented using the regular edge template files. For example: You can create a file named button.edge with the following markup

<button type="{{ type }}">
{{ text }}
</button>

And then use it as a component inside other templates.

@!component('button', {
text: 'Login',
type: 'submit'
})

The @component tag accepts a total of two arguments. First is the component path (relative from the views directory) and second is the component state (aka props).

Components do not have access to the state of the parent template. However, they can access templates globals and locals .

Props

The props are passed to a component as the second argument as an object of key-value pairs. You can access the props directly within the component file by using the object property name. For example:

@!component('button', {
text: 'Login',
type: 'submit'
})

Then button component can access the text and the type props as follows.

<button type="{{ type }}">{{ text }}</button>

$props

Another way to access the props is to use the $props property. It is an instance of the Props class and comes with a few extra capabilities to make components authoring easier.

In the following example, the serializeExcept method will convert all the props, except the text prop into HTML attributes.

<button {{ $props.serializeExcept(['text']) }}>{{ text }}</button>
@!component('button', {
text: 'Login',
type: 'submit',
class: 'py-2 px-8 text-white bg-gray-800',
})

Similar to the serializeExcept method, you can use the serializeOnly method to serialize selected props only or use the serialize method to convert all props to HTML attributes.

Slots

Along with the props, components can also accept slots. Slots are named outlets, that the parent component can define markup for.

As an example, lets accept the text for our button component as a slot and not as a prop.

<button type="{{ type }}">
{{{ await $slots.main() }}}
</button>

The component caller can define the markup for the main slot as follows.

@component('button', {
type: 'submit'
})
<i class="fa-icon-lock" />
<span> Login </span>
@end

Main slot

All the contents between the @component opening and the closing tag is part of the main slot, unless you move it inside a named slot.

In the following example, all the content outside the @slot('trigger') is part of the main slot.

@component('modal')
@slot('trigger')
<a href=""> Show modal </a>
@end
<h1> Modal title </h1>
<p> Modal content </p>
@end

Named slots

Named slots allow the component to accept markup for multiple outlets. For example, a modal component can accept markup for the modal header, body and its actions

components/modal.edge
<div>
<header>
{{{ await $slots.header() }}}
</header>
<main>
{{{ await $slots.main() }}}
</main>
<footer>
{{{ await $slots.actions() }}}
</footer>
</div>

The parent template can define them as follows

@component('components/modal')
@slot('header')
<h1> Delete post </h1>
@end
@slot('main')
<div>
<p>Are you sure, you want to delete the post</p>
</div>
@end
@slot('actions')
<div class="flex">
<button>Yes, delete it</button>
<button>Cancel</button>
</div>
@end
@end

Output

Slots scope

The slots have access to the state of the template in which they are defined (aka the parent template).

Following is the markup for the button component

components/button.edge
@set('title', 'I am a button')
<button>
{{{ await $slots.main() }}}
</button>

The parent template attempting to access the title property defined inside the component.

home.edge
@component('components/button')
<span>{{ title }}</span>
@end
Output
<button>
<span>undefined</span>
</button>

You can pass the data from the component to the parent as arguments to the slot method.

components/button.edge
@set('title', 'I am a button')
<button>
{{{ await $slots.main({ title }) }}}
</button>
home.edge
@component('components/button')
@slot('main', scope)
<span>{{ scope.title }}</span>
@end
@end

To summarize:

Injecting data to the component tree

The data injection API of Edge is inspired by the Svelte context API and Vue provide/inject API .

The goal is to simplify the communication between the components inside a tree. However, do note that this is an advanced API and must only be used when you are self authoring a group of components and want transparent communication between them.

Basic example

Let's start with the most basic example to see the injection API in action. You can make use of the @inject tag to share an object with the component tree.

Component file
{{-- Declare a local variable --}}
@set('counter', { value: 0 })
{{-- Inject it to the components tree --}}
@inject({ counter })
{{{ await $slots.main() }}}

Since the @inject tag shares the object by reference, any part of the component tree can mutate it's properties, as shown in the following snippet.

The injected values are available on the $context variable.

@component('components/parent')
<p> Value of counter is {{ $context.counter.value }} </p>
{{-- Bump the value by one --}}
@set($context, 'counter.value', $context.counter.value + 1)
<p> Updated value is {{ $context.counter.value }} </p>
@end

Components as tags

Edge allows you reference the components stored inside the ./resources/views/components directory as edge tags.

Let's create the following template inside the resources/views/components/button.edge file.

resources/views/components/button.edge
<button type="{{ type }}">
{{ text }}
</button>

Now, instead of referencing the button component using the @component tag. You can also reference it as @button tag.

@!button({
type: 'primary',
text: 'Login'
})

You can reference the components inside the nested directories with a dot notation. For example, the file stored inside the ./components/form/input.edge is referenced as follows:

@!form.input({
type: 'text',
placeholder: 'Enter username'
})

Following is a reference table to understand the transformations applied to the component path to compute the tag name.

Template pathTag name
form/input.edge@form.input
tool_tip.edge@toolTip
checkout_form/input.edge@checkoutForm.input