Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Upload Field Styling & Bootstrap

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.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

If 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) }}
... lines 2 - 4
{{ form_row(articleForm.imageFile, {
attr: {
'placeholder': 'Select an article image'
}) }}
... 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.

... line 1
<html lang="en">
... lines 3 - 15
... lines 17 - 82
{% block javascripts %}
... lines 84 - 86
... line 88
$('.custom-file-input').on('change', function(event) {
... lines 90 - 93
{% endblock %}

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.

... line 1
<html lang="en">
... lines 3 - 15
... lines 17 - 82
{% block javascripts %}
... lines 84 - 86
... line 88
$('.custom-file-input').on('change', function(event) {
var inputFile = event.currentTarget;
{% endblock %}

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.

Leave a comment!

This tutorial is built on Symfony 4 but works great in Symfony 5!

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
        "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
        "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.17.6
        "symfony/form": "^4.0", // v4.2.3
        "symfony/framework-bundle": "^4.0", // v4.2.3
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.3
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "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/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.3