Skip to content
Snippets Groups Projects
Commit 94872f23 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat(ui): create ViewComponents library to enable building class and view files components

- replace some helper components and forms with class components in the ui
- create viewcomponents
service and load the component function to be used in views
parent fcecbe1c
No related branches found
No related tags found
No related merge requests found
Showing
with 635 additions and 128 deletions
...@@ -46,6 +46,7 @@ class Autoload extends AutoloadConfig ...@@ -46,6 +46,7 @@ class Autoload extends AutoloadConfig
'Config' => APPPATH . 'Config', 'Config' => APPPATH . 'Config',
'ActivityPub' => APPPATH . 'Libraries/ActivityPub', 'ActivityPub' => APPPATH . 'Libraries/ActivityPub',
'Analytics' => APPPATH . 'Libraries/Analytics', 'Analytics' => APPPATH . 'Libraries/Analytics',
'ViewComponents' => APPPATH . 'Libraries/ViewComponents',
]; ];
/** /**
...@@ -84,5 +85,5 @@ class Autoload extends AutoloadConfig ...@@ -84,5 +85,5 @@ class Autoload extends AutoloadConfig
* ``` * ```
* @var array<int, string> * @var array<int, string>
*/ */
public $files = []; public $files = [APPPATH . 'Libraries/ViewComponents/Helpers/view_components_helper.php'];
} }
...@@ -110,7 +110,9 @@ if (! function_exists('button')) { ...@@ -110,7 +110,9 @@ if (! function_exists('button')) {
CODE_SAMPLE; CODE_SAMPLE;
} }
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('icon_button')) { if (! function_exists('icon_button')) {
/** /**
* Icon Button component * Icon Button component
...@@ -145,6 +147,7 @@ if (! function_exists('icon_button')) { ...@@ -145,6 +147,7 @@ if (! function_exists('icon_button')) {
} }
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('hint_tooltip')) { if (! function_exists('hint_tooltip')) {
/** /**
* Hint component * Hint component
...@@ -167,7 +170,9 @@ if (! function_exists('hint_tooltip')) { ...@@ -167,7 +170,9 @@ if (! function_exists('hint_tooltip')) {
return $tooltip . '">' . icon('question') . '</span>'; return $tooltip . '">' . icon('question') . '</span>';
} }
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('data_table')) { if (! function_exists('data_table')) {
/** /**
* Data table component * Data table component
...@@ -223,7 +228,9 @@ if (! function_exists('data_table')) { ...@@ -223,7 +228,9 @@ if (! function_exists('data_table')) {
'</div>'; '</div>';
} }
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('publication_pill')) { if (! function_exists('publication_pill')) {
/** /**
* Publication pill component * Publication pill component
...@@ -250,7 +257,9 @@ if (! function_exists('publication_pill')) { ...@@ -250,7 +257,9 @@ if (! function_exists('publication_pill')) {
'</span>'; '</span>';
} }
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('publication_button')) { if (! function_exists('publication_button')) {
/** /**
* Publication button component * Publication button component
...@@ -508,27 +517,5 @@ if (! function_exists('relative_time')) { ...@@ -508,27 +517,5 @@ if (! function_exists('relative_time')) {
CODE_SAMPLE; CODE_SAMPLE;
} }
} }
// ------------------------------------------------------------------------
if (! function_exists('xml_editor')) {
/**
* XML Editor field
*
* @param array<string, mixed> $customData
* @param array<string, mixed> $extra
*/
function xml_editor(array $customData = [], string $value = '', array $extra = []): string
{
$defaultData = [
'slot' => 'textarea',
'rows' => 5,
];
$data = array_merge($defaultData, $customData);
$textarea = form_textarea($data, $value, $extra);
return <<<CODE_SAMPLE
<xml-editor>{$textarea}</time-ago>
CODE_SAMPLE;
}
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
...@@ -141,38 +141,12 @@ if (! function_exists('form_label')) { ...@@ -141,38 +141,12 @@ if (! function_exists('form_label')) {
//-------------------------------------------------------------------- //--------------------------------------------------------------------
if (! function_exists('form_multiselect')) {
/**
* Multi-select menu
*
* @param array<string, string> $options
* @param string[] $selected
* @param array<string, string> $customExtra
*/
function form_multiselect(
string $name = '',
array $options = [],
array $selected = [],
array $customExtra = []
): string {
$defaultExtra = [
'data-class' => $customExtra['class'],
'multiple' => 'multiple',
];
$extra = array_merge($defaultExtra, $customExtra);
return form_dropdown($name, $options, $selected, $extra);
}
}
//--------------------------------------------------------------------
if (! function_exists('form_dropdown')) { if (! function_exists('form_dropdown')) {
/** /**
* Drop-down Menu (based on html select tag) * Drop-down Menu (based on html select tag)
* *
* @param array<string, mixed> $options * @param array<string, mixed> $options
* @param string[] $selected * @param array<string|int> $selected
* @param array<string, mixed> $customExtra * @param array<string, mixed> $customExtra
*/ */
function form_dropdown( function form_dropdown(
...@@ -236,81 +210,3 @@ if (! function_exists('form_dropdown')) { ...@@ -236,81 +210,3 @@ if (! function_exists('form_dropdown')) {
return $form . "</select>\n"; return $form . "</select>\n";
} }
} }
//--------------------------------------------------------------------
if (! function_exists('form_editor')) {
/**
* Markdown editor
*
* @param array<string, mixed> $data
* @param array<string, mixed>|string $extra
*/
function form_markdown_editor(array $data = [], string $value = '', string | array $extra = ''): string
{
$editorClass = 'w-full flex flex-col bg-white border border-gray-500 focus-within:ring-1 focus-within:ring-blue-600';
if (array_key_exists('class', $data) && $data['class'] !== '') {
$editorClass .= ' ' . $data['class'];
unset($data['class']);
}
$data['class'] = 'border-none outline-none focus:border-none focus:outline-none w-full h-full';
return '<div class="' . $editorClass . '">' .
'<header class="sticky top-0 z-20 flex flex-wrap justify-between bg-white border-b border-gray-500">' .
'<markdown-write-preview for="' . $data['id'] . '" class="relative inline-flex h-8">' .
'<button type="button" slot="write" class="px-2 font-semibold focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
'Common.forms.editor.write'
) . '</button>' .
'<button type="button" slot="preview" class="px-2 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
'Common.forms.editor.preview'
) . '</button>' .
'</markdown-write-preview>' .
'<markdown-toolbar for="' . $data['id'] . '" class="flex gap-4 px-2 py-1">' .
'<div class="inline-flex text-2xl gap-x-1">' .
'<md-header class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'heading'
) . '</md-header>' .
'<md-bold class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'bold'
) . '</md-bold>' .
'<md-italic class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'italic'
) . '</md-italic>' .
'</div>' .
'<div class="inline-flex text-2xl gap-x-1">' .
'<md-unordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'list-unordered'
) . '</md-unordered-list>' .
'<md-ordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'list-ordered'
) . '</md-ordered-list>' .
'</div>' .
'<div class="inline-flex text-2xl gap-x-1">' .
'<md-quote class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'quote'
) . '</md-quote>' .
'<md-link class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'link'
) . '</md-link>' .
'<md-image class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'image-add'
) . '</md-image>' .
'</div>' .
'</markdown-toolbar>' .
'</header>' .
'<div class="relative">' .
form_textarea($data, $value, $extra) .
'<markdown-preview for="' . $data['id'] . '" class="absolute top-0 left-0 hidden w-full h-full p-2 overflow-y-auto prose bg-gray-50" showClass="bg-white"></markdown-preview>' .
'</div>' .
'<footer class="flex px-2 py-1 bg-gray-100 border-t">' .
'<a href="https://commonmark.org/help/" class="inline-flex items-center text-xs font-semibold text-gray-500 hover:text-gray-700" target="_blank" rel="noopener noreferrer">' . icon(
'markdown',
'mr-1 text-lg text-gray-400'
) . lang('Common.forms.editor.help') . '</a>' .
'</footer>' .
'</div>';
}
}
// ------------------------------------------------------------------------
<?php
declare(strict_types=1);
namespace ViewComponents;
class Component implements ComponentInterface
{
/**
* @var array<string, string>
*/
protected array $attributes = [
'class' => '',
];
/**
* @param array<string, mixed> $properties
* @param array<string, string> $attributes
*/
public function __construct(
protected array $properties,
array $attributes
) {
// overwrite default properties if set
foreach ($properties as $key => $value) {
$this->{$key} = $value;
}
$this->attributes = array_merge($this->attributes, $attributes);
}
public function render(): string
{
return static::class . ': RENDER METHOD NOT IMPLEMENTED';
}
}
<?php
declare(strict_types=1);
namespace ViewComponents;
interface ComponentInterface
{
public function render(): string;
}
<?php
declare(strict_types=1);
namespace ViewComponents;
use ViewComponents\Config\ViewComponents;
use ViewComponents\Exceptions\ComponentNotFoundException;
class ComponentLoader
{
protected ViewComponents $config;
protected string $name;
/**
* @var array<string, mixed>
*/
protected array $properties = [];
/**
* @var array<string, string>
*/
protected array $attributes = [];
public function __construct()
{
$this->config = config('ViewComponents');
}
public function __get(string $property): mixed
{
if (property_exists($this, $property)) {
return $this->{$property};
}
}
// @phpstan-ignore-next-line
public function __set(string $property, mixed $value)
{
if (property_exists($this, $property)) {
$this->{$property} = $value;
}
return $this;
}
/**
* @throws ComponentNotFoundException
*/
public function load(): string
{
// first, check if there exists a component class to load in class components path
if (file_exists("{$this->config->classComponentsPath}/{$this->name}.php")) {
return $this->loadComponentClass();
}
// check for the existence of a view file if no component class has been found
// component view files are camel case
$camelCaseName = strtolower(preg_replace('~(?<!^)(?<!\/)[A-Z]~', '_$0', $this->name) ?? '');
if (file_exists("{$this->config->componentsViewPath}/{$camelCaseName}.php")) {
return $this->loadComponentView($camelCaseName);
}
throw new ComponentNotFoundException("Could not find component \"{$this->name}\"");
}
private function loadComponentClass(): string
{
$classComponentsNamespace = $this->config->classComponentsNamespace;
$namespacedName = str_replace('/', '\\', $this->name);
$componentClassNamespace = "{$classComponentsNamespace}\\{$namespacedName}";
$component = new $componentClassNamespace($this->properties, $this->attributes);
return $component->render();
}
private function loadComponentView(string $name): string
{
$viewData = [...$this->properties, ...$this->attributes];
return view("components/{$name}", $viewData);
}
}
<?php
declare(strict_types=1);
namespace ViewComponents\Config;
use CodeIgniter\Config\BaseService;
use ViewComponents\ComponentLoader;
/**
* Services Configuration file.
*
* Services are simply other classes/libraries that the system uses to do its job. This is used by CodeIgniter to allow
* the core of the framework to be swapped out easily without affecting the usage within the rest of your application.
*
* This file holds any application-specific services, or service overrides that you might need. An example has been
* included with the general method format you should use for your service methods. For more examples, see the core
* Services file at system/Config/Services.php.
*/
class Services extends BaseService
{
public static function viewcomponents(bool $getShared = true): ComponentLoader
{
if ($getShared) {
return self::getSharedInstance('viewcomponents');
}
return new ComponentLoader();
}
}
<?php
declare(strict_types=1);
namespace ViewComponents\Config;
use CodeIgniter\Config\BaseConfig;
class ViewComponents extends BaseConfig
{
public string $classComponentsNamespace = APP_NAMESPACE . '\View\Components';
public string $classComponentsPath = APPPATH . 'View/Components';
public string $componentsViewPath = APPPATH . 'Views/components';
}
<?php
declare(strict_types=1);
namespace ViewComponents\Exceptions;
use CodeIgniter\Exceptions\ExceptionInterface;
use RuntimeException;
class ComponentNotFoundException extends RuntimeException implements ExceptionInterface
{
}
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
if (! function_exists('component')) {
/**
* Loads the specified class or view file component in the parameters
*
* @param array<string, array<string, mixed>> $properties
* @param array<string, array<string, mixed>> $attributes
*/
function component(string $name, array $properties = [], array $attributes = []): string
{
$componentLoader = service('viewcomponents');
$componentLoader->name = $name;
$componentLoader->properties = $properties;
$componentLoader->attributes = $attributes;
return $componentLoader->load();
}
}
...@@ -60,6 +60,10 @@ export class XMLEditor extends LitElement { ...@@ -60,6 +60,10 @@ export class XMLEditor extends LitElement {
border: 1px solid #6b7280; border: 1px solid #6b7280;
background-color: #ffffff; background-color: #ffffff;
} }
.cm-editor.cm-focused {
outline: 2px solid transparent;
box-shadow: 0 0 0 1px #2563eb;
}
`; `;
render(): TemplateResult<1> { render(): TemplateResult<1> {
......
<?php
declare(strict_types=1);
namespace App\View\Components;
use ViewComponents\Component;
class Button extends Component
{
protected string $label = '';
protected string $uri = '';
protected string $variant = 'default';
protected string $size = 'base';
protected string $iconLeft = '';
protected string $iconRight = '';
protected bool $isSquared = false;
public function render(): string
{
$baseClass =
'inline-flex items-center font-semibold shadow-xs rounded-full focus:outline-none focus:ring';
$variantClass = [
'default' => 'text-black bg-gray-300 hover:bg-gray-400',
'primary' => 'text-white bg-pine-700 hover:bg-pine-800',
'secondary' => 'text-white bg-gray-700 hover:bg-gray-800',
'accent' => 'text-white bg-rose-600 hover:bg-rose-800',
'success' => 'text-white bg-green-600 hover:bg-green-700',
'danger' => 'text-white bg-red-600 hover:bg-red-700',
'warning' => 'text-black bg-yellow-500 hover:bg-yellow-600',
'info' => 'text-white bg-blue-500 hover:bg-blue-600',
];
$sizeClass = [
'small' => 'text-xs md:text-sm',
'base' => 'text-sm md:text-base',
'large' => 'text-lg md:text-xl',
];
$basePaddings = [
'small' => 'px-2 md:px-3 md:py-1',
'base' => 'px-3 py-1 md:px-4 md:py-2',
'large' => 'px-3 py-2 md:px-5',
];
$squaredPaddings = [
'small' => 'p-1',
'base' => 'p-2',
'large' => 'p-3',
];
$buttonClass =
$baseClass .
' ' .
($this->isSquared
? $squaredPaddings[$this->size]
: $basePaddings[$this->size]) .
' ' .
$sizeClass[$this->size] .
' ' .
$variantClass[$this->variant];
if (array_key_exists('class', $this->attributes)) {
$buttonClass .= ' ' . $this->attributes['class'];
unset($this->attributes['class']);
}
if ($this->iconLeft !== '') {
$this->label = icon($this->iconLeft, 'mr-2') . $this->label;
}
if ($this->iconRight !== '') {
$this->label .= icon($this->iconRight, 'ml-2');
}
if ($this->uri !== '') {
return anchor($this->uri, $this->label, array_merge([
'class' => $buttonClass,
], $this->attributes));
}
$defaultButtonAttributes = [
'type' => 'button',
];
$attributes = stringify_attributes(array_merge($defaultButtonAttributes, $this->attributes));
return <<<CODE_SAMPLE
<button class="{$buttonClass}" {$attributes}>{$this->label}</button>
CODE_SAMPLE;
}
}
<?php
declare(strict_types=1);
namespace App\View\Components\Forms;
use ViewComponents\Component;
class Input extends Component
{
public function render(): string
{
return '';
}
}
<?php
declare(strict_types=1);
namespace App\View\Components\Forms;
use ViewComponents\Component;
class Label extends Component
{
/**
* @var array<string, string>
*/
protected array $attributes = [
'for' => '',
'name' => '',
'value' => '',
'class' => '',
];
protected string $text = '';
protected string $hint = '';
protected bool $isOptional = false;
public function render(): string
{
$labelClass = $this->attributes['class'];
unset($this->attributes['class']);
$attributes = stringify_attributes($this->attributes);
$optionalText = $this->isOptional ? '<small class="ml-1 lowercase">(' .
lang('Common.optional') .
')</small>' : '';
$hint = $this->hint !== '' ? hint_tooltip($this->hint, 'ml-1') : '';
return <<<CODE_SAMPLE
<label class="{$labelClass}" {$attributes}>{$this->text}{$optionalText}{$hint}</label>
CODE_SAMPLE;
}
}
<?php
declare(strict_types=1);
namespace App\View\Components\Forms;
use ViewComponents\Component;
class MarkdownEditor extends Component
{
protected string $content = '';
public function render(): string
{
$editorClass = 'w-full flex flex-col bg-white border border-gray-500 focus-within:ring-1 focus-within:ring-blue-600';
if ($this->attributes['class'] !== '') {
$editorClass .= ' ' . $this->attributes['class'];
unset($this->attributes['class']);
}
$this->attributes['class'] = 'border-none outline-none focus:border-none focus:outline-none w-full h-full';
return '<div class="' . $editorClass . '">' .
'<header class="sticky top-0 z-20 flex flex-wrap justify-between bg-white border-b border-gray-500">' .
'<markdown-write-preview for="' . $this->attributes['id'] . '" class="relative inline-flex h-8">' .
'<button type="button" slot="write" class="px-2 font-semibold focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
'Common.forms.editor.write'
) . '</button>' .
'<button type="button" slot="preview" class="px-2 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
'Common.forms.editor.preview'
) . '</button>' .
'</markdown-write-preview>' .
'<markdown-toolbar for="' . $this->attributes['id'] . '" class="flex gap-4 px-2 py-1">' .
'<div class="inline-flex text-2xl gap-x-1">' .
'<md-header class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'heading'
) . '</md-header>' .
'<md-bold class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'bold'
) . '</md-bold>' .
'<md-italic class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'italic'
) . '</md-italic>' .
'</div>' .
'<div class="inline-flex text-2xl gap-x-1">' .
'<md-unordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'list-unordered'
) . '</md-unordered-list>' .
'<md-ordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'list-ordered'
) . '</md-ordered-list>' .
'</div>' .
'<div class="inline-flex text-2xl gap-x-1">' .
'<md-quote class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'quote'
) . '</md-quote>' .
'<md-link class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'link'
) . '</md-link>' .
'<md-image class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
'image-add'
) . '</md-image>' .
'</div>' .
'</markdown-toolbar>' .
'</header>' .
'<div class="relative">' .
form_textarea($this->attributes, $this->content) .
'<markdown-preview for="' . $this->attributes['id'] . '" class="absolute top-0 left-0 hidden w-full h-full p-2 overflow-y-auto prose bg-gray-50" showClass="bg-white"></markdown-preview>' .
'</div>' .
'<footer class="flex px-2 py-1 bg-gray-100 border-t">' .
'<a href="https://commonmark.org/help/" class="inline-flex items-center text-xs font-semibold text-gray-500 hover:text-gray-700" target="_blank" rel="noopener noreferrer">' . icon(
'markdown',
'mr-1 text-lg text-gray-400'
) . lang('Common.forms.editor.help') . '</a>' .
'</footer>' .
'</div>';
}
}
<?php
declare(strict_types=1);
namespace App\View\Components\Forms;
use ViewComponents\Component;
class MultiSelect extends Component
{
/**
* @var array<string, string>
*/
protected array $options = [];
/**
* @var string[]
*/
protected array $selected = [];
public function render(): string
{
$defaultAttributes = [
'data-class' => $this->attributes['class'],
'multiple' => 'multiple',
];
$extra = array_merge($defaultAttributes, $this->attributes);
return form_dropdown($this->attributes['name'], $this->options, $this->selected, $extra);
}
}
<?php
declare(strict_types=1);
namespace App\View\Components\Forms;
use ViewComponents\Component;
class MultiSelect extends Component
{
/**
* @var array<string, string>
*/
protected array $options = [];
/**
* @var string[]
*/
protected array $selected = [];
public function render(): string
{
$defaultAttributes = [
'data-class' => $this->attributes['class'],
'multiple' => 'multiple',
];
$extra = array_merge($defaultAttributes, $this->attributes);
return form_dropdown($this->attributes['name'], $this->options, $this->selected, $extra);
}
}
<?php
declare(strict_types=1);
namespace App\View\Components\Forms;
use ViewComponents\Component;
/**
* Form Checkbox Switch
*
* Abstracts form_label to stylize it as a switch toggle
*/
class Toggler extends Component
{
/**
* @var array<string, string>
*/
protected array $attributes = [
'id' => '',
'name' => '',
'value' => '',
'class' => '',
];
protected string $label = '';
protected string $hint = '';
protected bool $checked = false;
public function render(): string
{
$wrapperClass = $this->attributes['class'];
unset($this->attributes['class']);
$this->attributes['class'] = 'form-switch';
$checkbox = form_checkbox($this->attributes, $this->attributes['value'], $this->checked);
$hint = $this->hint !== '' ? hint_tooltip(lang('Podcast.form.lock_hint'), 'ml-1') : '';
return <<<CODE_SAMPLE
<label class="relative inline-flex items-center {$wrapperClass}">
{$checkbox}
<span class="form-switch-slider"></span>
<span class="ml-2">{$this->label}{$hint}</span>
</label>
CODE_SAMPLE;
}
}
<?php
declare(strict_types=1);
namespace App\View\Components\Forms;
use ViewComponents\Component;
class XMLEditor extends Component
{
protected string $content = '';
/**
* @var array<string, string>
*/
protected array $attributes = [
'slot' => 'textarea',
'rows' => '5',
'class' => 'textarea',
];
public function render(): string
{
$textarea = form_textarea($this->attributes, $this->content);
return <<<CODE_SAMPLE
<xml-editor>{$textarea}</time-ago>
CODE_SAMPLE;
}
}
<?php
declare(strict_types=1);
namespace App\View\Components;
use ViewComponents\Component;
class Icon extends Component
{
public string $glyph = '';
public function render(): string
{
$svgContents = file_get_contents('assets/icons/' . $this->glyph . '.svg');
if ($svgContents) {
if ($this->attributes['class'] !== '') {
$svgContents = str_replace('<svg', '<svg class="' . $this->attributes['class'] . '"', $svgContents);
}
return $svgContents;
}
return '□';
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment