Skip to content

[LiveComponent][RFC] Simple file upload handling #299

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

Closed
wants to merge 29 commits into from

Conversation

Lustmored
Copy link
Contributor

Q A
Bug fix? no
New feature? yes
Tickets Fix #289
License MIT

This is a proof of concept for very simplistic file upload support within live components. It is by no mean finished (docs and tests are missing for sure), but due to a few design choices I've made I want to get some feedback before finishing it :)

Summary of changes:

  • data will be sent to server as a "data" key withing FormData object instead of directly in body. This is required to make multipart form sending easily doable.
  • file inputs require to be marked as file target within Stimulus live controller to be handled in a special way. I did it because I believe it's a good stimulus way and also to allow user to be able to handle some file inputs their own way.
  • there is a new modifier to action string - files. When set without any parameter it will send all files from all targets to the server. When passed parameter it will only send target with name matching parameter. So it is possible to specify for example: <input type="file" name="file" data-live-target="file" data-action="change->live#action" data-action-name="stop|files(file)|action" /> that will send just this file(s) as soon as they're chosen and let backend handle it and skip it for any other actions.

This allows for simple cases of file uploads, no forms integration at this point and I believe it'd be a good idea to get this in a good shape first and then think about possible form extension. There is a good chance this change with good documentation will even be enough :)

@Lustmored Lustmored force-pushed the file-upload branch 2 times, most recently from 9374bbd to b65677e Compare April 22, 2022 10:27
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.

Apologies for the delay. I was thinking a lot about this PR last night. I think there is some room in the future for a super fancy upload system. But, to get something functional, I think this is the correct approach and you've implemented it very nicely! If someone comes along with a better idea in the future, great! But at this moment, I think this is it. So, let's finish this off... if you still can... 1+ months later 😇

// Don't update file targets
if (this.fileTargets.includes(fromEl)) {
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Hmm. I see 2 possible ways that file uploads could be used

  1. Traditional Form Submit: the user has an upload field... but they don't want to process it or re-render it in any way with the live component. If they select a file, nothing happens: it just sits there and waits. Eventually, the user submits the form via a traditional form submit.

  2. LiveAction Process: the user has na upload field and, on "change", they trigger a LiveAction that will then process that file upload. Later, if there is a full form, they would submit the entire form via another LiveAction.

What you have here works great for (1), but not for (2). We may (? need to experiment) not need this code at all. morphdom is already really good at "not updating things that didn't change".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually I have tampered with it a bit and found that chosen files are reset on file field when I don't have this code in place (so (2) is broken for me). I didn't play around file inputs not handled by live actions for now, so I am not sure how (1) behaves right now. Will surely come back to this when other pieces will get into place 👍


.. versionadded:: 2.2

The ability to pass arguments to actions was added in version 2.2.
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be updated ;)


<div
data-action="change->live#action"
data-action-name="prevent|files(my_file)|upload"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
data-action-name="prevent|files(my_file)|upload"
data-action-name="upload_files(my_file)|handleMyFileUpload"

So, I changed a few things:

A) No prevent. Not needed here, and I think it takes away focus.
B) I like upload_files as the modifier name. It feels like a "verb", which "feels" correct here. I want to "trigger the action of uploading"
C) Made the live action name super explicit that it is a custom thing for this example. At first, I was thinking |upload was another modifier 🙃

<input type="file" {{ stimulus_target('live', 'file') }} name="my_file" />
</div>

This will send files from ``my_file`` file input. When used without argument
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
This will send files from ``my_file`` file input. When used without argument
This will upload the file from the ``my_file`` file input. If you use `upload_files` without any arguments,
it would upload all files that have the `file` target.

This will send files from ``my_file`` file input. When used without argument
it would send all files from all ``file`` targets of the controller.

If you want to send multiple files from a single input remember to suffix its' name
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
If you want to send multiple files from a single input remember to suffix its' name
If you want to send multiple files from a single input, remember to suffix its name

it would send all files from all ``file`` targets of the controller.

If you want to send multiple files from a single input remember to suffix its' name
with ``[]`` - both in HTML name attribute and ``files`` modifier argument.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
with ``[]`` - both in HTML name attribute and ``files`` modifier argument.
with ``[]`` - both in the HTML name attribute and the ``upload_files`` modifier argument - e.g. ``upload_files('photos[]')``.


