Part 3 - Custom JavaScript


I'm fairly sure this is the longest video I have ever posted up here on CodeReviewVideos, and that is not necessarily a good thing! It certainly didn't feel like it was a long video whilst recording.

Anyway, the premise in this video is to create a JavaScript function that allows us to show or hide the manual entry box, depending on whether the user has selected 'Other' or not.

To make things a little more useful, I decided to set the challenge to myself that this JavaScript should still work if there are multiple instances of the TimetableFormType on the page at any given time. That is to say that if there are two or more select boxes that contain the 'Other' field, then changing the first one to 'Other' should not also show the manual entry box for the second form, and so on.

I'm not going to walk through the entire JavaScript in this write up. If you want to follow along with my reasoning then I think it's fair to say that I explained myself (in greater detail than usual!) in the video.

Instead, here is the 'finished' JavaScript:

// /web/js/has-other.js

var manageOther = function(selectElement, manualEntry) {

    var getValueOfOther = function (selectElement) {
        var filteredResults = Array.prototype.filter.call(selectElement, function(optionElement) {
            return 'other' === optionElement.innerHTML.toLowerCase();
        });

        return filteredResults[0].value;
    };

    var isOther = function (currentlySelectedIndex) {
        return currentlySelectedIndex == getValueOfOther(selectElement);
    };

    var showOrHideManualEntryBox = function() {
        var currentlySelectedIndex = selectElement.options[selectElement.selectedIndex].value;
        var label = $('label[for="' + manualEntry.id + '"]');

        if (isOther(currentlySelectedIndex)) {
            manualEntry.style.display = 'block';
            label[0].style.display = 'block';
        } else {
            manualEntry.style.display = 'none';
            manualEntry.value = '';
            label[0].style.display = 'none';
        }
    };

    selectElement.addEventListener('change', showOrHideManualEntryBox);

    showOrHideManualEntryBox();
};

var presetChoice1 = document.getElementById('data_feed_timetable_presetChoice');
var manualEntry1 = document.getElementById('data_feed_timetable_manualEntry');

manageOther(presetChoice1, manualEntry1);

var presetChoice2 = document.getElementById('data_feed_anotherTimetable_presetChoice');
var manualEntry2 = document.getElementById('data_feed_anotherTimetable_manualEntry');

manageOther(presetChoice2, manualEntry2);

This code could be improved - but then, is that not always the case?

In hindsight it might have made more sense to create an object and have each of the inner functions be properties on that object. Ultimately for a script this size I personally believe the argument isn't worth the time spent in refactoring, but of course, if this were to grow or to be used in production then that argument (or discussion at least) suddenly becomes one worth having.

Reliance On jQuery

As part of this video series we have been using the Bootstrap 3 layout along with including jQuery to help with earlier video functionality.

For the sake of this particular piece of functionality, jQuery would not be required. I am only making use of it because it is there. If you don't have jQuery, getting access to the label property could be accomplished using plain old JavaScript, as per this StackOverflow post.

This becomes more relevant in my personal circumstances as I find I use jQuery less and less these days. Or at least, I find jQuery is not typically immediately available, as it perhaps once would have been.

Additional Field

In this example I added a second Timetable property to my DataFeed entity to make testing the form with multiple select boxes that little bit easier.

The changes are straightforward:

// /src/AppBundle/Entity/DataFeed.php

    /**
     * @ORM\OneToOne(targetEntity="Timetable", cascade={"persist"})
     * @ORM\JoinColumn(name="another_timetable_id", referencedColumnName="id")
     *
     * @Assert\Valid()
     */
    protected $anotherTimetable;

And on the form type:

// /src/AppBundle/Form/Type/DataFeedType.php

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // * snip * 
            ->add('timetable', TimetableType::class)
            ->add('anotherTimetable', TimetableType::class)
            ->add('save', SubmitType::class)
        ;
    }

And due to the way that the Bootstrap 3 horizontal template currently works in Symfony 3 - as discussed in the previous video - we would also need to update the Twig template to manually render out this additional form field:

<!-- /app/Resources/views/form-example/index.html.twig --> 

{% extends '::base.html.twig' %}

{% block body %}

    <h2>Symfony 3 Form Example</h2>

    <hr />

    {#{{ form(myForm) }}#}

    {{ form_start(myForm) }}

    {{ form_errors(myForm) }}

    {{ form_row(myForm.name) }}
    {{ form_row(myForm.url) }}
    {{ form_row(myForm.enabled) }}

    <div class="{% if not myForm.timetable.vars.valid %}has-error{% endif %}">
        {{ form_row(myForm.timetable.presetChoice) }}
        {{ form_row(myForm.timetable.manualEntry) }}

        <div class="col-sm-offset-2">
            {{ form_errors(myForm.timetable) }}
        </div>
    </div>

    <hr>

    <div class="{% if not myForm.anotherTimetable.vars.valid %}has-error{% endif %}">
        {{ form_row(myForm.anotherTimetable.presetChoice) }}
        {{ form_row(myForm.anotherTimetable.manualEntry) }}

        <div class="col-sm-offset-2">
            {{ form_errors(myForm.anotherTimetable) }}
        </div>
    </div>

    {{ form_row(myForm.save, { 'attr': { 'class': 'btn btn-success' } }) }}

    {{ form_end(myForm) }}

{% endblock %}

Wrapping Up

By now you (hopefully) feel comfortable with the common day-to-day tasks of using Symfony 3's form component.

You've seen how many of the fields work, and how the foundational building blocks stack on top of each other to make use of the more advanced fields.

Sometimes Symfony won't offer you exactly what you need right out of the box. In those instances, don't be afraid to create your own form types and functionality, such as in these previous three videos on adding in the 'enhanced' drop down feature.

Often, as with anything software related, there are many possible ways to achieve your desired outcome. Don't be put off by trying to figure out the perfect way. Just get started, and use what you learn as you build to better your understanding.

Throughout this entire series we have made use of the same two controller methods - one for adding, and one for editing. This has been incredibly useful, but also should help you see that changing your form should not require changing much - if any - logic inside your controller action(s).

There are certainly more advanced things you can do with Symfony's form component. Form events, data transformers, collections, and more.

For now I hope you have found this entire series to be informative (groan), and that you are starting to formulate (lol) your own ideas of how you can use Symfony's form to better your own projects.

Thanks for watching :)

Code For This Course

Get the code for this course.

Episodes