Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Skipping Ajax: Sending JSON Straight to Vue

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

With both the categories and products loading dynamically, our app is starting to get really exciting! But there's a part of the user experience that I'm not happy about: there are a lot of things loading!

The "too much loading" Problem

When we get to a page, it's probably okay for some things to load. But right now, the page basically looks empty at first. The categories form part of the page layout... and it's a bit jarring when the sidebar is empty.

And... it could get worse! What if we wanted to include the current category name in the page title... or as the h1 on the page! In that case, both of those would be missing on load! And if we started to render info about the authenticated user in Vue - like a user menu, if we loaded that data via Ajax, then we would need to hide that menu at first and then show it.

The point is: too much loading can be a big problem.

What's the solution? Well, we're already making a request to the server each time we visit a category. When we do that, our server is already primed to make fast database queries. So, in theory, we should be able to fetch data - like for the categories or user information - during that page load and avoid the slow Ajax request.

In general, there are two solutions to this problem of "too much loading". The first is called server-side rendering where you render the Vue app on your server, get the HTML and deliver that on the initial page load.

That's a great solution. But it's also a bit complex because you need to install and execute Node on your server.

Passing the Categories from the Server to Vue

The second option, which is a lot simpler and almost as fast, is to pass the data from our server into Vue. Literally, in the controller, we're going to load all the categories, pass them into Twig and set them on a variable that we can read in JavaScript. That will make the data instantly available: no Ajax call needed!

Ok, let's do this! Remember: the controller for this page is src/Controller/ProductController.php. And actually, there are two controllers: index() - which is the homepage - and showCategory() for an individual category.

So if we're going to pass the categories to Vue, we'll need to pass it into the template for both pages.

Start in index(): autowire a service called CategoryRepository $categoryRepository. Now, add a second argument to Twig so that we can pass in a new variable called categories set to $categoryRepository->findAll().

... lines 1 - 12
class ProductController extends AbstractController
{
... lines 15 - 17
public function index(CategoryRepository $categoryRepository): Response
{
return $this->render('product/index.html.twig', [
'categories' => $categoryRepository->findAll(),
]);
}
... lines 24 - 42
}

That will query for all the categories.

Do the same thing down in showCategory(): add the CategoryRepository $categoryRepository argument, go steal the categories variable... and paste it here.

... lines 1 - 27
public function showCategory(Category $category, IriConverterInterface $iriConverter, CategoryRepository $categoryRepository): Response
{
return $this->render('product/index.html.twig', [
... line 31
'categories' => $categoryRepository->findAll(),
]);
}
... lines 35 - 44

Woo! We now have a categories variable available in the Twig template.

Serializing to JSON in the Template

Open it up: templates/product/index.html.twig. We're already setting a window.currentCategoryId global variable to an IRI string. But this situation is more interesting: the categories variable is an array of Category objects. And what we really want to do is transform those into JSON.

Go to /api/categories.jsonld: that's a quick way to see what the API response for categories looks like. So if we're going to send categories data from the server instead of making an Ajax call, that data should, ideally, look exactly like this.

This means that, in our Symfony app, we somehow need to serialize these Category objects into the JSON-LD format.

Open the src/Twig/ directory to find a shiny class called SerializerExtension. I created this file, which adds a filter to Twig called jsonld. By using it, we can serialize anything into that format.

... lines 1 - 8
class SerializerExtension extends AbstractExtension
{
... lines 11 - 17
public function getFilters(): array
{
return [
new TwigFilter('jsonld', [$this, 'serializeToJsonLd'], ['is_safe' => ['html']]),
];
}
public function serializeToJsonLd($data): string
{
return $this->serializer->serialize($data, 'jsonld');
}
}

Awesome! Back in the template, add window.categories set to {{ categories|jsonld }}.

... lines 1 - 12
{% block javascripts %}
... lines 14 - 15
<script>
... lines 17 - 21
window.categories = {{ categories|jsonld }};
</script>
... lines 24 - 25
{% endblock %}

Let's go see what that looks like! Find your browser, refresh and view the page source. Near the bottom... there it is! It's has the same JSON-LD format as the API! In the console, try to access it: window.categories. Yes! Here are the four categories with the normal @context, @id and @type.

