Skip to content

Commit bbb1167

Browse files
authored
Merge pull request #319 from youda97/file-uploader
feat(file-uploader): Add file-uploader
2 parents f701aa6 + 9b4c9dc commit bbb1167

File tree

7 files changed

+435
-0
lines changed

7 files changed

+435
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface FileItem {
2+
file: File;
3+
state: "edit" | "upload" | "complete";
4+
uploaded: boolean;
5+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {
2+
Component,
3+
Input,
4+
Output,
5+
ViewChild,
6+
EventEmitter,
7+
OnInit
8+
} from "@angular/core";
9+
import { NG_VALUE_ACCESSOR } from "@angular/forms";
10+
11+
import { I18n } from "../i18n/i18n.module";
12+
import { FileItem } from "./file-item.interface";
13+
14+
const noop = () => {};
15+
16+
@Component({
17+
selector: "ibm-file-uploader",
18+
template: `
19+
<strong class="bx--label">{{title}}</strong>
20+
<p class="bx--label-description">{{description}}</p>
21+
<div class="bx--file">
22+
<button
23+
ibmButton="secondary"
24+
(click)="fileInput.click()"
25+
[attr.for]="fileUploaderId">
26+
{{buttonText}}
27+
</button>
28+
<input
29+
#fileInput
30+
type="file"
31+
class="bx--file-input"
32+
[accept]="accept"
33+
[id]="fileUploaderId"
34+
[multiple]="multiple"
35+
(change)="onFilesAdded()"/>
36+
<div class="bx--file-container">
37+
<ibm-file *ngFor="let fileItem of files" [fileItem]="fileItem" (remove)="removeFile(fileItem)"></ibm-file>
38+
</div>
39+
</div>
40+
`,
41+
providers: [
42+
{
43+
provide: NG_VALUE_ACCESSOR,
44+
useExisting: FileUploader,
45+
multi: true
46+
}
47+
]
48+
})
49+
export class FileUploader implements OnInit {
50+
/**
51+
* Counter used to create unique ids for file-uploader components
52+
*/
53+
static fileUploaderCount = 0;
54+
/**
55+
* Accessible text for the button that opens the upload window.
56+
*
57+
* Defaults to the `FILE_UPLOADER.OPEN` value from the i18n service
58+
*/
59+
@Input() buttonText = this.i18n.get().FILE_UPLOADER.OPEN;
60+
/**
61+
* Text set to the title
62+
*/
63+
@Input() title: string;
64+
/**
65+
* Text set to the description
66+
*/
67+
@Input() description: string;
68+
/**
69+
* Specify the types of files that the input should be able to receive
70+
*/
71+
@Input() accept = [];
72+
/**
73+
* Set to `false` to tell the component to only accept a single file on upload.
74+
*
75+
* Defaults to `true`. Accepts multiple files.
76+
*/
77+
@Input() multiple = true;
78+
/**
79+
* Provides a unique id for the underlying <input> node
80+
*/
81+
@Input() fileUploaderId = `file-uploader-${FileUploader.fileUploaderCount}`;
82+
/**
83+
* Maintains a reference to the view DOM element of the underlying <input> node
84+
*/
85+
@ViewChild("fileInput") fileInput;
86+
/**
87+
* The list of files that have been submitted to be uploaded
88+
*/
89+
@Input() files: Set<FileItem>;
90+
@Output() filesChange = new EventEmitter<any>();
91+
92+
protected onTouchedCallback: () => void = noop;
93+
protected onChangeCallback: (_: Set<FileItem>) => void = noop;
94+
95+
constructor(protected i18n: I18n) {
96+
FileUploader.fileUploaderCount++;
97+
}
98+
99+
/**
100+
* Specifies the property to be used as the return value to `ngModel`
101+
*/
102+
get value(): Set<FileItem> {
103+
return this.files;
104+
}
105+
set value(v: Set<FileItem>) {
106+
if (v !== this.files) {
107+
this.files = v;
108+
this.onChangeCallback(v);
109+
}
110+
}
111+
112+
ngOnInit() {
113+
// overrides the undefined files value set by the user
114+
if (!this.files) {
115+
this.files = new Set();
116+
this.filesChange.emit(this.files);
117+
}
118+
}
119+
120+
onBlur() {
121+
this.onTouchedCallback();
122+
}
123+
124+
/**
125+
* Propagates the injected `value`.
126+
*/
127+
writeValue(value: Set<FileItem>) {
128+
if (value !== this.value) {
129+
this.files = value;
130+
}
131+
}
132+
133+
onFilesAdded() {
134+
const files = this.fileInput.nativeElement.files;
135+
for (let file of files) {
136+
const fileItem: FileItem = {
137+
uploaded: false,
138+
state: "edit",
139+
file: file
140+
};
141+
this.files.add(fileItem);
142+
this.filesChange.emit(this.files);
143+
}
144+
145+
this.value = this.files;
146+
}
147+
148+
removeFile(fileItem) {
149+
this.files.delete(fileItem);
150+
this.fileInput.nativeElement.value = "";
151+
this.filesChange.emit(this.files);
152+
}
153+
154+
/**
155+
* Registers the injected function to control the touch use of the `FileUploader`.
156+
*/
157+
registerOnTouched(fn: any) {
158+
this.onTouchedCallback = fn;
159+
}
160+
/**
161+
* Sets a method in order to propagate changes back to the form.
162+
*/
163+
registerOnChange(fn: any) {
164+
this.onChangeCallback = fn;
165+
}
166+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NgModule } from "@angular/core";
2+
import { CommonModule } from "@angular/common";
3+
4+
import { FileUploader } from "./file-uploader.component";
5+
import { File } from "./file.component";
6+
import { ButtonModule } from "../button/button.module";
7+
import { LoadingModule } from "../loading/loading.module";
8+
9+
export { FileUploader } from "./file-uploader.component";
10+
11+
@NgModule({
12+
declarations: [FileUploader, File],
13+
exports: [FileUploader],
14+
imports: [CommonModule, ButtonModule, LoadingModule]
15+
})
16+
export class FileUploaderModule { }
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { Component, Input } from "@angular/core";
2+
import { storiesOf, moduleMetadata } from "@storybook/angular";
3+
4+
import { action } from "@storybook/addon-actions";
5+
import {
6+
withKnobs,
7+
boolean,
8+
text,
9+
array
10+
} from "@storybook/addon-knobs";
11+
12+
import { FileUploaderModule, NotificationModule, ButtonModule } from "../";
13+
import { NotificationService } from "../notification/notification.service";
14+
15+
@Component({
16+
selector: "app-file-uploader",
17+
template: `
18+
<ibm-file-uploader
19+
[title]="title"
20+
[description]="description"
21+
[buttonText]="buttonText"
22+
[accept]="accept"
23+
[multiple]="multiple"
24+
[(files)]="files">
25+
</ibm-file-uploader>
26+
27+
<div [id]="notificationId" style="width: 300px; margin-top: 20px"></div>
28+
<button ibmButton *ngIf="files && files.size > 0" (click)="onUpload()">
29+
Upload
30+
</button>
31+
`
32+
})
33+
class FileUploaderStory {
34+
static notificationCount = 0;
35+
36+
@Input() notificationId = `notification-${FileUploaderStory.notificationCount}`;
37+
@Input() files: any;
38+
@Input() title;
39+
@Input() description;
40+
@Input() buttonText;
41+
@Input() accept;
42+
@Input() multiple;
43+
44+
protected maxSize = 500000;
45+
46+
constructor(protected notificationService: NotificationService) {
47+
FileUploaderStory.notificationCount++;
48+
}
49+
50+
onUpload() {
51+
this.files.forEach(fileItem => {
52+
if (fileItem.file.size > this.maxSize) {
53+
this.notificationService.showNotification({
54+
type: "error",
55+
title: `'${fileItem.file.name}' exceeds size limit`,
56+
message: `500kb max size. Please select a new file and try again`,
57+
target: `#${this.notificationId}`
58+
});
59+
}
60+
});
61+
62+
let filesArray = Array.from<any>(this.files);
63+
if (filesArray.every(fileItem => fileItem.file.size <= this.maxSize)) {
64+
            this.files.forEach(fileItem => {
65+
                if (!fileItem.uploaded) {
66+
fileItem.state = "upload";
67+
setTimeout(() => {
68+
fileItem.state = "complete";
69+
fileItem.uploaded = true;
70+
console.log(fileItem);
71+
}, 1500);
72+
}
73+
});
74+
}
75+
}
76+
}
77+
78+
@Component({
79+
selector: "app-ngmodel-file-uploader",
80+
template: `
81+
<ibm-file-uploader
82+
[title]="title"
83+
[description]="description"
84+
[buttonText]="buttonText"
85+
[accept]="accept"
86+
[multiple]="multiple"
87+
[(ngModel)]="model">
88+
</ibm-file-uploader>
89+
90+
<br><div [id]="notificationId" style="width: 300px"></div>
91+
<button ibmButton *ngIf="model && model.size > 0" (click)="onUpload()">
92+
Upload
93+
</button>
94+
`
95+
})
96+
class NgModelFileUploaderStory {
97+
static notificationCount = 0;
98+
99+
@Input() notificationId = `notification-${FileUploaderStory.notificationCount}`;
100+
@Input() title;
101+
@Input() description;
102+
@Input() buttonText;
103+
@Input() accept;
104+
@Input() multiple;
105+
106+
protected model = new Set();
107+
protected maxSize = 500000;
108+
109+
constructor(protected notificationService: NotificationService) {
110+
FileUploaderStory.notificationCount++;
111+
}
112+
113+
onUpload() {
114+
this.model.forEach(fileItem => {
115+
if (fileItem.file.size > this.maxSize) {
116+
this.notificationService.showNotification({
117+
type: "error",
118+
title: `'${fileItem.file.name}' exceeds size limit`,
119+
message: `500kb max size. Please select a new file and try again`,
120+
target: `#${this.notificationId}`
121+
});
122+
}
123+
});
124+
125+
let filesArray = Array.from<any>(this.model);
126+
if (filesArray.every(fileItem => fileItem.file.size <= this.maxSize)) {
127+
            this.model.forEach(fileItem => {
128+
                if (!fileItem.uploaded) {
129+
fileItem.state = "upload";
130+
setTimeout(() => {
131+
fileItem.state = "complete";
132+
fileItem.uploaded = true;
133+
console.log(fileItem);
134+
}, 1500);
135+
}
136+
});
137+
}
138+
}
139+
}
140+
141+
storiesOf("File Uploader", module)
142+
.addDecorator(
143+
moduleMetadata({
144+
imports: [FileUploaderModule, NotificationModule, ButtonModule],
145+
declarations: [FileUploaderStory, NgModelFileUploaderStory]
146+
})
147+
)
148+
.addDecorator(withKnobs)
149+
.add("Basic", () => ({
150+
template: `
151+
<app-file-uploader
152+
[title]="title"
153+
[description]="description"
154+
[buttonText]="buttonText"
155+
[accept]="accept"
156+
[multiple]="multiple">
157+
</app-file-uploader>
158+
`,
159+
props: {
160+
title: text("The title", "Account Photo"),
161+
description: text("The description", "only .jpg and .png files. 500kb max file size."),
162+
buttonText: text("Button text", "Add files"),
163+
accept: array("Accepted file extensions", [".png", ".jpg"], ","),
164+
multiple: boolean("Supports multiple files", true)
165+
}
166+
}))
167+
.add("Using ngModel", () => ({
168+
template: `
169+
<app-ngmodel-file-uploader
170+
[title]="title"
171+
[description]="description"
172+
[buttonText]="buttonText"
173+
[accept]="accept"
174+
[multiple]="multiple">
175+
</app-ngmodel-file-uploader>
176+
`,
177+
props: {
178+
title: text("The title", "Account Photo"),
179+
description: text("The description", "only .jpg and .png files. 500kb max file size."),
180+
buttonText: text("Button text", "Add files"),
181+
accept: array("Accepted file extensions", [".png", ".jpg"], ","),
182+
multiple: boolean("Supports multiple files", true)
183+
}
184+
}));

0 commit comments

Comments
 (0)