Skip to content

Commit

Permalink
Merge pull request #16 from enflow/livewireV3DotNotation
Browse files Browse the repository at this point in the history
  • Loading branch information
mbardelmeijer authored Sep 27, 2023
2 parents 6a25c7e + 702e889 commit a7dad7f
Show file tree
Hide file tree
Showing 25 changed files with 366 additions and 99 deletions.
44 changes: 36 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@

The `enflow/livewire-twig` package provides the option to load Livewire components in your Twig templates.

## Versions

### <= 3.x.x
Version 3.x.x supports Livewire 2.

### >= 4.x.x
Version 4.xxx supports Livewire 3.

This version changes from the `{% livewire.component test %}` syntax to the `{% livewire.component 'test' %}` syntax.

The name argument for {% livewire.component %} and the other directives is now interpreted as an expression, allowing the use of variables or Twig expressions as a name. Note that for this reason a constant name now must be enclosed in quotes.

## Installation
You can install the package via composer:

Expand All @@ -17,12 +29,10 @@ composer require enflow/livewire-twig
The Twig extension will automatically register when `rcrowe/twigbridge` is used.
If you're using another configuration, you may wish to register the extension manually by loading the extension `Enflow\LivewireTwig\LivewireExtension`.

This package only provides a wrapper for the `@livewireScripts`, `@livewireStyles` & `@livewire` calls. Everything else under the hood is powered by `livewire/livewire`.
This package provides wrappers for the `@livewireScripts`, `@livewireStyles`, `@livewireScriptConfig`, `@livewire`, `@entangle`, `@this` and `@persist`, directives. Everything else under the hood is powered by `livewire/livewire`.
You can register your Livewire components like normal.

## Installation

Add the following tags in the `head` tag, and before the end `body` tag in your template.
To use Livewire, add the following tags in the `head` tag, and before the end `body` tag in your template.