Well, technically this is a little bit different than what the API returns. Go back to /api/categories.jsonld. In the true API response, the array is actually under a key called hydra:member. And if this were a long collection with pagination, the JSON would have extra keys with information about how to get the rest of the results.

The JSON we're printing is really just the stuff inside hydra:member. But most of the time, this is all you really need.

But if you did need all of the data, you could pass a 3rd argument to serialize() - an array - with a resource_class option set to whatever class you're serializing, like Category::class. That would give you more structure. If you need pagination info, that's also possible. Let us know in the comments if you need that.

But for us this data is going to be perfect, because all we need are the categories. Next, let's use this data in our Vue app to avoid the Ajax call! When we do, suddenly, our Ajax service function will change to be synchronous. But by leveraging a Promise directly, we can hide that fact from the rest of our code.

Leave a comment!

10
Login or Register to join the conversation

Hey hi!, I need pagination info,

how can it be done?

ty!

Reply

Hey diegoinaui!

That is an excellent question! And one that I have thought about before... solved... and forgotten how 🤦‍♂️. I believe what you need to do is this:

A) Create your query. Then use that to create a Doctrine paginator object. That's an instance of Doctrine\ORM\Tools\Pagination\Paginator .
B) Use THAT to create an ApiPlatform Paginator object - that is an instance of ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator.

Finally, you will pass the Paginator object from part B to the serialize filter.

To help a bit more, if I'm reading the code correctly, here is where these objects are created in API Platform: https://github.com/api-platform/core/blob/c81eaae600d9472f65df7d4d8bf41145b06390b8/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php#L160-L185 - note that the DoctrineOrmPaginator class in there is an alias to the Paginator class from part (A).

Let me know if that helps!

Cheers!

Reply
Kiuega Avatar

Hi Ryan! This question interests me as well.

But do you think it would also be possible to do it this way:

1. I am using your (beautiful) KnpPaginator bundle which will contain the data (not serialized), the pagination info.

2. I serialize the category data in some way (from the controller by modifying the data that will be stored in the knpPaginator paginator if possible).

3. I pass the object to Twig and put it in global to be able to access it from the JS.

What do you think of this alternative? (Or maybe it should get some fixes?)

Reply

Hey Kiuega,

I'm not sure about KnpPaginator, but I think a good idea to try. Doctrine Paginator is just a bit more lightweight and low level, Knp Paginator has more overhead, but if you get used to it - why not to try it instead?

> 3. I pass the object to Twig and put it in global to be able to access it from the JS.

Depends on how your JS code works, but sounds good to me too, simple and working solution.

Cheers!

1 Reply

Hi there!

I really don't like this inline script tag nor the global variables. Is there a reason why we use that instead of some data attributes on the body or div#app tags?

Thanks

Reply

Hey @Julien!

No reason at all :). This is a total personal preference - feel free to use data-attributes on body. They take 1 extra line of code to read them (no bid deal) and be careful with types (everything will be a string), but that’s it. I *do* like that data- attributes can be more localized - you can even put them in the div that you’re rendering into and read them from that :).

Cheers!

Reply
Nathan D. Avatar
Nathan D. Avatar Nathan D. | posted 3 years ago

Hello !

Is there a way to solve the loading issue when your API is on another server ? WEB-CLIENT on https://myapp.com and API on https://api.myapp.com for example.

Reply

Hi again! :p

What issue are we talking about exactly? How would you be loading the data? If you're referring to a possible CORS issue, yes, there is a way to fix that with proper headers.

Here's an article about it: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

Reply
Nathan D. Avatar

Talking about the categories loading side effect of the AJAX call. If we have the API and the frontend on 2 different servers this is not solvable by pushing directly inside Twig. Or maybe by doing a call to the API in PHP and sending data through Twig. Anyway that's not a big issue for now :)

Reply

That's right. Sending data directly from the server to twig makes sense if you have your API in the same server as your Symfony App. Alternatively, you may be able to perform calls to the API directly from the back end server, so they are ready to be injected as data inside of your component on page load, but this is a far more complex scenario!

1 Reply
Cat in space

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

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.0
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.1.0
        "eslint-plugin-import": "^2.19.1", // 2.20.2
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.5
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.11
        "vue-loader": "^15.9.1", // 15.9.2
        "vue-template-compiler": "^2.6.11", // 2.6.11
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}
userVoice