-
-
Notifications
You must be signed in to change notification settings - Fork 340
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
[LiveComponent] Allow binding LiveProp to URL query parameter #1230
[LiveComponent] Allow binding LiveProp to URL query parameter #1230
Conversation
e25e877
to
42998d4
Compare
Nice work here! I'm curious how multiple components using this feature would behave. What about multiple of the same component? |
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.
SO excited for this! I'll do a more thorough review tomorrow
|
||
$data = $event->getData(); | ||
|
||
$request = $this->requestStack->getCurrentRequest(); |
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.
Should this be getMainRequest()
? It would only make a difference if this component were being rendered inside of a sub-request. And in that case, we want to read the query param from the main, real request.
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.
Yes it should, thanks! I change it
|
||
$request = $this->requestStack->getCurrentRequest(); | ||
|
||
$queryStringData = $this->queryStringPropsExtractor->extract($request->getQueryString(), $metadata); |
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.
Does it make sense to pass $request->query->all()
instead of the string (so we don't need to re-parse the string inside the extractor)?
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.
Yes, and I'm about to pass the Request
object directly, so the strategy to extract the data is fully managed in this class. I think it's better, wdyt?
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 works for me 👍
Hum that's actually a very good question 😄 I think this is related to the alias feature in some way; and we should be able to configure/override some part of the mapping in the component attributes. Like, maybe {% component('MyComponent', { query-mapping: {"prop": "alias1}}) %}
{% component('MyComponent', { query-mapping: {"prop": "alias2}}) %} |
Actually I'm wondering if the DTO support really worth it. For the moment it's quite limited:
The DTO support, if shipped with this feature, seems close to the Serializer concepts (max depth, projection, sub context, naming strategy, etc.). On the other hand, handling scalar values is really easy. So handling a custom DTO in a custom way, so the developer can eventually manage the scalar properties he/she wants, should be easy too. That's all to say, I suggest to drop the incomplete support of DTOs, and instead provide an extension point (an interface or some tag injection) so :
|
My immediate reaction when I saw you had DTO support was "wow" but also "that sounds like it may get super complex" :).
Agreed! Though I am also not that concerned immediately about adding the extension point. If you have something obvious in mind, sure, let's add it. But it is not a need. |
😞 Live component could set same urls as like submitting a form with GET method where query string is set to |
As I said, I'm interested in getting a rich feature without going crazy on complexities. However, @norkunas may have a point here. We already have a dehydration system that is able to convert a DTO to an array of simple data - e.g. |
|
||
foreach ($metadata->getAllLivePropsMetadata() as $livePropMetadata) { | ||
$queryStringBinding = $livePropMetadata->getQueryStringMapping(); | ||
foreach ($queryStringBinding['parameters'] ?? [] as $parameterName => $paramConfig) { |
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.
No need to code defensively here - if parameters
is missing, it would be a bug in our code I think
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 chose to always return an array from $livePropMetadata->getQueryStringMapping()
, possibly populated with configuration values, such as parameters
. So all unbound props have an empty array here. Maybe returning false
instead of the empty array could be better?
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 see - I'm ok with it.
settype($value, $config['type']); | ||
} | ||
|
||
return $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.
Yup, I'd definitely like to see this simpler & supporting less complex cases :)
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.
Using the LiveComponentHydrator
simplified this part a lot!
} | ||
|
||
private registerBindings(): void { | ||
const rawQueryMapping = (this.element as HTMLElement).dataset.liveQueryMapping; |
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.
Hmm, probably better if we make this a real Stimulus value on the element. Then:
A) Read the value from the stimulus controller
B) Pass the mapping into the constructor of this class
I try to keep the Component
class and plugins not ultimately dependent on Stimulus or the HTML structure as much as possible.
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.
Done in 717f726!
Should we update the polling plugin too?
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.
Hmm, I think so yes! But in a different PR :)
this.registerBindings(); | ||
|
||
component.on('connect', (component: Component) => { | ||
this.updateUrl(component); |
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.
My impression from Livewire is that we would only update the URL on page load if they have a (future) keep: true
option. But, by default, the URL only changes after the value has been changed on the page - https://livewire.laravel.com/docs/url#display-on-page-load
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 wasn't sure about this one, Livewire illustrates the keep
behavior with an initial empty value, and I also considered a non-empty initial value, that could be set on component mount for example, based on some context. But it may be too much for the moment, so let's wait for the keep
implementation!
} | ||
} | ||
} | ||
} |
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.
My initial thought is that it looks like you've done a really nice job. If It helps as a reference, I know that Caleb from Livewire worked very hard on the more complex query param creation for Livewire 3 - https://github.com/livewire/livewire/blob/d4839e3b2c23fc71e615e68bc29ff4de95751810/js/plugins/history/index.js#L176
Depending on which more-complex values we handle (e.g. do we handle a LiveProp
that is an array of strings? What about an array, or array of strings?), we'll need to make sure the query param format works.
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.
Thanks for the reference! I'm about to setup more complex use cases to determine the limits of my current implementation. This will help a lot!
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.
Can you add a little note, perhaps new the top of the file, with a link to this file in Livewire - something like:
Adapted from https://github.com/livewire/livewire/blob/d4839e3b2c23fc71e615e68bc29ff4de95751810/js/plugins/history/index.js#L176
It's nice to give Livewire the deserved nod and also will help us, in the future, answer "why did we do it this way?"
Seems legit, associative arrays should already be supported. That would have been a better solution that the underscores for the current DTO support. I'll take a look :) |
I agree here, but I think we should do some refactoring on the Hydrator class instead of building new hydration logic. We don't want to maintain two hydrations logic since we want this feature to mirror the rest. So we should add some public function to hydrade or dehydrate function. What do you think about making some refactoring? I have the following implementation in mind:
interface PropertyHydratorInterface
{
public function hydrate(): mixed;
public function dehydrate(): array;
public function support(): bool;
}
```
- The `LiveComponentHydrator` foreach properties of the component loop over the tag iterator of `PropertyHydrator` find the one that support the property type.
This is a similar implementation we already did for the DoctrineHydrator. We can have the following `PropertyHydrator`:
DateTime, Enum, Scalar, Dto, Collection, Array.
What do you think? |
Hi there! I addressed most of your comments, in separated commits:
The latter now allows to initialize complex objects from the URL, with the |
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.
Looking closer and closer
import { PluginInterface } from './PluginInterface'; | ||
import { | ||
setQueryParam, removeQueryParam, | ||
} from '../../url_utils'; |
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 line - sometimes I'll use multiple lines for a lot, but not this short
src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts
Outdated
Show resolved
Hide resolved
@@ -410,7 +410,7 @@ private function dehydrateObjectValue(object $value, string $classType, ?string | |||
return $dehydratedObjectValues; | |||
} | |||
|
|||
private function hydrateValue(mixed $value, LivePropMetadata $propMetadata, object $parentObject): mixed | |||
public function hydrateValue(mixed $value, LivePropMetadata $propMetadata, object $parentObject): mixed |
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 should add a phpdoc method description to this and it should be moved up above the private methods
|
||
foreach ($metadata->getAllLivePropsMetadata() as $livePropMetadata) { | ||
$queryStringBinding = $livePropMetadata->getQueryStringMapping(); | ||
foreach ($queryStringBinding['parameters'] ?? [] as $parameterName => $paramConfig) { |
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 see - I'm ok with it.
Hi! I made some updates, especially on the URL utils. I ported the code from Livewire as suggested, which brings some improvements:
The only main change I made to the original code is to avoid null values in the URL. I found it annoying that setting a single property in a DTO brings all other uninitialized values in the query string. Furthermore, depending on the input type of the prop, nulls may be wrongly casted (got the case with a numeric input filled with Also, now the URL is updated once after render. But in Livewire, it seems it is updated on each prop change, because all props may have their own history strategy. Not sure about how we could handle this in our feature? |
db3eeb1
to
36fb1f0
Compare
Hmm, that's a good point. Though, if I have 2 props that both will use But what if |
That seems to me the better way to go. Thanks for your advises! I keep this in mind for the future implementation of the |
Hello! I made some experimentation on my local project to handle the case of multiple components. I think we should handle an optional attribute to prefix all parameter names in the component (so it acts like a "scope"). Thought about something like: <twig:MyComponent query-param-prefix='prefix_'> So the URL looks like And maybe we could also leverage the special |
That seems reasonable. Though, if 2 components are on the same page and are using the Also, I'm super busy this week - but this is an important PR. As far as you know, is this ready to go (other than needing a rebase - thanks!) and is it waiting for a detailed review? |
3517dcb
to
23d4b7c
Compare
Yes, I confirm that two different components can share the URL if their parameters don't collide. I added an implementation that uses the special
Rebase done 😉 I think the current code runs well and is tested enough. We already discussed the expected behaviors a lot so it should be fine. But I added two more commits which may need a quick check, if you have the time of course!
Btw, I also worked on the other features (keep, alias, history) and they work pretty nicely. I can create a new PR when this one gets merged :) |
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.
Down to the small stuff. I played with this and it works FANTASTICALLY - super excited to have this!
|
||
Object.entries(this.mapping).forEach(([prop, mapping]) => { | ||
const value = component.valueStore.get(prop); | ||
if (this.isEmpty(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.
Can you explain the motivation for taking time to remove the "empty" query params? What if I have a LiveProp
called search
which defaults to foo
in my PHP class. Then, the user changes an input bound to this to ''
. If that removes ?search
from the URL, then if I refresh, won't my search
property once again be set to foo
(since there was no query parameter to override it)?
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 didn't consider this use case. You're right, providing an empty value in the URL should override values defined in PHP. The initial motivation for this check was to avoid empty values for objects and arrays. But I'll find a better way to manage these edge cases!
@@ -0,0 +1,138 @@ | |||
function isObject(subject: any) { | |||
return typeof subject === 'object' && subject !== null; | |||
} |
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.
function is only used once - let's inline the logic below where it's used
} | ||
} | ||
} | ||
} |
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.
Can you add a little note, perhaps new the top of the file, with a link to this file in Livewire - something like:
Adapted from https://github.com/livewire/livewire/blob/d4839e3b2c23fc71e615e68bc29ff4de95751810/js/plugins/history/index.js#L176
It's nice to give Livewire the deserved nod and also will help us, in the future, answer "why did we do it this way?"
{ | ||
$metadata = new LiveComponentMetadata( | ||
$event->getMetadata(), | ||
$this->metadataFactory->createPropMetadatas(new \ReflectionClass($event->getComponent()::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 it just be this?
$metadata = $this->metadataFactory->getMetadata($event->getMetadata()->getName());
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 hoped that would have worked, but it breaks the hydrator tests because the tested component is mounted manually and not registered in the class map:
InvalidArgumentException: Unknown component "__testing". And no matching anonymous component template was found.
Is there something to fix here?
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.
Is this only a tests problem ? If yes let's find a way to resolve it :)
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 is blocking precisely ? Can i help you with something ?
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.
Sorry I missed that one, I take a closer look
|
||
$prefix = $data[LiveControllerAttributesCreator::URL_PREFIX_PROP_NAME] | ||
?? $data[LiveControllerAttributesCreator::KEY_PROP_NAME] | ||
?? ''; |
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 not sold on this automatic collision avoidance. My gut is that I'd rather keep things simple. Then (later) give users the ability to control the name of the query parameter in the URL. Then, if they have a collision, they can decide which of the 2 colliding props to change the "parameter" for.
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.
In think the prefix will still help in solving collisions if we have the same component rendered multiple times. Even if users can control the mapping from the template in the future, they will still need to override the full mapping, while a prefix easily solves the collision with a single attribute. And without the ability to override the mapping in attributes, it's a way to avoid collisions in other use cases.
However, I agree that adds more complexity to this first implementation, and maybe it's not worth it right now. If you prefer I can drop this attribute support until we have a better solution.
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 worried about people getting confused when they're query params suddenly have a different name in the URL. Heck, it's also possible that people will purposely want to have a collision e.g. 2 separate search forms that both have query
(though, they would need to do extra work to make it so that when the query
changes in one component, it also updates the other - but this is totally possible)
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.
Alright, I dropped the commit with the prefix management
{ | ||
} | ||
|
||
public function extract(Request $request, LiveComponentMetadata $metadata, object $component, string $prefix = ''): array |
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.
Add phpdoc:
Extracts relevant query parameters from the current URL & hydrates them.
{ | ||
$event = new PostMountEvent($component, $data); | ||
$event = new PostMountEvent($component, $data, $componentMetadata); |
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.
Add a CHANGELOG entry for TwigComponents for this please!
@@ -21,6 +22,7 @@ final class PostMountEvent extends Event | |||
public function __construct( | |||
private object $component, | |||
private array $data, | |||
private ComponentMetadata $metadata, | |||
private array $extraMetadata = [], | |||
) { |
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.
This is a BC break, though almost definitely minor. But we should handle it. General steps:
A) Remove the types from the 3rd and 4th argument
B) If the 3rd argument is an array, trigger a deprecation - something like:
In TwigComponent 3.0, the 3rd argument of
PostMountEvent
will be aComponentMetadata
object.
And, in this case, set the 3rd arg $metadata
will be set onto the $extraMetadata
property (so, also, we will need to remove the private
from args 3 and 4 and set them as normal properties).
Also in this situation, set the $metadata
property to null. We'll need to make the return type on getMetadata()
nullable for now.
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.
If both $metadata
and $extraMetadata
are provided, should I enforce their types in the constructor body so the error comes from the constructor and not the assignment?
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.
Yup. So, use an if statement and trigger a deprecation if they pass the "old" way of doing things. But then, if they just pass some bad value - e.g. they pass a string to the 3rd arg - which was NEVER legal - check that and throw an exception.
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 also would require to check in LiveComponent if the installed TwigComponent has the good version (as they are two different packages)
|
||
/** | ||
* @author Ryan Weaver <[email protected]> | ||
*/ | ||
final class PreMountEvent extends Event | ||
{ | ||
public function __construct(private object $component, private array $data) | ||
public function __construct(private object $component, private array $data, private ComponentMetadata $metadata) |
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.
Make this argument optional defaulting to null. If it's null, trigger a deprecation. Make the getter below have a nullable return type.
23d4b7c
to
c73a24e
Compare
I'm glad to hear that! Latest commit handles bc break for TwigComponent and fix empty params removal :) |
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.
Played with it again - I thing we're there, except for this last item I found :)
|
||
$queryStringData = $this->queryStringPropsExtractor->extract($request, $metadata, $event->getComponent()); | ||
|
||
$event->setData(array_merge($event->getData(), $queryStringData)); |
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 have a tricky problem here. Suppose I have:
#[LiveProp(writable: true, url: true)]
public array $other = [];
Then I purposely go to /my-page?other=a-string
. Obviously, this a-string
is an invalid value. And so this shouldn't happen in practice. However, it currently triggers a 500 error:
As you can see the 500 comes from the mounting process. Normally, it is totally ok to fail with a 500 during the mounting process, as the developer is providing the mounting props directly (so if they mess something up, we want to tell them). But in this case, the mounted data is coming from the user. So if it's invalid, it needs to be ignored. To make things trickier, a could be mounted directly on a property, or via a setter or even by passing it as an argument to the mount()
method.
Here's my idea, which I think will be solid enough. In QueryStringsPropsExtractor
, after hydrating the data, we do a type-check on the property. If the type of the hydrated data is incompatible with the property type (if there is one), we do NOT add the hydrates value to $data
. WDYT?
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 makes sense for me! I updated the code with your suggestion. I also ignored issues from the hydrator itself to prevent errors when hydrating objects from invalid values. I ignored HydrationException
errors, but I wonder if ignoring any error would be better.
Sorry for the flood i did not start thinking i'd write many comments :) |
Finally took the time to install it and play a bit and .... this demo is 🔥 !! (this PR too) |
@smnandre Updates are coming, I compile all fixes in a single commit ;) |
public static function getSubscribedEvents(): array | ||
{ | ||
return [ | ||
PreMountEvent::class => 'onPreMount', |
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.
Can you add some tests to see how the different listener interfere (or not) with this listener ?
I think IntreceptChild / Defer / DataModel but maybe more, maybe less
And i think we should add priorities to ensure a determined order.
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 it would be useful to test with InterceptChild and Defer, as they don't subscribe to the PreMount event.
For DataModel, I'll have to setup some advanced use cases with parent and child components in order to test it efficiently.
My first thought is the query string initialization should come before the DataModel listener. The latter purpose is to set the child prop value from the parent bound prop, and I think replacing this value with the one extracted from the query string will lead the child component in an inconsistent state regarding its parent. Wdyt?
return; | ||
} | ||
|
||
$metadata = new LiveComponentMetadata( |
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 less and less convinced with this one... Could we use a single factory everywhere.. This new
make me fear future problems if we add cache for example in a place and not the other one.
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 just tried again with the factory and now it works, because we check if there is a request to process (wasn't checked last time I tried). Not sure if there is still something to fix now.
Not sure yet about those things (is there something or ... not at all) But could you explain me what happens if:
My intuition would be "it must be writable if a query can change that value" but i'm not sure if some use cases contredict that On the opposite, i think the "standard" live hydration starts by setting values, before hydrating the "new ones"... Would that not interfere in a way with the query data ? |
You mean the route used by the LiveComponent bundle to render components? I'd say it should not be processed at all.
You can consider a prop that is not writable directly from the component but can be updated on server side. For example, an array whom items can be added through an input field and deleted with a button, but with a LiveAction. It is possible, even if I'd tend to avoid in favor of a stimulus controller.
I don't think there could be interference here, because the LiveComponentHydrator is used only on live updates. The query data are used on the first load, and at this point the component is mounted with the regular TwigComponent's factory. |
e700c92
to
9c1a62f
Compare
9c1a62f
to
1301ae8
Compare
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.
Thanks for keeping this up to date! For me, just one comment on naming. Then let's get this thing merged :)
* | ||
* Tells if this property should be bound to the URL | ||
*/ | ||
private bool $queryMapping; |
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 know you and @smnandre discussed this change... but I did like url
more. A url: true
that, potentially later, could be changed to a bool|UrlMapping
type with some new UrlMapping
object. Then the user could have url: new UrlMapping(..)
.
Though, I am ok with keeping the "query mapping" or "url mapping" terminology deeper in the code.
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.
Fixed!
Sorry - one last change :p. Can you rebase? We have a conflict, I believe, from #1341. Then we'll get this merged! |
360282c
to
d894632
Compare
Should be ok now ;) |
d894632
to
68e4dd3
Compare
Thank you 1 million for this HUGE effort and your constant responsiveness. I can't wait to use this! Cheers @squrious! |
Btw, this does still needs some docs. @squrious do you want to make a PR for that? It doesn't need to be perfect - just getting something is 95% of the important part. |
I cannot say it better ! I have been really attentive to some details but this PR is really amazing and i’m so glad you worked as much to deliver. Congrats and thank you ! |
Thanks to you @weaverryan @smnandre for all the time you invested in the reviews! That was so cool to work on a such feature. I'll make the PR for the documentation ;) |
@@ -15,6 +15,7 @@ | |||
- Fix instantiating LiveComponentMetadata multiple times. | |||
- Change JavaScript package to `type: module`. | |||
- Throwing an error when setting an invalid model name. | |||
- Add support for URL binding in `LiveProp` |
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 think this should be updated. It looks like it has not been released in v2.13.3: https://github.com/symfony/ux/blob/v2.13.3/src/LiveComponent/CHANGELOG.md
This PR was squashed before being merged into the 2.x branch. Discussion ---------- Add doc for URL binding feature | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | Issues | NA | License | MIT Add documentation for symfony/ux#1230 Commits ------- 4f0d3132 Add doc for URL binding feature
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [LiveComponent] Alias URL bound props | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | N/A | License | MIT Following #1230. Allow custom parameter names for URL bound props, and mapping specification from Twig templates. ## Usage From PHP definition: ```php #[AsLiveComponent()] final class MyComponent { // ... #[LiveProp(writable: true, url: new QueryMapping(alias: 'q')) public ?string $search = null; } ``` From templates: ```twig {{ component('MyComponent', { 'data-live-url-mapping-search': { 'alias': 'q' } }) }} {{ component('MyComponent', { 'data-live-url-mapping-search-alias': 'q' }) }} ``` HTML syntax also works: ```twig <twig:MyComponent :data-live-url-mapping-search="{ alias: 'q'}" /> <twig:MyComponent data-live-url-mapping-search-alias="q" /> ``` ## Result Changing the value of `search` will update the url to `https://my.domain?q=my+search+string`. Mappings provided in Twig templates are merged into those provided in PHP. Thus, query mappings in PHP act as defaults, and we can override them in templates (e.g. for specific page requirements). So a page with: ```twig <twig:MyComponent/> <twig:MyComponent data-live-url-mapping-search-alias="q" /> ``` will update its URL to `http://my.domain?search=foo&q=bar`. Commits ------- 828e34e [LiveComponent] Alias URL bound props
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [LiveComponent] Alias URL bound props | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | N/A | License | MIT Following symfony/ux#1230. Allow custom parameter names for URL bound props, and mapping specification from Twig templates. ## Usage From PHP definition: ```php #[AsLiveComponent()] final class MyComponent { // ... #[LiveProp(writable: true, url: new QueryMapping(alias: 'q')) public ?string $search = null; } ``` From templates: ```twig {{ component('MyComponent', { 'data-live-url-mapping-search': { 'alias': 'q' } }) }} {{ component('MyComponent', { 'data-live-url-mapping-search-alias': 'q' }) }} ``` HTML syntax also works: ```twig <twig:MyComponent :data-live-url-mapping-search="{ alias: 'q'}" /> <twig:MyComponent data-live-url-mapping-search-alias="q" /> ``` ## Result Changing the value of `search` will update the url to `https://my.domain?q=my+search+string`. Mappings provided in Twig templates are merged into those provided in PHP. Thus, query mappings in PHP act as defaults, and we can override them in templates (e.g. for specific page requirements). So a page with: ```twig <twig:MyComponent/> <twig:MyComponent data-live-url-mapping-search-alias="q" /> ``` will update its URL to `http://my.domain?search=foo&q=bar`. Commits ------- 828e34e5 [LiveComponent] Alias URL bound props
Inspired from the Livewire Url attribute, let's bind our LiveProp props to the URL query string!
This is a first proposal, with the minimum feature behavior:
url
constructor parameter toLiveProp
Currently supported data types:
A bound LiveProp doesn't need to be writable, so it could be updated on server side based on a custom action.
BUT for DTOs, for the moment I bind the writable props only. Maybe another config option could be a better solution?
What could be next?
search
=>q
,dto.id
=>some_id
)keep
option to force presence in the URL even if the prop is emptygetQueryString
method in the component instead of attributesYou can also check my demo app to try the feature.