From 18b17da4e97cead97d0cc1f82c44ebf201a245f6 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Thu, 21 Dec 2023 15:00:19 +0100 Subject: [PATCH 1/2] Support for raw JSON attributes --- app/app/View/Components/ToVueProp.php | 2 + .../views/components/to-vue-prop.blade.php | 37 +++++++------- app/tests/Browser/ToVuePropTest.php | 3 +- ...entsTest__it_builds_the_components__12.vue | 1 + src/Attributes/VuePropRaw.php | 8 +++ src/BladeViewExtractor.php | 12 ++++- src/ComponentSerializer.php | 49 +++++++++++++++---- src/View/Factory.php | 28 ++++++++++- 8 files changed, 109 insertions(+), 31 deletions(-) create mode 100644 src/Attributes/VuePropRaw.php diff --git a/app/app/View/Components/ToVueProp.php b/app/app/View/Components/ToVueProp.php index a6a2ff8..fefbf0e 100644 --- a/app/app/View/Components/ToVueProp.php +++ b/app/app/View/Components/ToVueProp.php @@ -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 @@ -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"}', ) { } diff --git a/app/resources/views/components/to-vue-prop.blade.php b/app/resources/views/components/to-vue-prop.blade.php index dbc95ba..1586468 100644 --- a/app/resources/views/components/to-vue-prop.blade.php +++ b/app/resources/views/components/to-vue-prop.blade.php @@ -1,21 +1,22 @@
-

Mixed:

-

String:

-

Default String:

-

Nullable String:

-

Int:

-

Bool:

-

Array:

-

Object:

-

Nullable Int:

-

Nullable Bool:

-

Nullable Array:

-

Nullable Object:

-

Default Int:

-

Default Bool:

-

Default Array:

-

Multiple Types:

-

Data From Method:

-

Renamed:

+

Mixed: ()

+

String: ()

+

Default String: ()

+

Nullable String: ()

+

Int: ()

+

Bool: ()

+

Array: ()

+

Object: ()

+

Nullable Int: ()

+

Nullable Bool: ()

+

Nullable Array: ()

+

Nullable Object: ()

+

Default Int: ()

+

Default Bool: ()

+

Default Array: ()

+

Multiple Types: ()

+

Data From Method: ()

+

Renamed: ()

+

JSON: ()

\ No newline at end of file diff --git a/app/tests/Browser/ToVuePropTest.php b/app/tests/Browser/ToVuePropTest.php index 0d60ad2..48658fe 100644 --- a/app/tests/Browser/ToVuePropTest.php +++ b/app/tests/Browser/ToVuePropTest.php @@ -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)'); }); } } diff --git a/app/tests/Unit/Console/__snapshots__/BuildComponentsTest__it_builds_the_components__12.vue b/app/tests/Unit/Console/__snapshots__/BuildComponentsTest__it_builds_the_components__12.vue index 6a93a18..a97b729 100644 --- a/app/tests/Unit/Console/__snapshots__/BuildComponentsTest__it_builds_the_components__12.vue +++ b/app/tests/Unit/Console/__snapshots__/BuildComponentsTest__it_builds_the_components__12.vue @@ -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({ diff --git a/src/Attributes/VuePropRaw.php b/src/Attributes/VuePropRaw.php new file mode 100644 index 0000000..f5e807f --- /dev/null +++ b/src/Attributes/VuePropRaw.php @@ -0,0 +1,8 @@ +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}}" diff --git a/src/ComponentSerializer.php b/src/ComponentSerializer.php index 104e499..df4f5c1 100644 --- a/src/ComponentSerializer.php +++ b/src/ComponentSerializer.php @@ -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; @@ -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. @@ -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(); @@ -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, ]; @@ -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, ]; @@ -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) @@ -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), ]; @@ -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]), ]; @@ -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()) diff --git a/src/View/Factory.php b/src/View/Factory.php index bc63fc5..eb6826e 100644 --- a/src/View/Factory.php +++ b/src/View/Factory.php @@ -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. */ @@ -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 @@ -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(); From dfd486507909a302716bcd4af2dd139ca6e1538d Mon Sep 17 00:00:00 2001 From: pascalbaljet Date: Thu, 21 Dec 2023 14:00:46 +0000 Subject: [PATCH 2/2] Fix styling --- src/ComponentSerializer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ComponentSerializer.php b/src/ComponentSerializer.php index df4f5c1..3edccbc 100644 --- a/src/ComponentSerializer.php +++ b/src/ComponentSerializer.php @@ -371,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())