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
<body>
... lines 17 - 82
{% block javascripts %}
... lines 84 - 86
<script>
... line 88
$('.custom-file-input').on('change', function(event) {
... 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.

... line 1
<html lang="en">
... lines 3 - 15
<body>
... lines 17 - 82
{% block javascripts %}
... lines 84 - 86
<script>
... 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.

Leave a comment!

  • 2020-03-10 Diego Aguiar

    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

  • 2020-03-10 Tomasz Gąsior

    Probably you are right but please consult https://caniuse.com/#feat=let . For my country it shows 95% coverage. :)

  • 2020-03-10 weaverryan

    Hey Tomasz Gąsior!

    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!

  • 2020-03-08 Tomasz Gąsior
  • 2020-02-03 Diego Aguiar

    Hey Carlo Mario Chierotti

    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!

  • 2020-02-03 Carlo Mario Chierotti

    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

  • 2019-07-25 Lydie

    Thx for your answer. It helps a lot to understand the validation process. I will have to parse the csv file in my project so the latest validation step will be done there :)

    Thx again!

  • 2019-07-22 weaverryan

    Hey Lydie!

    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!

  • 2019-07-18 Lydie

    Hello!

    The form_start call is in the template :) When I dump the information about the uploaded file (dd($uploadedfile)), it displayes:

    UploadedFile^ {#64 ▼
    -test: false
    -originalName: "leads_solvary.csv"
    -mimeType: "text/csv"
    -error: 0
    path: "/private/var/folders/c_/t_wbmqbd3dz003yhxr8fgsqw0000gn/T"
    filename: "phpHshBYz"
    basename: "phpHshBYz"
    pathname: "/private/var/folders/c_/t_wbmqbd3dz003yhxr8fgsqw0000gn/T/phpHshBYz"
    extension: ""
    realPath: "/private/var/folders/c_/t_wbmqbd3dz003yhxr8fgsqw0000gn/T/phpHshBYz"
    aTime: 2019-07-18 16:10:23
    mTime: 2019-07-18 16:10:23
    cTime: 2019-07-18 16:10:23
    inode: 8844478793
    size: 1889
    perms: 0100600
    owner: 501
    group: 20
    type: "file"
    writable: true
    readable: true
    executable: false
    file: true
    dir: false
    link: false
    }

    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?

  • 2019-07-16 Diego Aguiar

    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!

  • 2019-07-16 Lydie

    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!

  • 2019-07-12 Diego Aguiar

    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

  • 2019-07-12 Lydie

    Hi Diego,

    I am using the FileType element so the multipart/form-data is added automatically to the form.

    Cheers!

  • 2019-07-11 Diego Aguiar

    Hey Lydie

    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!

  • 2019-07-11 Lydie

    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!