hackingharold / ngx-dropzone

The missing file input component for Angular Material.
MIT License
32 stars 5 forks source link

Cannot distinguish between event handlers when doing drag-n-drop #79

Closed alexandis closed 2 months ago

alexandis commented 3 months ago

Hi, I am back :) I've used the following markup to cover both drag-n-drop and usual selection for multiple file upload:

<ngx-mat-dropzone (drop)="onDropDocuments($event)">
    <input type="file" fileInput [ngModel]="files" [ngModelOptions]="{ standalone: true }" [multiple]="true"(ngModelChange)="onSelectDocuments($event)" />
</ngx-mat-dropzone>

Unfortunately, it works well only for an ordinary select, because only onSelectDocuments is triggered. When I use drag-n-drop, both onSelectDocuments and onDropDocuments handlers are triggered and as a result, I get the issue, because the code has to process the same logic twice. How to avoid this and clearly divide the handlers?

I used some custom logic, that's why I use onDropDocuments handler. And also I found using ngModel in my case instead of formControl to be preferrable.

In fact, the main reason for having the custom code was to get the initial source path of the file by all means, since we needed to pass this info to server. If I could obtain this information in other way, probably I would get rid of the issue as well by eliminating some custom handlers...

hackingharold commented 3 months ago

Hi @alexandis,

I want to confirm I understand your problem correctly. You want to access the files source paths before upload. I assume you want to use the webkitRelativePath property. What exactly are you checking in both your handlers, onDropDocuments and onSelectDocuments?

alexandis commented 3 months ago

Hi.

I use my custom logic in onDropDocuments method to handle a drag-n-drop scenario. This logic creates the collection of files to-be-uploaded, preserving the original file path using FileSystemEntry => fullPath property (otherwise the original file name does not contain this information). Here, I have to copy the original file to a new file to change its name, because File is immutable (this.files.push(this.copyFile(file, fileEntry.fullPath.replace(/^\/[A-Z]_drive/, '')));)

And I use onSelectDocuments to handle an ordinary logic - selecting files folder with standard system "Open" button - in this case the original file path is available straight in File => name. In general, I use onSelectDocuments to build a common logic of displaying the files to-be-uploaded (the folder structure with the total amount of files and its volume per each subfolder), no matter if it's a drag-n-drop or a system select.

And I am getting issues related to the fact that for drag-n-drop both methods are called. So I would appreciate your proposals how to simplify my logic to still achieve the goal.

The team member broke the page availability and I cannot test my page right away. Are you implying that I can use webkitRelativePath next to webkitdirectory and it will resolve my issue and besides I will be able to through away some of my code?

hackingharold commented 3 months ago

Please have a look at PR #80. This might fix your problem.

I first decided to ignore the relative path info, but I thinks it's time to add it back in. The only way to achieve a consistent behavior is to add a new property.

Please review the PR code. You might want to checkout the branch and try it locally. If this fits your needs, we may release a new version.

alexandis commented 3 months ago

Thank you. I will check and let you know. Another small question: if i am going to make the list of file replenishable (i.e. i can delete the selected files from the list, make selection again from other location merging with the previous selection, i.e. I am going to store the list of all files internally) - then what is the best way of using this component in terms of memory efficiency? <input type="file" fileInput (change)="onSelectDocuments($event)" [multiple]="true" />, so no model or control to store the selection or I still have to use formControl / ngModel and the relevant events?

UPDATE: I've cloned your repo and took PR branch, but I am not sure what to do next. We use Angular v15.2 in all the projects, you use version 18, so I don't know where my attempts will get me after all... :) Maybe I should have used another approach - just patch the existing code, not trying to install a patched version of the component?

hackingharold commented 3 months ago

After cloning the repo and npm install follow these steps to run a test app. Then upload some files and check if the relativePath property is set. You should be able to keep the file paths on drop AND when using the file picker. Migrating to Angular 18 is recommended anyway, as a workaround you could install the latest version with --force flag in your project.

