Lift Stuff! The js- Prefix

Guys, get ready to pump up... on your JavaScript skills! No, no, I'm not talking about the basics. Look, I get it: you know how to write JavaScript, you're a ninja and a rock star all at once with jQuery. That's awesome! In fact, it's exactly where I want to start. Because in this tutorial, we're going to flex our muscles and start asking questions about how things - that we've used for years - actually work.

And this will make us more dangerous right away. But, but but! It's also going to lead us to our real goal: building a foundation so we can learn about ridiculously cool things in future tutorials, like module loaders and front-end frameworks like ReactJS. Yep, in a few short courses, we're going to take a traditional HTML website and transform it into a modern, hipster, JavaScript-driven front-end. So buckle up.

The Project: Pump Up!

As always, please, please, please, do the heavy-lifting and code along with me. By the way, in 30 seconds, I promise you'll understand why I'm making all these amazing weight-lifting puns. I know, you just can't... weight.

Anyways, download the course code from any page and unzip it to find a start/ directory. That will have the same code that you see here. Follow the details in the README.md file to get your project set up.

The last step will be to open a terminal, move into your project and do 50 pushups. I mean, run:

./bin/console server:run

to start the built-in PHP web server. Now, this is a Symfony project but we're not going to talk a lot about Symfony: we'll focus on JavaScript. Pull up the site by going to http://localhost:8000.

Welcome... to Lift Stuff: an application for programmers, like us, who spend all of their time on a computer. With Lift Stuff, they can stay in shape and record the things that they lift while working.

Let me show you: login as ron_furgandy, password pumpup. This is the only important page on the site. On the left, we have a history of the things that we've lifted, like our cat. We can lift many different things, like a fat cat, our laptop, or our coffee cup. Let's get in shape and lift our coffee cup 10 times. I lifted it! Our progress is saved, and we're even moving up the super-retro leaderboard on the right! I'm coming for you Meowly Cyrus!

But, from a JavaScript standpoint, this is all incredibly boring, I mean traditional! Our first job - in case I fall over my keyboard while eating a donut and mess up - is to add a delete icon to each row. When we click that, it should send an AJAX request to delete that from the database, remove the row entirely from the page, and update the total at the bottom.

Right now, this entire page is rendered on the server, and the template lives at app/Resources/views/lift/index.html.twig:

60 lines app/Resources/views/lift/index.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<div class="row">
<div class="col-md-7">
<h2>
Lift History
<a href="#list-stuff-form" class="btn btn-md btn-success pull-right">
<span class="fa fa-plus"></span> Add
</a>
</h2>
<table class="table table-striped">
<thead>
<tr>
<th>What</th>
<th>How many times?</th>
<th>Weight</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for repLog in repLogs %}
<tr>
<td>{{ repLog.itemLabel|trans }}</td>
<td>{{ repLog.reps }}</td>
<td>{{ repLog.totalWeightLifted }}</td>
<td>
&nbsp;
</td>
</tr>
{% else %}
<tr>
<td colspan="4">Get liftin'!</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td>&nbsp;</td>
<th>Total</th>
<th>{{ totalWeight }}</th>
<td>&nbsp;</td>
</tr>
</tfoot>
</table>
{{ include('lift/_form.html.twig') }}
</div>
<div class="col-md-5">
<div class="leaderboard">
<h2 class="text-center"><img class="dumbbell" src="{{ asset('assets/images/dumbbell.png') }}">Leaderboard</h2>
{{ include('lift/_leaderboard.html.twig') }}
</div>
</div>
</div>
{% endblock %}

Inside, we're looping over something I call a repLog to build the table:

60 lines app/Resources/views/lift/index.html.twig
... lines 1 - 2
{% block body %}
<div class="row">
<div class="col-md-7">
... lines 6 - 12
<table class="table table-striped">
... lines 14 - 22
{% for repLog in repLogs %}
<tr>
<td>{{ repLog.itemLabel|trans }}</td>
<td>{{ repLog.reps }}</td>
<td>{{ repLog.totalWeightLifted }}</td>
<td>
&nbsp;
</td>
</tr>
... lines 32 - 35
{% endfor %}
... lines 37 - 45
</table>
... lines 47 - 49
</div>
... lines 51 - 57
</div>
{% endblock %}

