Reordering the Files

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

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

Login Subscribe

What else do you want to add to our file gallery widget? How about allowing them to be reordered? Yea, that isn't really related to uploading either, but a lot of people asked for it... so, let's do it!

Adding the position Field

To start, the ArticleReference entity needs a field that can store its order in the list. Find your terminal and run:

php bin/console make:entity

Update ArticleReference and add one new field position. This is an integer and make it not nullable. Cool!

Go find the property... there it is. Make it default to 0: until the user decides to reorder stuff, setting them all to 0 is fine.

... lines 1 - 12
class ArticleReference
{
... lines 15 - 48
/**
* @ORM\Column(type="integer")
*/
private $position = 0;
... lines 53 - 120
}

Create the migration with the usual:

php bin/console make:migration

and go to the src/Migrations directory so we can make sure it doesn't contain any surprises. Looks perfect! Close that and run:

php bin/console doctrine:migrations:migrate

Adding the Sortable Library

Ok, the database is ready! For the frontend, there are a ton of libraries that can help you sort and reorder stuff. I'm going to use one called Sortable. It's got a lot of support and tons of options. We'll need a few of them.

If you're using Webpack Encore, I'd recommend installing this via yarn and then importing the library when you need it. Because we're not, I'll Google for "sortablejs cdn". It's this one, from jsdelivr - the first is a different library. It turns out "Sortable"... is a pretty generic name.

Click to copy the HTML+SRI script tag, then go find the edit template. Scroll down to the JavaScript block and... paste!

... lines 1 - 35
{% block javascripts %}
... lines 37 - 39
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.8.3/Sortable.min.js" integrity="sha256-uNITVqEk9HNQeW6mAAm2PJwFX2gN45l8a4yocqsFI6I=" crossorigin="anonymous"></script>
... line 41
{% endblock %}

Hey! We now have a global Sortable variable.

Integrating Sortable

Next, open admin_article_form.js and scroll up to the constructor so we can start using this. Here's the plan: we're going to make each element - each "row" - sortable. And when we finish dragging, we'll send an AJAX request to save the new positions.

Add this.sortable = Sortable.create(). We're storing the instance of our new sortable object onto a property because we'll need it later. Pass this the parent of the elements that should be sortable. So... hmm... in our case, we want to attach sortable to the <ul> element that's around everything. Fortunately, that's exactly what this.$element represents! So we can say this.$element, and, this actually wants a raw HTMLElement, not a jQuery object, so add [0].

... lines 1 - 34
class ReferenceList
{
constructor($element) {
... line 38
this.sortable = Sortable.create(this.$element[0]);
... lines 40 - 56
}
... lines 58 - 110
}
... lines 112 - 137

Give it a test! Refresh... and grab... sweet! When we finish ordering, nothing saves yet, but we'll get there.

Making it Nicer!

Before we do, I think we can make this a bit nicer. Pass a second argument to create(): an array of options. Pass one called handle set to .drag-handle.

... lines 1 - 34
class ReferenceList
{
constructor($element) {
... line 38
this.sortable = Sortable.create(this.$element[0], {
handle: '.drag-handle',
... line 41
});
... lines 43 - 59
}
... lines 61 - 114
}
... lines 116 - 141

With this, instead of being able to grab anywhere to start sorting, we'll only be able to grab elements with this class. Down in render, how about, before the text field, add <span class="drag-handle">, and fa and fa-reorder.

... lines 1 - 34
class ReferenceList
{
... lines 37 - 97
render() {
const itemsHtml = this.references.map(reference => {
return `
<li class="list-group-item d-flex justify-content-between align-items-center" data-id="${reference.id}">
<span class="drag-handle fa fa-reorder"></span>
... lines 103 - 108
</li>
`
});
... lines 112 - 113
}
}
... lines 116 - 141

Oh, and while we're making this fancy, add animation: 150... it just makes it look cooler. Try it! There's our drag handle and... nice - it's a bit smoother.

... lines 1 - 34
class ReferenceList
{
constructor($element) {
... line 38
this.sortable = Sortable.create(this.$element[0], {
handle: '.drag-handle',
animation: 150,
});
... lines 43 - 59
}
... lines 61 - 114
}
... lines 116 - 141

This library doesn't require any CSS, which is cool... but we can make it a little nicer by adding some. In the public/css/ directory, open styles.css. This is a nice, boring, normal CSS file that's included on every page.

Add .sortable-ghost. When you're dragging, Sortable adds this class to where the element will be added if you stop sorting at that moment. Give this a background color. Oh, and also, give the drag-handle a cursor: grab.

... lines 1 - 251
/* Sortable */
.sortable-ghost {
background-color: lightblue;
}
.drag-handle {
cursor: grab;
}

Try it one more time - do a force refresh if it doesn't show up at first. And... there's the blue background!

Ok, the database is setup and the frontend is ready. Next, let's add an API endpoint to save the positions and make sure they're rendered in the right order.

Leave a comment!

This tutorial is built on Symfony 4 but works great in Symfony 5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.87.10
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.3
        "symfony/console": "^4.0", // v4.2.3
        "symfony/flex": "^1.0", // v1.6.2
        "symfony/form": "^4.0", // v4.2.3
        "symfony/framework-bundle": "^4.0", // v4.2.3
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.3
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.3
        "symfony/validator": "^4.0", // v4.2.3
        "symfony/web-server-bundle": "^4.0", // v4.2.3
        "symfony/yaml": "^4.0", // v4.2.3
        "twig/extensions": "^1.5" // v1.5.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "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.2.3
        "symfony/dotenv": "^4.0", // v4.2.3
        "symfony/maker-bundle": "^1.0", // v1.11.3
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.3
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.3
    }
}