About your other question, the internal state is stored in the value property. I would recommend to use a FormControl for the most flexible state management. Use the property mode (see config) to change the behavior. By default, a second file selection will overwrite the first one. But you may also "stack" all selected files.

alexandis commented 3 months ago

Hi Paul. Thank you, I've installed PR version to my project. Seems like it does not work fully correct or I am missing something. Here is my usage (for "folder selection" scenario):

  <ngx-mat-dropzone>
    <input type="file" fileInput [(ngModel)]="files" [ngModelOptions]="{ standalone: true }" [multiple]="true" relativePath mode="append" webkitdirectory (ngModelChange)="buildFolderOutput()" />
  </ngx-mat-dropzone>

When I select the folder in a system way, via button click - everything looks as expected: webkitRelativePath property is filled with the correct subfolder path. However, when I drag-n-drop it - webkitRelativePath property of its files is empty.

Another issue: I have the button to remove the selected files, which does this: this.files = []; However, when I select the files again, it pulls the previously selected files from somewhere and merges them with the ones currently selected...

And probably the last question here: how to avoid adding duplicates? It might look like a user responsibility, but maybe the component should offer some fool-proof mechanisms?

hackingharold commented 3 months ago

Hey, thanks for the feedback. You're almost there. On drop, the webkitRelativePath property will be empty, since I can't overwrite it with the full path value because it's readonly. Therefore, I added the new relativePath property which should have the expected value.

As for your other bug of clearing the array, I will have to look into this in the next few days. Maybe use a FormControl as a workaround?

When using mode="replace" (which is the default) on the <input type="file"> element, no duplicates should be possible because the files array will be reset on each change. Any other validation will be left to you as the consumer. You might check for duplicate file names in the FormControl valueChanges handler.

alexandis commented 3 months ago

Maybe I am missing something obvious, but on drag-n-drop console.log((file as any).relativePath) returns me undefined inside buildFolderOutput ngModelChange handler when I loop through files array... Probably it was taken care only for formControl scenario? I think it should be consistent for both scenarios, because someone will always prefer one over another.

Maybe use a FormControl as a workaround

I might check it out later on, but if it was possible to make it work for ngModel, I would be happy ;)

hackingharold commented 3 months ago

Okay, so let's try to figure this out. Please setup the local playground dev app as described in my previous post. With this minimum example I got it working as expected.

import { Component } from '@angular/core';
import { File } from '@ngx-dropzone/cdk';

@Component({
  selector: 'app-root',
  template: `<div class="app-container">
    <mat-form-field appearance="fill">
      <mat-label>Drop anything!</mat-label>
      <ngx-mat-dropzone>
        <input
          type="file"
          fileInput
          multiple
          webkitdirectory
          [(ngModel)]="files"
          [ngModelOptions]="{ standalone: true }"
          (ngModelChange)="buildFolderOutput()"
        />
      </ngx-mat-dropzone>
    </mat-form-field>
  </div>`,
})
export class AppComponent {
  files: File[] = [];

  buildFolderOutput() {
    this.files.forEach((f) => console.log(f.relativePath));
  }
}
hackingharold commented 3 months ago

To be able to reset the internal value, even with mode="append", I expanded the PR to add a new clear() method to the directive. Use it like so.

<input type="file" fileInput #fi="fileInput" mode="append" />

<button (click)="fi.clear()">Reset</button>
alexandis commented 3 months ago

Thank you, i will try this on Monday. Quick question: is it possible to delete a single item from files array? I should have asked that in the first place, not deleting all the elements, sorry.

hackingharold commented 3 months ago

Yes, if you have set mode="replace" you can just update your files array.

this.files = this.files.filter(f => f.name !== "test");
alexandis commented 3 months ago

Yes, if you have set mode="replace" you can just update your files array.

this.files = this.files.filter(f => f.name !== "test");

Well, I prefer this.files.splice(index, 1) - I think it might be faster... But anyway, the deletion of one item seems to be OK.

