Skip to content

[LiveComponent] Add modifier option to LiveProp #1507

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

Merged
merged 1 commit into from
Mar 5, 2024

Conversation

squrious
Copy link
Contributor

Q A
Bug fix? no
New feature? yes
Issues N/A
License MIT

Hi!

Following discussions in #1396, here is a proposal to allow a LiveProp to be modified at runtime.

The feature adds a modifier option to LiveProp, so users can use a custom method to modify the LiveProp's options
depending on the context.

This context could be a service, or even another prop within the component.

Usage

Here is the example given in the doc:

#[AsLiveComponent]
class ProductSearch
{
    #[LiveProp(writable: true, modifier: 'modifyAddedDate')]
    public ?\DateTimeImmutable $addedDate;

    #[LiveProp]
    public string $dateFormat = 'Y-m-d';

    // ...

    public function modifyAddedDate(LiveProp $prop): LiveProp
    {
        return $prop->withFormat($this->dateFormat);
    }
}
{{ component('ProductSearch', {
    dateFormat: 'd/m/Y'
}) }}

Changes overview

First, we have to make the LiveProp attribute editable in some way. I opted out for immutable methods, so caching is preserved when applicable.

Then add a LivePropMetadata::withModifier(object $component) method that returns a new instance modified by the user-defined method. This is called in a few places, similarly to LiveComponentMetadata::calculateFieldName().

The tricky part is component hydration. If we use props in modifiers to change other LiveProp options, they have to be hydrated before the modifier is applied. This is only useful for options related to hydration, but it concerns most of them... The trick is to sort metadata, so modified ones are hydrated at the end.

@carsonbot carsonbot added the Status: Needs Review Needs to be reviewed label Feb 16, 2024
Copy link
Contributor

@WebMamba WebMamba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, just let me summarize the approch here:
When a method have a modifier attribute, we execute the modifier function, get the modified value, we create a clone of the PropMetada with the new value. All of that happen during the hydratation.
What about the dehydratation?

Comment on lines 150 to 158
uasort($allMetadata, static function (LivePropMetadata $a, LivePropMetadata $b) {
if ($a->hasModifier() && !$b->hasModifier()) {
return 1;
} elseif (!$a->hasModifier() && $b->hasModifier()) {
return -1;
} else {
return 0;
}
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to put the method with a modifier in priority of others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata with a modifier are considered "greater" than others, and the array is sorted in asc direction. I'd say this code prioritize metadata without modifier?

@kbond
Copy link
Member

kbond commented Feb 18, 2024

I actually had a need recently to only enable live props that update the URL under certain conditions. I believe this PR would allow that, correct?

@squrious
Copy link
Contributor Author

I actually had a need recently to only enable live props that update the URL under certain conditions. I believe this PR would allow that, correct?

Yes totally!

When a method have a modifier attribute, we execute the modifier function, get the modified value, we create a clone of the PropMetada with the new value. All of that happen during the hydratation.
What about the dehydratation?

Modifiers are not only used on hydration. Actually this is used a bit like calculateFieldName: we need it applied on hydration and dehydration, but also in other places like when processing/rendering URL mappings.

Copy link
Member

@weaverryan weaverryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very tricky PR - I'm hoping we can make this work - excellent job 🤞

.. caution::

You must not rely on props that also use a modifier in other modifiers methods, as their modifiers will not
necessarily be applied in the correct order when hydrating components.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must not rely on properties that also use a modifier in other modifiers methods. This is because
those properties may not have been hydrated by this point.

@@ -66,6 +66,7 @@ public function dehydrate(object $component, ComponentAttributes $attributes, Li

$dehydratedProps = new DehydratedProps();
foreach ($componentMetadata->getAllLivePropsMetadata() as $propMetadata) {
$propMetadata = $propMetadata->withModifier($component);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the riskiest part of this PR. Currently, if you want the LivePropMetadata[] for a component, there is one source of truth: getAllLivePropsMetadata(). But now, someone needs to know to loop over these LivePropMetadata and call withModifier($component) to get the final version. That bothers me :/. I understand the "chicken and egg" problem, though: we need to hydrate the component (or as much of it as possible) before we can execute the modifier callbacks. I wish there was a way around that, but I can't think of anything more clever than your sorting algo.

However, to minimize the problem I mentioned above, would it be possible to add an object $component argument to getAllLivePropsMetadata() and make it return an iterable? Internally, it would do the sorting, then, one-by-one, call ->withModifier() and yield the final LiveProp. Would that work? Then everywhere in the code would call $componentMetadata->getAllLivePropsMetadata($component).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested this, and it works perfectly!
But I suggest that we sort these LivePropMetadata in the LiveComponentMetadata constructor, so they are sorted once. As the modifier option is not editable, this should be fine, WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I think that's ok 👍

@squrious squrious force-pushed the live-component/live-prop-modifier branch 2 times, most recently from 20ead2c to 3ef4ace Compare February 26, 2024 10:09
@squrious
Copy link
Contributor Author

Modifiers are now applied each time getAllLivePropsMetadata is called. Maybe we could add an internal cache for this, based on spl_object_id?

I also had to move the listener for query string initialization to PostMount instead of PreMount, as it needs the component to be "pre-mounted" to know if some props are bound with modifiers. The test for this was not right in the first place... I set the priority to 256 to ensure it is executed before other custom listeners.

Some tests failed after rebasing, but I don't think this is related to this PR.

if ($livePropMetadata->queryStringMapping()) {
$frontendName = $livePropMetadata->calculateFieldName($mounted, $livePropMetadata->getName());
$frontendName = $livePropMetadata->calculateFieldName($mounted->getComponent(), $livePropMetadata->getName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this change. Rather, it looks right now, but the old code looks totally wrong. Was this an unrelated bug that snuck in? And is it not tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused by the (my) old code too. It's a bug that wasn't covered because I used a string field name in tests, and the component object is only used when the field name is computed by a method... Not directly related to this PR though, but I thought fixing the typo was worth it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added another prop in test component to test with the method for field name

@carsonbot carsonbot added Status: Needs Work Additional work is needed and removed Status: Needs Review Needs to be reviewed labels Feb 29, 2024
@weaverryan
Copy link
Member

Modifiers are now applied each time getAllLivePropsMetadata is called. Maybe we could add an internal cache for this, based on spl_object_id?

It sounds nice... but I don't know if it will make enough difference. Most components will not have a modifier, so the extra work will be a foreach over all the LiveProps & a call to the withModifier() method that exits immediately. That might (?) be something we should cache away... I just don't know.

@carsonbot carsonbot added Status: Needs Review Needs to be reviewed and removed Status: Needs Work Additional work is needed labels Feb 29, 2024
@squrious
Copy link
Contributor Author

It sounds nice... but I don't know if it will make enough difference. Most components will not have a modifier, so the extra work will be a foreach over all the LiveProps & a call to the withModifier() method that exits immediately. That might (?) be something we should cache away... I just don't know.

Yup, that's probably too much for the moment. I was wondering about modifiers that could use services, maybe heavy, to get configuration etc. But that could be cached in the component class directly. In the end, no need to generalize it 😄

@weaverryan weaverryan force-pushed the live-component/live-prop-modifier branch from 11b1ebc to 3bcc5c2 Compare March 5, 2024 16:55
@weaverryan
Copy link
Member

Thanks Nicolas for your hard and careful work on this - well done!

@weaverryan weaverryan merged commit cae0f0e into symfony:2.x Mar 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Needs Review Needs to be reviewed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants