Buy
Buy

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:

{% 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:

... 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:

... 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:

... 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:

... 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:

... 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!'):

... 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:

<!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!

  • 2018-08-30 weaverryan

    Hey Tomasz Gąsior!

    Yep, you're totally right - that was lazy on my part :). Thanks for the comment - I'll pay closer attention to this in the future!

    Cheers!

  • 2018-08-30 Victor Bocharsky

    Hey Tomasz,

    Ah, now I see... Yeah, makes sense, and it good to know! Thanks for pointing us about it.

    Cheers!

  • 2018-08-29 Tomasz Gąsior

    Thank you for your response.

    Sorry! I was talking about "delete" button on each row. But I given you wrong time stamp. It should be around 4:30.

    Hyperlinks have to be used to indicate URL change. Buttons (<button type="button">) should be used if URL is not changed and action is fired on JavaScript side. This is important for a11y. Please look at this articles:
    * https://blogs.ancestry.com/...
    * https://marcysutton.com/lin...

  • 2018-08-29 Victor Bocharsky

    Hey Tomasz,

    Are you talking about "I lifted it" button? That's exactly what we press at 3:00 in the video, and it is a button tag. But anyway, using "a" tag with href="#" is totally OK as for me, because it has a valid HTML syntax. Why don't you like it?

    Cheers!

  • 2018-08-28 Tomasz Gąsior

    3:00 — from HTML semantic perspective you should use <button type="button"> instead of (a href="#"). (a) should not be used with senseless href="" value.
    (For some reason Discuss does not escape "A" html tag in comments.)

  • 2018-06-01 Victor Bocharsky

    Hey Leora,

    Thanks for sharing this with others! Yes, new version of FOSUserBundle requires this configuration to be able to run the project, so if you download our code and run "composer update" - you need to specify those keys.

    Cheers!

  • 2018-05-30 Leora Wenger

    I added this to config.yml and it was happy.


    fos_user:
    from_email:
    address: "info@somewhere.com"
    sender_name: "No Reply"


  • 2018-05-30 Leora Wenger

    Hi, I am going through the instructions in the ReadMe.
    I got this one:
    [Symfony\Component\Config\Definition\Exception\InvalidConfigurationException]
    The child node "from_email" at path "fos_user" must be configured.

  • 2018-05-24 Victor Bocharsky

    Hey Xi,

    Thanks for sharing it! Yes, probably some important BC break was provided by PHP 7.2, so upgrading deps is always a good idea if you have errors in vendors.

    Cheers!

  • 2018-05-23 Xi Wang

    Same happened while doing a revision - was fine before PHP 7.2

    I fixed it by doing a 'composer update'. Hope it helps.

  • 2018-05-23 Adam

    Hi

    I'm getting the same error when I click 'I lifted it'.
    I'm using PHP 7.2

    Adam

  • 2018-04-30 weaverryan

    Hey sandeep sangole!

    Hmmm. Can you post your RepLogApp.js? Exactly how far through the tutorial are you? I'm asking because we don't create the RepLogApp.js file until chapter 12 of this tutorial (https://knpuniversity.com/s..., so I need to know exactly where you are :).

    But, I also think that I recognize the error. Near the end of the tutorial (https://knpuniversity.com/s..., we update our code to make an AJAX request to an API and we *expect* our API endpoint to return JSON. We then use JSON.parse() to parse that JSON, so we can handle it.

    I believe the error is coming from there. Specifically, for some reason, your API endpoint is NOT returning JSON - it's probably returning HTML, maybe an error page. And so, RepLogApp.js throws an error when it tries to parse this as JSON. The "Unrecognized token '<'" is because it is seeing an HTML tag, instead of JSON.

    So, check your AJAX request and see why it's not returning JSON :).

    Cheers!

  • 2018-04-30 Victor Bocharsky

    Hey Sandeep,

    I already answered this question replying your email, so I just duplicate the answer here:

    Thanks for reporting it! Hm, probably you just missed some steps in the README files of the code downloads? If all of the precondition steps were executed, could you tell me a bit more how did you get this error? I really interested in steps to reproduce it. Did you unzip the code, change the current directory to "start/", run all the necessary steps from both README files: in rood dir and in start/ dir, and run the project? On what page did you get this error? What do you do exactly, do you press any buttons? And btw, what is your PHP version?

    I'm trying to understand whether it's user specific error or we really have a bug somewhere in our code.

    Cheers!

  • 2018-04-30 Victor Bocharsky

    Hey Sandeep,

    You can easily edit your comments, so no need to write a several comments if you forgot some details in the previous message ;) Also, we're tracking each Disqus comment, so do not need to duplicate the question in direct message. But yeah, Disqus comments is the best way to get help from us, and it's also may be useful for other people who will see comments, they may find already answered questions. Unfortunately, we do not able to answer your questions immediately, so we may reply back to you with some delay, please, keep in mind it. Thanks for understanding.

    Cheers!

  • 2018-04-29 sandeep sangole

    What is the best way to get help on below issues ??

  • 2018-04-29 sandeep sangole

    Below error is on finish project. Getting error on start project too :(

    Warning: count(): Parameter must be an array or an object that implements Countable

    500 Internal Server Error - ContextErrorException

    Stack Trace
    in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 716 -
    return FormUtil::isEmpty($this->modelData) ||
    // arrays, countables
    0 === count($this->modelData) ||
    // traversables that are not countable
    ($this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData));
    }

  • 2018-04-29 sandeep sangole

    Hi there , I have just started course , downloaded source code and setup the application. While adding to lift history I am getting below error ,

    [Warning] jQuery.Deferred exception: JSON Parse error: Unrecognized token '<' (2) (jquery-3.1.1.min.js, line 2)

    parse

    (anonymous function) — RepLogApp.js:126

    j — jquery-3.1.1.min.js:1349

    (anonymous function) — jquery-3.1.1.min.js:1356

    undefined

  • 2018-04-10 Diego Aguiar

    Hey Prout

    I'm sorry that you are feeling that way. It has been never our intention to teach any bad practices, but when we do (because we are not aware of, or maybe the technology just evolved) we like to be told so. Anyways, this tutorial is not about JQuery, we used JQuery because is a handy library for doing a lot of common stuff, but, you can use any library you want, or even pure JS if you like.

    If you have any question or feedback, don't hesitate to contact us. Cheers!

  • 2018-04-08 Prout

    I'm discouraged that it's showing bad practices. jQuery IS a bad practice. Why the heck should I import 10k rows of stuffed code to do something I can do in 10 rows ?

  • 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.