Skip to content

feat(form-file): add in prop and scoped slot for formatting selected file names #2902

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/components/form-file/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<template>
<b-form-file multiple :file-name-formatter="formatNames"></b-form-file>
</template>

<script>
export default {
methods: {
formatNames(files) {
if (files.length === 1) {
return files[0].name
} else {
return `${files.length} files selected`
}
}
}
}
</script>

<!-- file-formatter-function.vue -->
```

### 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
<template>
<b-form-file multiple>
<template slot="file-name" slot-scope="{ names }">
<b-badge variant="dark">{{ names[0] }}</b-badge>
<b-badge v-if="names.length > 1" variant="dark" class="ml-1">
+ {{ names.length - 1 }} More files
</b-badge>
</template>
</b-form-file>
</template>

<!-- file-formatter-slot.vue -->
```

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 `<b-form-file>` render a browser native file input by setting the `plain` prop. Note
Expand Down
37 changes: 27 additions & 10 deletions src/components/form-file/form-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -49,6 +50,10 @@ export default {
noDrop: {
type: Boolean,
default: false
},
fileNameFormatter: {
type: Function,
default: null
}
},
data() {
Expand All @@ -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: {
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
65 changes: 65 additions & 0 deletions src/components/form-file/form-file.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
6 changes: 6 additions & 0 deletions src/components/form-file/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down