Skip to content
This repository has been archived by the owner on Feb 27, 2024. It is now read-only.

Commit

Permalink
Support for raw JSON attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalbaljet committed Dec 21, 2023
1 parent 8338a72 commit 18b17da
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 31 deletions.
2 changes: 2 additions & 0 deletions app/app/View/Components/ToVueProp.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
use ProtoneMedia\SpladeCore\Attributes\VueProp;
use ProtoneMedia\SpladeCore\Attributes\VuePropRaw;
use Psr\SimpleCache\CacheInterface;

class ToVueProp extends Component
Expand Down Expand Up @@ -39,6 +40,7 @@ public function __construct(
#[VueProp] public array $defaultArray = ['foo'],
#[VueProp] public array|bool|string $multipleTypes = ['foo'],
#[VueProp(as: 'renamed')] public string $name = 'renamed-foo',
#[VuePropRaw] public string $json = '{"foo":"bar"}',
) {
}

Expand Down
37 changes: 19 additions & 18 deletions app/resources/views/components/to-vue-prop.blade.php
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
<script setup></script>
<div>
<p>Mixed: <span v-text="mixed" /></p>
<p>String: <span v-text="string" /></p>
<p>Default String: <span v-text="defaultString" /></p>
<p>Nullable String: <span v-text="nullableString" /></p>
<p>Int: <span v-text="int" /></p>
<p>Bool: <span v-text="bool" /></p>
<p>Array: <span v-text="array" /></p>
<p>Object: <span v-text="object" /></p>
<p>Nullable Int: <span v-text="nullableInt" /></p>
<p>Nullable Bool: <span v-text="nullableBool" /></p>
<p>Nullable Array: <span v-text="nullableArray" /></p>
<p>Nullable Object: <span v-text="nullableObject" /></p>
<p>Default Int: <span v-text="defaultInt" /></p>
<p>Default Bool: <span v-text="defaultBool" /></p>
<p>Default Array: <span v-text="defaultArray" /></p>
<p>Multiple Types: <span v-text="multipleTypes" /></p>
<p>Data From Method: <span v-text="dataFromMethod" /></p>
<p>Renamed: <span v-text="renamed" /></p>
<p>Mixed: <span v-text="mixed" /> (<span v-text="typeof mixed" />)</p>
<p>String: <span v-text="string" /> (<span v-text="typeof string" />)</p>
<p>Default String: <span v-text="defaultString" /> (<span v-text="typeof defaultString" />)</p>
<p>Nullable String: <span v-text="nullableString" /> (<span v-text="typeof nullableString" />)</p>
<p>Int: <span v-text="int" /> (<span v-text="typeof int" />)</p>
<p>Bool: <span v-text="bool" /> (<span v-text="typeof bool" />)</p>
<p>Array: <span v-text="array" /> (<span v-text="typeof array" />)</p>
<p>Object: <span v-text="object" /> (<span v-text="typeof object" />)</p>
<p>Nullable Int: <span v-text="nullableInt" /> (<span v-text="typeof nullableInt" />)</p>
<p>Nullable Bool: <span v-text="nullableBool" /> (<span v-text="typeof nullableBool" />)</p>
<p>Nullable Array: <span v-text="nullableArray" /> (<span v-text="typeof nullableArray" />)</p>
<p>Nullable Object: <span v-text="nullableObject" /> (<span v-text="typeof nullableObject" />)</p>
<p>Default Int: <span v-text="defaultInt" /> (<span v-text="typeof defaultInt" />)</p>
<p>Default Bool: <span v-text="defaultBool" /> (<span v-text="typeof defaultBool" />)</p>
<p>Default Array: <span v-text="defaultArray" /> (<span v-text="typeof defaultArray" />)</p>
<p>Multiple Types: <span v-text="multipleTypes" /> (<span v-text="typeof multipleTypes" />)</p>
<p>Data From Method: <span v-text="dataFromMethod" /> (<span v-text="typeof dataFromMethod" />)</p>
<p>Renamed: <span v-text="renamed" /> (<span v-text="typeof renamed" />)</p>
<p>JSON: <span v-text="json" /> (<span v-text="typeof json" />)</p>
</div>
3 changes: 2 additions & 1 deletion app/tests/Browser/ToVuePropTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public function it_can_pass_blade_props_as_vue_rops()
->assertSee('Default Array: [ "foo" ]')
->assertSee('Multiple Types: [ "foo" ]')
->assertSee('Data From Method: [ "foo", "bar", "baz" ]')
->assertSee('Renamed: renamed-foo');
->assertSee('Renamed: renamed-foo')
->assertSee('JSON: { "foo": "bar" } (object)');
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const props = defineProps({
defaultArray: { type: Array, default: JSON.parse('[\u0022foo\u0022]') },
multipleTypes: { type: [Array, String, Boolean], default: JSON.parse('[\u0022foo\u0022]') },
renamed: { type: String, default: 'renamed-foo' },
json: { default: JSON.parse('{\u0022foo\u0022:\u0022bar\u0022}') },
dataFromMethod: { type: Array, default: null },
})
const spladeRender = h({
Expand Down
8 changes: 8 additions & 0 deletions src/Attributes/VuePropRaw.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace ProtoneMedia\SpladeCore\Attributes;

#[Attribute]
class VuePropRaw extends VueProp
{
}
12 changes: 10 additions & 2 deletions src/BladeViewExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Support\Str;
use InvalidArgumentException;
use ProtoneMedia\SpladeCore\Facades\SpladePlugin;
use ProtoneMedia\SpladeCore\View\Factory;

class BladeViewExtractor
{
Expand Down Expand Up @@ -320,8 +321,15 @@ protected function extractDefinePropsFromScript(): array
{
$bladePropsAsVueProps = Collection::make($this->getBladePropsThatArePassedAsVueProps())
->map(function (object $specs) {
$type = is_array($specs->type) ? '['.implode(',', $specs->type).']' : "{$specs->type}";
$default = Js::from($specs->default)->toHtml();
$default = $specs->raw
? Factory::convertJsonToJavaScriptExpression($specs->default)
: Js::from($specs->default)->toHtml();

$type = null;

if (! $specs->raw) {
$type = is_array($specs->type) ? '['.implode(',', $specs->type).']' : "{$specs->type}";
}

return $type
? "{type: {$type}, default: {$default}}"
Expand Down
49 changes: 40 additions & 9 deletions src/ComponentSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
use JsonSerializable;
use ProtoneMedia\SpladeCore\Attributes\Vue;
use ProtoneMedia\SpladeCore\Attributes\VueProp;
use ProtoneMedia\SpladeCore\Attributes\VuePropRaw;
use ProtoneMedia\SpladeCore\Attributes\VueRef;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
Expand Down Expand Up @@ -163,6 +165,23 @@ public static function getDataFromComponentClass(string $componentClass): array
return $values;
}

private static function getVuePropAttribute(ReflectionProperty|ReflectionMethod $propertyOrMethod): ?ReflectionAttribute
{
$prop = $propertyOrMethod->getAttributes(VueProp::class);

if (! empty($prop)) {
return $prop[0];
}

$propRaw = $propertyOrMethod->getAttributes(VuePropRaw::class);

if (! empty($propRaw)) {
return $propRaw[0];
}

return null;
}

/**
* Same as getPropsFromComponent() but for a component class, so
* without any values.
Expand All @@ -186,11 +205,13 @@ public static function getPropsFromComponentClass(string $componentClass): array
continue;
}

if (empty($vuePropAttributes = $property->getAttributes(VueProp::class))) {
$vuePropAttribute = static::getVuePropAttribute($property);

if (! $vuePropAttribute) {
continue;
}

$as = $vuePropAttributes[0]->getArguments()['as'] ?? $name;
$as = $vuePropAttribute->getArguments()['as'] ?? $name;

$defaultValue = $property->getDefaultValue();

Expand All @@ -202,6 +223,7 @@ public static function getPropsFromComponentClass(string $componentClass): array

$values[$as] = (object) [
'default' => $defaultValue,
'raw' => $vuePropAttribute->getName() === VuePropRaw::class,
'type' => static::mapTypeToVueType($property->getType()),
'value' => null,
];
Expand All @@ -222,14 +244,17 @@ public static function getPropsFromComponentClass(string $componentClass): array
continue;
}

if (empty($vuePropAttributes = $method->getAttributes(VueProp::class))) {
$vuePropAttribute = static::getVuePropAttribute($method);

if (! $vuePropAttribute) {
continue;
}

$as = $vuePropAttributes[0]->getArguments()['as'] ?? $name;
$as = $vuePropAttribute->getArguments()['as'] ?? $name;

$values[$as] = (object) [
'default' => null,
'raw' => $vuePropAttribute->getName() === VuePropRaw::class,
'type' => static::mapTypeToVueType($method->getReturnType()),
'value' => null,
];
Expand Down Expand Up @@ -260,11 +285,13 @@ public function getPropsFromComponent(): array
continue;
}

if (empty($vuePropAttributes = $property->getAttributes(VueProp::class))) {
$vuePropAttribute = static::getVuePropAttribute($property);

if (! $vuePropAttribute) {
continue;
}

$as = $vuePropAttributes[0]->getArguments()['as'] ?? $name;
$as = $vuePropAttribute->getArguments()['as'] ?? $name;

$value = $property->isInitialized($this->component)
? $property->getValue($this->component)
Expand Down Expand Up @@ -301,6 +328,7 @@ public function getPropsFromComponent(): array

$values[$as] = (object) [
'default' => $defaultValue,
'raw' => $vuePropAttribute->getName() === VuePropRaw::class,
'type' => static::mapTypeToVueType($property->getType()),
'value' => $this->getSerializedPropertyValue($value),
];
Expand All @@ -321,14 +349,17 @@ public function getPropsFromComponent(): array
continue;
}

if (empty($vuePropAttributes = $method->getAttributes(VueProp::class))) {
$vuePropAttribute = static::getVuePropAttribute($method);

if (! $vuePropAttribute) {
continue;
}

$as = $vuePropAttributes[0]->getArguments()['as'] ?? $name;
$as = $vuePropAttribute->getArguments()['as'] ?? $name;

$values[$as] = (object) [
'default' => null,
'raw' => $vuePropAttribute->getName() === VuePropRaw::class,
'type' => static::mapTypeToVueType($method->getReturnType()),
'value' => Container::getInstance()->call([$this->component, $name]),
];
Expand All @@ -340,7 +371,7 @@ public function getPropsFromComponent(): array
/**
* Maps a PHP type to a Vue type.
*/
public static function mapTypeToVueType(?ReflectionType $type = null): array|string|null
public static function mapTypeToVueType(ReflectionType $type = null): array|string|null
{
if ($type instanceof \ReflectionUnionType) {
$types = collect($type->getTypes())
Expand Down
28 changes: 27 additions & 1 deletion src/View/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

class Factory extends BaseFactory
{
const REQUIRED_JSON_FLAGS = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;

/**
* Whether to track Splade components.
*/
Expand Down Expand Up @@ -93,6 +95,28 @@ public function pushSpladeTemplate($id, $value): void
);
}

/**
* Convert the given JSON to a JavaScript expression.
*
* @param string $json
* @param int $flags
* @return string
*
* @throws \JsonException
*/
public static function convertJsonToJavaScriptExpression($json, $flags = 0)
{
if ($json === '[]' || $json === '{}') {
return $json;
}

if (Str::startsWith($json, ['"', '{', '['])) {
return "JSON.parse('".substr(json_encode($json, $flags | static::REQUIRED_JSON_FLAGS), 1, -1)."')";
}

return $json;
}

/**
* Temporarily store the passed attributes, render the component and
* push it to the Splade templates stack. Then return a generic Vue
Expand Down Expand Up @@ -139,7 +163,9 @@ public function renderComponent()
$key = 'v-bind:'.Str::kebab($key);
}

$attributes[$key] = Js::from($specs->value)->toHtml();
$attributes[$key] = $specs->raw
? static::convertJsonToJavaScriptExpression($specs->value)
: Js::from($specs->value)->toHtml();
});

$attrs = $attributes->toHtml();
Expand Down

0 comments on commit 18b17da

Please sign in to comment.