Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The buildView() Method

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 $12.00

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

Login Subscribe

The autocomplete setup works nicely on the edit page. But, if you click to create an article... it looks like it's working, but it's not! This is just the normal autocomplete from my browser.

There's no JavaScript error and we do have the class and the data- attribute. We expected this: we just... haven't added the JavaScript to this page!

In edit.html.twig, the javascripts and stylesheets blocks bring in the magic. Let's solve this in the simplest way possible. Copy both of these blocks. Open new.html.twig and paste! Oh, and I mentioned earlier, that we're going to eventually tweak things so that the author field is only filled in on create: we're going to disable it on edit.

... lines 1 - 2
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/autocomplete.js/0/autocomplete.jquery.min.js"></script>
<script src="{{ asset('js/algolia-autocomplete.js') }}"></script>
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/algolia-autocomplete.css') }}">
{% endblock %}
... lines 15 - 23

That means... we won't need any of this stuff on the edit page. Let's delete it now. But, if you did need some JavaScript and CSS on both templates and you did not want to duplicate the blocks, you could create a new template, like article_admin_base.html.twig. It would extend content_base.html.twig and include the javascripts and stylesheets blocks. Then, edit.html.twig and new.html.twig would extend this.

Anyways, now that the JavaScript and CSS live in the new template, when we refresh, we have autocomplete.

The buildView() Form Class Method

Before we move on, I have one more cool thing I want to show you! And, it solves a real problem... just not a problem we realize we had yet. Close a few files then go to UserSelectTextType. The whole autocomplete system works because we are setting the attr option with class and data-autocomplete-url keys. Now open ArticleFormType where we use this field type. One of the things that we're allowed to do here is override that attr option. But, if we did that, our custom attr option would completely replace the attr default from the type class! In other words, we would lose all of the special attributes that we need!

To fix this, at the bottom of UserSelectTextType, go to the Code -> Generate menu, or command+N on a Mac, select Override methods and choose buildView(). Oh, there's also a method called finishView() and its purpose is almost identical to buildView() - it's just called a bit later.

... lines 1 - 14
class UserSelectTextType extends AbstractType
{
... lines 17 - 48
public function buildView(FormView $view, FormInterface $form, array $options)
{
... lines 51 - 57
}
}

Here's what's going on: to render each field, Symfony creates a bunch of variables that are used in the form theme system. We already knew that: in register.html.twig we're overriding the attr variable. And in our form theme blocks, we use different variables to do our work.

And, of course, we know that, thanks to the profiler, we can see the exact view variables that exist for each field. But... where do these variables come from? For example, why does each field have a full_name variable? Who added that?

The answer is buildView(): Symfony calls this method on every field, and it is the place where these variables are created and can be changed.

We do that with this $view variable, which is kind of a strange object. Start with $attr = $view->vars['attr'];. This $view object has a public ->vars array property that holds all of the things that will eventually become the "variables". At this moment, the core form system has already set this variable up for us: it will either be equal to the attr option passed for this field, or an empty array.

... lines 1 - 48
public function buildView(FormView $view, FormInterface $form, array $options)
{
$attr = $view->vars['attr'];
... lines 52 - 57
}
... lines 59 - 60

Next: grab the class: if class is set on $attr, use it, but add a space on the end. If there is no class yet, set this to be blank. Now, here's the key: let's always append js-user-autocomplete: that's the class we're using above. Call $attr['class'] = to set the new class string back on.

... lines 1 - 48
public function buildView(FormView $view, FormInterface $form, array $options)
{
... line 51
$class = isset($attr['class']) ? $attr['class'].' ' : '';
$class .= 'js-user-autocomplete';
$attr['class'] = $class;
... lines 56 - 57
}
... lines 59 - 60

Oh, and we also need to add the data-autocomplete-url attribute. Copy that from above and say $attr['data-autocomplete-url'] equals the generated URL. Perfect! Finally, set all of this back onto the view object with $view->vars['attr'] = $attr.

