A photo-upload form can become invalid when the user changes templates, even though every selected file is still a valid image. A two-portrait scene and a one-photo dance clip need different input counts.
For a visible example, AI Pop Video presents photo requirements alongside its video templates. The Hotel Lobby example requests two individual portraits; other examples request one photo. Requirements should come from the selected template rather than a count hard-coded into the upload component.
Disclosure: I am affiliated with AI Pop Video. AI assistance was used to prepare this post and its teaching example. The executable cases below were run locally. This code is independent of the product's implementation and does not call its service.
Define what a passing result means
Here, canContinue means the selected files match the local preflight rules, so the UI can let the user review the request. It does not authorize an upload, confirm available credits, or start generation.
The example policy uses JPEG/PNG, a 10 MiB limit per file, and an exact photo count. These are illustrative policy values, not claims about any particular service's supported formats or upload limits. In an integration, obtain the policy from an authorized, versioned source and match it to the backend's rules.
Keep these checks separate:
- Missing or malformed template requirements: block continuation and explain what is unavailable.
- Incorrect file count: tell the user the count for the currently selected template.
- Unknown or unsupported MIME metadata: ask for a supported file rather than silently treating it as an image.
- Empty or oversized file: identify the affected selection.
A pure validator
Save this as dev-photo-selection.mjs:
export function validateSelection(template, files) {
const validTemplate = template &&
Number.isInteger(template.requiredPhotos) && template.requiredPhotos > 0 &&
Number.isFinite(template.maxBytes) && template.maxBytes > 0 &&
Array.isArray(template.allowedTypes) && template.allowedTypes.length > 0 &&
template.allowedTypes.every(type => typeof type === 'string' && type.length > 0);
if (!validTemplate) {
return { canContinue: false, errors: ['Template requirements unavailable.'] };
}
if (!Array.isArray(files)) {
return { canContinue: false, errors: ['Choose your photos again.'] };
}
const errors = [];
if (files.length !== template.requiredPhotos) {
errors.push(`Choose exactly ${template.requiredPhotos} photo(s).`);
}
files.forEach((file, index) => {
if (!file || !template.allowedTypes.includes(file.type)) {
errors.push(`Photo ${index + 1}: unsupported or unknown file type.`);
}
if (!file || !Number.isFinite(file.size) || file.size <= 0 ||
file.size > template.maxBytes) {
errors.push(`Photo ${index + 1}: file size is outside the allowed range.`);
}
});
return { canContinue: errors.length === 0, errors };
}
The function accepts an ordinary array. In a browser, convert the chosen FileList with Array.from(input.files) before calling it. The test objects below contain only type and size; the function does not read file bytes.
An invalid selection can produce multiple useful errors. For example, switching from a two-photo template to a one-photo template must invalidate a two-file selection even if both files meet the type and size rules. No file is silently discarded to make the count pass.
Exercise the boundaries
Save this next to the module as dev-photo-selection.test.mjs:
import assert from 'node:assert/strict';
import { validateSelection } from './dev-photo-selection.mjs';
const one = {
requiredPhotos: 1,
allowedTypes: ['image/jpeg', 'image/png'],
maxBytes: 10 * 1024 * 1024
};
const two = { ...one, requiredPhotos: 2 };
const photo = { type: 'image/jpeg', size: 2048 };
const cases = [
['one photo', one, [photo], true],
['two photos', two, [photo, { ...photo }], true],
['missing photo', two, [photo], false],
['extra photo after switching template', one, [photo, photo], false],
['empty selection', one, [], false],
['empty file', one, [{ ...photo, size: 0 }], false],
['exact byte limit', one, [{ ...photo, size: one.maxBytes }], true],
['one byte over limit', one, [{ ...photo, size: one.maxBytes + 1 }], false],
['wrong MIME', one, [{ ...photo, type: 'video/mp4' }], false],
['unknown MIME', one, [{ ...photo, type: '' }], false],
['invalid size', one, [{ ...photo, size: Infinity }], false],
['missing metadata', null, [photo], false],
['invalid count metadata', { ...one, requiredPhotos: 0 }, [photo], false],
['empty MIME policy', { ...one, allowedTypes: [] }, [photo], false],
['malformed file list', one, null, false],
['malformed file entry', one, [null], false]
];
const before = JSON.stringify({ one, two, photo });
for (const [name, template, files, expected] of cases) {
const result = validateSelection(template, files);
assert.equal(result.canContinue, expected, name);
assert.equal(result.errors.length === 0, expected, name + ' error messages');
}
assert.equal(JSON.stringify({ one, two, photo }), before, 'inputs preserved');
console.log(`${cases.length} cases passed; inputs preserved. No network or uploads.`);
Run:
node dev-photo-selection.test.mjs
The local result was:
16 cases passed; inputs preserved. No network or uploads.
The byte-limit boundary is intentional: a file at the limit passes; one byte above it fails. Empty metadata also fails explicitly. It does not become a one-photo template through a default value.
Revalidate when the template changes
Run the validator on both file selection and template changes. If your UI loads template requirements asynchronously, keep continuation disabled while the new requirements are unavailable. Associate the response with the selected template ID and version so an older response cannot replace the current requirements.
Show the returned messages near the file input. Keep an explicit next step for reviewing the selected template and any current credit cost. Do not trigger a paid generation request just because local validation passed.
Keep server validation authoritative
A browser file's reported type is a preflight hint, not proof of its contents. The server still needs to enforce its size and count limits, inspect and decode the received files, verify ownership and authorization, and validate the template and charging rules. The browser's accept attribute helps selection; it does not replace those checks.
The example also says nothing about portrait suitability, consent, image dimensions, pose, or the generated video's quality. Add those checks where your actual workflow requires them, with clear messages and appropriate review steps.
References: MDN's file input documentation and Using files from web applications.
Top comments (0)