For the deletion of all files i've tried to use clear() method of the directive... But it does not always work. The matter is that the moment of clearing the model and creating the dragzone component are separated in time. So I have to use @ViewChild to access the directive. Though, sometimes it give me "TypeError: Cannot read properties of undefined (reading 'clear')" error.

I've tried to use this.files = [] instead, which supposedly has to work identically? But nevertheless I believe I saw some cases, when the previous "state" has not been cleared and I saw the previously selected files after clearing the list this way...

hackingharold commented 3 months ago

Thanks for your feedback again. I looked into this again and as it seems like I introduced some inconsistencies. I updated the implementation again and removed the clear() method. Please pull the latest branch changes and try

this.files = [];

again. It should now work as expected.

alexandis commented 2 months ago

Looks fine now, thank you.

UPDATE: on future check, I've observed yet another thing :)

When I select a folder targeting a whole USB flashdrive - both webkitRelativePath and relativePath look identical and predictable, i.e. let's say D:\info.txt. However, when I drag-n-drop a whole USB flashdrive - webkitRelativePath is empty (which is probably expected, since I here need to rely on relativePath), but relativePath itself contains a long weird prefix text, like "D_drive/System Volume Information/info.txt... Is it possible to uniform it - so the path always look in a predictable way, no matter if it's a local folder, external drive or network mapped path...

hackingharold commented 2 months ago

I want to interfere with the default Browser behavior as less as possible. Therefore, I will leave the handling of this edge case to the consumer (n this case you), depending on the use case.

I will now merge the PR and release a new version that you might want to pull into your project.

alexandis commented 2 months ago

Hi, @hackingharold.

I occasionally found out, that relativePath does not work anymore as it worked before - at least, on the latest version of Chrome. Here is the source file: image

Please have a look - this is what is looks like when I drag-n-drop a file: image

And here is what it looks like when I use a standard "open" way: image.

If I use webkitdirectory attribute on other page, where I need to select a whole folder - it does not look right either, you can check it out yourself...

hackingharold commented 1 month ago

Hey @alexandis,

I just tested this with ngx-dropzone@18.1.0 in Chrome 126 using my StackBlitz example and it's still working as expected. Note that the webkitdirectory attribute is required to be able to get the relative path when using the native file picker.

Please provide some more information if you cannot get it to work.

alexandis commented 1 month ago

Hi. I was able to repro the issue on your example with some modification based on my case. Interesting fact is that it USED TO work some time ago - that's why I thought it might be due to the change in the browser's security or something like that :) Also the question: if I need to use webkitdirectory always - how to make native file picker give me a file path, if i use it for a file selection, not for a folder selection?

Relative path is empty

Browsers Chrome Version 126.0.6478.183 (Official Build) (64-bit)

image

hackingharold commented 1 month ago

I am confused now, your example is working as expected for me. Please see the following table and explain what you expect differently:

webkitdirectory webkitdirectory
selecting file(s) from file picker relativePath is empty because no directory was selected not possible, forbidden by Browser
selecting folder from file picker not possible, forbidden by Browser relativePath is set based on selected folder
dropping file(s) relativePath equals the name because no directory was selected relativePath equals the name because no directory was selected
dropping folder relativePath is set based on dropped folder relativePath is set based on dropped folder

For the file picker, you have to decide to either allow folders or single files by setting the webkitdirectory attribute. This is native Browser behavior I cannot do anything about.

alexandis commented 1 month ago

Thank you, I understand now... A directory selection mode works well, you are right - my bad (indeed - it's RELATIVE path, not ABSOLUTE :)).

Regarding a file selection mode, which now I am coping with... So, to your knowledge, there's no any way to find out an absolute source path of the file to be uploaded via javascript?

hackingharold commented 1 month ago

No, reading the absolute path of a user-selected file is not possible for security reasons. You might want to read into the Browser's File System API, but this is out of scope of this library.

alexandis commented 1 month ago

Thank you so much.