Skip to content

Commit

Permalink
Component Attribute Bag
Browse files Browse the repository at this point in the history
Component Attribute Bag Methods
  • Loading branch information
soysudhanshu authored Jul 29, 2024
2 parents 94f62b3 + 66cb395 commit 4cd207c
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 39 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ Template inheritance allows you to create layouts by defining a master template
| `{{ $attributes->merge() }}` | ||
| `{{ $attributes->class() }}` | ||
| `{{ $attributes->class() }} Conditional` | ||
| `{{ $attributes->prepends() }}` | | |
| `{{ $attributes->filter() }}` | | |
| `{{ $attributes->whereStartsWith() }}` | | |
| `{{ $attributes->whereDoesntStartWith() }}` | | |
| `{{ $attributes->whereDoesntStartWith()->first() }}` | | |
| `{{ $attributes->prepends() }}` | | |
| `{{ $attributes->filter() }}` | | |
| `{{ $attributes->whereStartsWith() }}` | | |
| `{{ $attributes->whereDoesntStartWith() }}` | | |
| `{{ $attributes->whereDoesntStartWith()->first() }}` | | |
| `{{ $attributes->has() }}` | ||
| `{{ $attributes->hasAny() }}` | ||
| `{{ $attributes->get() }}` | ||
Expand Down
101 changes: 101 additions & 0 deletions docs/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Components

Components provide a way to create reusable elements that can be used in multiple places in your application, in Katana they look and act like html tags. Currently, Katana only supports anynomous components which are simple blade template files, but in the future, Katana will support class components which will be more powerful and flexible.

## Creating a component

To create a component, you need to create a blade template file in the inside `components` directory of your `views` directory. The file name should be the name of the component with the `.blade.php` extension. For example, if you want to create a component called `button`, you should create a file called `button.blade.php`.

Here is an example of a simple component:

```blade
{{-- components/button.blade.php --}}
<button> Simple </button>
{{-- Using button --}}
<x-button></x-button> or <x-button/>
```

## Passing data to components

On its own, a component is just a static element, but you can pass data to a component using attributes. Here is an example of a component that accepts a `name` attribute:

```blade
{{-- components/alert.blade.php --}}
<div class="alert alert-{{ $type }}">
{{ $message }}
</div>
{{-- Using alert --}}
<x-alert type="info" message="Hello world"/>
```

### Passing Variables

You can pass variables to components using the `:variable` syntax. Here is an example:

```blade
{{-- components/alert.blade.php --}}
<div class="alert alert-{{ $type }}">
{{ $message }}
</div>
{{-- Using alert --}}
@php
$message = "Hello world";
$type = "info";
@endphp
<x-alert :type="$type" :message="$message"/>
```

> [!Info]
> Variables with multiple words should be separated by a dash `-` in the component tag. For example, `alert-type` instead of `alertType`.
> [!Info]
> Multiple word attributes are converted to camelCase in the component file. For example, `alert-type` will be accessible using `$alertType` in the component file.
### Default values

You can set default values for attributes in a component using the `@props` directive. Here is an example:

```blade
{{-- components/alert.blade.php --}}
@props(['alertType' => 'info', 'message'])
<div class="alert alert-{{ $alertType }}">
{{ $message }}
</div>
{{-- Using alert --}}
<x-alert message="Hello world"/>
```

### Component Attributes

You can access all the attributes passed to a component using the `$attributes` variable. Here is an example:

```blade
{{-- components/alert.blade.php --}}
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $message }}
</div>
{{-- Using alert --}}
<x-alert type="info" message="Hello world" class="mt-4"/>
``
```

#### Attributes methods

- `merge(array $defaultAttributes)`: Merges the given array with the component's attributes.
- `except(array $keys)`: Remove the given keys from the component's attributes.
- `first(string $key)`: Get the first value from the component's attributes for the given key.
- `get(string $key)`: Get the value from the component's attributes for the given key.
- `has(string $key)`: Determine if the component's attributes contain a value for the given key.
- `hasAny(array $keys)`: Determine if the component's attributes contain a value for any of the given keys.
21 changes: 21 additions & 0 deletions src/AppendableAttributeValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Blade;

use Stringable;

/**
* Represents an attribute value that should be appended
* to the existing attribute value.
*/
final class AppendableAttributeValue implements Stringable
{
public function __construct(private string $value)
{
}

public function __toString(): string
{
return $this->value;
}
}
104 changes: 78 additions & 26 deletions src/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,57 @@ public function __construct(protected array $attributes)
$this->attributes = $attributes;
}

public function class(string | array $classes): static
public function class(string | array $defaultClasses): static
{
if (empty($this->attributes['class'])) {
$this->attributes['class'] = '';
}
return new static(
array_merge(
$this->attributes,
['class' => $this->mergeClass($defaultClasses)]
)
);
}

