Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.1.3
Subscribe to download the code!Compatible PHP versions: ^7.1.3
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Upload Field Styling & Bootstrap
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIf you use the Bootstrap 4 theme with Symfony... things get weird with upload fields! Yea, there is a good reason for why, but out-of-the-box, it's... just super weird. The problem? Select a file and... get rewarded by seeing absolutely nothing! Did the file actually attach? We should see the filename somewhere. What happened?
Why Doesn't it Work?
The thing is... styling a file upload field is kinda hard. So, if you really want to control how it looks and make it super shiny, Bootstrap allows you to create a "custom" file input structure, which is what Symfony uses by default. Check this out: see the <input type="file"...>
field? That's hidden by Bootstrap! Try removing the opacity: 0
part and... say hello to the real file upload field... with the filename that we selected!
Bootstrap hides the input so that it, or we, can completely control how this whole field looks. Everything you actually see comes from the label
: it takes up the entire width. Even the "Browse" button comes from some :after
content.
The great thing about this is that styling a label
element is easy. The sad panda part is that we don't see the filename when we select a file! We can fix that - but it takes a little bit of JavaScript.
Customizing the Text in the Upload Field
Before we do that, we can also put a message in the main part of the file field by putting some content in the label
element. But... it doesn't work like a normal label.
In the templates/
directory, open article_admin/_form.html.twig
. Here's our imageFile
field. The second argument to form_row
is an array of variables you can use to customize... basically anything. One of the most important ones is called attr
: it's how you attach custom HTML attributes to the input field. Pass an attribute called placeholder
set to Select an article image
.
{{ form_start(articleForm) }} | |
Show Lines
|
// ... lines 2 - 4 |
{{ form_row(articleForm.imageFile, { | |
attr: { | |
'placeholder': 'Select an article image' | |
} | |
}) }} | |
Show Lines
|
// ... lines 10 - 27 |
{{ form_end(articleForm) }} |
This would normally add a placeholder
attribute to the input so you can have some text on the field if it's empty. But when you're dealing with a file upload field with the Bootstrap theme, this is used in a different way... but it accomplishes the same thing.
Refresh! Cool! The empty part of the file field now gets this text.
Showing the Selected Filename
But if you select a file... the filename still doesn't show. Let's fix that already. Look at the structure again: Symfony's form theme is using this custom-file-input
class on the input. Ok, so what we need to do is this: on change of that field, we need to set the HTML of the label to the filename, which is something we have access to in JavaScript.
To keep things simple, open base.html.twig
: we'll write some JavaScript that will work across the entire site. I'd recommend using Webpack Encore, and putting this code in your main entry file if you want it to be global. But, without Encore, down here works fine.
Use $('.custom-file-input')
- that's the class that's on the input
field itself, .on('change')
and pass this a callback with an event
argument. Inside, we need to find the label
element: I'll do that by finding the parent of the input
and then looking for the custom-file-label
class so we can set its HTML.
Show Lines
|
// ... line 1 |
<html lang="en"> | |
Show Lines
|
// ... lines 3 - 15 |
<body> | |
Show Lines
|
// ... lines 17 - 82 |
{% block javascripts %} | |
Show Lines
|
// ... lines 84 - 86 |
<script> | |
Show Lines
|
// ... line 88 |
$('.custom-file-input').on('change', function(event) { | |
Show Lines
|
// ... lines 90 - 93 |
}); | |
</script> | |
{% endblock %} | |
</body> | |
</html> |
In the callback, set var inputFile = event.currentTarget
- that's the DOM node for the input type="file"
element. Next, $(inputFile).parent().find('.custom-file-label').html()
and pass this the filename that was just selected: inputFile.files[0].name
. The 0
part looks a bit weird, but technically a file upload field can upload multiple files. We're not doing that, so we get to take this shortcut.
Show Lines
|
// ... line 1 |
<html lang="en"> | |
Show Lines
|
// ... lines 3 - 15 |
<body> | |
Show Lines
|
// ... lines 17 - 82 |
{% block javascripts %} | |
Show Lines
|
// ... lines 84 - 86 |
<script> | |
Show Lines
|
// ... line 88 |
$('.custom-file-input').on('change', function(event) { | |
var inputFile = event.currentTarget; | |
$(inputFile).parent() | |
.find('.custom-file-label') | |
.html(inputFile.files[0].name); | |
}); | |
</script> | |
{% endblock %} | |
</body> | |
</html> |
Give it a try! Refresh... browse... select rocket.jpg
and... yea! Our placeholder gets replaced by the filename. That's what we expect and the field is easier to style thanks to this.
Next: the upload side of things is looking good. It's time to start rendering the URL to the upload files... but without letting things get crazy-disorganized. I want to love our setup.
22 Comments
Hey Mamour W.!
Nice catch on that edge case! I see it now: if I (A) select a file and then (B) go back and select NO file, then the change
event is triggered, but the files are empty. Excellent catch - thanks for sharing that :).
It Rocks #RyanVoice
😂
Cheers!
Is there a way to get the upload field to display the value of a currently uploaded image (which, in this case, would be a string representing an image path, in my case), before clicking "Choose File" and replacing the currently uploaded image? Can a FileType field store a string value for a currently uploaded URL?
Hey @Jeff!
Hmm. Part of this answer depends on exactly WHERE you want to show the filename. Fore example, on solution would be to do a "form theme" for just this one field - e.g. - https://symfonycasts.com/screencast/symfony-forms/custom-field-theme - or render the field a bit more manually - something like this:
<div>
{{ form_label(articleForm.imageFilename) }}
{{ form_widget(articleForm.imageFilename) }}
{% if articleForm.imageFilename.vars.data %}
({{ articleForm.imageFilename.vars.data }})
{% endif %}
{{ form_errors(articleForm.imageFilename) }}
</div>
(the trick here is getting all the styling / CSS classes correct since you're taking more control over things).
OR, you might want to do something a bit more similar to what we do in the video: where we replace the label
with the current filename. In that case, it can be done when rendering the field:
{{ form_row(articleForm.imageFile, {
label: articleForm.imageFilename.vars.data ?: 'Choose an image'
}) }}
Let me know if that helps :)
Cheers!
$(inputFile).parent().find('.custom-file-label').html()<br />
Shouldn't that be .text()
?
Hey there!
Yea, I think you're right, since we're not injecting any HTML content, it would be safer to use the .text()
function
Cheers!
Really thanks, save my day
Hey TomaszGasior!
You're right! But in this case, because I'm writing this code in my template (and not in a system where it will be compiled/transpiled with Babel), I'm being conservative: about 5% of browsers still don't support "var". So it's "probably" safe to use on my sites... but I'm being conservative. Of course, inside a Webpack-powered system, it's fine because if you need to support older browser, you can allow it to rewrite that code for you.
Cheers!
Probably you are right but please consult https://caniuse.com/#feat=let . For my country it shows 95% coverage. :)
Yea, you may want to ignore that missing 5% but as Ryan said, the best way would be to use Encore Webpack and let it compile your javascript
Hello Ryan,
sorry to disturb.
I would like to customize also the label of the file field (ie the "Browse" button) changing the text and adding a on hover effect, but I have big difficulties in finding where and what should change.
can you please just give me a hint in the right direction?
thank you for your attention and for the great job you do!
cheers
Hey chieroz
Nice question! You made me dig :)
What's happening here is that Bootstrap4 adds the text of the button via CSS. If you inspect that element, you will notice that the button is added via a pseudo-element (check the "custom-file-label" CSS class). What you can do is to override it by defining your own CSS rules for that class, or do it via Javascript (ugly)
Cheers!
Hello!
I have added as mimeTypes "text/csv" but validation failed even if the file is a csv file.
Error displayed:The mime type of the file is invalid ("text/plain"). Allowed mime types are "text/csv".
If I dd my $uploadedFile variable, I can see that the mimeType value is set to "text/csv". Any idea?
Thx!
Hey be_tnt
How are you uploading your file? Remember that you need to specify the encryption type of your form as multipart/form-data
. Also, just in case, double check your submitted file :)
Cheers!
Hi Diego,
I am using the FileType element so the multipart/form-data is added automatically to the form.
Cheers!
Oh, I see, that's even better :)
but then, why it's not working? Are you submitting your form via JS? I may need to see your code, if you can upload it to Github, so I can give it a look
Hello!
I do not have the code on Github. Let me try to give you as much information I can :)
In my controller:
`public function upload(Request $request, UploaderHelper $uploaderHelper, ValidatorInterface $validator)
{
$form = $this->createForm(UploadProspectType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/**
* @var UploadedFile $uploadedFile
*/
$uploadedFile = $form['prospectsFile']->getData();
$violations = $validator->validate(
$uploadedFile,
new File([
'mimeTypes' => [
'text/csv',
]
])
);
if ($violations->count() > 0) {
/** @var ConstraintViolation $violation */
$violation = $violations[0];
$this->addFlash('error', $violation->getMessage());
return $this->render('prospect/upload.html.twig', [
'form' => $form->createView()
]);
}
if ($uploadedFile) {
$newFilename = $uploaderHelper->uploadCSVFile($uploadedFile);
}
}
return $this->render('prospect/upload.html.twig', [
'form' => $form->createView()
]);
}`
In the form type:
public function buildForm(FormBuilderInterface $builder, array $options)
` {
$builder
->add('prospectsFile', FileType::class, [
'label' => $this->translator->trans('form.file',[], 'prospects'),
])
;
}`
In the view:
`{{ form_errors(form) }}
{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit" class="btn btn-primary">{{ button_text|trans }}</button>
{{ form_end(form) }}`
I also tried to add the constraint about extension in the buildForm function. Same result.
Thx in advance for your help!
Hmm, everything looks good... the only thing I can tell is that you didn't call {{ form_start(form) }}
on your template, if that doesn't fix it, we are in troubles haha. Try clearing the cache, submit another csv file, double check your imports (you may have chosen a wrong one)
Cheers!
Hello!
The form_start call is in the template :) When I dump the information about the uploaded file (dd($uploadedfile)), it displayes:
UploadedFile^ {#64 ▼<br /> -test: false<br /> -originalName: "leads_solvary.csv"<br /> -mimeType: "text/csv"<br /> -error: 0<br /> path: "/private/var/folders/c_/t_wbmqbd3dz003yhxr8fgsqw0000gn/T"<br /> filename: "phpHshBYz"<br /> basename: "phpHshBYz"<br /> pathname: "/private/var/folders/c_/t_wbmqbd3dz003yhxr8fgsqw0000gn/T/phpHshBYz"<br /> extension: ""<br /> realPath: "/private/var/folders/c_/t_wbmqbd3dz003yhxr8fgsqw0000gn/T/phpHshBYz"<br /> aTime: 2019-07-18 16:10:23<br /> mTime: 2019-07-18 16:10:23<br /> cTime: 2019-07-18 16:10:23<br /> inode: 8844478793<br /> size: 1889<br /> perms: 0100600<br /> owner: 501<br /> group: 20<br /> type: "file"<br /> writable: true<br /> readable: true<br /> executable: false<br /> file: true<br /> dir: false<br /> link: false<br />}
As you can see, the mimeType is "text/csv". But the validation failed saying that I uploaded a "text/plain" file instead of a csv ...
By the way, in the mime types, I have added pdf ("application/pdf", "application/x-pdf") and this is working. So the problem is really with csv.
Any idea?
Hey be_tnt!
Nice just checking into this and dumping the UploadedFile object. When it comes to asking "What is the mime type of this file?" there are *two* places that information can come from. First, when the user submits the form, *their* browser sends information that says "this is text/csv". But, this can't be trusted... as a user could "spoof" this and send anything. So, second, what Symfony can do (and *does* do) is look at the uploaded file's contents and try to guess the mime type. I believe this what you're seeing: the user's browser is saying "Hey! This is text/csv", but then Symfony looks inside of the file and just sees "text/plain"... probably because CSV basically is a text/plain format (or very similar).
To solve this, I would just allow text/plain. If you really wanted to make sure that *only* CSV files (and not just random text files) were uploaded, you could add your own next layer of validation that tried to parse the file's contents as CSV and creates a validation if it fails (that would be a custom validator).
Let me know if this helps!
Cheers!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"aws/aws-sdk-php": "^3.87", // 3.87.10
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.10.1
"doctrine/doctrine-bundle": "^1.6.10", // 1.10.2
"doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
"doctrine/orm": "^2.5.11", // v2.7.2
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.9.0
"league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
"league/flysystem-cached-adapter": "^1.0", // 1.0.9
"liip/imagine-bundle": "^2.1", // 2.1.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
"oneup/flysystem-bundle": "^3.0", // 3.0.3
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.0
"sensio/framework-extra-bundle": "^5.1", // v5.2.4
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.2.3
"symfony/console": "^4.0", // v4.2.3
"symfony/flex": "^1.9", // v1.21.6
"symfony/form": "^4.0", // v4.2.3
"symfony/framework-bundle": "^4.0", // v4.2.3
"symfony/property-access": "4.2.*", // v4.2.3
"symfony/property-info": "4.2.*", // v4.2.3
"symfony/security-bundle": "^4.0", // v4.2.3
"symfony/serializer": "4.2.*", // v4.2.3
"symfony/twig-bundle": "^4.0", // v4.2.3
"symfony/validator": "^4.0", // v4.2.3
"symfony/web-server-bundle": "^4.0", // v4.2.3
"symfony/yaml": "^4.0", // v4.2.3
"twig/extensions": "^1.5" // v1.5.4
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.2.3
"symfony/dotenv": "^4.0", // v4.2.3
"symfony/maker-bundle": "^1.0", // v1.11.3
"symfony/monolog-bundle": "^3.0", // v3.3.1
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.3
"symfony/stopwatch": "4.2.*", // v4.2.3
"symfony/var-dumper": "^3.3|^4.0", // v4.2.3
"symfony/web-profiler-bundle": "4.2.*" // v4.2.3
}
}
A small contribution:
The javascript code to change the placeholder don't always work, when click on browse and dont actually choose a file the place holder wont change because
inputFile.files[0].name
is not define and will throw an exception I fixed it by counting the number of files selected and if its zero then put "select a file instead of the file name.Bonus, for a multiple file field you should use this one :
<b>It Rocks #RyanVoice
</b>