Skip to content

Commit b1a9035

Browse files
committed
Added MultipleFileField instead of having a 'multiple' option on FileField
Only set form.files onChange if the input has a 'files' property Set an array or single file based on type of FileField Browser: retrieval of FileField data from input now falls back to data if not present in files Related to #61
1 parent bf19a2c commit b1a9035

12 files changed

+269
-55
lines changed

docs/fields.rst

+33-2
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ Built-in Field type hierarchy
351351
* :ref:`FileField <ref-fields-FileField>`
352352
353353
* :ref:`ImageField <ref-fields-ImageField>`
354+
* :ref:`MultipleFileField <ref-fields-MultipleFileField>`
354355
* :ref:`MultiValueField <ref-fields-MultiValueField>`
355356
356357
* :ref:`SplitDateTimeField <ref-fields-SplitDateTimeField>`
@@ -580,8 +581,12 @@ Build-in Fields (A-Z)
580581
581582
* Default widget: :js:class:`ClearableFileInput`
582583
* Empty value: ``null``
583-
* Normalises to: The given object in ``files`` - this field just validates
584-
what's there and leaves the rest up to you.
584+
* Normalises to:
585+
586+
* Client: a native `File`_ object, when supported by the browser, otherwise
587+
the ``value`` of the ``<input type="file">``.
588+
* Server: the given object in ``files`` - this field just validates
589+
what's there and leaves the rest up to you.
585590
* Can validate that non-empty file data has been bound to the form.
586591
* Error message keys: ``required``, ``invalid``, ``missing``, ``empty``,
587592
``maxLength``
@@ -767,6 +772,31 @@ Build-in Fields (A-Z)
767772
768773
Takes one extra required argument, ``choices``, as for ``ChoiceField``.
769774
775+
.. _ref-fields-MultipleFileField:
776+
777+
:js:class:`MultipleFileField`
778+
-----------------------------
779+
780+
.. versionadded:: 0.11
781+
782+
* Default widget: :js:class:`FileInput` with ``multiple`` attribute
783+
* Empty value: ``[]`` (an empty list)
784+
* Normalises to:
785+
786+
* Client: a list of `File`_ objects, when supported by the browser,
787+
otherwise the ``value`` of the ``<input type="file" multiple>``.
788+
* Server: the given object in ``files`` - this field just validates
789+
what's there and leaves the rest up to you.
790+
* Can validate that non-empty file data has been bound to the form.
791+
* Error message keys: ``required``, ``invalid``, ``missing``, ``empty``,
792+
``maxLength``
793+
794+
The ``empty`` and ``maxLength`` error messages may contain ``{name}``, which
795+
will be replaced with the name of the file which failed validation.
796+
797+
Has two optional arguments for validation, ``maxLength`` and
798+
``allowEmptyFile`` as for ``FileField``.
799+
770800
.. _ref-fields-TypedMultipleChoiceField:
771801
772802
:js:class:`TypedMultipleChoiceField`
@@ -1050,4 +1080,5 @@ requirements are that it implement a ``clean()`` method and that its
10501080
(``required``, ``label``, ``initial``, ``widget``, ``helpText``) in an argument
10511081
object.
10521082
1083+
.. _`File`: https://developer.mozilla.org/en-US/docs/Web/API/File
10531084
.. _`format strings`: https://github.com/insin/isomorph#formatting-directives

docs/fields_api.rst

+18-3
Original file line numberDiff line numberDiff line change
@@ -284,12 +284,19 @@ File fields
284284

285285
.. js:class:: FileField([kwargs])
286286

287-
Validates that its input is a valid uploaded file -- the behaviour of this
288-
field varies depending on the environmnet newforms is running in:
287+
Validates that its input is a valid file -- the behaviour of this field
288+
varies depending on the environmnet newforms is running in:
289289

290290
**On the client**
291291