private function mergeClass(string | array $defaultClasses): string
{
$defaultClasses = is_string($defaultClasses) ? explode(" ", $defaultClasses) : $defaultClasses;

$classes = is_string($classes) ? explode(" ", $classes) : $classes;
$classes = $this->attributes['class'] ?? '';

$this->attributes['class'] = sprintf(
$classes = sprintf(
"%s %s",
$this->attributes['class'],
implode(" ", Blade::filterConditionalValues($classes))
$classes,
implode(" ", Blade::filterConditionalValues($defaultClasses))
);

$this->attributes['class'] = trim($this->attributes['class']);
$classes = trim($classes);

return $this;
return $classes;
}

public function merge(array $attributes): static
public function merge(array $defaultAttributes): static
{
foreach ($attributes as $key => $value) {
$attributes = $this->attributes;

foreach ($defaultAttributes as $key => $value) {

if (!isset($attributes[$key])) {
$attributes[$key] = '';
}

if ($key === 'class') {
$attributes['class'] = $this->mergeClass($value);
continue;
}

if (!isset($this->attributes[$key])) {
$this->attributes[$key] = '';
} else {
$value = " $value";
if ($value instanceof AppendableAttributeValue) {
$attributes[$key] = $value . $attributes[$key];
continue;
}

$this->attributes[$key] .= $value;
$attributes[$key] .= $value;
}

return $this;
return new static($attributes);
}

/**
Expand All @@ -58,11 +76,7 @@ public function except(string|array $attributes): static
{
$attributes = is_string($attributes) ? [$attributes] : $attributes;

foreach ($attributes as $attribute) {
unset($this->attributes[$attribute]);
}

return $this;
return $this->filter(fn (string $key) => !in_array($key, $attributes));
}

public function __toString(): string
Expand Down Expand Up @@ -90,7 +104,7 @@ public function toHtml(): string

public function get(string $key)
{
return $this->attributes[toCamelCase($key)] ?? null;
return $this->attributes[$key] ?? null;
}

public function toArray(): array
Expand All @@ -116,7 +130,7 @@ public function has(string|array $keys): bool
$keys = is_string($keys) ? [$keys] : $keys;

foreach ($keys as $key) {
if (!array_key_exists(toCamelCase($key), $this->attributes)) {
if (!array_key_exists($key, $this->attributes)) {
$found = false;
break;
}
Expand All @@ -136,12 +150,50 @@ public function hasAny(array $keys): bool
$found = false;

foreach ($keys as $key) {
if (array_key_exists(toCamelCase($key), $this->attributes)) {
if (array_key_exists($key, $this->attributes)) {
$found = true;
break;
}
}

return $found;
}


public function filter(callable $callback): static
{
$attributes = array_filter(
$this->attributes,
fn (string $value, string $key) => $callback($key, $value),
ARRAY_FILTER_USE_BOTH
);

return new static($attributes);
}

public function whereStartsWith(string $needle): static
{
return $this->filter(fn (string $key) => str_starts_with($key, $needle));
}

public function whereDoesntStartWith(string $needle): static
{
return $this->filter(fn (string $key) => !str_starts_with($key, $needle));
}

public function first(): static
{
return new static(array_slice($this->attributes, 0, 1, true));
}

/**
* Prepends a value to the attribute.
*
* @param string $value
* @return AppendableAttributeValue
*/
public function prepends(string $value): AppendableAttributeValue
{
return new AppendableAttributeValue($value);
}
}
10 changes: 4 additions & 6 deletions src/ComponentRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,14 @@ public function getViewData(): array


foreach ($attributes as $key => $value) {
$data[$key] = $value;
$data[toCamelCase($key)] = $value;
}

/**
* Setup remaining props and remove
* them from the attributes array.
*/
$propsKeys = [];
foreach ($component->props as $key => $prop) {
if (is_int($key)) {
$key = $prop;
Expand All @@ -118,13 +119,10 @@ public function getViewData(): array
$data[$key] = $prop;
}

if ($attributes->has($key)) {
$attributes->except($key);
}
$propsKeys[] = $key;
}


$data['attributes'] = $attributes;
$data['attributes'] = $attributes->except($propsKeys);;
$data['slot'] = $component->slot;
$data['component_renderer'] = $this;

Expand Down
2 changes: 1 addition & 1 deletion src/ComponentTagsCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ protected function parseAttributes(string $attribute): string
)/x
REGEX,
function ($matches) {
$name = $this->toCamelCase($matches['name']);
$name = $matches['name'];
$value = $matches['value'] ?? null;

/**
Expand Down
Loading

0 comments on commit 4cd207c

Please sign in to comment.