Skip to content

Commit f66c103

Browse files
committed
add dynamic component
1 parent 7e94e5e commit f66c103

File tree

7 files changed

+238
-1
lines changed

7 files changed

+238
-1
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<?php
2+
3+
namespace Illuminate\View;
4+
5+
use Illuminate\Container\Container;
6+
use Illuminate\Support\Str;
7+
use Illuminate\View\Compilers\ComponentTagCompiler;
8+
9+
class DynamicComponent extends Component
10+
{
11+
/**
12+
* The name of the component.
13+
*
14+
* @var string
15+
*/
16+
public $component;
17+
18+
/**
19+
* The component tag compiler instance.
20+
*
21+
* @var \Illuminate\View\Compilers\BladeTagCompiler
22+
*/
23+
protected static $compiler;
24+
25+
/**
26+
* The cached component classes.
27+
*
28+
* @var array
29+
*/
30+
protected static $componentClasses = [];
31+
32+
/**
33+
* The cached binding keys for component classes.
34+
*
35+
* @var array
36+
*/
37+
protected static $bindings = [];
38+
39+
/**
40+
* Create a new component instance.
41+
*
42+
* @return void
43+
*/
44+
public function __construct(string $component)
45+
{
46+
$this->component = $component;
47+
}
48+
49+
/**
50+
* Get the view / contents that represent the component.
51+
*
52+
* @return \Illuminate\View\View|string
53+
*/
54+
public function render()
55+
{
56+
$template = <<<'EOF'
57+
<?php extract(collect($attributes->getAttributes())->mapWithKeys(function ($value, $key) { return [Illuminate\Support\Str::camel($key) => $value]; })->all(), EXTR_SKIP); ?>
58+
{{ props }}
59+
<x-{{ component }} {{ bindings }} {{ attributes }}>
60+
{{ slots }}
61+
{{ defaultSlot }}
62+
</x-{{ component }}>
63+
EOF;
64+
65+
return function ($data) use ($template) {
66+
$bindings = $this->bindings($class = $this->classForComponent());
67+
68+
return str_replace(
69+
[
70+
'{{ component }}',
71+
'{{ props }}',
72+
'{{ bindings }}',
73+
'{{ attributes }}',
74+
'{{ slots }}',
75+
'{{ defaultSlot }}',
76+
],
77+
[
78+
$this->component,
79+
$this->compileProps($bindings),
80+
$this->compileBindings($bindings),
81+
class_exists($class) ? '{{ $attributes }}' : '',
82+
$this->compileSlots($data['__laravel_slots']),
83+
'{{ $slot ?? "" }}',
84+
],
85+
$template
86+
);
87+
};
88+
}
89+
90+
/**
91+
* Compile the @props directive for the component.
92+
*
93+
* @param array $bindings
94+
* @return string
95+
*/
96+
protected function compileProps(array $bindings)
97+
{
98+
if (empty($bindings)) {
99+
return '';
100+
}
101+
102+
return '@props('.'[\''.implode('\',\'', collect($bindings)->map(function ($dataKey) {
103+
return Str::camel($dataKey);
104+
})->all()).'\']'.')';
105+
}
106+
107+
/**
108+
* Compile the bindings for the component.
109+
*
110+
* @param array $bindings
111+
* @return string
112+
*/
113+
protected function compileBindings(array $bindings)
114+
{
115+
return collect($bindings)->map(function ($key) {
116+
return ':'.$key.'="$'.Str::camel($key).'"';
117+
})->implode(' ');
118+
}
119+
120+
/**
121+
* Compile the slots for the component.
122+
*
123+
* @param array $slots
124+
* @return string
125+
*/
126+
protected function compileSlots(array $slots)
127+
{
128+
return collect($slots)->map(function ($slot, $name) {
129+
return $name === '__default' ? null : '<x-slot name="'.$name.'">{{ $'.$name.' }}</x-slot>';
130+
})->filter()->implode(PHP_EOL);
131+
}
132+
133+
/**
134+
* Get the class for the current component.
135+
*
136+
* @return string
137+
*/
138+
protected function classForComponent()
139+
{
140+
if (isset(static::$componentClasses[$this->component])) {
141+
return static::$componentClasses[$this->component];
142+
}
143+
144+
return static::$componentClasses[$this->component] =
145+
$this->compiler()->componentClass($this->component);
146+
}
147+
148+
/**
149+
* Get the names of the variables that should be bound to the component.
150+
*
151+
* @param string $class
152+
* @return array
153+
*/
154+
protected function bindings(string $class)
155+
{
156+
if (! isset(static::$bindings[$class])) {
157+
[$data, $attributes] = $this->compiler()->partitionDataAndAttributes($class, $this->attributes->getAttributes());
158+
159+
static::$bindings[$class] = array_keys($data->all());
160+
}
161+
162+
return static::$bindings[$class];
163+
}
164+
165+
/**
166+
* Get an instance of the Blade tag compiler.
167+
*
168+
* @return \Illuminate\View\Compilers\ComponentTagCompiler
169+
*/
170+
protected function compiler()
171+
{
172+
if (! static::$compiler) {
173+
static::$compiler = new ComponentTagCompiler(
174+
Container::getInstance()->make('blade.compiler')->getClassComponentAliases(),
175+
Container::getInstance()->make('blade.compiler')
176+
);
177+
}
178+
179+
return static::$compiler;
180+
}
181+
}

src/Illuminate/View/ViewServiceProvider.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ public function registerViewFinder()
8888
public function registerBladeCompiler()
8989
{
9090
$this->app->singleton('blade.compiler', function ($app) {
91-
return new BladeCompiler($app['files'], $app['config']['view.compiled']);
91+
return tap(new BladeCompiler($app['files'], $app['config']['view.compiled']), function ($blade) {
92+
$blade->component('dynamic-component', DynamicComponent::class);
93+
});
9294
});
9395
}
9496

tests/Integration/View/BladeTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\View;
4+
5+
use Illuminate\Support\Facades\View;
6+
use Orchestra\Testbench\TestCase;
7+
8+
/**
9+
* @group integration
10+
*/
11+
class BladeTest extends TestCase
12+
{
13+
public function test_basic_blade_rendering()
14+
{
15+
$view = View::make('hello', ['name' => 'Taylor'])->render();
16+
17+
$this->assertEquals('Hello Taylor', trim($view));
18+
}
19+
20+
public function test_rendering_a_component()
21+
{
22+
$view = View::make('uses-panel', ['name' => 'Taylor'])->render();
23+
24+
$this->assertEquals('<div class="ml-2">
25+
Hello Taylor
26+
</div>', trim($view));
27+
}
28+
29+
public function test_rendering_a_dynamic_component()
30+
{
31+
$view = View::make('uses-panel-dynamically', ['name' => 'Taylor'])->render();
32+
33+
$this->assertEquals('<div class="ml-2">
34+
Hello Taylor
35+
</div>', trim($view));
36+
}
37+
38+
protected function getEnvironmentSetUp($app)
39+
{
40+
$app['config']->set('view.paths', [__DIR__.'/templates']);
41+
}
42+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@props(['name'])
2+
3+
<div {{ $attributes }}>
4+
Hello {{ $name }}
5+
</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello {{ $name }}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<x-dynamic-component component="panel" class="ml-2" :name="$name">
2+
Panel contents
3+
</x-dynamic-component>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<x-panel class="ml-2" :name="$name">
2+
Panel contents
3+
</x-panel>

0 commit comments

Comments
 (0)