... lines 1 - 48
public function buildView(FormView $view, FormInterface $form, array $options)
{
... lines 51 - 55
$attr['data-autocomplete-url'] = $this->router->generate('admin_utility_users');
$view->vars['attr'] = $attr;
}
... lines 59 - 60

Phew! We're done! Now that we're setting the attr variable directly, we don't need to set the option anymore. And the best part is that we know our attributes will be rendered no matter what the user passes to the attr option.

Let's try it! Move over, refresh and cool! Nice work team! The element still has the attributes we need.

Oh, and open the profiler for this form. Click on the author field and check out the View Variables. So cool! That's exactly what we set!

Next: the form component has a crazy powerful plugin system. Want to make some tweak to every form or even every field in your entire app? That's possible, and it's fun!

Leave a comment!

18
Login or Register to join the conversation

Not sure why but adding the `javascripts` block to the edit or new template make the debug bar disappear into thin air

Console shows the following error:

```js
Uncaught ReferenceError: Sfjs is not defined
at new:89:44359
at new:89:44392
```

Reply

Hey danresmejia

Is it possible you forgot to call the parent? {{ parent() }}

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago | edited

On the Edit page I am getting this error: Can only be used with user objects.

\Form\DataTransformer\FormToUserTransformer.php (line 32)
`if (!$value instanceof User) {

        throw new \LogicException('Can only be used with user objects.');        

}`

Reply

Hey Farry7,

Check your FormToUserTransformer, you wether apply that data transformer to a wrong entity, or it does not allow null probably. To know for sure, try to debug the $value, you can add "dd($value);" before throwing the exception - that will give you some hint about the value of that variable. Or maybe you just forgot to use the correct namespace for your User entity in FormToUserTransformer :)

I hope this helps!

Cheers!

Reply
Giacomo V. Avatar
Giacomo V. Avatar Giacomo V. | posted 3 years ago | edited

Hi,

when i try to insert any parameter (for exemple i need RouterInterface $router) in the Article Constructor (my Article is Gruppi), i get the message

Too few arguments to function App\Form\Form\Gruppi\GruppiType::__construct(), 0 passed in C:\Users\Giacomo\Documents\GitHub\fgcweb\vendor\symfony\form\FormRegistry.php on line 91 and exactly 2 expected

I think this come from the controller's line:
$form = $this->createForm(GruppiType::class, $gr);

Now, there is a way to use RouterInterface from inside Gruppi without get that error, so i can set:

'data-autocomplete-url' => $this->router->generate('get_group_fields');

without the need to create a new form and use it in Gruppi's buildForm?

If i instance
$this->router = RouterInterface::class; i
inside the constructor, the error is solved but i get:

Call to a member function generate() on string
in
$attr['data-autocomplete-url'] = $this->router->generate('get_group_fields')

Reading docs i did:

`

   $route = new Route('/gr/utility/gruppi', ['_controller' => GruppiController::class]);
    $routes = new RouteCollection();
    $routes->add('get_group_fields', $route);
    $context = new RequestContext('/');
    $generator = new UrlGenerator($routes, $context);
    $url = $generator->generate('get_group_fields');

`

but it's very verbose and the output is:
data-autocomplete-url="//gr/utility/gruppi"

Any suggestion for a shorted and correct solution?

Thanks a lot!

Reply

Hey Giacomo V.

