diff --git a/src/components/form-file/README.md b/src/components/form-file/README.md
index 157b18ed912..467386ba54c 100644
--- a/src/components/form-file/README.md
+++ b/src/components/form-file/README.md
@@ -119,6 +119,73 @@ stylesheets. Also it is advised to use
Alternatively you can set the content of the custom file browse button text via the `browse-text`
prop. Note, only plain text is supported. HTML and components are not supported.
+## Customize the foratting of the selected file names
+
+By default, the custom styled file input lists the file names separated by commas. You can customize
+how the file names are shown either via a custom formatter function or the `file-name` scoped slot.
+
+### File name formatter functon
+
+Set the prop `file-name-formatter` to a function that accepts a single argument which is an array of
+[`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) objects. The function should return
+a single formatted string (HTML is not supported). The formatter will not be called if no files are
+selected.
+
+Regardless of if the prop `multiple` is set or not, the argument to the formatter will always be an
+array.
+
+```html
+
+
+
+
+
+
+
+```
+
+### File name formatting via scoped slot
+
+Alternatively, you can use the scoped slot `file-name` to render the file names. The scoped slot
+will receive the following properties:
+
+| Property | Type | Description |
+| -------- | ----- | ----------------------- |
+| `files` | Array | Array of `File` objects |
+| `names` | Array | Array of file names |
+
+Both properties are always arrays, regarless of the setting of the `multiple` prop.
+
+```html
+
+
+
+ {{ names[0] }}
+
+ + {{ names.length - 1 }} More files
+
+
+
+
+
+
+```
+
+When using the `file-name` slot, the `file-name-formatter` prop is ignored. Also, the slot will
+not be rendered when there are no file(s) selected.
+
## Non custom file input
You can have `` render a browser native file input by setting the `plain` prop. Note
diff --git a/src/components/form-file/form-file.js b/src/components/form-file/form-file.js
index 8945f06f962..3888e5be8c4 100644
--- a/src/components/form-file/form-file.js
+++ b/src/components/form-file/form-file.js
@@ -2,12 +2,13 @@ import idMixin from '../../mixins/id'
import formMixin from '../../mixins/form'
import formStateMixin from '../../mixins/form-state'
import formCustomMixin from '../../mixins/form-custom'
-import { from as arrayFrom, isArray } from '../../utils/array'
+import normalizeSlotMixin from '../../mixins/normalize-slot'
+import { from as arrayFrom, isArray, concat } from '../../utils/array'
// @vue/component
export default {
name: 'BFormFile',
- mixins: [idMixin, formMixin, formStateMixin, formCustomMixin],
+ mixins: [idMixin, formMixin, formStateMixin, formCustomMixin, normalizeSlotMixin],
props: {
value: {
// type: Object,
@@ -49,6 +50,10 @@ export default {
noDrop: {
type: Boolean,
default: false
+ },
+ fileNameFormatter: {
+ type: Function,
+ default: null
}
},
data() {
@@ -70,13 +75,23 @@ export default {
return this.placeholder
}
- // Multiple files
- if (this.multiple) {
- return this.selectedFile.map(file => file.name).join(', ')
- }
+ // Convert selectedFile to an array (if not already one)
+ const files = concat(this.selectedFile).filter(Boolean)
- // Single file
- return this.selectedFile.name
+ if (this.hasNormalizedSlot('file-name')) {
+ // There is a slot for formatting the files/names
+ return [
+ this.normalizeSlot('file-name', {
+ files: files,
+ names: files.map(f => f.name)
+ })
+ ]
+ } else {
+ // Use the user supplied formatter, or the built in one.
+ return typeof this.fileNameFormatter === 'function'
+ ? String(this.fileNameFormatter(files))
+ : files.map(file => file.name).join(', ')
+ }
}
},
watch: {
@@ -265,7 +280,8 @@ export default {
const label = h(
'label',
{
- class: ['custom-file-label', this.dragging ? 'dragging' : null],
+ staticClass: 'custom-file-label',
+ class: [this.dragging ? 'dragging' : null],
attrs: {
for: this.safeId(),
'data-browse': this.browseText || null
@@ -278,7 +294,8 @@ export default {
return h(
'div',
{
- class: ['custom-file', 'b-form-file', this.stateClass],
+ staticClass: 'custom-file b-form-file',
+ class: this.stateClass,
attrs: { id: this.safeId('_BV_file_outer_') },
on: {
dragover: this.onDragover,
diff --git a/src/components/form-file/form-file.spec.js b/src/components/form-file/form-file.spec.js
index ea22c0b1bb9..822c5fe8ae1 100644
--- a/src/components/form-file/form-file.spec.js
+++ b/src/components/form-file/form-file.spec.js
@@ -418,4 +418,69 @@ describe('form-file', () => {
wrapper.destroy()
})
+
+ it('file-name-formatter works', async () => {
+ let called = false
+ let filesIsArray = false
+ const wrapper = mount(Input, {
+ propsData: {
+ id: 'foo',
+ fileNameFormatter: files => {
+ called = true
+ filesIsArray = Array.isArray(files)
+ return 'foobar'
+ }
+ }
+ })
+ const file = new File(['foo'], 'foo.txt', {
+ type: 'text/plain',
+ lastModified: Date.now()
+ })
+
+ // Emulate the files array
+ wrapper.vm.setFiles([file])
+ expect(wrapper.emitted('input')).toBeDefined()
+ expect(wrapper.emitted('input').length).toEqual(1)
+ expect(wrapper.emitted('input')[0][0]).toEqual(file)
+
+ // Formatter should have been called, and passed an array
+ expect(called).toBe(true)
+ expect(filesIsArray).toBe(true)
+ // Should have our custom formatted "filename"
+ expect(wrapper.find('label').text()).toContain('foobar')
+
+ wrapper.destroy()
+ })
+
+ it('file-name slot works', async () => {
+ let slotScope = null
+ const wrapper = mount(Input, {
+ propsData: {
+ id: 'foo'
+ },
+ scopedSlots: {
+ 'file-name': scope => {
+ slotScope = scope
+ return 'foobar'
+ }
+ }
+ })
+ const file = new File(['foo'], 'foo.txt', {
+ type: 'text/plain',
+ lastModified: Date.now()
+ })
+
+ // Emulate the files array
+ wrapper.vm.setFiles([file])
+ expect(wrapper.emitted('input')).toBeDefined()
+ expect(wrapper.emitted('input').length).toEqual(1)
+ expect(wrapper.emitted('input')[0][0]).toEqual(file)
+
+ // scoped slot should have been called, with expected scope
+ expect(slotScope).toEqual({ files: [file], names: [file.name] })
+ // Should have our custom formatted "filename"
+ expect(wrapper.find('label').text()).toContain('foobar')
+
+ wrapper.destroy()
+ })
})
diff --git a/src/components/form-file/package.json b/src/components/form-file/package.json
index dd986c4b728..68cb4db2bc3 100644
--- a/src/components/form-file/package.json
+++ b/src/components/form-file/package.json
@@ -9,6 +9,12 @@
"aliases": [
"BFile"
],
+ "slots": [
+ {
+ "name": "file-name",
+ "description": "Scoped slot for formatting the file names. Scoped props: files - array of File objects, names: array of file names"
+ }
+ ],
"events": [
{
"event": "change",