292-
Validates that a file has been selected if the field is ``required``.
292+
If the browser supports the `File API <http://www.w3.org/TR/FileAPI/>`_,
293+
``form.files`` will be populated with a
294+
`File <https://developer.mozilla.org/en-US/docs/Web/API/File>`_ object and
295+
validation will be performed on its name and size. The ``File`` object
296+
will be available via ``form.cleanedData`` when valid.
297+
298+
Otherwise, this field can only validate that a file has been selected at
299+
all, if the field is ``required```.
293300

294301
**On the server**
295302

@@ -311,6 +318,14 @@ File fields
311318
:param Boolean kwargs.allowEmptyFile:
312319
if ``true``, empty files will be allowed -- defaults to ``false``.
313320

321+
.. js:class:: MultipleFileField([kwargs])
322+
323+
A version of FileField which expects to receive a list of files as input and
324+
renders to an ``<input type="file" multiple>`` by default.
325+
326+
:param Object kwargs:
327+
field options, as in :js:class:`FileField`
328+
314329
.. js:class:: ImageField([kwargs])
315330

316331
Validates that its input is a valid uploaded image -- the behaviour of this

docs/forms.rst

+30-13
Original file line numberDiff line numberDiff line change
@@ -872,18 +872,34 @@ form its own namespace, use the ``prefix`` argument:
872872
</div>
873873
*/
874874
875-
.. _binding-uploaded-files:
875+
Client: Working with files
876+
==========================
876877

877-
Binding uploaded files to a form
878-
================================
878+
If your browser implements the `File API <http://www.w3.org/TR/FileAPI>`_,
879+
``form.cleanedData`` will contain native
880+
`File <https://developer.mozilla.org/en-US/docs/Web/API/File>`_ objects for any
881+
``FileField``, ``MultipleFileField`` and ``ImageField`` fields in your form.
879882

880-
.. Warning::
881-
Since handling of file uploads in single page apps is a little bit different
882-
than a regular multipart form submission, this section isn't worth much! This
883-
subject will be revisited in a future release.
883+
While these fields are only currently capable of performing limited validation,
884+
having access to ``File`` objects allows you to more easily implement your own
885+
validation based on file size, type and contents at whichever of the available
886+
:ref:`validation steps <ref-validation-steps-and-order>` is most appropriate for
887+
your needs.
884888

885-
Dealing with forms that have ``FileField`` and ``ImageField`` fields
886-
is a little more complicated than a normal form.
889+
Server: Binding uploaded files to a form
890+
========================================
891+
892+
.. Note::
893+
This section deals with a very specific use case: using React to render
894+
forms, but performing and subsequently handling regular form submissions.
895+
896+
This may only be relevant if you're using React purely for server-side
897+
rendering, or you're creating an isomorphic app which progressively enhances
898+
regular form submissions.
899+
900+
Dealing with forms that have ``FileField``, ``MultipleFileField`` or
901+
``ImageField`` fields and will be submitted via a regular form POST is a little
902+
more complicated than a regular form.
887903

888904
Firstly, in order to upload files, you'll need to make sure that your
889905
``<form>`` element correctly defines the ``enctype`` as
@@ -896,7 +912,7 @@ Firstly, in order to upload files, you'll need to make sure that your
896912
Secondly, when you use the form, you need to bind the file data. File
897913
data is handled separately to normal form data, so when your form
898914
contains a ``FileField`` and ``ImageField``, you will need to specify
899-
a ``files`` argument when you bind your form. So if we extend our
915+
a ``files`` argument when creating a form instance. So if we extend our
900916
ContactForm to include an ``ImageField`` called ``mugshot``, we
901917
need to bind the file data containing the mugshot image:
902918

