19

I want to use javascript in custom Symfony 2 form field type extension. So, I have Twig extension template like this:

{% block some_widget %}
    <input ... />

    <script src="some.js"></script>
    <link href="some.css" />
{% endblock %}

But I want to have these script and link tags only once in my HTML, ideally in head tag, without modifing base template. I tried to extend Twig blocks, but I have no access to action template blocks inside form template. Or maybe something like this:

{# widget tempate #}
{% block some_widget %}
    <input ... />

    {{ use_javascript('some.js') }}
    {{ use_css('some.css') }}
{% endblock %}

{# main action template #}
...
<head>
{{ dump_javascripts() }}
{{ dump_css() }}
</head>
...

How to do this with Symfony 2 Forms + Twig?

P.S. Sorry for my bad english.

4
  • Why don't you want to modify the base template? This is the place to set which js\css files you want to import Commented Apr 6, 2012 at 21:19
  • 2
    I want to create some self-containing form widget extension bundle and enable this bundle only once in AppKernel.php. Moreover I want don't include js/css if widget has no use in current page. Commented Apr 13, 2012 at 8:18
  • 1
    This seems to be a missing feature in Symfony (until now). Maybe we should inspire from django widget media which includes the assets once only if the widget present Commented Nov 28, 2015 at 6:51
  • Still appears to be missing in 2021 unless there's something I'm not seeing... Commented Oct 4, 2021 at 19:24

5 Answers 5

10

I had to write a self contained form widget that requires javascript, I was able to achieve what you are trying to do through the event_dispatcher listening on the kernel.response to append the javascript at the end of the Symfony\Component\HttpFoundation\Response. Here's a snippet of my form type :

<?php

namespace AcmeBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;

class AcmeFileType extends AbstractType{

  private $twig;
  private $dispatcher;

  public function __construct(\Twig_Environment $twig, EventDispatcherInterface $dispatcher){
    $this->twig = $twig;
    $this->dispatcher = $dispatcher;
  }

  public function buildView(FormView $view, FormInterface $form, array $options){

    $javascriptContent = $this->twig->render('AcmeBundle:Form:AcmeFileType.js.twig', array());

    $this->dispatcher->addListener('kernel.response', function($event) use ($javascriptContent) {

      $response = $event->getResponse();
      $content = $response->getContent();
      // finding position of </body> tag to add content before the end of the tag
      $pos = strripos($content, '</body>');
      $content = substr($content, 0, $pos).$javascriptContent.substr($content, $pos);

      $response->setContent($content);
      $event->setResponse($response);
    });

  }

  ... 

When you define your form type in your services.yml it looks like this :

acme.form.acme_file_type:
    class: AcmeBundle\Form\AcmeFileType
    arguments:
        - @twig
        - @event_dispatcher
    tags:
        - { name: form.type, alias: acmefile }

So now, everytime you build a form with acmefile the javascript will be appended to the <body>. This solution does not prevent the javascript from being present multiple time though, but you should easily be able to improve this to suit your needs.

You can also play around with the $response object to modify the headers instead if you wish.

Sign up to request clarification or add additional context in comments.

2 Comments

Seems legit. Symfony uses a similar trick to load the web debug toolbar.
You should prepend if (!$event->isMasterRequest()) return; to the kernel.response listener in order to avoid adding this snippet to every single subrequest/-response.
1

The best way is to provide the separate template with css & scripts loading. With the comments in readme so only thing developer will must do is to

{% block stylesheets %}
  {{ parent() }}

  include "@MyBestBundle/Resources/view/styles.html.twig"

{% endblock %}

or try to intercept the form rendering with DI and add the assets. But it more difficult to do , if possible to implement.

Comments

1

My way of doing was by creating a custom twig extension where I add JS to a buffer and during form rendering and later dump it at the of my layout.

Something like this:

<?php

namespace AppBundle\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class WysiwygExtension extends AbstractExtension
{
    /**
     * @var array
     *
     * A pool of elements IDs for Wysiwyg binding.
     */
    private $wysiwygElements = [];

    /**
     * {@inheritdoc}
     */
    public function getFunctions()
    {
        return array(
            new TwigFunction('addWysiwygBinding', [$this, 'addWysiwygBinding']),
            new TwigFunction('popWysiwygBindings', [$this, 'popWysiwygBindings']),
        );
    }

    public function addWysiwygBinding(string $id): void
    {
        $this->wysiwyglements[] = $id;
    }

    public function popWysiwygBindings(): array
    {
        $elements = array_unique($this->wysiwygElements);

        $this->wysiwygElements = [];

        return $elements;
    }
}

Then form-fields.html.twig:

...
{% block wysiwyg_widget %}
{% apply spaceless %}
    {{ form_widget(form) }}
    {% do addWysiwygBinding(id) %}
{% endapply %}
{% endblock %}
...

Then layout.html.twig:

<!DOCTYPE html>
<html>
  <head>
    ...
  </head>
  <body>
    ...

        {% set ids = popWysiwygBindings() %}

        {% if ids is not empty %}
            {% javascripts
                'bundles/admin/plugins/wysiwyg_1.js'
                'bundles/admin/plugins/wysiwyg_2.js'
            %}
                <script type="text/javascript" src="{{ asset_url }}"></script>
            {% endjavascripts %}
        {% endif %}

        {% for id in ids %}
            {{ include('_wysiwyg.html.twig', { id: id }) }}
        {% endfor %}
  </body>
</html>

Comments

0

This is how I use it. Hope it's what you're looking for.

base.html.twig

<head>
   {% block stylesheets %}
       css...
   {% endblock %}
</head>

foo.html.twig

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

{% block stylesheets %}
   {{ parent() }}

   css that you need in foo.html.twig

{% endblock %}

1 Comment

Yes, this is usual way for regular view tempates (and I use it). But this don't work with form type templates.
0

I found some 'dirty' method used by many peoples in other situations. We check the loading of script on client side. In case we have a zlkladr.js file, that have a global object 'zlkladr'

{% block our_widget %}
{% spaceless %}
  ...
  <script>
    // We must load the script only once, even if many widgets on form
    if ( !window.zlkladr ) {
      document.write('<script src="{{ asset('bundles/kladr/js/zlkladr.js') }}"></sc'+'ript>');
    }
  </script>
{% endspaceless %}
{% endblock %}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.