Webpack-encore: How to handle form_widget specific Javascript ?

Created on 13 Jul 2017  路  4Comments  路  Source: symfony/webpack-encore

Hello,

We are currently trying to integrate Encore for a new project.

We used to have form_widget sepcific Javascript. For example, we had the following in form_fields.html.twig to create an AceEditor in the page (we'll assume that the AceEditor is already loaded in some kind of vendor.js):

{% block aceeditor_widget %}
    {{ block('textarea_widget') }}
    <div id="{{ form.vars.id }}_aceeditor"></div>

    <script type="text/javascript">
        var editor = ace.edit("{{ form.vars.id }}_aceeditor");
        var textarea = $('#{{ form.vars.id }}').hide();
        editor.setTheme("ace/theme/monokai");
        editor.setOptions({
            maxLines: 50,
        });
        editor.$blockScrolling = Infinity;
        editor.getSession().setMode("ace/mode/twig");
        editor.getSession().setValue(textarea.val());
        editor.getSession().on('change', function(){
            textarea.val(editor.getSession().getValue());
        });
    </script>
{% endblock %}

The idea was that all the required JS is bundled in the template, anytime we need an AceEditor in a form, we just have to specify the AceEditorType in the WhateverFormType and without having to care about manually including the Javascript in everypage where we needed the AceEditor.

How to handle this with Encore ?
We thought about making it parse Twig files, but that causes a whole bunch of other issues with third party bundles using Assetic.

Any ideas ?

Most helpful comment

So in every page I would load manifest.js, vendor.js, app.js and form-fields.js ?
Or do you recommend doing a require('form-fields') in app.js instead ?

Instead of including form-fields.js on every page, it would be better to require everything you need from inside app.js. Basically, it's the same end result in either situation: you're including your form JS on ever page. But, it's slightly better for performance to avoid that extra form-fields.js download.

@davidmpaz's tip is about code organization. You could have one big form-fields.js file (which you would then require from inside app.js). But, now that we are able to use the awesome require statement, just realize that you can being to break things into smaller files, just to keep things organized. So instead of form-fields.js, you might have aceeditor-widget.js and datetime-picker-widget.js. Then you would require and use those in app.js. Something like:

var aceEditorWidget = require('./fields/aceeditor-widget');
var dateTimePickerWidget = require('./fields/datetime-picker-widget');
var $ = require('jquery');

$(document).ready(function() {
    aceEditorWidget($('.js-aceeditor'));
    dateTimePickerWidget($('.js-datetime-picker'));
});

... where aceeditor-widget.js might look like this:

var ace = require('ace-builds');
var $ = require('jquery');

function createAceEditor($element) {
    // create an editor by reading the id from the element
    var editor = ace.edit($element.attr('id'));    
    // read the data-target-id attribute to find the textarea
    var textarea = $('#'+$element.data('target-id').hide();

    // then everything else is copy and paste from before :)
}

module.exports = function($elements) {
    // loop over all elements with this class and add an editor!
    // then go celebrate with pizza :) 
    $elements.each(function() {
        createAceEditor($(this));
    });
}

And boom! You've got a bit more organization... and these "modules" start to become a bit re-usable :).

Cheers!

All 4 comments

Hey @Growiel!

So, the easiest way is to continue to basically do the same thing as before :). Even before, you needed to "remember" to include the ace <script tag on the page. Now, you would need to "remember" to include a <script tag for some entry file, which includes ace. So:

// webpack.config.js
Encore
    .addEntry('main', './assets/main.js')
// main.js

// this only creates a local variable
var ace = require('ace-builds');
// but, you can expose it globally if you want
global.ace = ace

Then, you would have access to the normal, global ace variable on any page that included the script tag for build/main.js.

If you want to be a bit more awesome, you could remove the JavaScript entirely from your Twig template. Instead, you would tag which elements need the editor with a class (js-aceeditor) and add any configuration you need to data-* attributes (in this case, we need to know the id to the textarea):

{% block aceeditor_widget %}
    {{ block('textarea_widget') }}
    <div id="{{ form.vars.id }}_aceeditor" class="js-aceeditor data-target-id="{{ form.vars.id }}"></div>
{% endblock %}

Then, do everything in main.js:

// main.js
var ace = require('ace-builds');
var $ = require('jquery');

$(document).ready(function() {
    // loop over all elements with this class and add an editor!
    // then go celebrate with pizza :) 
    $('.js-aceeditor').each(function() {
        createAceEditor($(this));
    });
});

function createAceEditor($element) {
    // create an editor by reading the id from the element
    var editor = ace.edit($element.attr('id'));    
    // read the data-target-id attribute to find the textarea
    var textarea = $('#'+$element.data('target-id').hide();

    // then everything else is copy and paste from before :)
}

Let me know if that helps!

Hello, @weaverryan !

It did help a lot, thanks.

Would you say that a "good practice" for this would be to create form-fields.js (with a new entryPoint) that specifically handles all the form_widgets Javascript ?

So in every page I would load manifest.js, vendor.js, app.js and form-fields.js ?
Or do you recommend doing a require('form-fields') in app.js instead ?

We have quite a few javascript-heavy form elements that we handle with Twig (datetime-picker, select2, aceeditor, jquery-file-upload, etc...) so I'm just looking for the best way to get us started.

In general, try to get all your inline javascript code out of html and make libraries out of them, aka external files you can later include separately in the page or better yet, like explained, require in your files acting as entry point for the application like app.js for example

So in every page I would load manifest.js, vendor.js, app.js and form-fields.js ?
Or do you recommend doing a require('form-fields') in app.js instead ?

Instead of including form-fields.js on every page, it would be better to require everything you need from inside app.js. Basically, it's the same end result in either situation: you're including your form JS on ever page. But, it's slightly better for performance to avoid that extra form-fields.js download.

@davidmpaz's tip is about code organization. You could have one big form-fields.js file (which you would then require from inside app.js). But, now that we are able to use the awesome require statement, just realize that you can being to break things into smaller files, just to keep things organized. So instead of form-fields.js, you might have aceeditor-widget.js and datetime-picker-widget.js. Then you would require and use those in app.js. Something like:

var aceEditorWidget = require('./fields/aceeditor-widget');
var dateTimePickerWidget = require('./fields/datetime-picker-widget');
var $ = require('jquery');

$(document).ready(function() {
    aceEditorWidget($('.js-aceeditor'));
    dateTimePickerWidget($('.js-datetime-picker'));
});

... where aceeditor-widget.js might look like this:

var ace = require('ace-builds');
var $ = require('jquery');

function createAceEditor($element) {
    // create an editor by reading the id from the element
    var editor = ace.edit($element.attr('id'));    
    // read the data-target-id attribute to find the textarea
    var textarea = $('#'+$element.data('target-id').hide();

    // then everything else is copy and paste from before :)
}

module.exports = function($elements) {
    // loop over all elements with this class and add an editor!
    // then go celebrate with pizza :) 
    $elements.each(function() {
        createAceEditor($(this));
    });
}

And boom! You've got a bit more organization... and these "modules" start to become a bit re-usable :).

Cheers!

Was this page helpful?
0 / 5 - 0 ratings