Skip to content
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

Refactor to a controlled file input #388

Merged
merged 17 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 2 additions & 2 deletions .storybook/styles/components/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ Uploader
}
}

.button-secondary-light>input{
input[type="file"]{
position:absolute;
top:0;
right:0;
Expand Down Expand Up @@ -572,4 +572,4 @@ Color Picker
.hex{
@include position(absolute, 35px null null 38px);
}
}
}
1 change: 0 additions & 1 deletion docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,6 @@ A component passed using `previewComponent` will receive the following props:
- `input` **[Object][142]** A `redux-forms` [input][140] object
- `meta` **[Object][142]** A `redux-forms` [meta][143] object
- `multiple` **[Boolean][136]** A flag indicating whether or not to accept multiple files (optional, default `false`)
- `onLoad` **[Function][135]?** A callback fired when the file is loaded
- `onRemove` **[Function][135]?** A callback fired when the file is removed (only available when multiple files can be uploaded)
- `thumbnail` **[String][134]?** A placeholder image to display before the file is loaded
- `hidePreview` **[Boolean][136]** A flag indicating whether or not to hide the file preview (optional, default `false`)
Expand Down
9 changes: 9 additions & 0 deletions src/forms/helpers/cast-form-value-to-array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { castArray } from '../../utils'

// Casts value(s) to an array
function castFormValueToArray (value) {
if (!value) return []
return castArray(value)
}

export default castFormValueToArray
12 changes: 12 additions & 0 deletions src/forms/helpers/field-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,15 @@ export const checkboxGroupPropTypes = fieldPropTypesWithValue(
])
)
)

const file = PropTypes.shape({
file: PropTypes.shape({
name: PropTypes.string.isRequired,
}).isRequired,
fileUpload: PropTypes.object,
fileData: PropTypes.string,
})

export const fileInputPropTypes = fieldPropTypesWithValue(
PropTypes.arrayOf(file).isRequired,
)
2 changes: 2 additions & 0 deletions src/forms/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export blurDirty from './blur-dirty'
export buttonClasses from './button-classes'
export castFormValueToArray from './cast-form-value-to-array'
export convertNameToLabel from './convert-name-to-label'
export DropdownSelect from './dropdown-select'
export fromHex from './from-hex'
Expand All @@ -9,3 +10,4 @@ export omitLabelProps from './omit-label-props'
export replaceEmptyStringValue from './replace-empty-string-value'
export toHex from './to-hex'
export hasInputError from './has-input-error'
export readFilesAsDataUrls from './read-files-as-data-urls'
39 changes: 39 additions & 0 deletions src/forms/helpers/read-files-as-data-urls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Reads files and returns objects with file information and base64 encoded string url
async function readFilesAsDataUrls (files) {
const filePromises = files.map(async (file) => {
const fileData = await readFile(file)
return createFileValueObject(file, fileData)
})

return Promise.all(filePromises)
}

// ----- PRIVATE ------

// Read a file and convert it to a base64 string (promisified)
function readFile (file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (readEvent) => {
resolve(readEvent.target.result)
}
reader.onerror = reject

reader.readAsDataURL(file)
})
}

// Copy metadata related to a file, but not the actual File object. These
// properties are not enumerable / visible in Redux.
function createFileValueObject (file, dataUrl='') {
return {
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
url: dataUrl,
}
}

export default readFilesAsDataUrls

54 changes: 37 additions & 17 deletions src/forms/inputs/cloudinary-file-input.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
import FileInput from './file-input'
import { fieldPropTypes } from '../helpers'
import { compose, cloudinaryUploader, noop } from '../../utils'
import DefaultFileInput from './file-input'
import { fileInputPropTypes, readFilesAsDataUrls } from '../helpers'
import { compose, cloudinaryUploader, noop, set } from '../../utils'
import classnames from 'classnames'

/**
*
* A wrapper around the {@link FileInput} component that automatically uploads files to cloudinary via the [cloudinaryUploader](https://github.com/LaunchPadLab/lp-hoc/blob/master/docs.md#cloudinaryuploader) HOC.
* The value of this input is the public URL of the uploaded file.
* Additionally, the `uploadStatus` passed down from `cloudinaryUploader` will be added as a class on the input.
Expand Down Expand Up @@ -41,37 +40,58 @@ import classnames from 'classnames'
*/

const propTypes = {
...fieldPropTypes,
...fileInputPropTypes,
fileInput: PropTypes.func,
onUploadFailure: PropTypes.func,
onUploadSuccess: PropTypes.func,
upload: PropTypes.func.isRequired,
uploadStatus: PropTypes.string.isRequired,
}

const defaultProps = {
fileInput: DefaultFileInput,
onUploadSuccess: noop,
onUploadFailure: noop,
}

function CloudinaryFileInput ({
function mapCloudinaryResponse (file, response) {
return compose(
set('url', response.url),
set('meta.cloudinary', response)
)(file)
}

function CloudinaryFileInput ({
input,
className,
onUploadFailure,
onUploadSuccess,
upload,
uploadStatus,
...rest
upload,
uploadStatus,
fileInput: FileInput,
...rest
}) {
const { onChange } = input
return (
<FileInput
input={{ ...input, onChange: noop }}
onLoad={ (fileData, file) => upload(fileData, file)
.then((res) => {
onChange(res.url)
return onUploadSuccess(res)
}, (err) => onUploadFailure(err))
}
input={ input }
readFiles={async (files) => {
let uploadedFiles = null
try {
const filesWithDataUrls = await readFilesAsDataUrls(files)
const uploadFilePromises = filesWithDataUrls.map(async (file) => {
const cloudinaryRes = await upload(file.url, file)
return mapCloudinaryResponse(file, cloudinaryRes)
})

uploadedFiles = await Promise.all(uploadFilePromises)
} catch (e) {
chawes13 marked this conversation as resolved.
Show resolved Hide resolved
onUploadFailure(e)
throw e
}

onUploadSuccess(uploadedFiles)
return input.onChange(uploadedFiles)
chawes13 marked this conversation as resolved.
Show resolved Hide resolved
}}
className={ classnames(uploadStatus, className) }
{ ...rest }
/>
Expand Down
Loading