Each repLog represents one item we've lifted, and it's the only important table in the database. It has an id, the number of reps that we lifted and the total weight:

200 lines src/AppBundle/Entity/RepLog.php
... lines 1 - 8
/**
* RepLog
*
* @ORM\Table(name="rep_log")
* @ORM\Entity(repositoryClass="AppBundle\Repository\RepLogRepository")
*/
class RepLog
{
... lines 17 - 27
/**
* @var integer
*
* @Serializer\Groups({"Default"})
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var integer
*
* @Serializer\Groups({"Default"})
* @ORM\Column(name="reps", type="integer")
* @Assert\NotBlank(message="How many times did you lift this?")
* @Assert\GreaterThan(value=0, message="You can certainly life more than just 0!")
*/
private $reps;
/**
* @var string
*
* @Serializer\Groups({"Default"})
* @ORM\Column(name="item", type="string", length=50)
* @Assert\NotBlank(message="What did you lift?")
*/
private $item;
/**
* @var float
*
* @Serializer\Groups({"Default"})
* @ORM\Column(name="totalWeightLifted", type="float")
*/
private $totalWeightLifted;
... lines 64 - 198
}

To add the delete link, inside the last <td> add a new anchor tag. Set the href to #, since we plan to let JavaScript do the work. And then, give it a class: js-delete-rep-log:

72 lines app/Resources/views/lift/index.html.twig
... lines 1 - 2
{% block body %}
<div class="row">
<div class="col-md-7">
... lines 6 - 12
<table class="table table-striped">
... lines 14 - 22
{% for repLog in repLogs %}
<tr>
... lines 25 - 27
<td>
<a href="#" class="js-delete-rep-log">
... line 30
</a>
</td>
</tr>
... lines 34 - 37
{% endfor %}
... lines 39 - 47
</table>
... lines 49 - 51
</div>
... lines 53 - 59
</div>
{% endblock %}
... lines 62 - 72

Inside, add our cute little delete icon:

72 lines app/Resources/views/lift/index.html.twig
... lines 1 - 2
{% block body %}
<div class="row">
<div class="col-md-7">
... lines 6 - 12
<table class="table table-striped">
... lines 14 - 22
{% for repLog in repLogs %}
<tr>
... lines 25 - 27
<td>
<a href="#" class="js-delete-rep-log">
<span class="fa fa-trash"></span>
</a>
</td>
</tr>
... lines 34 - 37
{% endfor %}
... lines 39 - 47
</table>
... lines 49 - 51
</div>
... lines 53 - 59
</div>
{% endblock %}
... lines 62 - 72

Adorable! Ok, first! Why did we add this js-delete-rep-log class? Well, there are only ever two reasons to add a class: to style that element, or because you want to find it in JavaScript.

Our goal is the second, and by prefixing the class with js-, it makes that crystal clear. This is a fairly popular standard: when you add a class for JavaScript, give it a js- prefix so that future you doesn't need to wonder which classes are for styling and which are for JavaScript. Future you will... thank you.

Copy that class and head to the bottom of the template. Add a block javascripts, endblock and call the parent() function:

72 lines app/Resources/views/lift/index.html.twig
... lines 1 - 62
{% block javascripts %}
{{ parent() }}
... lines 65 - 70
{% endblock %}

This is Symfony's way of adding JavaScript to a page. Inside, add a <script> tag and then, use jQuery to find all .js-delete-rep-log elements, and then .on('click'), call this function. For now, just console.log('todo delete!'):

72 lines app/Resources/views/lift/index.html.twig
... lines 1 - 62
{% block javascripts %}
{{ parent() }}
<script>
$('.js-delete-rep-log').on('click', function() {
console.log('todo delete!');
});
</script>
{% endblock %}

Resolving External JS in PHPStorm

But hmm, PhpStorm says that $ is an unresolved function or method. Come on! I do have jQuery on the page. Open the base layout file - base.html.twig - and scroll to the bottom:

