Skip to content

Commit

Permalink
Merge pull request #7 from markhuot/dynamic-exports
Browse files Browse the repository at this point in the history
  • Loading branch information
markhuot authored Nov 4, 2023
2 parents 6807875 + 37fceaa commit 0371331
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 103 deletions.
44 changes: 1 addition & 43 deletions src/actions/CompileTwigComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@
namespace markhuot\keystone\actions;

use Craft;
use markhuot\keystone\base\AttributeBag;
use markhuot\keystone\base\ComponentType;
use markhuot\keystone\base\FieldDefinition;
use markhuot\keystone\base\SlotDefinition;
use markhuot\keystone\models\Component;
use markhuot\keystone\models\ComponentData;
use markhuot\keystone\twig\Exports;
use PhpParser\Node;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Namespace_;
Expand Down Expand Up @@ -48,44 +42,18 @@ public function handle($force = false)
return $fqcn;
}

$fullTwigPath = Craft::$app->getView()->resolveTemplate($twigPath, $viewMode);

Craft::$app->getView()->renderTemplate($twigPath, [
'component' => $component = (new Component),
'exports' => $exports = new Exports,
'props' => $props = new ComponentData,
'attributes' => new AttributeBag,
], $viewMode);

$slotNames = $component->getAccessed()->map(fn (SlotDefinition $defn) => $defn->getConfig())->toArray();

$exportedPropTypes = collect($exports->exports['propTypes'] ?? [])
->map(fn (FieldDefinition $defn, string $key) => $defn->handle($key));

$propTypes = $props->getAccessed()
->merge($exportedPropTypes)
->map(fn (FieldDefinition $defn) => $defn->config)
->toArray();

$slotNameArray = '<'.'?php '.var_export($slotNames, true).';';
$propTypeArray = '<'.'?php '.var_export($propTypes, true).';';

$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$propTypeAst = $parser->parse($propTypeArray)[0]->expr;
$slotNameAst = $parser->parse($slotNameArray)[0]->expr;

$ast = $parser->parse(file_get_contents(__DIR__.'/../base/ComponentType.php'));

$traverser = new NodeTraverser();
$traverser->addVisitor(new class($this->handle, $viewMode.':'.$twigPath, $exports->exports, $propTypeAst, $className, $slotNameAst) extends NodeVisitorAbstract
$traverser->addVisitor(new class($this->handle, $viewMode.':'.$twigPath, [], $className) extends NodeVisitorAbstract
{
public function __construct(
protected string $handle,
protected string $twigPath,
protected array $exports,
protected $propTypes,
protected string $className,
protected $slotNames,
) {
}

Expand Down Expand Up @@ -118,16 +86,6 @@ public function enterNode(Node $node)
new Stmt\Return_(new Node\Scalar\String_($this->twigPath)),
];
}
if ($node instanceof Stmt\ClassMethod && $node->name->name === 'getFieldConfig') {
$node->stmts = [
new Stmt\Return_($this->propTypes),
];
}
if ($node instanceof Stmt\ClassMethod && $node->name->name === 'getSlotConfig') {
$node->stmts = [
new Stmt\Return_($this->slotNames),
];
}
}

public function leaveNode(Node $node)
Expand Down
12 changes: 9 additions & 3 deletions src/actions/GetComponentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
use Illuminate\Support\Collection;
use markhuot\keystone\base\ComponentType;
use markhuot\keystone\events\RegisterComponentTypes;
use markhuot\keystone\models\Component;

class GetComponentType
{
const EVENT_REGISTER_COMPONENT_TYPES = 'registerKeystoneComponentTypes';

public function __construct(
protected ?Component $context = null
) {
}

/**
* @return Collection<ComponentType>
*/
Expand All @@ -22,7 +28,7 @@ public function all(): Collection
return $event->getTwigComponents()
->mapInto(CompileTwigComponent::class)->map->handle()
->merge($event->getClassComponents())
->map(fn ($className) => new $className);
->map(fn ($className) => new $className($this->context));
}

public function byType(string $type): ComponentType
Expand All @@ -31,11 +37,11 @@ public function byType(string $type): ComponentType
Event::trigger(static::class, static::EVENT_REGISTER_COMPONENT_TYPES, $event);

if ($twigPath = $event->getTwigComponents()->get($type)) {
return new ((new CompileTwigComponent($twigPath, $type))->handle());
return new ((new CompileTwigComponent($twigPath, $type))->handle())($this->context);
}

if ($className = $event->getClassComponents()->get($type)) {
return new $className;
return new $className($this->context);
}

throw new \RuntimeException('Could not find a component type definition for '.$type);
Expand Down
86 changes: 74 additions & 12 deletions src/base/ComponentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
use craft\base\FieldInterface;
use craft\helpers\Html;
use Illuminate\Support\Collection;
use markhuot\keystone\models\Component;
use markhuot\keystone\models\ComponentData;
use markhuot\keystone\twig\Exports;
use Twig\Markup;

