Your first Component
To create your first component, you first have to create a file with the .pi
extention.
pinc files neither need to be in a specific folder structure, nor specify how many components you declare inside of
them. In theory, you could write your whole application inside of a single .pi
file.
Creating a Button
Every component needs to start with the component
keyword, and has to be given a unique uppercase identifier as a name
(in our case Button
).
component Button {
<button class="Button">
<span class="Button-icon Button-icon--arrowRight">
<span class="Button-text">Click me!</span>
</button>
}
The body of your component contains the logic needed to render your template. In the example above, the template is completely static and cannot be passed any options to change its appearance.
Let's change that:
component Button {
let text = #String(key: "text");
let icon = #String(key: "icon");
<button class="Button">
<span class="Button-icon Button-icon--$(icon)">
<span class="Button-text">{text}</span>
</button>
}
We now added two variables to our Button component: text
and icon
.
Both have a so called Tag
assigned as
their value, more specifically a String-Tag.
Tags are the way for you to ask the compiler to do something. In the case of a #String
the compiler searches for the
closest value with the provided key it can find and validates it to be a string value. The value may either be the
render call of the parent component or the implementing application (CMS), if there is no parent component. You can read
more about tags here.
In our template, we can render variables by wrapping them in curly braces ({}
) as seen with the text
variable.
With the icon
, we use a string template, to append its value to the class attribute.
If the name of the let-declaration and the key are the same, you can remove the key
attribute from the tag.
So
these two tags do the same:
let text = #String(key: "text");
let text = #String;
Rendering our Button component
In another component we are now able to render our Button
and change its text and icon value:
component Header {
<header class="Header">
<img class="Header-logo" src="/path/to/logo.svg" />
<div class="Header-actions">
<Button icon="login" text="Login" />
</div>
</header>
}
Components are rendered by using it's uppercase identifier as a html tag name.
All uppercase html tags are
assumed to be components and are searched for in your application.
If they cannot be found, pinc fails compiling.
As you can see, we don't have to import our component anywhere.
pinc resolves all components automatically by its
name, which means that every component needs to have a unique name.
Using Slots
In the current implementation of our Header, we have to statically add all Buttons for our actions inside the template.
If we assume, that the Buttons are more dynamic and can change based on some criteria, it would make more sense
for them to be provided from outside of our Header.
Thats what slots are used for. They are similar to the ones found in Vue or Web-Components, but there are some key differences you have to look out for.
Let's define our slot and render it where our buttons should appear:
component Header {
let actions = #Slot(key: "actions");
<header class="Header">
<img class="Header-logo" src="/path/to/logo.svg" />
<div class="Header-actions">
{actions}
</div>
</header>
}
A #Slot
is just another tag, which instructs the compiler to look for provided Template-Nodes in the parent component.
In our App
component, we are now able to provide the button from outside our Header:
component App {
<>
<Header>
<Button slot="actions" icon="login" text="Login" />
</Header>
<Main />
<Footer />
</>
}
To put a Template-Node inside a slot, you place it between the opening and closing tag of the component and (optionally)
add the slot
attribute with the same name as the declared #Slot
.
You may also add a default
-Slot, by setting the key of the #Slot
to an empty string (""
). The default slots get
all nodes passed in, which do not have a slot attribute.
let default_slot = #Slot(key: "");
Restricting our Slot
With the current implementation of our Header, you are able to place everything inside the slot.
Let's add a
restriction, so you are only able to place Button
components inside it:
component Header {
let actions = #Slot(key: "actions", constraints: [Button]);
// ...
}
We now added an constraints
attribute to our #Slot
, with which we may declare a set of restrictions for this slot.
You are able to allow a set of components by adding them to the instance array. As soon as you do that, all other
components and html-tags are automatically disallowed.
If you want to disallow a set of components and allow
everything else, you have to prefix the component name with a !
.
For example:
// allow only Button and Link components:
let allow = #Slot(constraints: [Button, Link]);
// allow everything but Button and Link components:
let disallow = #Slot(constraints: [!Button, !Link]);
With that change, our Header is only able to recieve Button
components and fails compiling, when you try to put some
other component inside it's actions slot.