If you want to send multiple files from a single input remember to suffix its' name
with ``[]`` - both in HTML name attribute and ``files`` modifier argument.

Copy link
Member

Choose a reason for hiding this comment

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

The missing piece (which I'm guessing you were waiting on to get more feedback) is the PHP logic. We're going to need a really nice documentation example of how the LiveAction looks, including executing validation (and re-rendering with an error), processing the file upload (i.e. moving it somewhere) and setting the final filename (e.g. tulips.jpg) from that process onto some other LiveAction property... or setting it into an entity and saving immediately.

@Lustmored
Copy link
Contributor Author

Thanks for the review. Better late than never 😉 I definitely will come back to this, hopefully next week and then you can expect answers to the comments and more code to come by 👍

@Lustmored
Copy link
Contributor Author

After working on comments from @weaverryan I have started to work on backend file upload handling, taking inspiration from live arguments, as both seem reasonably similar. I am leaving all docs and tests for now and will come back to them at the end, when the final shape of this feature will be decided :)

I have added LiveFileArg attribute to mark component method parameter as an uploaded file (or files). It solves the problem of file input names - attribute takes name parameter (or falls back to property name) and seeks file with that name. It works for both file and form[embedded][file] names. It also contains parameter for handling multiple files at a time and wires ?UploadedFile or array, depending on it.

I would love to add some validation to uploaded files. For now I've added constraints parameter to LiveFileArg, so that any validations may be configurable by consumer. But unfortunately I don't have a vision about how to proceed with it. Where to wire this validation (some event?) and how to proceed with errors (they probably could be displayed nicely by component?). Please let me know what do you think and give me some ropes :)

Or maybe it is better to leave validation out of this scope, so consumer code can validate files themselves and display errors the way they want?

@Lustmored
Copy link
Contributor Author

Also - I have made very simple file upload component example on live-demo for testing purposes: https://github.com/Lustmored/live-demo-1/tree/local

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.

Hey @Lustmored!

Ok, I just had some good time to play with this. THANK YOU for adding your demo - that was super helpful - I played with that directly.

First, I like LiveFileArg. It means that any "action" arguments need to have LiveArg or LiveFileArg. How nice!

I would love to add some validation to uploaded files.

Let's FIRST get a working "file" component where we do the validation manually. Ideally, we would try to use ValidatableComponentTrait in some way... or maybe not. Either way, we would probably validate the UploadedFile manually inside the action. Then, if there are errors, we set those somewhere: it could be as simple as setting them on a non-writable LiveProp - e.g. uploadErrors. Then, in the template, we render those. Start there so we can see how it looks and feels. If we can identify some patterns where we could add something into LiveComponents itself to make this process easier, then we can do it.

My biggest feedback is about the syntax in the templates themselves. I'd like to not have the file targets. I see 3 possible "scenarios" with upload fields:

  1. A file upload is inside of a live component... but will be 100% submitted normally. We've talked about this use-case. All we need to do is make sure that we don't re-render the <input file> when the component re-renders. In [Live] Do not re-render <input file> if input holds unuploaded files #323, I've done a slightly different approach. So, this scenario is a ✅ already.

  2. After the user selects a file, nothing immediately happens (no re-render). But then, later (e.g. via a button click), we upload the file to some custom action. For this scenario, I'm imagining this syntax:

<input
    type="file"
    multiple

    data-file="images"
    data-action="live#update"
/>

<button
    data-action="live#action"
    data-action-name="upload_files|save"
></button>

The idea is this: on "change" of the file input, we trigger the same data-action="live#update" that we trigger with normal "model" fields. However, in that function, we would determine that the input is a FILE input. And so, instead of updating the data, we look for data-file (or fallback to name=""... because that is super handy for Symfony forms) and then set the current <input type="file" HtmlInputElement into some fileInputs = [] array property on the controller object. Basically, on change, we store the HtmlInputElement for later. This removes the need for the targets and makes file inputs work very similar to normal "model" fields, except that instead of storing the VALUE of the input, we're storing the input itself.