```twig
<html>
Expand All @@ -41,13 +51,32 @@ In your body you may include the component like:

```twig
{# The Twig version of '@livewire' #}
{% livewire counter %}
{% livewire.component 'counter' %}
{# If you wish to pass along variables to your component #}
{% livewire counter with {'count': 3} %}
{% livewire.component 'counter' with {'count': 3} %}
{# To include a nested component (or dashes), you need to use '' #}
{% livewire 'nested.component' %}
{% livewire.component 'nested.component' %}
{# To use key tracking, you need to use key(<expression>) #}
{% livewire.component 'counter' key('counterkey') %}
{# The Twig version of '@persist' #}
{% livewire.persist 'name' %}
<div>
...
</div>
{% livewire.endpersist %}
{# The Twig version of '@entangle' (as of Livewire 3.01 poorly documented, need to check the source code) #}
{% livewire.entangle 'expression' %}
{# The Twig version of '@this' (Can only be used after Livewire initialization is complete) #}
{% livewire.this %}
{# The Twig version of '@livewireScriptConfig' (as of Livewire 3.01 poorly documented, need to check the source code) #}
{{ livewireScriptConfig(<options>) }}
```

### Example
Expand Down Expand Up @@ -86,7 +115,6 @@ class Counter extends Component
```

## Todo
- [ ] Implement support for `key` tracking
- [ ] Moar tests.

## Testing
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
],
"require": {
"php": "^8.2",
"livewire/livewire": "^2.2"
"livewire/livewire": "^3.0"
},
"require-dev": {
"laravel/pint": "^1.0",
"orchestra/testbench": "^8.0",
"phpunit/phpunit": "^10.0",
"phpunit/phpunit": "^10.1",
"rcrowe/twigbridge": "^0.14.1"
},
"suggest": {
Expand Down
13 changes: 7 additions & 6 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" backupGlobals="false" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
</coverage>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" backupGlobals="false" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<coverage/>
<testsuites>
<testsuite name="enflow/livewire-twig">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src/</directory>
</include>
</source>
</phpunit>
27 changes: 22 additions & 5 deletions src/LivewireExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,45 @@

namespace Enflow\LivewireTwig;

use Livewire\Livewire;
use Illuminate\Support\Facades\Blade;
use Livewire\Mechanisms\FrontendAssets\FrontendAssets;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class LivewireExtension extends AbstractExtension
{
public function callDirective(string $directive, array $args = []): string
{
$directives = Blade::getCustomDirectives();
$call = $directives[$directive] ?? null;

$r = call_user_func_array($call, $args);

return "?> $r <?php\n";
}

public function getFunctions(): array
{
return [
new TwigFunction('livewireStyles', [$this, 'livewireStyles'], ['is_safe' => ['html']]),
new TwigFunction('livewireScripts', [$this, 'livewireScripts'], ['is_safe' => ['html']]),
new TwigFunction('livewireScriptConfig', [$this, 'livewireScriptConfig'], ['is_safe' => ['html']]),
];
}

public function livewireStyles()
public function livewireStyles($args = ''): string
{
return FrontendAssets::styles($args);
}

public function livewireScripts($args = ''): string
{
return Livewire::styles();
return FrontendAssets::scripts($args);
}

public function livewireScripts()
public function livewireScriptConfig($args = []): string
{
return Livewire::scripts();
return FrontendAssets::scriptConfig($args);
}

public function getTokenParsers(): array
Expand Down
29 changes: 0 additions & 29 deletions src/LivewireNode.php

This file was deleted.

119 changes: 90 additions & 29 deletions src/LivewireTokenParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,104 @@

namespace Enflow\LivewireTwig;

use Illuminate\Support\Str;
use Enflow\LivewireTwig\Nodes\EntangleNode;
use Enflow\LivewireTwig\Nodes\LivewireNode;
use Enflow\LivewireTwig\Nodes\PersistNode;
use Enflow\LivewireTwig\Nodes\ThisNode;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;

class LivewireTokenParser extends AbstractTokenParser
{
public function parse(Token $token): LivewireNode
protected array $types = [
'this',
'entangle',
'persist',
'endpersist',
'component',
];

public function parse(Token $token)
{
$componentNameToken = $this->parser->getStream()->next();

if ($componentNameToken->test(Token::NAME_TYPE) || $componentNameToken->test(Token::STRING_TYPE)) {
$component = $componentNameToken->getValue();
} else {
throw new SyntaxError(
sprintf(
'Unexpected token "%s"%s ("%s" or "%s" expected).',
Token::typeToEnglish($componentNameToken->getType()),
$componentNameToken->getValue() ? sprintf(' of value "%s"', $componentNameToken->getValue()) : '',
Token::typeToEnglish(Token::NAME_TYPE),
Token::typeToEnglish(Token::STRING_TYPE)
),
$componentNameToken->getLine(),
$this->parser->getStream()->getSourceContext()
);
}

if ($this->parser->getStream()->nextIf(/* Token::NAME_TYPE */ 5, 'with')) {
$variables = $this->parser->getExpressionParser()->parseExpression();
} else {
$variables = new ArrayExpression([], $token->getLine());
}

$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);

return new LivewireNode(Str::kebab($component), $variables, $token->getLine(), $this->getTag());
$lineno = $token->getLine();
$stream = $this->parser->getStream();

// Expect the '.' after `livewire`
$stream->expect(Token::PUNCTUATION_TYPE, '.');

// Detect the type after the dot
$type = $stream->expect(Token::NAME_TYPE)->getValue();

// Go into the type specific logic
return (match ($type) {
'this' => function () use ($stream, $lineno) {
$stream->expect(Token::BLOCK_END_TYPE); // Expect the end block

return new ThisNode([], [], $lineno, $this->getTag());
},

'entangle' => function () use ($stream, $lineno) {
$entVar = $this->parser->getExpressionParser()->parseExpression();
$stream->expect(Token::BLOCK_END_TYPE);

return new EntangleNode(['EntangleValue' => $entVar], [], $lineno, $this->getTag());
},

'persist' => function () use ($stream, $lineno) {
// Parse the name.
$name = $this->parser->getExpressionParser()->parseExpression();

// Expect the end block for the start tag.
$stream->expect(Token::BLOCK_END_TYPE);

// Parse the body until the 'endpersist' tag.
$body = $this->parser->subparse(fn (Token $token) => $token->test('livewire'), true);

// Now, since we know we're at a 'livewire.' token, we should expect the 'endpersist' after it.
$stream->expect(Token::PUNCTUATION_TYPE, '.');

// Now, we should expect the 'endpersist' name.
$stream->expect(Token::NAME_TYPE, 'endpersist');

// We can expect the closing block now: `%}`
$stream->expect(Token::BLOCK_END_TYPE);

return new PersistNode([$body], ['name' => $name], $lineno, $this->getTag());
},

'component' => function () use ($stream, $lineno) {
// Proceed with parsing the livewire.component
$component = $this->parser->getExpressionParser()->parseExpression();

$variables = new ArrayExpression([], $lineno);
$key = new ConstantExpression('', $lineno);

while (! $stream->test(Token::BLOCK_END_TYPE)) {
if ($stream->test(Token::NAME_TYPE, 'with')) {
$stream->next(); // Consume the 'with' token
$variables = $this->parser->getExpressionParser()->parseExpression();
} elseif ($stream->test(Token::NAME_TYPE, 'key')) {
$stream->next(); // Consume the 'key' token
$stream->expect(Token::PUNCTUATION_TYPE, '(');
$key = $this->parser->getExpressionParser()->parseExpression();
$stream->expect(Token::PUNCTUATION_TYPE, ')');
} else {
throw new SyntaxError(sprintf('Unexpected token in livewire tag. Twig was expecting the end of the directive starting at line %d).', $lineno), $lineno, $stream->getSourceContext());
}
}

$stream->expect(Token::BLOCK_END_TYPE); // Expect the end block

$attrs = ['variables' => $variables, 'key' => $key];

return new LivewireNode($component, $attrs, $lineno, $this->getTag());
},

default => fn () => throw new SyntaxError(sprintf('Unexpected token after "livewire.". Expected %s but got "%s".', implode(' or ', $this->types), $type), $lineno, $stream->getSourceContext()),
})();
}

public function getTag(): string
Expand Down
2 changes: 1 addition & 1 deletion src/LivewireTwigServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

class LivewireTwigServiceProvider extends ServiceProvider
{
public function boot()
public function boot(): void
{
$this->app->afterResolving('twig', fn () => $this->app['twig']->addExtension(new LivewireExtension()));
}
Expand Down
21 changes: 21 additions & 0 deletions src/Nodes/EntangleNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Enflow\LivewireTwig\Nodes;

use Twig\Compiler;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Node;

class EntangleNode extends Node
{
public function compile(Compiler $compiler)
{
$livewire = new NameExpression('__livewire', $this->lineno);

$compiler
->write('$__livewire = ')->subcompile($livewire)->raw(";\n")
->write('$expression = ')->subcompile($this->getNode('EntangleValue'))->raw(";\n")
->write("\$instance_id = \"window.Livewire.find('{\$__livewire->getId()}')\";\n")
->write("if ((object) (\$expression) instanceof \\Livewire\\WireDirective) echo \"\$instance_id.entangle(\$expression->value()).\$expression->hasModifier('live') ? '.live' : ''\"; else echo \"\$instance_id.entangle('\$expression')\";")->raw("\n");
}
}
39 changes: 39 additions & 0 deletions src/Nodes/LivewireNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Enflow\LivewireTwig\Nodes;

use Enflow\LivewireTwig\LivewireExtension;
use Twig\Compiler;
use Twig\Node\Node;

class LivewireNode extends Node
{
public function __construct(Node $component, array $attributes, int $lineno, string $tag = null)
{
$nodes = ['variables' => $attributes['variables'], 'key' => $attributes['key']];
parent::__construct($nodes, ['component' => $component], $lineno, $tag);
}

public function compile(Compiler $compiler)
{
$ext = $compiler->getEnvironment()->getExtension(LivewireExtension::class);

$component = $this->getAttribute('component');
$expr = $this->getNode('variables');
$key = $this->getNode('key');
$hasKey = ! $key->hasAttribute('value') || $key->getAttribute('value') !== '';

$compiler
->write('$_name = ')->subcompile($component)->raw(";\n")
->write('$_vars = ')->subcompile($expr)->raw(";\n");

if ($hasKey) {
$compiler
->write('$_key = ')->subcompile($key)->raw(";\n")
->write($ext->callDirective('livewire', ['$_name, $_vars, key($_key)']));
} else {
$compiler
->write($ext->callDirective('livewire', ['$_name, $_vars']));
}
}
}
Loading

0 comments on commit a7dad7f

Please sign in to comment.