Skip to content

Commit 5d216d5

Browse files
feat(#79): add relative path property to keep tree structures.
1 parent bee7641 commit 5d216d5

File tree

7 files changed

+85
-18
lines changed

7 files changed

+85
-18
lines changed

bun.lockb

1.23 KB
Binary file not shown.

package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,29 @@
1414
"lint:material": "ng lint material"
1515
},
1616
"dependencies": {
17-
"@angular/animations": "^18.0.1",
18-
"@angular/cdk": "~18.0.1",
19-
"@angular/common": "^18.0.1",
20-
"@angular/compiler": "^18.0.1",
21-
"@angular/core": "^18.0.1",
22-
"@angular/forms": "^18.0.1",
23-
"@angular/material": "~18.0.1",
24-
"@angular/platform-browser": "^18.0.1",
25-
"@angular/platform-browser-dynamic": "^18.0.1",
26-
"@angular/router": "^18.0.1",
17+
"@angular/animations": "^18.0.2",
18+
"@angular/cdk": "~18.0.2",
19+
"@angular/common": "^18.0.2",
20+
"@angular/compiler": "^18.0.2",
21+
"@angular/core": "^18.0.2",
22+
"@angular/forms": "^18.0.2",
23+
"@angular/material": "~18.0.2",
24+
"@angular/platform-browser": "^18.0.2",
25+
"@angular/platform-browser-dynamic": "^18.0.2",
26+
"@angular/router": "^18.0.2",
2727
"rxjs": "~7.8.1",
2828
"tslib": "^2.3.0",
2929
"zone.js": "^0.14.2"
3030
},
3131
"devDependencies": {
32-
"@angular-devkit/build-angular": "^18.0.2",
32+
"@angular-devkit/build-angular": "^18.0.3",
3333
"@angular-eslint/builder": "^18.0.1",
3434
"@angular-eslint/eslint-plugin": "18.0.1",
3535
"@angular-eslint/eslint-plugin-template": "18.0.1",
3636
"@angular-eslint/schematics": "^18.0.1",
3737
"@angular-eslint/template-parser": "18.0.1",
38-
"@angular/cli": "^18.0.2",
39-
"@angular/compiler-cli": "^18.0.1",
38+
"@angular/cli": "^18.0.3",
39+
"@angular/compiler-cli": "^18.0.2",
4040
"@types/jasmine": "~3.8.0",
4141
"@types/node": "^20.5.9",
4242
"@typescript-eslint/eslint-plugin": "^6.10.0",

projects/cdk/src/lib/dropzone/dropzone.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@angular/core';
22
import { nonNullable } from '../coercion';
3+
import { File } from './../file-input';
34

45
@Injectable({
56
providedIn: 'root',
@@ -43,6 +44,10 @@ export class DropzoneService {
4344

4445
if (this._isFile(entry)) {
4546
const file = await this._readFilePromise(entry);
47+
48+
// Manually set the `relativePath` property for dropped files.
49+
file.relativePath = entry.fullPath?.slice(1) ?? '';
50+
4651
return [file];
4752
}
4853

projects/cdk/src/lib/file-input/file-input-value.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
/**
2+
* To achieve a consistent behavior when dropping directories,
3+
* we add an additional property to the `File` object.
4+
* This will only work with the `webkitdirectories` attribute.
5+
*/
6+
export type File = globalThis.File & { relativePath?: string };
7+
18
/**
29
* A file input element can either be empty,
310
* hold a single file or hold multiple files.

projects/cdk/src/lib/file-input/file-input.directive.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ describe('FileInputDirective', () => {
8080
expect(element.selectionChange.emit).toHaveBeenCalledWith(file);
8181
});
8282

83+
it('should reset value on clear', () => {
84+
const element = selectors.fileInput;
85+
const file = getFile();
86+
87+
element._fileValue = file;
88+
expect(element.value).toEqual(file);
89+
90+
element.writeValue(null);
91+
expect(element.value).toBeNull();
92+
});
93+
8394
it('should return the focused state correctly', () => {
8495
const element = selectors.fileInput;
8596
const input = selectors.inputElement.nativeElement as HTMLInputElement;
@@ -175,6 +186,28 @@ describe('FileInputDirective', () => {
175186
element.handleFileDrop(files2);
176187
expect(element.value).toEqual(files2);
177188
});
189+
190+
it('should replace value with empty array', () => {
191+
const element = selectors.fileInput;
192+
const files = [getFile(), getFile()];
193+
194+
element.handleFileDrop(files);
195+
expect(element.value).toEqual(files);
196+
197+
element.handleFileDrop([]);
198+
expect(element.value).toEqual([]);
199+
});
200+
201+
it('should reset multiple value when overriding value', () => {
202+
const element = selectors.fileInput;
203+
const files = [getFile(), getFile()];
204+
205+
element.handleFileDrop(files);
206+
expect(element.value).toEqual(files);
207+
208+
element.writeValue([]);
209+
expect(element.value).toEqual([]);
210+
});
178211
});
179212

180213
describe('mode=append', () => {

projects/cdk/src/lib/file-input/file-input.directive.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class FileInputDirective implements ControlValueAccessor, OnInit, OnChang
5757
if (newValue !== this._value || Array.isArray(newValue)) {
5858
this._assertMultipleValue(newValue);
5959

60-
this._value = this._appendOrReplace(newValue);
60+
this._value = newValue;
6161
this._updateErrorState();
6262

6363
this._onTouched?.();
@@ -178,7 +178,10 @@ export class FileInputDirective implements ControlValueAccessor, OnInit, OnChang
178178
@HostListener('change', ['$event.target.files'])
179179
_handleChange(fileList: FileList) {
180180
if (this.disabled) return;
181-
this._fileValue = this.multiple ? Array.from(fileList) : fileList.item(0);
181+
182+
const files = this.multiple ? Array.from(fileList) : fileList.item(0);
183+
const filesWithPaths = this._copyRelativePaths(files);
184+
this._fileValue = this._appendOrReplace(filesWithPaths);
182185

183186
this.selectionChange.emit(this._fileValue);
184187
this._onChange?.(this._fileValue);
@@ -190,7 +193,7 @@ export class FileInputDirective implements ControlValueAccessor, OnInit, OnChang
190193
/** Handles the drop of a file array. */
191194
handleFileDrop(files: File[]) {
192195
if (this.disabled) return;
193-
this._fileValue = this.multiple ? files : files[0];
196+
this._fileValue = this._appendOrReplace(this.multiple ? files : files[0]);
194197

195198
this.selectionChange.emit(this._fileValue);
196199
this._onChange?.(this._fileValue);
@@ -225,6 +228,25 @@ export class FileInputDirective implements ControlValueAccessor, OnInit, OnChang
225228
}
226229
}
227230

231+
/**
232+
* On directory drops, the readonly `webkitRelativePath` property is not available.
233+
* We manually set the `relativePath` property for dropped file trees instead.
234+
* To achieve a consistent behavior when using the file picker, we copy the value.
235+
*/
236+
private _copyRelativePaths(value: FileInputValue) {
237+
if (!value) return value;
238+
239+
if (Array.isArray(value)) {
240+
return value.map((file) => {
241+
file.relativePath = file.webkitRelativePath;
242+
return file;
243+
});
244+
}
245+
246+
value.relativePath = value.webkitRelativePath;
247+
return value;
248+
}
249+
228250
/** Asserts that the provided value type matches the input element's multiple attribute. */
229251
private _assertMultipleValue(value: FileInputValue) {
230252
if (this.multiple && !Array.isArray(value || [])) {

readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ Now you can use it in your markup.
7878
</mat-form-field>
7979
```
8080

81-
The dropzone supports dropping folders by default.
82-
All files from subdirectories will be provided as a flat `File[]`.
81+
Use the `webkitdirectories` attribute to support uploading folders.
82+
All files from subdirectories will be provided as a flat `File[]`, but with an additional `relativePath` property to keep tree structures.
8383

8484
## Usage with FormControl and validation
8585

0 commit comments

Comments
 (0)