abstract class ComponentType
Expand All @@ -22,10 +25,21 @@ abstract class ComponentType
*/
protected string $category = 'General';

protected ?array $_exports = null;

protected array $_accessedSlots = [];

protected ?array $_schema = null;

public function __construct(
protected ?Component $context = null
) {
}

public function getName(): string
{
if ($this->name !== null) {
return $this->name;
if ($name = $this->getExport('name', $this->name)) {
return $name;
}

$parts = explode('/', $this->handle);
Expand All @@ -35,7 +49,7 @@ public function getName(): string

public function getCategory(): string
{
return $this->category;
return $this->getExport('category', $this->category);
}

public function getHandle(): string
Expand All @@ -45,7 +59,9 @@ public function getHandle(): string

public function getIcon(array $attributes = []): Markup|string
{
return new Markup(Html::modifyTagAttributes($this->icon, $attributes), 'utf-8');
$icon = $this->getExport('icon', $this->icon);

return new Markup(Html::modifyTagAttributes($icon, $attributes), 'utf-8');
}

public function render(array $variables = []): string
Expand All @@ -63,8 +79,7 @@ abstract public function getTemplatePath(): string;

public function getSlotDefinitions()
{
return collect($this->getSlotConfig())
->mapIntoSpread(SlotDefinition::class);
return $this->getSchema()[1];
}

public function getSlotDefinition(?string $slot)
Expand All @@ -79,8 +94,7 @@ public function getSlotDefinition(?string $slot)
*/
public function getFieldDefinitions(): Collection
{
return collect($this->getFieldConfig())
->mapInto(FieldDefinition::class);
return $this->getSchema()[0];
}

/**
Expand All @@ -97,12 +111,60 @@ public function getField(string $handle): ?FieldInterface
return $this->getFields()->first(fn (FieldInterface $field) => $field->handle === $handle);
}

public function hasSlots(): bool
public function getExports($dumb = false): array
{
if ($this->_exports) {
return $this->_exports;
}

$componentData = $this->context?->getProps() ?? new ComponentData;
$componentData->type = $this->getHandle();
$component = $this->context ?? new Component;
$component->populateRelation('data', $componentData);
$attributes = $component->getAttributeBag() ?? new AttributeBag;

$this->render([
'component' => $component,
'props' => $props = ($dumb ? new ComponentData : $componentData),
'attributes' => $attributes,
'exports' => $exports = new Exports,
]);

$exports = ['exports' => $exports->exports, 'props' => $props];

if ($dumb) {
return $exports;
}

return $this->_exports = $exports;
}

public function getExport(string $name, mixed $default = null)
{
return (bool) count($this->getSlotConfig());
return $this->getExports()['exports'][$name] ?? $default;
}

abstract protected function getFieldConfig(): array;
public function defineSlot(string $slotName = null): SlotDefinition
{
return $this->_accessedSlots[$slotName] ??= new SlotDefinition($this->context, $slotName);
}

abstract protected function getSlotConfig(): array;
protected function getSchema(): array
{
if ($this->_schema !== null) {
return $this->_schema;
}

['exports' => $exports, 'props' => $props] = $this->getExports(true);

$slotDefinitions = collect($this->_accessedSlots);

$exportedFieldDefinitions = collect($exports['propTypes'] ?? [])
->map(fn (FieldDefinition $defn, string $key) => $defn->handle($key));

$fieldDefinitions = $props->getAccessed()
->merge($exportedFieldDefinitions);

return $this->_schema = [$fieldDefinitions, $slotDefinitions];
}
}
56 changes: 20 additions & 36 deletions src/models/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use craft\base\ElementInterface;
use craft\base\FieldInterface;
use craft\db\ActiveQuery;
use craft\helpers\Html;
use Illuminate\Support\Collection;
use markhuot\keystone\actions\GetComponentType;
use markhuot\keystone\actions\NormalizeFieldDataForComponent;
Expand All @@ -32,16 +31,15 @@
*/
class Component extends ActiveRecord
{
/** @var array<SlotDefinition> */
protected array $accessed = [];

/** @var array<Component> */
protected ?array $slotted = null;

protected array $context = [];

protected ?Component $renderParent = null;

protected ?ComponentType $_type = null;

public static function factory(): \markhuot\keystone\factories\Component
{
return new \markhuot\keystone\factories\Component;
Expand Down Expand Up @@ -93,7 +91,21 @@ public function setType(string $type): self

public function getType(): ComponentType
{
return (new GetComponentType)->byType($this->data->type);
return $this->_type ??= (new GetComponentType($this))->byType($this->data->type);
}

public function setComponentType(ComponentType $type): self
{
$this->_type = $type;

return $this;
}

public function refresh()
{
$this->slotted = null;

return parent::refresh();
}

public function __get($name)
Expand Down Expand Up @@ -225,32 +237,9 @@ public function getAttributeBag(): AttributeBag
return new AttributeBag($this->data->getDataAttributes());
}

/**
* @return array<mixed>
*/
public function getExports(): array
{
$exports = new class
{
/** @var array<mixed> */
public array $exports = [];

public function add(mixed $key, mixed $value): void
{
$this->exports[$key] = $value;
}
};

$this->render([
'exports' => $exports,
]);

return $exports->exports;
}

public function getIcon(?array $attributes)
public function getSummary(): ?string
{
return Html::modifyTagAttributes($this->getExports()['icon'], $attributes) ?? $this->getType()->getIcon($attributes);
return $this->getType()->getExport('summary');
}

public function __toString(): string
Expand All @@ -270,14 +259,9 @@ public function isParentOf(Component $component, string $slotName = null): bool
return $this->getChildPath() === $component->path && $slotName === $component->slot;
}

public function defineSlot(string $slotName = null): SlotDefinition
{
return $this->accessed[$slotName] ??= new SlotDefinition($this, $slotName);
}

public function getSlot(string $name = null): SlotCollection
{
$this->accessed[$name] ??= new SlotDefinition($this, $name);
$this->getType()->defineSlot($name);

if ($this->slotted !== null) {
$components = collect($this->slotted)
Expand Down
2 changes: 1 addition & 1 deletion src/templates/components/elementquery.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{% export propTypes = {
search: field('\\markhuot\\keystone\\fields\\Condition')
} %}
{% set defaultSlot = component.defineSlot() %}
{% set defaultSlot = component.getType().defineSlot() %}
{% set elements = props.search.all()|default([]) %}
{% for element in elements %}
{{ defaultSlot.render({element: element}) }}
Expand Down
2 changes: 1 addition & 1 deletion src/templates/components/icon.twig
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
})))
} %}
{% set defaultIcon %}<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M69.69,62.1a6,6,0,0,0-11.38,0l-40,120A6,6,0,0,0,24,190h80a6,6,0,0,0,5.69-7.9ZM32.32,178,64,83l31.68,95ZM206,76a50,50,0,1,0-50,50A50.06,50.06,0,0,0,206,76Zm-88,0a38,38,0,1,1,38,38A38,38,0,0,1,118,76Zm106,70H136a6,6,0,0,0-6,6v56a6,6,0,0,0,6,6h88a6,6,0,0,0,6-6V152A6,6,0,0,0,224,146Zm-6,56H142V158h76Z"></path></svg>{% endset %}
{% export icon = props.icon ? svg(parseEnv('@templates/icons/' ~ props.icon)) : defaultIcon %}
{% export icon = props.icon|length > 0 ? svg(parseEnv('@templates/icons/' ~ props.icon)) : defaultIcon %}
{% if props.icon %}
{{ svg(parseEnv('@templates/icons/' ~ props.icon))|attr(attributes.toArray()) }}
{% endif %}
4 changes: 2 additions & 2 deletions src/templates/field.twig
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
<div class="foo k-hidden k-pointer-events-none" style="height:10px; width: 2rem; position: absolute; top: 0.4em; left: -20px; border-radius: 0 0 0 10px; border-width: 0 0 1px 1px; border-color: rgba(96,125,159,.25); box-shadow: -2px 2px 0 white;"></div>
{% set params = {id: child.id, elementId: child.elementId, fieldId: field.id} %}
<a class="k-text-link" href="{{ cpUrl('keystone/components/edit', params) }}" data-open-keystone-component-editor='{{ params|json_encode }}'>
<div style="background: white; display: inline-block; position: relative; padding: 4px;">{{ child.getIcon({class: 'k-w-4 k-inline'})|raw }}</div>
<div style="background: white; display: inline-block; position: relative; padding: 4px;">{{ child.getType().getIcon({class: 'k-w-4 k-inline'})|raw }}</div>
{{ child.getType().getName() }}
<span class="k-text-gray-400 [.k-collapsed_&]:k-hidden">{{ child.getExports().summary|default }}</span>
<span class="k-text-gray-400 [.k-collapsed_&]:k-hidden">{{ child.getSummary() }}</span>
</a>
<a class="k-expander k-hidden k-ml-1 [.k-collapsed_&]:k-inline-flex k-bg-gray-200 k-rounded k-w-5 k-h-3 k-text-gray-800 k-justify-center k-items-center hover:k-no-underline hover:k-bg-blue-500 hover:k-text-white" href="#" @click="$event.target.closest('li').classList.toggle('k-collapsed')">⋯</a>
{{ hiddenInput('nodes[]', child.id ~ '@' ~ child.dateUpdated) }}
Expand Down
2 changes: 1 addition & 1 deletion src/twig/SlotTokenNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function compile(\Twig\Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write('echo $context[\'component\']?->defineSlot(');
->write('echo $context[\'component\']?->getType()->defineSlot(');

$name = $this->getAttribute('name');
if ($name) {
Expand Down
Loading

0 comments on commit 0371331

Please sign in to comment.