Which version of Symfony are you using? If you are using auto-wire and auto-configure features, then you should be able to just type-hint your services in the Form class and Symfony should do the rest (https://symfony.com/doc/cur... ). If not, you will have to define such Form class as a service

Cheers!

Reply
Jose carlos C. Avatar
Jose carlos C. Avatar Jose carlos C. | posted 3 years ago | edited

Hello there ,
This error occurs when saving the form.

Error: <blockquote>The choices "single" do not exist in the choice list.</blockquote>

I don't understand why I can't define the choices in the buildView method
Where can I do it? So that on the parent form I don't have to do it. Choices will be generated based on an option in the parent form called "entity_name"


namespace App\Form\Type;
use App\Service\Theme;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TemplateType extends AbstractType
{
  private $theme;
  public function __construct(Theme $theme)
  {
    $this->theme = $theme;
  }
  public function configureOptions(OptionsResolver $resolver): void
  {
    $resolver->setDefaults([
      'expanded'         => true,
      'multiple'         => false,
      'required'         => true,
      'label'            => false,
      'entity_name'      => '',
      'default_template' => ''
    ]);
  }
  public function buildView(FormView $view, FormInterface $form, array $options): void
  {
    $entityName = $options[ 'entity_name' ];
    $view->vars = array_replace($view->vars, [
      'entity_name'      => $entityName,
      'default_template' => $options[ 'default_template' ],
      'choices'          => $this->theme->templatesByEntity($entityName),
    ]);
  }
  public function getParent(): string
  {
    return ChoiceType::class;
  }
}
Reply

Hey Jose carlos C.

Give it a try defining the choices option at the buildForm method

Cheers!

Reply
Jose carlos C. Avatar
Jose carlos C. Avatar Jose carlos C. | MolloKhan | posted 3 years ago | edited

How? I've tried a thousand ways in the buildForm and I couldn't.

Reply

Ohh I see now why it's difficult, you are extending from ChoiceType and no adding any new fields. Hmm, what you are doing seems OK but since you are relying on the "field variables" I think you need to create a ChoiceView item per each option and put them onto an array. Check the "choices" definition here:https://symfony.com/doc/current/reference/forms/types/choice.html#field-variables
I haven't done that before so I might be wrong

Another thing you can try is to define the "choices" inside configureOptions() method as you did to: expanded, multiple, etc

Reply
Jose carlos C. Avatar
Jose carlos C. Avatar Jose carlos C. | MolloKhan | posted 3 years ago | edited

In configureOptions this is not possible, because the list depends on a newly defined option called 'entity_name'.
This is the parent form field:


.....
$builder->add('template', TemplateType::class, array(
      'entity_name'      => 'blog',
      'default_template' => 'single.html.twig'
    ));
Reply

Have you tried adding the choices in there?


$builder->add('template', TemplateType::class, array(
      'entity_name'      => 'blog',
      'default_template' => 'single.html.twig',
      'choices' => $arrayWithYourChoices
    ));
Reply
Jose carlos C. Avatar
Jose carlos C. Avatar Jose carlos C. | MolloKhan | posted 3 years ago

I want to do it without having to add it to the parent form, as I said before.

Reply

Yeea, it's kind of the diffcult what you are trying to achieve. Did you try to create your own ChoiceView list and use it in the buildView() method? If that doesn't work I think the answer to your problem may be managed via FormEvents https://symfony.com/doc/current/form/events.html

Reply
Default user avatar
Default user avatar MASQUERADE promotion | posted 4 years ago | edited

BTW, when I am trying to put in twig this
`

{{ form_row(articleForm.author, {
    attr: {class: 'the-author'}
}) }}

`
I am getting broken autocomplete. I mean it won't work. Still I have correctly appended the needed class...

Reply

Hey MASQUERADE promotion!

Do you get any JavaScript errors? And when you inspect your HTML, do you see the class being printed on your element? Finally, in our code, we added the class js-user-autocomplete - but it looks like you're adding a class called the-author - is that in purpose?

Cheers!

Reply
Default user avatar
Default user avatar MASQUERADE promotion | weaverryan | posted 4 years ago

thanks

Reply
Default user avatar
Default user avatar MASQUERADE promotion | weaverryan | posted 4 years ago

yeah, you are correct.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.6
        "symfony/console": "^4.0", // v4.1.6
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.6
        "symfony/framework-bundle": "^4.0", // v4.1.6
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.6
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.6
        "symfony/validator": "^4.0", // v4.1.6
        "symfony/web-server-bundle": "^4.0", // v4.1.6
        "symfony/yaml": "^4.0", // v4.1.6
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "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.1.6
        "symfony/dotenv": "^4.0", // v4.1.6
        "symfony/maker-bundle": "^1.0", // v1.8.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.6
    }
}
userVoice