@@ -912,9 +928,10 @@ need to bind the file data containing the mugshot image:
912928
var fileData = {mugshot: {name: 'face.jpg', size: 123456}}
913929
var f = new ContactFormWithMugshot({data: data, files: fileData})
914930
915-
Assuming that you're using `Express`_ and its ``bodyParser()`` on the server
916-
side, you will usually specify ``req.files`` as the source of file data (just
917-
like you'd use ``req.body`` as the source of form data):
931+
Assuming you're using `Express`_, or a similar library which supports middleware
932+
for processing file uploads, you will usually specify ``req.files`` as the
933+
source of file data (just like you'd use ``req.body`` as the source of form
934+
data):
918935

919936
.. code-block:: javascript
920937

docs/validation.rst

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ easily. Validators are functions that take a single argument and throw a
1818
``ValidationError`` on invalid input. Validators are run after the field's
1919
``toJavaScript()`` and ``validate()`` methods have been called.
2020

21+
.. _ref-validation-steps-and-order:
22+
2123
Validation steps and order
2224
==========================
2325

src/Form.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var DeclarativeFieldsMeta = require('./forms/DeclarativeFieldsMeta')
1313
var ErrorList = require('./ErrorList')
1414
var ErrorObject = require('./ErrorObject')
1515
var FileField = require('./fields/FileField')
16+
var MultipleFileField = require('./fields/MultipleFileField')
1617

1718
var {ValidationError} = require('validators')
1819
var {cancellable, debounce, info, warning, normaliseValidation} = require('./util')
@@ -575,9 +576,11 @@ Form.prototype._handleFieldEvent = function(validation, e) {
575576
var field = this.fields[fieldName]
576577
var targetData = getFormData.getNamedFormElementData(e.target.form, htmlName)
577578
this.data[htmlName] = targetData
578-
if (field instanceof FileField) {
579+
if (field instanceof FileField && 'files' in e.target) {
579580
var files = e.target.files
580-
this.files[htmlName] = field.multiple ? Array.prototype.slice.call(files) : files[0]
581+
this.files[htmlName] = (field instanceof MultipleFileField
582+
? Array.prototype.slice.call(files)
583+
: files[0])
581584
}
582585
if (this.isInitialRender) {
583586
this.isInitialRender = false

src/fields/FileField.js

+20-29
Original file line numberDiff line numberDiff line change
@@ -28,53 +28,44 @@ var FileField = Field.extend({
2828

2929
, constructor: function FileField(kwargs) {
3030
if (!(this instanceof FileField)) { return new FileField(kwargs) }
31-
kwargs = object.extend({maxLength: null, allowEmptyFile: false, multiple: false}, kwargs)
31+
kwargs = object.extend({maxLength: null, allowEmptyFile: false}, kwargs)
3232
this.maxLength = kwargs.maxLength
3333
this.allowEmptyFile = kwargs.allowEmptyFile
34-
this.multiple = kwargs.multiple
3534
delete kwargs.maxLength
3635
Field.call(this, kwargs)
3736
}
3837
})
3938

40-
FileField.prototype.getWidgetAttrs = function(widget) {
41-
var attrs = Field.prototype.getWidgetAttrs.call(this, widget)
42-
attrs.multiple = this.multiple
43-
return attrs
44-
}
45-
4639
FileField.prototype.toJavaScript = function(data, initial) {
4740
if (this.isEmptyValue(data)) {
4841
return null
4942
}
5043

5144
// If the browser doesn't support File objects, we can't do anything more
52-
if (env.browser && (is.String(data) || is.Array(data) && is.String(data[0]))) {
45+
if (env.browser && is.String(data)) {
5346
return data
5447
}
5548

56-
;[].concat(data).forEach(function(file) {
57-
// File objects should have name and size attributes
58-
if (typeof file.name == 'undefined' || typeof file.size == 'undefined') {
59-
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'})
60-
}
49+
// File objects should have name and size attributes
50+
if (typeof data.name == 'undefined' || typeof data.size == 'undefined') {
51+
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'})
52+
}
6153

62-
var fileName = file.name
63-
var fileSize = Number(file.size)
54+
var name = data.name
55+
var suze = Number(data.size)
6456

65-
if (this.maxLength !== null && fileName.length > this.maxLength) {
66-
throw ValidationError(this.errorMessages.maxLength, {
67-
code: 'maxLength'
68-
, params: {max: this.maxLength, length: fileName.length}
69-
})
70-
}
71-
if (!fileName) {
72-
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'})
73-
}
74-
if (!this.allowEmptyFile && fileSize === 0) {
75-
throw ValidationError(this.errorMessages.empty, {code: 'empty'})
76-
}
77-
}.bind(this))
57+
if (this.maxLength !== null && name.length > this.maxLength) {
58+
throw ValidationError(this.errorMessages.maxLength, {
59+
code: 'maxLength'
60+
, params: {max: this.maxLength, length: name.length}
61+
})
62+
}
63+
if (!name) {
64+
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'})
65+
}
66+
if (!this.allowEmptyFile && suze === 0) {
67+
throw ValidationError(this.errorMessages.empty, {code: 'empty'})
68+
}
7869

7970
return data
8071
}

src/fields/MultipleFileField.js

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use strict';
2+
3+
var is = require('isomorph/is')
4+
5+
var env = require('../env')
6+
7+
var Field = require('../Field')
8+
var FileInput = require('../widgets/FileInput')
9+
var FileField = require('./FileField')
10+
11+
var {ValidationError} = require('validators')
12+
13+
/**
14+
* Validates that its input is a list of valid files.
15+
* @constructor
16+
* @extends {FileField}
17+
* @param {Object=} kwargs
18+
*/
19+
var MultipleFileField = FileField.extend({
20+
widget: FileInput,
21+
22+
defaultErrorMessages: {
23+
invalid: 'No files were submitted. Check the encoding type on the form.',
24+
missing: 'No files were submitted.',
25+
empty: '{name} is empty.',
26+
maxLength: 'Ensure filenames have at most {max} characters ({name} has {length}).'
27+
},
28+
29+
constructor: function MultipleFileField(kwargs) {
30+
if (!(this instanceof MultipleFileField)) { return new MultipleFileField(kwargs) }
31+
FileField.call(this, kwargs)
32+
}
33+
})
34+
35+
MultipleFileField.prototype.getWidgetAttrs = function(widget) {
36+
var attrs = FileField.prototype.getWidgetAttrs.call(this, widget)
37+
attrs.multiple = true
38+
return attrs
39+
}
40+
41+
MultipleFileField.prototype.toJavaScript = function(data, initial) {
42+
if (this.isEmptyValue(data)) {
43+
return []
44+
}
45+
46+
// If the browser doesn't support File objects, we can't do anything more
47+
if (env.browser && is.String(data)) {
48+
return data
49+
}
50+
51+
for (var i = 0, l = data.length; i < l; i++) {
52+
var file = data[i]
53+
54+
// File objects should have name and size attributes
55+
if (typeof file.name == 'undefined' || typeof file.size == 'undefined') {
56+
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'})
57+
}
58+
59+
var name = file.name
60+
var size = Number(file.size)
61+
62+
if (this.maxLength !== null && name.length > this.maxLength) {
63+
throw ValidationError(this.errorMessages.maxLength, {
64+
code: 'maxLength',
65+
params: {max: this.maxLength, name, length: name.length}
66+
})
67+
}
68+
if (!name) {
69+
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'})
70+
}
71+
if (!this.allowEmptyFile && size === 0) {
72+
throw ValidationError(this.errorMessages.empty, {
73+
code: 'empty',
74+
params: {name}
75+
})
76+
}
77+
}
78+
79+
return data
80+
}
81+
82+
MultipleFileField.prototype.clean = function(data, initial) {
83+
if (this.isEmptyValue(data) && !this.isEmptyValue(initial)) {
84+
return initial
85+
}
86+
return Field.prototype.clean.call(this, data)
87+
}
88+
89+
MultipleFileField.prototype.validate = function(value) {
90+
if (this.required && !value.length) {
91+
throw ValidationError(this.errorMessages.required, {code: 'required'})
92+
}
93+
}
94+
95+
MultipleFileField.prototype._hasChanged = function(initial, data) {
96+
return !this.isEmptyValue(data)
97+
}
98+
99+
module.exports = MultipleFileField

src/newforms.js

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ module.exports = {
5151
, isFormAsync: require('./forms/isFormAsync')
5252
, locales: locales
5353
, MultipleChoiceField: require('./fields/MultipleChoiceField')
54+
, MultipleFileField: require('./fields/MultipleFileField')
5455
, MultipleHiddenInput: require('./widgets/MultipleHiddenInput')
5556
, MultiValueField: require('./fields/MultiValueField')
5657
, MultiWidget: require('./widgets/MultiWidget')

0 commit comments

Comments
 (0)