Then, very similar to what you already have, when you want to upload the field, you trigger a custom action but use a modifier. I like the upload_files modifier vs files that you have. And upload_files could take zero arguments (send everything) or take an argument - e.g. upload_files(images).

  1. Immediately after the user selects a file, we want to trigger the "upload" live action. For this scenario, I'm thinking this syntax:
<input
    type="file"
    multiple

    data-file="images"
    data-action="live#uploadFile"
    data-action-name="uploadImage()"
/>

The new uploadFile() method would basically FIRST trigger the update() method's logic so that this HtmlInputElement is stored onto the fileInputs array property I described above. THEN it would effectively call the action() method so that the custom uploadImage() LiveAction is triggered. So this is a shortcut version of scenario (B).

WDYT?

if ($files = $accessor->getValue($allFiles, $path)) {
$request->attributes->set(
$parameter,
$fileArg->multiple ? $files : $files[0]
Copy link
Member

Choose a reason for hiding this comment

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

Do we need $fileArg->multiple at all? If the <input file has multiple, then $files will be an array of UploadedFile. If it is NOT multiple, then it will already be a single UploadedFile. I don't think we need to do anything.

Copy link
Member

Choose a reason for hiding this comment

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

This was my thought as well but do we need to be worried about someone fudging the <file> input to add multiple then uploading multiple files?

What about checking the typehint? array === multiple, UploadedFile === single.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. It we do nothing, and someone messes with things, it would explode with a 500 error (since an array would fail to be passed to an arg typed with UploadedFile). So, I don't think there's an attack vector. However, allowing bad users to trigger a 500 isn't too great either ;).

So, yea, we COULD check the uploaded data first to see if we're dealing with ONE or MULTIPLE files. Then we COULD check the type on the arg to see if it's something compatible (e.g. no type, or UploadedFile for a single, etc). If it is NOT compatible, we return a 400 error.

That DOES feel a little magic... I can't think of any similar thing that's done in Symfony. Like, is there a way in php to basically ask "Hey: would this value be compatible with this argument type?". WDYT?

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 don't think there is a simple, PHP way of doing it. And I agree removing multiple on fileArg could be beneficial. After all we have typehints. I will refactor this part for sure 👍

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 have refactored the logic here so it goes like that:

  • in LiveFileArg::liveFileArgs(...) I collect types compatible with argument declaration
  • I have added LiveFileArg->isValueCompatible(mixed $value): bool that does its best to determine if passed value is compatible with argument typedef. It works for untyped parameters too.
  • In listener I check if there is only one file uploaded. If it is and UploadedFile is compatible with argument - I autowire that.
  • If there are multiple files uploaded or UploadedFile is not compatible and array is compatible - I autowire that.
  • Otherwise I throw BadRequestHttpException to mitigate 500 as at that point I know autowiring will fail (or wire default value, but that is also incorrect behavior).

Hope that solves all the cases 😺

weaverryan added a commit that referenced this pull request May 18, 2022
…ed files (weaverryan)

This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[Live] Do not re-render <input file> if input holds unuploaded files

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| Tickets       | Related to #299
| License       | MIT

On #299, we're working on proper file upload support for live components. BUT, I think one thing is for sure: if you simply select a file and then (later) the component re-renders for some reason, we do NOT want to "replace" the `<input file` that has the attached file with an empty `<input file`.

I've tested locally - testing proved troublesome, due to some differences in how the `.value` attribute seems to work on browser vs the testing environment.

Cheers!

Commits
-------

28b29e9 dropping 7.2 from CI: it is no longer supported by CI tools
2ded2f2 do not re-render <input file> if input holds unuploaded files
@Lustmored
Copy link
Contributor Author

First of all - thanks a lot for very deep and constructive review. I must say working on this project is both demanding and a great opportunity to learn (both by myself and from you guys) and I love it ❤️

Your ideas from the comment sound great and I intend to work on them. I have quite a busy time right now, but will try to contribute within a week or two with the next iteration of this feature 👍

@Lustmored
Copy link
Contributor Author

@weaverryan I have pushed another step in outlined directions. I have refactored frontend following your suggestions and also refactored autowiring logic to be simpler and more friendly. I have also pushed updated demo working with changed version.

One thing I have dropped is support for file model paths like form[data][file] and instead rely on simple model names, like simpleFile (we always can supply it as data-model so I think that's not a big deal). I will reflect it in the docs when it comes to that.

I did not get to the validation yet, so I will come back to this next week. For now if you have time to take a look - comments are more than welcome 😺

@Lustmored
Copy link
Contributor Author

PS: I believe file inputs are still being rerendered even when files are selected within them. I have removed code that prevented rerendering "file" targets. You can see this in action in my demo - select some files for "manual" files, don't upload them and then select some files in "auto" files. Manual input is cleared on render.

@Lustmored
Copy link
Contributor Author

Fun fact - I had absolutely nothing to do to achieve validation via ValidatableComponentTrait. It just required creation of properties for uploaded files (that are not LiveProp as UploadedFiles are not serializable) with validation attributes an retrieve them in component (you can check it out in my updated live-demo). It goes like that:

class FileComponent
{
    use DefaultActionTrait;
    use ValidatableComponentTrait;

    #[Valid, All([new Image()])]
    public array $autoFiles = [];

    #[Valid, Image]
    public ?UploadedFile $manualFile = null;

    #[LiveAction]
    public function test(
        #[LiveFileArg]
        ?UploadedFile $manualFile = null,
        #[LiveFileArg]
        ?array $autoFiles = null
    ): void{
        if ($manualFile) {
            $this->manualFile = $manualFile;
        }
        if ($autoFiles) {
            $this->autoFiles = $autoFiles;
        }

        $this->validate();
    }
}

I'm still putting docs and tests on hold to make sure I get it done right at once. So I'd love to get some more feedback before getting into it. Is architecture of this feature right? Do you see some spots that could be done better?

@weaverryan
Copy link
Member

I'm hoping to review this early next week - MAYBE this week - some conference prep has me bogged down. But I'm excited to give this a look!

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.

Hey @Lustmored!

Thanks for your continued patience. This is a really complex issue, and the exact right "end product" isn't immediately clear... but it's slowly coming into focus. Each time you work, we get a bit further and learn more :).

I've left some minor code reviews. Here are the bigger things I'm "pondering":

  1. I noticed (I'm still using your demo, so thanks for keeping that up-to-date) that, after uploading, the file input don't clear. I... think they should. Even if there were a validation error, the user would need to re-select a file and re-upload. Do you agree? If so, we need to find a nice way to do this. It's a bit tricky because. Suppose a component has 2 file uploads and 2 different buttons: one to upload file A and one to upload file B. If the user clicks to upload file A, we need to reset ONLY that file. Normally, we allow the server to return HTML and then use morphdom to "update" the DOM. That may be possible in this case... but right now, the server is returning the same HTML as is on the page (for the file input)... and so the field is not re-rendered and "cleared". Again, that IS the behavior we want sometimes (e.g. we uploaded file A but not file B... or we re-rendered the component for a reason unrelated to file uploading), but not always. Right now, I'm wondering if the only/best way to clear the field is actually from live_component.ts: after a file upload request finishes, clear the files for fields that were included in that upload.

  2. At first, it looked strange to me have the $autoFiles and $manualFile properties: https://github.com/Lustmored/live-demo-1/blob/85dfd0d1a272193c3686377018c9c4c2ab924a57/src/Twig/Components/FileComponent.php#L22-L26. These are NOT LiveProp and they CAN'T be LiveProp, as there is no way to dehydrate an UploadedFile. That is why, of course, we force uploads to go through a LiveAction, so that the user can process and move the files. Anyways, it occurred to me that these files exist 100% so that they can be momentarily set so that validation works. To extend your LiveAction code a bit to a more realistic example, it would look like this:

#[LiveAction]
public function test(#[LiveFileArg] UploadedFile $manualFile = null): void
{
    if ($manualFile) {
        $this->manualFile = $manualFile;
    }

    $this->validate();
    
    $this->manualFile->move(__DIR__, 'test.jpg');
}

Do you see what I mean? We $this->manualFile = $manualFile; SIMPLY so that $this->validate() works. And so, I thought, for a moment that that the $manualFile property is kind of pointless and instead the user should maybe manually validate this value without setting it onto a property. But... that's harder... and then you don't get the nice this.getError() in the template.

Anyways, this is a LONG way of saying that I think the $manualFile property DOES make sense. In fact, it makes SO much sense that I think EVERY "upload" LiveAction will start with code that looks like $this->manualFile = $manualFile. So NOW I'm thinking that instead of LiveFileArg (where we pass the UploadedFile as an argument to the LiveAction), it might make more sense to set the UploadedFile onto a non-"Live" property... right before we call the action. Something like:

class FileComponent
{
    use DefaultActionTrait;
    use ValidatableComponentTrait;

    #[Valid, Image]
    #[LiveProp(isFile: true)]
    public ?UploadedFile $manualFile = null;

    #[LiveAction]
    public function test(): void
    {
        $this->validate();
    
        $this->manualFile->move(__DIR__, 'test.jpg');
    }
}

The key new thing is LiveProp(file: true). This would tell us to fetch that value from $request->files. This could also be a different class - e.g. LiveFile(). If it is a LiveProp(file: true), then the system will try to dehydrate the UploadedFile... which might be ok: we would just need to add a custom dehydrator that sets it to null. But, if this causes too many problems, then a new class - e.g. LiveFile - might make more sense (instead of trying to extend the LiveProp system.

WDYT? Please don't take my thoughts/opinions as "definitely the best". If you disagree or have any input, I would love to hear it. Thanks!

case 'upload_files':
if (modifier.value) {
const input = this.fileInputs[modifier.value];
if (input && input.files) {
Copy link
Member

Choose a reason for hiding this comment

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

Probably if there is no input, we should throw some sort of error. Or is there a legit case for this?

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 believe there could be forms that have the same action before processing the file and after it (like a single action to process all form that is aware of files).

I personally would probably remove the file input from resulting HTML upon successful validation and file handling. Then I'd need to update all the action modifiers for stimulus actions and that could be a lot of ifs in complex components, like:

<div data-action-name="{% if !uploadedAvatar %}upload_files(avatar)|{% endif %}my_action">...</div>

So I've decided to treat non-existent input the same way as empty ones.

delete this.fileInputs[modifier.value];
}
} else {
for (const [key, input] of Object.entries(this.fileInputs)) {
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 not sure if it can, but could this be done with an array filter on this.fileInputs? If I'm understanding it correctly, we basically want to send ALL of this.fileInputs... except skip anything where there are not attached files to the field.

if (input && input.files) {
files[modifier.value] = input.files;
} else if (input) {
delete this.fileInputs[modifier.value];
Copy link
Member

Choose a reason for hiding this comment

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

So, if there are not currently any "attached" files on this input, we delete it from this.fileInputs. Do we need to do that?

this.fileInputs[model] = element;

return;
} else if (/\[]$/.test(model)) {
Copy link
Member

Choose a reason for hiding this comment

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

this can just be an if (not else if), since we have the return in the first statement.

if (
\is_array($files)
&& 1 === \count($files)
&& $fileArg->isValueCompatible($files[0])
Copy link
Member

Choose a reason for hiding this comment

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

Is this allowing you to have a multiple file upload, but then use an UploadedFile type hint (instead of an array)? If so, let's remove that: it's too magical, and won't work if the user DOES decide to upload multiple files.

&& $fileArg->isValueCompatible($files[0])
) {
$value = $files[0];
} elseif ($fileArg->isValueCompatible($files)) {
Copy link
Member

Choose a reason for hiding this comment

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

This seems legit: makes sure that the files (whether single and UploadedFile or multiple an array) are compatible with the argument they are trying to "fit into".

@@ -148,6 +151,34 @@ public function onKernelController(ControllerEvent $event): void

$request->attributes->set('_mounted_component', $mounted);

// autowire live file arguments
if ($request->files->count()) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if ($request->files->count()) {
if ($request->files->count() > 0) {

@weaverryan
Copy link
Member

Btw, for the demo, to make it more realistic, I added:

class FileComponent
{
// ...
+     #[LiveProp()]
+     public array $uploadedFiles = [];

    public function test(...)
    {
        if ($manualFile) {
            $this->manualFile = $manualFile;
+            $this->uploadedFiles[] = $manualFile->getClientOriginalName();
        }
        if ($autoFiles) {
            $this->autoFiles = $autoFiles;
+            foreach ($this->autoFiles as $autoFile) {
+                $this->uploadedFiles[] = $autoFile->getClientOriginalName();
+            }
        }
    }
}

And in the template:

    <h2>ALL FILES</h2>
    <ul>
        {% for file in uploadedFiles %}
            <li>{{ file }}</li>
        {% endfor %}
    </ul>

In a real app, the manualFile and autoFiles properties are very temporary... they wouldn't ever even be rendered in the template like they are now. They would be processed inside the LiveAction, moved to their final location, then some other property would be populated with the final filename. This "kind of" shows that a bit more :).

@weaverryan
Copy link
Member

Sorry, one more thing :p. Perhaps before we make any of the big changes proposed above, we should build out the demo a little bit more to make it more realistic and make sure it's covering use-cases. Here are two big, but similar, use-cases that I'm thinking of:

A) A component (without using the form system) that renders an Article form with title, description and image field (which is an upload field). This Article (or BlogPost, whatever) should be an entity, and the component can be used to edit an existing Article or to create a new one. If the Article is being "edited", then as the image field is attached, it uploads immediately to a LiveAction. We then move it somewhere, and set some imageFilename property on the Article. When the component re-renders, we use the imageFilename to show a preview of the now-uploaded image. If the Article is new, then we do not upload the file immediately. Instead, when we press the Save button (which triggers a different live action to save the Article), the upload would be processed then. The form would also use "real-time" validation and the image upload would be required... but only for "new articles" (because an existing article will already have a populated imageFilename).

B) The same component as above, but built with the form component.

These represent a pretty complex, but also pretty standard use-case where I put a form into a live component so that I can have (A) real-time validation but also (B) a file upload that is processed immediately after attaching the file. If we can make this work, then I think we're in good shape.

Sorry for the WALL of text today - I'm available on Slack to chat as always :).

Thanks!

@Lustmored
Copy link
Contributor Author

I have just spent a lot of time figuring out what goes where, but in the end I have kinda working live demo that... Well, mostly just works in a "semi-nice" way. I think it's a good point to stop for now and show you what is where to decide what we really need :)

Please take a look at my last commit here - it's pretty small, but touches areas I have not explored yet, so might be wrong from separation of concerns point of view. But something along those lines is required to pass files from AJAX upload all the way to ComponentWithFormTrait without touching too much unrelated stuff.

Now to the demo:

  • I have added file upload with image validation to the EditPostNoFormComponent. It doesn't handle creation (just editing), but shows what is already possible now. It doesn't persist data upon upload, just upon clicking 'Save'. I left it that way as just calling $em->persist() would also save other fields and that's not expected probably.
  • In the PostFormComponent I have also added ImageType field. It saves to unmapped (by Doctrine) field on the entity for validation (it's expected method by some libraries AFAIK, so is probably the right way). Passing files around to the component with a form was the hard way, but now that it works and I might say it looks really simple to use and is just working :)

Obviously file persisting logic I put there is more of a placeholder than real-life scenario, but that part doesn't matter for now.

One final note - I am struggling thinking about file input clearing logic. Now that I have played with it a bit I think that maybe we should never clear them for the user. My reasoning is that upon uploading file, validation can fail for any field (not just the file). Therefore during morphing DOM there is no way to determine if the file was successfully persisted, other than removing file input on the template side (as I did in EditPostNoFormComponent).

On the other hand it could lead to the same file being upload multiple times which is... Not what we really want. For today I have no idea how to approach this problem from any other angle. I think we'll have to come back to this after setting all the backend logic in place.

@kbond
Copy link
Member

kbond commented Jun 27, 2022

Awesome, thanks for all this work @Lustmored! I will play with your demo later this week.

@Lustmored
Copy link
Contributor Author

Closing in favor of #834

@Lustmored Lustmored closed this May 9, 2023
@Lustmored Lustmored mentioned this pull request May 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[LiveComponent][RFC] File uploads in forms
3 participants