From 8836b03a8e18c7c0c560ed6778c5db607e831fcd Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 22 Mar 2019 17:47:55 -0300 Subject: [PATCH 1/9] feat(form-file): add in prop for providing custom selected files formatting Adds in a new prop `file-name-formatter`, which accepts a function that can return the file names formatted as the user sees fit. The optional formatter is called with a single argument which is an array of File object(s). It should return a string (no HTML supported). --- src/components/form-file/form-file.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/form-file/form-file.js b/src/components/form-file/form-file.js index 8945f06f962..5f51489691c 100644 --- a/src/components/form-file/form-file.js +++ b/src/components/form-file/form-file.js @@ -2,7 +2,7 @@ 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 { from as arrayFrom, isArray, concat } from '../../utils/array' // @vue/component export default { @@ -49,6 +49,10 @@ export default { noDrop: { type: Boolean, default: false + }, + fileNameFormatter: { + type: Function, + default: null } }, data() { @@ -70,13 +74,12 @@ export default { return this.placeholder } - // Multiple files - if (this.multiple) { - return this.selectedFile.map(file => file.name).join(', ') - } - - // Single file - return this.selectedFile.name + // Convert selectedFile to an array (if not already one) + const files = concat(this.selectedFile) + // 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: { From 72707590747a869455cfcb65b6ae980c79fe34f5 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 22 Mar 2019 18:11:09 -0300 Subject: [PATCH 2/9] ads scoped slot file-name --- src/components/form-file/form-file.js | 30 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/components/form-file/form-file.js b/src/components/form-file/form-file.js index 5f51489691c..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 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, @@ -75,11 +76,22 @@ export default { } // Convert selectedFile to an array (if not already one) - const files = concat(this.selectedFile) - // 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(', ') + const files = concat(this.selectedFile).filter(Boolean) + + 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: { @@ -268,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 @@ -281,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, From af9997fd62ce37a15d31353e81df6c52eb597e7d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 22 Mar 2019 18:57:12 -0300 Subject: [PATCH 3/9] Update form-file.spec.js --- src/components/form-file/form-file.spec.js | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/components/form-file/form-file.spec.js b/src/components/form-file/form-file.spec.js index ea22c0b1bb9..4c4af012b8a 100644 --- a/src/components/form-file/form-file.spec.js +++ b/src/components/form-file/form-file.spec.js @@ -418,4 +418,35 @@ 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) + + expect(called).toBe(true) + // Should have our custom formatted "filename" + expect(wrapper.find('label').text()).toContain('foobar') + + wrapper.destroy() + }) }) From 351ac39fc5aea0baf917eeee2ae55bbbfa4f5d13 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 22 Mar 2019 19:00:28 -0300 Subject: [PATCH 4/9] Update form-file.spec.js --- src/components/form-file/form-file.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/form-file/form-file.spec.js b/src/components/form-file/form-file.spec.js index 4c4af012b8a..c0f24734eb2 100644 --- a/src/components/form-file/form-file.spec.js +++ b/src/components/form-file/form-file.spec.js @@ -443,7 +443,9 @@ describe('form-file', () => { 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') From 1f52e9317550699f39f7e4ae980845bcd1fc7798 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 22 Mar 2019 19:08:24 -0300 Subject: [PATCH 5/9] Update form-file.spec.js --- src/components/form-file/form-file.spec.js | 34 +++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/components/form-file/form-file.spec.js b/src/components/form-file/form-file.spec.js index c0f24734eb2..fc6076b5ce4 100644 --- a/src/components/form-file/form-file.spec.js +++ b/src/components/form-file/form-file.spec.js @@ -443,7 +443,7 @@ describe('form-file', () => { expect(wrapper.emitted('input').length).toEqual(1) expect(wrapper.emitted('input')[0][0]).toEqual(file) - // FOrmatter should have been called, and passed an array + // Formatter should have been called, and passed an array expect(called).toBe(true) expect(filesIsArray).toBe(true) // Should have our custom formatted "filename" @@ -451,4 +451,36 @@ describe('form-file', () => { 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() + }) }) From fefe4ba2c9b49fff048094fa3aa90aa759565ff1 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 22 Mar 2019 19:10:36 -0300 Subject: [PATCH 6/9] Update form-file.spec.js --- src/components/form-file/form-file.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/form-file/form-file.spec.js b/src/components/form-file/form-file.spec.js index fc6076b5ce4..822c5fe8ae1 100644 --- a/src/components/form-file/form-file.spec.js +++ b/src/components/form-file/form-file.spec.js @@ -459,7 +459,7 @@ describe('form-file', () => { id: 'foo' }, scopedSlots: { - 'file-name': (scope) => { + 'file-name': scope => { slotScope = scope return 'foobar' } From 9d04dc602cb833e922ebb0cd55ecf8622b923a4e Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 22 Mar 2019 19:34:22 -0300 Subject: [PATCH 7/9] Update package.json --- src/components/form-file/package.json | 6 ++++++ 1 file changed, 6 insertions(+) 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", From ccd428102634896134c1fe838bbdec96e7f7dc7e Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 22 Mar 2019 20:08:27 -0300 Subject: [PATCH 8/9] Update README.md --- src/components/form-file/README.md | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/components/form-file/README.md b/src/components/form-file/README.md index 157b18ed912..787d9c986ad 100644 --- a/src/components/form-file/README.md +++ b/src/components/form-file/README.md @@ -119,6 +119,72 @@ 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 From 05d5d8dbe56314328464085cca1f1715490c3f1f Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 22 Mar 2019 20:17:44 -0300 Subject: [PATCH 9/9] Update README.md --- src/components/form-file/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/form-file/README.md b/src/components/form-file/README.md index 787d9c986ad..467386ba54c 100644 --- a/src/components/form-file/README.md +++ b/src/components/form-file/README.md @@ -172,8 +172,9 @@ Both properties are always arrays, regarless of the setting of the `multiple` pr