Skip to content

Commit ac298b9

Browse files
committed
feat: add configurability to file name and actions
- updated file component to accept name and actions templates to render in place of the default name and actions - updated file uploader component to accept file name and actions templates to forward to the inner file components - added storybook examples for file and file uploader components - replaced close icon to trash can icon for the default remove icon
1 parent ca741fd commit ac298b9

File tree

9 files changed

+369
-35
lines changed

9 files changed

+369
-35
lines changed

src/file-uploader/file-uploader.component.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ const noop = () => { };
7373
<ng-container *ngFor="let fileItem of files">
7474
<cds-file
7575
[fileItem]="fileItem"
76-
(remove)="removeFile(fileItem)"
77-
[size]="fileItemSize">
76+
[nameTpl]="fileNameTpl"
77+
[actionsTpl]="fileActionsTpl"
78+
[size]="fileItemSize"
79+
(remove)="removeFile(fileItem)">
7880
</cds-file>
7981
</ng-container>
8082
</div>
@@ -164,6 +166,14 @@ export class FileUploader implements ControlValueAccessor {
164166
* Set to `true` to disable upload button
165167
*/
166168
@Input() disabled = false;
169+
/**
170+
* Custom template used to render the file name of uploaded files
171+
*/
172+
@Input() fileNameTpl: TemplateRef<unknown>
173+
/**
174+
* Custom template used to render the file actions of uploaded files
175+
*/
176+
@Input() fileActionsTpl: TemplateRef<unknown>;
167177

168178
@Output() filesChange = new EventEmitter<any>();
169179

src/file-uploader/file-uploader.stories.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
/* tslint:disable variable-name */
2-
31
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
4-
import { moduleMetadata, Meta } from "@storybook/angular";
5-
import { FileUploaderModule, FileUploader } from "./";
6-
import { NotificationModule } from "../notification";
2+
import { Meta, moduleMetadata } from "@storybook/angular";
73
import { ButtonModule } from "../button";
4+
import { IconModule } from "../icon";
5+
import { NotificationModule } from "../notification";
6+
import { FileUploader, FileUploaderModule } from "./";
87
import {
8+
CustomFileIconsModule,
9+
DragAndDropStory,
910
FileUploaderStory,
11+
FileUploaderWithCustomFileStory,
1012
NgModelFileUploaderStory,
11-
DragAndDropStory,
1213
ReactiveFormsStory
1314
} from "./stories";
1415

@@ -18,16 +19,19 @@ export default {
1819
moduleMetadata({
1920
declarations: [
2021
FileUploaderStory,
22+
FileUploaderWithCustomFileStory,
2123
NgModelFileUploaderStory,
2224
DragAndDropStory,
2325
ReactiveFormsStory
2426
],
2527
imports: [
28+
ButtonModule,
29+
CustomFileIconsModule,
2630
FileUploaderModule,
2731
FormsModule,
28-
ReactiveFormsModule,
32+
IconModule,
2933
NotificationModule,
30-
ButtonModule
34+
ReactiveFormsModule
3135
]
3236
})
3337
],
@@ -81,6 +85,37 @@ const Template = (args) => ({
8185
});
8286
export const Basic = Template.bind({});
8387

88+
const CustomFile = (args) => ({
89+
props: args,
90+
template: `
91+
<!--
92+
app-* components are for demo purposes only.
93+
You can create your own implementation by using the component source found at:
94+
https://github.com/IBM/carbon-components-angular/tree/master/src/file-uploader/stories/uploader-custom-file.component.ts
95+
-->
96+
<app-file-uploader-with-custom-file
97+
[title]="title"
98+
[description]="description"
99+
[buttonText]="buttonText"
100+
[buttonType]="buttonType"
101+
[accept]="accept"
102+
[multiple]="multiple"
103+
[size]="size"
104+
[fileItemSize]="fileItemSize"
105+
[disabled]="disabled">
106+
</app-file-uploader-with-custom-file>
107+
`
108+
});
109+
export const UploaderWithCustomFile = CustomFile.bind({});
110+
UploaderWithCustomFile.argTypes = {
111+
size: {
112+
control: false
113+
},
114+
buttonType: {
115+
control: false
116+
}
117+
};
118+
84119
const DragAndDropTemplate = (args) => ({
85120
props: args,
86121
template: `

src/file-uploader/file.component.ts

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {
22
Component,
3-
Input,
4-
Output,
53
EventEmitter,
64
HostBinding,
7-
OnDestroy
5+
Input,
6+
OnDestroy,
7+
Output,
8+
TemplateRef,
89
} from "@angular/core";
910

1011
import { I18n } from "carbon-components-angular/i18n";
@@ -13,45 +14,74 @@ import { FileItem } from "./file-item.interface";
1314
@Component({
1415
selector: "cds-file, ibm-file",
1516
template: `
16-
<p class="cds--file-filename" [title]="fileItem.file.name">{{fileItem.file.name}}</p>
17+
<p class="cds--file-filename" [title]="fileItem.file.name">
18+
<ng-template
19+
*ngIf="isTemplate(nameTpl); else defaultName"
20+
[ngTemplateOutlet]="nameTpl"
21+
[ngTemplateOutletContext]="{ $implicit: fileItem }"
22+
>
23+
</ng-template>
24+
<ng-template #defaultName>{{ fileItem.file.name }}</ng-template>
25+
</p>
1726
<span
1827
*ngIf="fileItem.state === 'edit'"
19-
class="cds--file__state-container">
28+
class="cds--file__state-container"
29+
>
2030
<svg
2131
*ngIf="isInvalidText"
2232
cdsIcon="warning--filled"
2333
class="cds--file--invalid"
24-
size="16">
25-
</svg>
26-
<button
27-
type="button"
28-
class="cds--file-close"
29-
[attr.aria-label]="translations.REMOVE_BUTTON"
30-
tabindex="0"
31-
(click)="remove.emit()"
32-
(keyup.enter)="remove.emit()"
33-
(keyup.space)="remove.emit()">
34-
<svg cdsIcon="close" size="16"></svg>
35-
</button>
34+
size="16"
35+
></svg>
36+
<ng-template
37+
*ngIf="isTemplate(actionsTpl); else defaultActions"
38+
[ngTemplateOutlet]="actionsTpl"
39+
>
40+
</ng-template>
41+
<ng-template #defaultActions>
42+
<button
43+
type="button"
44+
cdsButton="ghost"
45+
iconOnly="true"
46+
[size]="size"
47+
[attr.aria-label]="translations.REMOVE_BUTTON"
48+
(click)="remove.emit()"
49+
(keyup.enter)="remove.emit()"
50+
(keyup.space)="remove.emit()"
51+
>
52+
<svg cdsIcon="trash-can" size="16"></svg>
53+
</button>
54+
</ng-template>
3655
</span>
3756
<span *ngIf="fileItem.state === 'upload'">
3857
<div class="cds--inline-loading__animation">
3958
<cds-loading size="sm"></cds-loading>
4059
</div>
4160
</span>
42-
<span *ngIf="fileItem.state === 'complete'" class="cds--file__state-container">
61+
<span
62+
*ngIf="fileItem.state === 'complete'"
63+
class="cds--file__state-container"
64+
>
4365
<svg
4466
cdsIcon="checkmark--filled"
4567
size="16"
4668
class="cds--file-complete"
47-
[ariaLabel]="translations.CHECKMARK">
48-
</svg>
69+
[ariaLabel]="translations.CHECKMARK"
70+
></svg>
4971
</span>
50-
<div class="cds--form-requirement" role="alert" *ngIf="fileItem.invalid">
51-
<div class="cds--form-requirement__title">{{fileItem.invalidTitle}}</div>
52-
<p class="cds--form-requirement__supplement">{{fileItem.invalidText}}</p>
72+
<div
73+
class="cds--form-requirement"
74+
role="alert"
75+
*ngIf="fileItem.invalid"
76+
>
77+
<div class="cds--form-requirement__title">
78+
{{ fileItem.invalidTitle }}
79+
</div>
80+
<p class="cds--form-requirement__supplement">
81+
{{ fileItem.invalidText }}
82+
</p>
5383
</div>
54-
`
84+
`,
5585
})
5686
export class FileComponent implements OnDestroy {
5787
/**
@@ -65,11 +95,22 @@ export class FileComponent implements OnDestroy {
6595

6696
@Input() size: "sm" | "md" | "lg" = "lg";
6797

98+
/**
99+
* A custom template for the file name
100+
*/
101+
@Input() nameTpl: TemplateRef<unknown>;
102+
103+
/**
104+
* A custom template for the available file actions
105+
*/
106+
@Input() actionsTpl: TemplateRef<unknown>;
107+
68108
@Output() remove = new EventEmitter();
69109

70110
@HostBinding("class.cds--file__selected-file") selectedFile = true;
71111

72-
@HostBinding("class.cds--file__selected-file--invalid") get isInvalidText() {
112+
@HostBinding("class.cds--file__selected-file--invalid")
113+
get isInvalidText() {
73114
return this.fileItem.invalidText;
74115
}
75116

@@ -84,8 +125,13 @@ export class FileComponent implements OnDestroy {
84125
@HostBinding("class.cds--file__selected-file--lg") get fileSizeLarge() {
85126
return this.size === "lg";
86127
}
128+
87129
constructor(protected i18n: I18n) {}
88130

131+
public isTemplate(value: unknown): boolean {
132+
return value instanceof TemplateRef;
133+
}
134+
89135
ngOnDestroy() {
90136
this.remove.emit();
91137
}

src/file-uploader/file.stories.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Meta, moduleMetadata } from "@storybook/angular";
2+
3+
import { ButtonModule } from "../button";
4+
import { IconModule } from "../icon";
5+
import { FileComponent, FileUploaderModule } from "./";
6+
import { BasicFileStory, CustomFileIconsModule, CustomFileStory } from "./stories";
7+
8+
export default {
9+
title: "Components/File",
10+
decorators: [
11+
moduleMetadata({
12+
declarations: [BasicFileStory, CustomFileStory],
13+
imports: [
14+
ButtonModule,
15+
CustomFileIconsModule,
16+
FileUploaderModule,
17+
IconModule
18+
],
19+
}),
20+
],
21+
args: {
22+
size: "md",
23+
},
24+
argTypes: {
25+
size: {
26+
options: ["sm", "md", "lg"],
27+
control: "radio",
28+
},
29+
},
30+
component: FileComponent,
31+
} as Meta;
32+
33+
const BasicFileTemplate = (args) => ({
34+
props: args,
35+
size: {
36+
options: ["sm", "md", "lg"],
37+
control: "radio",
38+
},
39+
template: `
40+
<!--
41+
app-* components are for demo purposes only.
42+
You can create your own implementation by using the component source found at:
43+
https://github.com/IBM/carbon-components-angular/tree/master/src/file-uploader/stories/basic-file.component.ts
44+
-->
45+
<app-basic-file [size]="size"></app-basic-file>
46+
`,
47+
});
48+
export const BasicFile = BasicFileTemplate.bind({});
49+
50+
const CustomFileTemplate = (args) => ({
51+
props: args,
52+
size: {
53+
options: ["sm", "md", "lg"],
54+
control: "radio",
55+
},
56+
template: `
57+
<!--
58+
app-* components are for demo purposes only.
59+
You can create your own implementation by using the component source found at:
60+
https://github.com/IBM/carbon-components-angular/tree/master/src/file-uploader/stories/custom-file.component.ts
61+
-->
62+
<app-custom-file [size]="size"></app-custom-file>
63+
`,
64+
});
65+
export const CustomFile = CustomFileTemplate.bind({});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Component, Input } from "@angular/core";
2+
3+
@Component({
4+
selector: "app-basic-file",
5+
template: `<cds-file [size]="size" [fileItem]="fileItem"></cds-file>`,
6+
})
7+
export class BasicFileStory {
8+
@Input() size = "sm";
9+
10+
fileItem = {
11+
file: new File(["foo"], "Lorem ipsum dolor sit amet.txt", { type: "text/plain" }),
12+
state: "edit",
13+
};
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { CommonModule } from "@angular/common";
2+
import { NgModule } from "@angular/core";
3+
import Download16 from "@carbon/icons/es/download/16";
4+
import View16 from "@carbon/icons/es/view/16";
5+
6+
import { IconModule, IconService } from "../../icon";
7+
8+
@NgModule({
9+
imports: [CommonModule, IconModule],
10+
})
11+
export class CustomFileIconsModule {
12+
constructor(private iconService: IconService) {
13+
this.iconService.registerAll([Download16, View16]);
14+
}
15+
}

0 commit comments

Comments
 (0)