97 lines app/Resources/views/base.html.twig
<!DOCTYPE html>
<html lang="en">
... lines 3 - 19
<body>
... lines 21 - 90
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>

Both jQuery and Bootstrap should be coming in from a CDN. Oh, but this note says that there is no locally stored library for the http link. Aha! Tell PhpStorm to download and learn all about the library by pressing Option+Enter on a Mac - or Alt+Enter on Linux or Windows - and choosing "Download Library". Do the same thing for Bootstrap.

Et voilĂ ! The error is gone, and we'll start getting at least some auto-completion.

Using .on() versus .click()

Oh, and I want you to notice one other thing: we're using .on('click') instead of the .click() function. Why? Well, they both do the same thing. But, there are an infinite number of events you could listen to on any element: click, change, keyup, mouseover or even custom, invented events. By using .on(), we have one consistent way to add a listener to any event.

It's a small start, but already when we refresh, open the console, and click delete, it works! Now, let's follow the rabbit hole deeper.

Leave a comment!

  • 2017-07-20 Diego Aguiar

    Hey julien moulis!

    There are a few things that changes when moving from Dev to Prod, like cache, you have to clear it after making changes to your files, another one is the DB, you have a separated DataBase for Prod, so you will have to update the schema too.

    Give it a try and let us know if you have more problems :)

    Cheers!

  • 2017-07-20 julien moulis

    Hi everyone.
    I followed this course, and applied it to my app. It works pretty well on dev environnement, but as soon as I go on prod, onsubmit any forms, instead of returning errors to be displayed into my form, I get a dirty 500 explosion and no datas to be displayed... Any ideas?
    Thanks

  • 2017-04-03 Diego Aguiar

    Hey Pawel!

    That means you still need to install and activate doctrine migrations bundle, it doesn't come by default
    just run:


    composer require doctrine/doctrine-migrations-bundle

    and then open up your AppKernel.php and register it:


    $bundles = array(
    //...
    new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
    );

    You can read more about it here:
    http://symfony.com/doc/curr...

    Have a nice day!

  • 2017-04-03 Pawel End

    I have error when I run command "php bin/console doctrine:migrations:migrate"

    error: [Symfony\Component\Console\Exception\CommandNotFoundException]
    There are no commands defined in the "doctrine:migrations" namespace.
    Did you mean one of these?
    doctrine:query
    doctrine:schema
    doctrine:cache
    doctrine:generate
    doctrine:mapping
    doctrine:database
    doctrine

  • 2017-03-29 Victor Bocharsky

    Yeah, agree, file uploading is tough, but file uploading with JS is more tough ;)

    Cheers!

  • 2017-03-28 julien moulis

    Thanks Ryan for the quick answer.
    I'll go diving in the ocean of api rest... It will help, I guess to understand the course as well.

  • 2017-03-28 julien moulis

    Thanks Victor. For the moment I'll continue using a normal php upload. This plugin is great but tough to get it work for me, now.

  • 2017-03-27 Victor Bocharsky

    Hey Julien,

    Yeah, I had the same problem and I solved it with a third-party JS library which helps uploading files. I used jQuery-File-Upload, here's a demo how it works: https://blueimp.github.io/j... . Sorry, I didn't dive into it and don't know how exactly it works, but I know that this library do everything you need to upload files via AJAX. I hope it helps you.

    Cheers!

  • 2017-03-26 weaverryan

    Hey julien moulis!

    It's possible/likely that with this FormData, the data is sent as normal application/x-www-form-urlencoded format instead of as JSON. What that means is that you probably won't fetch and decode the request body as JSON. Instead, you'll treat it more like a traditional form upload:


    $filename = $request->request->get('ifileName');
    $uploadedFile = $request->files->get('fileTemporary');

    I hope that helps :)

  • 2017-03-25 julien moulis

    Actually I was trying formData, but it seems to break with the actual code of the course. "Invalid Jason", which I guess is normal. I'll give it a try.

  • 2017-03-25 weaverryan

    Ah, I think I know the problem! Uploading files with AJAX... well, it doesn't work :). Well, at least not traditionally - AJAX didn't originally ship with the ability to upload files. I think this may have changed (http://blog.teamtreehouse.c..., but I believe that most people still use some library or jQuery plugin to help with AJAX file uploads (it actually has *always* been possible, but required some hacks that these libraries can help you with).

    So, I would recommend trying out the FormData() object that the Treehouse blog talks about (I've actually never tried this myself) or use some sort of a plugin - e.g. google "jQuery AJAX file upload". You'll need to do a bit more work, but I hope this will at least end your frustration! It's not something you're doing wrong - AJAX itself is causing the problems!

    Cheers!

  • 2017-03-25 julien moulis

    This is making me crazy. Everything is working so well. It is just the input file(named fileTemporary beccause not persisted) that is not sent in the ajax call.
    {links: {_self: "/document/docs/106"}, id: 106, fileTemporary: null, fileName: "Test file"}
    Is there a specific action for the input file to do on an ajax call?

  • 2017-03-24 julien moulis

    Hi Victor,
    The Ajax request is sent, the entity is persisted. But the file upload, move and all the work that is done by vichuploadBundle doesn't work. On my form I have a input file and a textfield. When post the form and I check the Ajax request, and then the form (symphony debug toolbar), the input file is empty, so I guess that's probably why vich doctrine listener doesn't work. If you code or anything else tell me
    And everything works fine without using Ajax.

  • 2017-03-24 Victor Bocharsky

    Hey Julien,

    Ah, it's difficult to understand the problem. Could you debug it a bit more? Was the AJAX request sent? Were any errors occurred? You can use Google Chrome dev toolbar (there, in Console tab, you can see any JS errors that were occurred) and Symfony debug toolbar in dev environment, and also look for more information in your logs in "var/logs/" directory.

    Cheers!

  • 2017-03-24 julien moulis

    Hey everyone.
    I'm trying to apply the tutorial to my app. But I encounter a problem with the use of ajax and upload file. I'm using VichUploader. Haphazardly, have you ever encountered the pb? The file doesn't upload.

  • 2017-02-22 weaverryan

    Thanks Kieran Mathieson I always appreciate your feedback :). And I agree with both points - would probably be even better if we had a working, non-AJAX delete link first (to establish its purpose), then added the "fancy". And I don't often do the "So far, we've", but since it's easy to keep this to one sentence, I think it would add context without dragging things out.

    In other words, cheers! Will keep these things in mind for future tutorials. And nice to hear from you!

  • 2017-02-20 Kieran Mathieson

    Good stuff, Ryan. As usual. Good learning design, good implementation.

    Great positioning - people who use jQuery already, but need to adapt to the New Order.

    Perhaps show the delete button in action before coding it. Give the steps we'll be coding, e.g., (1) add an icon for the button, then (2)... Then refer back to the list at the start of each lesson. "So far, we've... Now, we'll..."

    Good operational hints, like starting class names with js-.

  • 2017-01-19 weaverryan

    Ha, you live dangerously :p. Actually, fortunately, most projects have pretty-well-locked down version constraints in composer.json, so not THAT dangerous... as long as you don't do this on production ;).

    Thanks again!

  • 2017-01-19 Raymond A

    Thanks !
    I always do a composer update when receiving a symfony project.

  • 2017-01-19 weaverryan

    Hey Raymond!

    And thanks for letting me know about a potential bug! So, the code download DOES work... but I wonder if you somehow upgraded to the latest version of FOSUserBundle? In our version, the salt was automatically set in the base User class. But, regardless, you made me realize that we should probably just upgrade the project to the latest FOSUserBundle to avoid confusing, which I'm doing right now. Then, all should be good (it includes a new migration to make "salt" nullable, because salt is now nullable in the latest FOSUserBundle).

    Cheers!

  • 2017-01-19 Raymond A

    Hi,
    Thanks for the tuts!
    However , the download code is missing the salt value in the fixtures and since the column is defined as NOT NULL.
    This generating an error when trying to load the fixtures as defined in the README.md.