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 + + + +``` + +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",