-
-
Notifications
You must be signed in to change notification settings - Fork 364
Introduce Symfony UX Turbo #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
1fbc6b9
to
f7c6e63
Compare
/** | ||
* @author Titouan Galopin <[email protected]> | ||
* | ||
* @internal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need all those @internal
annotations, do we?
What about @experimental
also? Some have it, not all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may be a bit conservative regarding these annotations indeed :) . Other UX packages have these annotations as well, not sure what our policy should be on them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm often adding @internal
when the class should not be used in both framework and standalone usage. E.g. when someone is using this package outside the Symfony framework, they probably end up needing to initialize MercureStreamAdapter
. So I wouldn't add @internal
there.
As for @experimental
, I'm often not adding it to @internal
classes (as there is no BC promise in those anyway). Not sure if these are "Symfony policies" or only my personal policy regarding them :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @wouterj here, we should remove @internal
imho.
Maybe we can make the class final also?
@Kocal I'm not familiar with eslint, any idea why it fails on this package but not others? I may be missing required config but I don't see it |
Hi @tgalopin, I think it's because of this https://github.com/tgalopin/ux/blob/ux-turbo/package.json#L34 I don't think this glob pattern will match Can you try using |
Aah I was looking in the package, I didn't think of this file. Let me try, thanks! |
Missing LICENSE file, see #62 :) |
<a href="/messages">Back</a> | ||
|
||
<!-- End the frame --> | ||
{{ turbo_frame_end() }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this be a tag rather than a couple of _start
and _end
functions ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK, tags do things and functions display things, thus this notation. I do agree tags would probably look nicer though. Not fixed about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tags would help in IDE autocompletion (same as for 'apply' and 'endapply'), so it's less error-prone I guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have any examples of tags that output something?
forms use functions for the same reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One that is very close to this use-case is NelmioSecurityBundle's CSP tags:
{% cspscript %}
<script>
window.api_key = '{{ api_key }}';
</script>
{% endcspscript %}
(this modifies the <script>
tag to add the correct CSP digest)
Twig's {% autoescape %}
is also outputting (the escaped content). {% spaceless %}
used to do this (but has been moved to filter + apply, not sure if there is a particular reason for that). The {% apply %}
tag itself is outputting by definition (as it processes the contents using a Twig filter).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a different use case though, as it modifies the content of the tag (a bit like autoescape would). It doesn't display anything by itself.
I think I would be in favor of keeping in functions. It matches the way the rest of Symfony works (esp. forms) and it doesn't have that big of an impact on DX IMO. IDE can usually autocomplete functions as well as tags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well... what you are creating here is a particular type of block, isn't it? You always expect a start and an end so to me tags feel much more appropriate.
{{ turbo_frame_end() }} | |
{% turboframe "name" %} | |
Your stuff. | |
{% endturboframe %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, these 2 functions are about rendering some HTML opening and closing tags. they are indeed not a kind of twig block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if I forget some turbo_frame_end()
function calls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the HTML tag is not closed, and the browser may auto-close it in a different place than where you expect it to be closed.
Fixed some reviews, checking PHP 8 support |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might need a pack to install mercure-bundle + ux-turbo at the same time, with a working recipe
$config = $this->processConfiguration($configuration, $configs); | ||
|
||
// Register Turbo Streams config | ||
if (!empty($config['streams']['adapter'])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we try to avoid using empty as much as possible
} | ||
|
||
// Add Twig extension | ||
if (class_exists(Environment::class)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could use an early return statement
|
||
$rootNode | ||
->children() | ||
->arrayNode('streams') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we could remove this nesting level and rename adapter
to stream_adapter
? (same for options)
$rootNode | ||
->children() | ||
->arrayNode('streams') | ||
->info('Enable this section to use Turbo Streams') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is nothing to enable in this config :)
maybe this should be removed? I don't see similar comments elsewhere.
/** | ||
* @author Titouan Galopin <[email protected]> | ||
* | ||
* @internal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @wouterj here, we should remove @internal
imho.
Maybe we can make the class final also?
} | ||
} | ||
|
||
return trim($html).'></div>'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return substr_replace($html, '></div>', -1);
} | ||
} | ||
|
||
return trim($html).'>'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return substr_replace($html, '>', -1);
public function getFunctions(): array | ||
{ | ||
return [ | ||
new TwigFunction('turbo_stream_from', [$this, 'streamFrom'], ['needs_environment' => true, 'is_safe' => ['html']]), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turbo_stream_listen
?
{ | ||
return [ | ||
new TwigFunction('turbo_frame_start', [$this, 'startFrame'], ['needs_environment' => true, 'is_safe' => ['html']]), | ||
new TwigFunction('turbo_frame_end', [$this, 'endFrame'], ['is_safe' => ['html']]), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we don't need these helpers at all and we should tell ppl to write HTML directly?
// Let's imagine $message is the message received from the request (form, query param, ...) | ||
|
||
$publisher(new Update('chat', ' | ||
<turbo-stream action="append" target="messages"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it could be great to have a service to help generate these tags via some helper service,
the service should be able to wrap HTML content, text content or a twig template. We could take inspiration from e.g. TemplatedEmail
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep definitely agree with @nicolas-grekas here, feels bizarre to ask developers to write this while they write turbo_frame_start()
in Twig.
'symfony--ux-turbo--core' => [], | ||
]).' '; | ||
|
||
foreach ($attrs as $name => $value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we don't need attrs at all here btw?
{ | ||
$options = array_merge($this->options, ['id' => $id]); | ||
|
||
$html = '<div '; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering: should this generate an html tag, or a list of attributes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That looks fun!
// Let's imagine $message is the message received from the request (form, query param, ...) | ||
|
||
$publisher(new Update('chat', ' | ||
<turbo-stream action="append" target="messages"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep definitely agree with @nicolas-grekas here, feels bizarre to ask developers to write this while they write turbo_frame_start()
in Twig.
<a href="/messages">Back</a> | ||
|
||
<!-- End the frame --> | ||
{{ turbo_frame_end() }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well... what you are creating here is a particular type of block, isn't it? You always expect a start and an end so to me tags feel much more appropriate.
{{ turbo_frame_end() }} | |
{% turboframe "name" %} | |
Your stuff. | |
{% endturboframe %} |
|
||
{% block body %} | ||
{# This line configures Turbo to listen to the "chat" topic on Mercure #} | ||
{{ turbo_stream_from('chat') }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though I appreciate you technically want it to live within a block
in here, semantically it would make greater sense to register it within your Twig file like that:
{% listen_turbostream "chat" %}
I think this should be named |
Closing in favor of the other PR |
This PR introduces a new package to Symfony UX: UX Turbo, integrating the Hotwire Turbo library with Symfony. It is based on an initial prototype made by Kevin Dunglas (@dunglas) as a Mercure project.
Hotwire is an aggregation of several technologies developed by the Ruby on Rails ecosystem and the Bascamp company to build interactive applications with as less JavaScript code as possible. Stimulus, as the foundation of Symfony UX, is already well integrated into Symfony. This PR goes to the next step: integrating Turbo.
You can learn more about the Hotwire intiative on https://twitter.com/dhh/status/1341420143239450624. You can also read the README in this PR to better understand the usage in a Symfony application.
TLDR: Turbo is a generic way to build reactive applications (inline form edition, DOM changes without reload, real-time display of chat messages, etc.) without the need to develop any JavaScript (or at least as little as possible). This has two big advantages: it reduces dramatically the time necessary to build a reactive app and it eliminates in many places the need for a client-side state which could be a source of issues.
This PR is loosely based on the equivalent gem in Ruby that integrates Turbo in RoR. It has however a few key differences:
stimulus_controller
helper introduced a few days ago in Webpack Encore