Categories
Software Development Web Development

Symfony 5+ how to view dump output with AJAX requests

So the idea is to be able to use the Symfony VarDumper component to be able to dump values when sending an AJAX request and view them for debugging while in development mode, but not in production mode.

Create a new directory

To do this you will need to create an event subscriber and listen for the kernel.response event. To do this you create a new directory inside your root directory, I called mine EventSubscribers, the name doesn’t matter you could call yous BlueWaffle and it will work exactly the same.

Create a new file

Inside this new directory create a new subscriber class, I called mine AjaxDumpSubscriber. You can make your subscriber implement

 implements EventSubscriberInterface

I am not sure if that is needed, I saw it somewhere in the docs. They never mentioned if it was needed so I am assuming it is maybe needed.

Then add the method stub for

getSubscribedEvents()

Here is what the github source code comment says about this method

interface EventSubscriberInterface
{
    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * The array keys are event names and the value can be:
     *
     *  * The method name to call (priority defaults to 0)
     *  * An array composed of the method name to call and the priority
     *  * An array of arrays composed of the method names to call and respective
     *    priorities, or 0 if unset
     *
     * For instance:
     *
     *  * ['eventName' => 'methodName']
     *  * ['eventName' => ['methodName', $priority]]
     *  * ['eventName' => [['methodName1', $priority], ['methodName2']]]
     *
     * The code must not depend on runtime state as it will only be called at compile time.
     * All logic depending on runtime state must be put into the individual methods handling the events.
     *
     * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
     */
    public static function getSubscribedEvents();
}

The full class

The entire class should look like this, from what I can tell. This is working code.

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class AjaxDumpSubscriber  implements EventSubscriberInterface
{

    /**
     * @return array
     */
    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::RESPONSE];
    }

    public function onKernelResponse(ResponseEvent $event)
    {
        if (!$event->getKernel()->isDebug()) {
            return;
        }
        $request = $event->getRequest();

        if (!$request->isXmlHttpRequest()) {
            return;
        }
        $response = $event->getResponse();
        //set this one value only if this is in development mode
        $response->headers->set('Symfony-Debug-Toolbar-Replace', 1);
    }
}

That is it, now you can dump() values like normal… but you can’t view them like normal, not in Symfony 5.4, not for me anyways .

This only partially works, maybe I missed something or I expect something? Most of the time when you use dump() there will be an icon in the profiler bar that indicates debug output was output aka. dump() was used. Usually you can click on it and quickly see the values that were dumped. This feature doesn’t work with  AJAX requests at the moment, from what I can observe.

Instead you need to click the response when it indicates in the profiler then click the Debug tab on the left panel to see your dump() values.

Links

The VarDumper component. -> Docs link you must install this before you can dump anything.

The profiler component -> Docs link you need this to see dumped stuff, even though it doesn’t work properly with the VarDumper

Creating an EventSubscriber -> Docs link

Using event subscribers -> docs link

 

Categories
Software Development Web Development

Javascript and contenteditable how to move the cursor to the end of user input

This appears to work in all browsers. Let me know if you find issues.

So this is something I needed to do and all the examples I found didn’t work properly. Or they worked partially, or only in one browser.

The idea seems super simple, but in reality the problem becomes complex for several reasons. For one, you need to have event listeners to listen for input then perform the proper actions to update the content and move the cursor to the end.

My use case makes it even harder and maybe yours does too. What I needed was to allow a user to enter hashtags and then send an ajax request to the backend to fetch a list of matching hashtags to display in a drop list below the input(auto suggestions). Then when the user selects one, add it to the list by removing the typing first, then add the new hashtag and move the cursor to the end.

At first I was all confident like

This should be easy right?

So that sounded easy. I got most of it working by using Google, reading some articles, reading stack questions… but it wasn’t working properly. My cursor would appear in random places or not appear, or not be where I wanted it to be. I tried reading the MDN docs for range and well I didn’t quite grasp how the system works.

If you are reading this you probably have done all this too and may be having issues as well.

Trying to get this working

One stupid, nonsensical issue I ran into right away was when the user hit backspace instead of one letter or hashtag being removed, ALL CONTENT WAS REMOVED.

homer simpson meme
Not the action we expected now is it.

I fixed this by adding &nbsp; which is html for non breaking space. Explained in depth in this great article. Some people add an empty paragraph from what I read. I decided to use nbsp; because it is seen as a #text node not a text element like p for example.

So now when the user backspaces, the first backspace does nothing, but the next removes the hashtag. This repeats until there are no hashtags. I was using divs, with spans inside them. I moved to just a simple span element with @nbsp; added to the end.

My original html minus class and id’s

My first idea was to have a hashtag with an x to remove it. But then I was like, most people will just backspace and if they are on their phone no way they can accurately touch it.

I also discovered that you must add contenteditable=false to each element within the parent element so that when the user clicks, the cursor doesn’t appear inside some randomish point instead of at the end of the content.

So I adjusted it to be just the span with a following nbsp tag like this.

Ok so now I have these hashtags displaying correctly. I don’t like that I have to add the invisible space tag to make the contenteditable act right, but what can we do?

If you are wondering how I make these cool code images, you need to checkout this carbon app.

Below is the actual div and some sample hashtags I pulled from the developers console.

notice the html structure here.

See that autocomplete=off . Yeah you need that so that when a user types into input or contenteditable divs it doesn’t drop it’s own suggestion list over your suggestion list, it really makes things a mess.

Moving the cursor to the end.

In the video below you can see what I want and what I have gotten working so far. This pretty much works exactly how I wanted it to work. Notice the user can enter a hashtag at the beginning and when they choose enter the cursor moves to the end.

If you are working on a similar project and need the cursor to go to the end like this, then you know what a pain it was to get this figured out. LOL

I eventually got tired of the try, fail, google, curse, try, fail, curse some more routine. So I decided to dig directly into the W3C standards myself. It started making sense after  I read it a few times and then checked the MDN links above again and played around etc. I tried reading the DOM spec but I didn’t find it as helpful. But overall it took reading all of everything I could find and trial and error to figure it out.

Ok so what magical code have I used? I wont cover the entire class and how it works. I’ll just show the actual method that creates a range and moves to the end.

moveCursorAfterHashtags() {
        let hashtagRange = document.createRange();
        let windowSelection = window.getSelection();
        //remove any previously created ranges
        windowSelection.removeAllRanges();
        let theNodes = this.hashtagsDivElement.childNodes;
        this.hashtagsDivElement.focus();
        let firstNode = theNodes[0];
        let lastNode = theNodes[theNodes.length - 1];
        let start = theNodes[0];
        let end = theNodes[theNodes.length - 1];
        console.log('Start is ' + start.nodeName + ' end is ' + end.nodeName + " node count " + theNodes.length);
        hashtagRange.setStartBefore(firstNode);
        hashtagRange.setEndAfter(lastNode);
        hashtagRange.collapse(false);
       //add the range to a window selection object.
        windowSelection.addRange(hashtagRange);
        windowSelection.collapseToEnd();
        console.log('commonAncestorContainer contains ' + hashtagRange.commonAncestorContainer.id);
        console.log('The start container is ' + hashtagRange.startContainer.id);
        console.log('The end container is ' + hashtagRange.endContainer.id);
    }

Notice all the console logs needed to figure this out. LOL

So the code above creates a range. Then creates a selection object, this is needed to actually move the cursor to the end of the range with this line

windowSelection.collapseToEnd();

But before you move the cursor to the end you must collapse the range to a single point hence the line

 hashtagRange.collapse(false);

You will see people using setStart() and setEnd(). After reading the docs I found it MUCH EASIER. to just use setStartBefore() and setEndAfter().

This is because of the structure I am using a span followed by a plain #text node, which may or may not exist, and it makes calculating the offsets harder. This way if the user backspaces one time and the nbsp is removed, but the leading hashtag remains the range is still inclusive and less calculations needed.

so much thinking…

You can see I feed them a node. The first and last node are calculated with

let firstNode = theNodes[0];
let lastNode = theNodes[theNodes.length - 1];

hashtagRange.setStartBefore(firstNode); 
hashtagRange.setEndAfter(lastNode);

This is because .childNodes returns an array which starts with 0, so to get the last one you must reduce the count by 1. A 2 item array will have the values 0 and 1, because the numbering starts at 0.

In this terminology node is anything html element or just typed text or the nbsp; that makes the space. These are all called nodes.

The other start and end are old code just used for console logging to see WTF is going on LOL

Also notice where I set the focus to the parent div element before creating the range etc.

this.hashtagsDivElement.focus();

I am not sure if this makes a difference, I prefer to add focus to the element first.
And that is how you get the cursor to set at the end of the contenteditable div.

 

boom how it is done baby meme
Trial and error until it works

 

If this doesn’t work properly in all browsers it is the browser makes fault for not deciphering the cryptic standards better.

Categories
Web Development

Javascript event listeners be careful where you bind this

In Javascript User interface programming you often need to work with EventListeners. In modern Javascript you may also be doing this in a class, which is handy. One of the  things that sucks about Javascript event listeners is they eat memory and slow your app down, if you are not REALLY careful.

sweet meme
Slow down your app the easy way!

They do this because each time the action you use to add the event listener code fires, more and more code gets added to memory, more things need to be kept up with etc.

There are some best practices that help with this issue such as always remembering to remove the event listeners, never use anonymous functions, arrow functions etc. Here on MDN there is a lot of good information I won’t waste time repeating in this article.

What I want to cover in this article is properly binding this within a class when using Event Listeners so that the function signatures match and therefore the listeners can properly be removed. Don’t think reloading the page solves your problem either… it doesn’t. In fact it just makes the browser eat more and more memory.

The reason we need to bind this is the code in our Javascript classes that handle the events ( event handlers ) usually need to access other methods and properties within the class when called by the event listener code.

Magical Bugs

Worse than the memory issue is the magical, insanely, nearly impossible bugs that having multiple listeners calling the same function can create.

If for example the function changes a variable value so that some UI action changes etc. One example I just ran into was a method in a class that monitored a keypress then moved the cursor. This was for an auto completer with a drop list of suggestions. The idea was to have Javascript highlight the selected option and accept it when the user hits enter. Below is the code example.

keyPressHandler(event) {

        let target = event.target.id;
        this.keyPressed = event.keyCode;
        this.optionElementsList = document.getElementById(target + "-ez-hashtags-list");

        if (this.optionElementsList) {
            this.optionElementsList = this.optionElementsList.getElementsByTagName("div");
        }

        switch (event.keyCode) {
            /*If the arrow DOWN key is pressed,
           increase the currentChoice variable:*/
            case 40:
                console.log('inside keyPressHandler handling keydown event');
                this.currentChoice++;
                this.makeActive();
                break;
            /*If the arrow UP key is pressed,
       decrease the currentFocus variable:*/
            case 38 :
                console.log('inside keyPressHandler handling keydown event');
                this.currentChoice--;
                this.makeActive();
                break;
            /*If the ENTER key is pressed, prevent the form from being submitted,*/
            case 13 :
                event.preventDefault();

                if (this.currentChoice > -1) {
                    /*and simulate a click on the "active" item:*/
                    if (this.optionElementsList) {
                        this.optionElementsList[this.currentChoice].click();
                    }
                    //empty the keysTyped variable
                    this.keysTyped = '';
                }
                break;
            case 8 :
            case 9 :
                //9 is tab, close any list if they hit tab
                //8 is backspace close the current list if they hit backspace
                this.closeOpenLists();
                break;
        }
    }

This is similar to what you need to do when creating a custom WYSIWYG html editor. You need to monitor every damn key pressed and perform actions based on that. The real fun begins when you have two listeners say one for keyup and one for keydown or just two of the same.

The code above if the user hits the up or down arrow decreases or increases the variable used to track the users expected action. It then calls another method to make the correct option highlighted for the user as selected. Below is the code.

makeActive() {

        if (!this.optionElementsList) {
            return false;
        }
        /*start by removing the "active" class on all items:*/
        this.makeInactive();

        if (this.currentChoice >= this.optionElementsList.length) {
            this.currentChoice = 0;
        }
        if (this.currentChoice < 0) {
            this.currentChoice = (this.optionElementsList.length - 1);
        }
        /*add class "autocomplete-active":*/
        this.optionElementsList[this.currentChoice].classList.add("ez-hashtags-active");
    }

This is where the currentChoice variable comes in play. The up and down arrows add the class which changes the background color to make it look highlighted. Then when the user hits enter, the selected value is entered into the input, but in this case a div with contenteditable=true This UI stuff is a real pain in the ass.

The BAD CODE

Now lets look at the wrong way to bind this, even though it looks like this should totally work properly.

 createChoiceList(elementId, inputValue) {

        if (Utils.isEmpty(inputValue)) {
            return false;
        }
        this.currentChoice = -1;

        if (!Utils.isEmpty(this.listOptions)) {

            if (Utils.notEmpty(this.optionContainer)) {
                this.closeOpenLists();
            }

            /*create a DIV element that will contain the items (values):*/
            this.optionContainer = document.createElement("DIV");
            this.optionContainer.setAttribute("id", elementId + "-ez-hashtags-list");
            this.optionContainer.setAttribute("class", "ez-hashtags-items");
            /*append the DIV element as a child of the hashtag div container:*/
            this.hashtagsDivElement.parentNode.appendChild(this.optionContainer);

            /*for each item in the array...*/
            for (let i = 0; i < this.listOptions.length; i++) {

                /*check if the item starts with the same letters as the text field value:*/
                let firstLetter = this.listOptions[i].substr(0, inputValue.length).toUpperCase();

                if (firstLetter === inputValue.toUpperCase()) {

                    /*create a DIV element for each matching element:*/
                    let b = document.createElement("DIV");
                    /*make the matching letters bold:*/
                    b.innerHTML = "#" + this.listOptions[i].substr(0, elementId.length) + "";
                    b.innerHTML += this.listOptions[i].substr(elementId.length);
                    /*insert an input field that will hold the current array item's value:*/
                    b.innerHTML += "";
                    /*execute a function when someone clicks on the item value (DIV element):*/
                    this.optionContainer.appendChild(b);
                }
            }
            this.hashtagsDivElement.addEventListener('keydown', this.keyPressHandler.bind(this));
            this.optionContainer.addEventListener('click', this.choiceSelectionHandler.bind(this));
        }
    }

The offending code is the last two lines that add the even listeners. The intention here was to make the signature match the remove event listener code. Like  this below.

removeKeypressHandlers() {
        try {

            if (this.hashtagsDivElement) {
                this.hashtagsDivElement.removeEventListener('keydown',this.keyPressHandler.bind(this));
            }

            if (this.optionContainer) {
                this.optionContainer.removeEventListener('click', this.choiceSelectionHandler.bind(this));
            }

        } catch (e) {
            console.log("OOPS we caught an error " + e);
        }
    }

It looks good, like they match like it should function, right?

makes sense to me meme
Looks good to me…

WRONG

Even though those functions look like they match, bind(this) creates a new function every time it is called.

scared chimp
Oh that can’t be good. That can’t be good at all.

So now we have LOTS OF EVENT LISTENERS. And that causes magical bugs like the video below.  The real good example is after the 30 second mark.

Notice how when I first type the selection works. Then when I backspace and start again it just jumps to the last one. Then I backspace and start again and it works. Then it doesn’t I was like WTFF I am losing my mind. Watch towards the end for the magic to start really good.

God forbid you ever meet an articulate ass.

articulate ass
Meet the articulate ass

The right way

So how should we defeat this Javascript demon and bind this properly for our event handlers??? Well the answer is quite simple actually. You create another new variable in the constructor and bind this to the handler there. This way bind creates only 1 function, 1 time and it can easily be removed. As a bonus if you add the function 100 times in 100 calls, the browser will only actually add it 1 time.

So lets see the code.

        constructor(inputElementId, endpointUrl, fetchOnce = true, keysThreshold = 2) {

        //set the routes for the Routing object
        Routing.setRoutingData(routes);
        this.inputElementId = inputElementId;
        this.currentChoice = -1;
        this.endpointUrl = endpointUrl;
        this.fetchOnce = fetchOnce;
        this.inputElement = document.getElementById(inputElementId);
        this.hashtagsDivElement = null;
        this.hashtagValues = '';
        this.inputValues = '';
        this.optionContainer = null;
        this.optionElementsList = null;
        this.listOptions = '';
        this.keyPressed = '';
        this.keysTyped = '';
        this.boundChoiceSelectionHandler = this.choiceSelectionHandler.bind(this);
        this.boundKeypressHandler = this.keyPressHandler.bind(this);
        this.boundTypingHandler = this.handleTyping.bind(this);
        //add the hashtags div and hide the actual input element
        this.hideInputElement();
        this.addHashtagsDiv();
    }

As you can see in the code above,this goes in the class constructor. Anywhere else and you could be creating the same listeners over and over.

Then to actually add the listeners when you want them to be added you do this little trick below.

this.inputElement.addEventListener('keydown', this.boundKeypressHandler);
            this.optionContainer.addEventListener('click', this.boundSelectionHandler);

Notice now the binding is done in the constructor and the variable that holds the bound function is added in place of the previous binding in the addEventListener() calls. This way these eventListeners are only ever created once, the other times are ignored. Javascript bind actually creates and returns a new function with the scope needed to use this keyword.

To remove the event listeners later to cleanup memory, you do this.

removeKeypressHandlers() {
        try {

            if (this.inputElement) {
                console.log('Removing the input element keydown handler');
                this.inputElement.removeEventListener('keydown', this.boundKeypressHandler);
            }

            if (this.optionContainer) {
                console.log('removing the option container click handler');
                this.optionContainer.removeEventListener('click', this.boundSelectionHandler);
            }

        } catch (e) {
            console.log("OOPS we caught an error " + e);
        }

    }

Notice now the signatures of the calls match exactly this.boundxxx

I had learned this information many years ago and forgot it until I had this hard to catch bug above. I then finally remembered I was was doing stupid things and how to do this properly.

These kind of things will make you hate Javascript UI programming or any DOM action programming. I hope I never forget this lesson again.

Categories
Resources Web Development

How to view Symfony 5+ FosJsRoutingBundle routes

I can never remember this command. However, now that I am creating more AJAX and exposing endpoints I need this command more often.

This command is found in the docs too. But I can never remember where, so I will write about it here.
The command to view how FosJsRoutingBundle views your routes.

php bin/console fos:js-routing:debug

This will list something like this for you.

 Name                  | Method | Scheme | Host | Path                   |
+-----------------------+--------+--------+------+------------------------+
| menu                  | GET    | ANY    | ANY  | /menu                  |
| alias_exists          | ANY    | ANY    | ANY  | /alias_exists          |
| email_exists          | ANY    | ANY    | ANY  | /email_exists          |
| get_image_collections | GET    | ANY    | ANY  | /get_image_collections |
| save_user_image       | POST   | ANY    | ANY  | /save_user_image       |
| get_user_image_data   | GET    | ANY    | ANY  | /get_user_image_data   |
+-----------------------+--------+--------+------+------------------------+

Isn’t that pretty.This is not dynamically updated. You need to re-output your routes to the file FosJSRoutingBundle expects them to be in, so that when it creates a route it matches. I need to write an article about how to do only that. For now scroll to the bottom of the following article. How to get URL Routes in your Javascript in Symfony 5+

But if you don’t update your routes they won’t match the above output and your routes will fail when you use FosJS. Mine seemed to just redirect to my current page. Your setup/configs may differ.

Categories
Software Development Web Development

Symfony how to get query string values sent by AJAX calls

When using a Symfony API endpoint for getting values such as maybe something like an auto-complete feature you will need to be able to send some text(what the user enters) to the backend. Then have the backend return a response based on that.

For example with an auto complete feature, you might want to send 3 characters to an API endpoint and have it only return a list of words that start with those three characters, instead of returning every last word in the database. This will use less of your servers resources and the users while speeding your app up.

The way I suggest to do this is to first use FosJsRouting bundle in your Javascript to create the URL’s for you. This makes it much easier, faster ( Once you get it installed etc ) and consistent. Here is a long article I wrote about installing and using FosJsRouting bundle How to get URL Routes in your Javascript in Symfony 5+.

This is a short example and doesn’t include checking if the user is logged in ( has rights) if it is an AJAX request, check headers etc. it is just for this example.



/**
     * returns a list of the users image collection names
     * @Route("/get_image_collections", name="get_image_collections", options={"expose"=true},  methods={"GET"})
     */
    public function get_image_collections(Request $request): JsonResponse
    {
        $query = $request->getQueryString();
      $text2 = $request->query->get('text');
      $text =  $request->get("text");
        $values = array(
            'first',
            'second',
            'third',
            'fan',
            'free',
            'narcotics',
            'arse',
            'tardigrade',
            'tinnitus',
            'monkey',
            'mall',
            'doppleganger',
            'ballocks',
            'zoo',
            $query,
            $text,
            $text2
        );
        return new JsonResponse($values);
    }

This is very simple, it doesn’t even hit a database, it is just for this article and testing. All this does right now is return the list and add some text and query so I can see what was sent. In production you might even want to use caching so that your database only gets hit if it has to.

As you can see there appears to be two ways to get the value of the query. Both worked for me, but I am thinking that going through the query method is the proper way like follows.

$request->query->get('text');

The query being used for this endpoint looks like this

"http://sogi-test/get_image_collections?text=fa"

In the future I’ll update the code to include security checks, to return a matching list compiled from a database query and caching.

But notice the (Request $request) that is Dependency Injected ( see symfony dependency injection )

I should also add, below is how I am building the URL with FosJsBundle

let url = Routing.generate('get_image_collections', {text: text});

That is where the “text” named query key is coming from. You can name the key anything you want. If you have more than one named value then use a comma and add it after the first, like in the FosJsRoutingBundle docs under how to use above link.
For example say you want to include something like a page number for paging through results or limiting them etc. You could create the
url route like this

let url = Routing.generate('get_image_collections', {text: text, limit: limit});

This Javascript JSON looks confusing, the first part is the name(key) the second is a(value) variable containing the value
Then to get the values in your controller route you do this.


$text = $request->query->get('text');
$limit = $request->query->get('limit');

You can send as many values as you need to this way. Forms work a little differently. I’ll write an article about those soon too.

And that is how you get the values you send.
baby how it is done meme
That is how it is done

Interesting Error story

I figured I would mention this debugging story I had while sending an ajax request with an improperly formed URL ( aka not matching the route)

I kept getting errors when trying to use Promise.json() with the value returned from my AJAX call. What happened is the server was returning HTML instead of JSON which results in an error when Promise.json() is called.

A while of that and thinking it was my JS code but not really sure of WTF was going on. I finally started outputting my URL to see what was being created and low and behold that was it. I was even watching the Request/Response in the browser. But, it was not obvious until I really started inspecting the created URL vs the route output that I figured out my route and url didn’t match and that was the reason for the redirect.

How to view Symfony routes

 

My route didn’t match because at some point I changed how I wanted the route to look and I had not rerun the FosJsRouting bundle as mentioned in that article to renew the routes in it’s json file.  So it created a URL that didn’t match, the route was created like /route/{text} but the api route expected /route&text=….

I didn’t get an error. Nope and nothing was recorded that I could find in the error logs. Symfony simply redirected ( 301) sent back to my ajax which then somehow did another request to the page I was using the javascript in,  returning the pages HTML.

I was like

Uhm, wait… what?

I have no idea why Symfony redirects when the route doesn’t match.  I don’t even know what setting to adjust where to change this, I know I did something somewhere at sometime… I’ll have to dig into what I did.

Why this happend is because I didn’t re-update the FosJsRouting json file and then restart webpack so it pulls in the new file. But I know there is some sort of setting somewhere for Symfony for the redirect action I am seeing and I know I set it somewhere somehow.

Can I have hamburger now???

Categories
Web Development Web Security

Faking Enumerations with Vanilla javascript

What is an Enumeration?

An Enumeration is a way to create a limited list of options to choose from.This is useful for keeping a list of field names for a form so you can use javascript to animate something for example.

Having a limited list of options is helpful so that you can eliminate bugs due to misspellings (very common in Javascript UI programming).

A limited list also helps so that you can just type and your IDE gives you suggestions to jog your memory of the available options so you don’t have to dive into code.

See the limitations section.

Javascript has no Enums yet

Javascript has no such concept as an enumerated class… yet( the keyword enum is reserved so maybe in the future). Heck it is 2021 and PHP just got Enum classes.  While it does allow class level variables they are defined in the most funky way inside the constructor with this keyword.

Uhm, wait… what?

I say funky because with most other languages you define variables at the class level, then instantiate them ( give them a starting value ) inside the constructor for example. Just be glad you don’t have to use the old syntax What does prototypical Javascript look like?

So to define class level variables in Javascript you need to do so inside the constructor using the this keyword. The reason for this is how the Javascript prototype system works.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}
Before Javascript classes… remembering this

Now anywhere inside the class you can get or set the value by this.height or this.width. You can’t set any constants like this though. Constants have to be defined outside the class if you want to use them inside a class, in all methods/functions. You can define a constant inside a function ( aka constructor ) but it is limited to the function in scope just like the let keyword.

But what if you want something like a list of constants or values that can be used? For example I like to keep my form field id’s inside an Enum to be able to easily refer to the field I need, but how can I do this with Javascript?

The answer

The easiest way I have come up with is to NOT USE a class at all. Instead I just use a simple file with a constant set to an object with a list of values. Sure you could just make a list of constants, but there are downsides to that. For one you would need to export them in order to import and use them.

I prefer to create a constant set to a literal object value inside of a single file, like this.

const MEDIA_FORM_FIELDS_ENUM = {
    ALLOW_COMMENTS: 'allow_comments',
    COLLECTION: 'collection',
    CONTENT_RATING: 'content_rating',
    DESCRIPTION: 'description',
    HASHTAGS: 'hashtags',
    PUBLISHED: 'published',
    REUSE_TYPE: 'reuse_type',
    TITLE: 'title'
};

export {MEDIA_FORM_FIELDS_ENUM};

Note the export.

Then I use it like this in my form or form fragment in this case.

Note the import.

import {MEDIA_FORM_FIELDS_ENUM as fields} from "../enums/MediaFormFieldsEnum";

class MediaOptions {
    static getMediaOptions(mediaType) {
        let collection = fields.COLLECTION;
        let comments = fields.ALLOW_COMMENTS;
        let description = fields.DESCRIPTION;
        let hashtags = fields.HASHTAGS;
        let published = fields.PUBLISHED;
        let reuse = fields.REUSE_TYPE;
        let title = fields.TITLE;

        return `
        <div id="media-options" class="container">
            
            <div class="form-group">
             <label for="content-rating" >${mediaType} rating</label>
               <select name="content-rating" id="content-rating" class="form-control" >
                  <option selected value="rating-everyone" id="rating-everyone" >Everyone</option>
                  <option  value="rating-mature" id="rating-mature">Mature</option>
                  <option value="rating-xrated" id="rating-xrated" >Adult rated-x</option>
               </select>
            </div>
         
          <div class="form-group">
            <label for="${title}">${mediaType} Title</label>
            <input type="text" class="form-control" id="${title}" name="${title}">
          </div>
          
          <div class="form-group">
            <label for="${description}" >${mediaType} Description</label>
            <textarea rows="5" id="${description}" name="${description}" 
            placeholder="describe the image in 200 characters" class="form-control" ></textarea>
          </div>
          
          <div class="form-group">
                <label for="${hashtags}" >${mediaType} Hashtags</label>
                <textarea rows="2" id="${hashtags}" name="${hashtags}"
                 placeholder="separate hashtags with space" class="form-control" ></textarea>
          </div>
             
          <div class="form-group">
            <label for="${collection}">${mediaType} Collection</label>
            <input type="text" class="form-control" id="${collection}" name="${collection}">
          </div>
          
          <div class="form-row">Published/visible status</div>
          <div class="form-check">
            <input class="form-check-input" type="radio" name="${published}"
             id="${published}" value="published" checked>
            <label class="form-check-label" for="${published}">
             Published ( visible to others )
            </label>
          </div>
          <div class="form-check">
            <input class="form-check-input" type="radio" name="${published}"
             id="unpublished" value="unpublished">
            <label class="form-check-label" for="unpublished">
             Un-Published ( visible to only you )
            </label>
          </div>
          
          <div class="form-group">
             <label for="${comments}">Allow comments</label>
               <select name="${comments}" id="${comments}" class="form-control" >
                  <option value="followers" >Buyers only/no one/private</option>
                  <option selected value="everyone" >Everyone & Buyers</option>
                  <option value="followers" >Followers & Buyers</option>
               </select>
           </div>
             
          <div class="form-group">
             <label for="${reuse}" >Allow reuse</label>
               <select name="${reuse}" id="${reuse}" class="form-control" >
                  <option selected value="none" id="reuse-none" >None/private (me only)</option>
                  <option  value="free" id="reuse-free">Free</option>
                  <option value="credits" id="reuse-credits" >Credits</option>
               </select>
            </div>
           
        </div>
        `;
    }
}

export {MediaOptions}

That is a lot of code. Note it is HTML inside of a Javascript Literal. I’ll write another article about creating templates with Javascript literals later. For now note how I imported it and used it. I could have just called the fields.OPTIONS but that is longer than a variable name.

I use the above code by importing it into yet another file that builds a whole form but only when called. Like I said I’ll have to write an article about the Javascript Literals, because wow they are handy.

Vanilla javascript might be a little more work, but in the end when something doesn’t work you know exactly why and exactly where to look. And if it is a bug… IT IS YOUR BUG and you can quickly fix it and move right along.

Limitations

Javascript is a real screwy language with lots of limitations and quirks. Many don’t make a lick of sense, unless you have the unlimited free time to dig deeply into the internet to dig deeply into how the Interpreter works.

The above was working fine… until I tried to use it in an Object. When I try to use something like

let stupidObj = { ImageDataEnum.ALLOW_CONTENT_RATING_CHANGE : 'no', 'test' : 'value' };

But this does not work. You will get an error of some type or another. The error I got was something like ” , found expecting ; ” and then a lot of lines of barfarony.

Javascript interpreter was all like…

baby threw up meme
The limitations are real.

So I figured ok maybe store it in a variable and then try that.

const tester = ImageDataEnum.ALLOW_CONTENT_RATING_CHANGE;
let stupidObj = { tester : 'no', 'test' : 'value' };

That doesn’t work either. The interpreter doesn’t interpret the const tester as the tester I wanted to use as the object key so it outputs something different.
The idea here is I want to store all of the key names in a constant and use them later to build an object. This way I for sure know I will spell the key names correctly and I don’t have to remember them all, my IDE can show me the list.  This way I can request some data with AJAX and set the values for an object, then use the object to manipulate the values or use them.

Using the fake enums like this works perfectly fine.


import {JsCollection} from "./JsCollection";
import {ImageDataEnum} from "../enums/ImageDataEnum";
import {Utils} from "./Utils";

class ImageData {

    constructor(imageData) {
        this.imageData = new JsCollection();
        this.setValues(imageData);
    }

    getAllowComments(){
        return this.imageData.getElementValue(ImageDataEnum.ALLOW_COMMENTS);
    }

    getAllowResale(){
        return this.imageData.getElementValue(ImageDataEnum.ALLOW_RESALE);
    }

    getAllowContentRatingChange(){
        return this.imageData.getElementValue(ImageDataEnum.ALLOW_CONTENT_RATING_CHANGE);
    }

    getAltText(){
        return this.imageData.getElementValue(ImageDataEnum.ALT_TEXT);
    }

    getCollectionName(){
        return this.imageData.getElementValue(ImageDataEnum.COLLECTION_NAME);
    }

    getContentRating(){
        return this.imageData.getElementValue(ImageDataEnum.CONTENT_RATING);
    }

    getCreationDatetime(){
        return this.imageData.getElementValue(ImageDataEnum.CREATION_DATETIME);
    }

    getDescription(){
        return this.imageData.getElementValue(ImageDataEnum.DESCRIPTION);
    }

    getFileUrl(){
        return this.imageData.getElementValue(ImageDataEnum.IMAGE_FILE_URL);
    }

    getHashTags(){
        return this.imageData.getElementValue(ImageDataEnum.HASHTAGS);
    }

    getImageFileData(){
        return this.imageData.getElementValue(ImageDataEnum.IMAGE_DATA_FILE);
    }

    getImageId(){
        return this.imageData.getElementValue(ImageDataEnum.IMAGE_ID);
    }

    getLanguageCode(){
        return this.imageData.getElementValue(ImageDataEnum.LANGUAGE_CODE);
    }

    getLastEditDatetime(){
        return this.imageData.getElementValue(ImageDataEnum.LAST_EDIT_TIMESTAMP);
    }

    getPublishedStatus(){
        return this.imageData.getElementValue(ImageDataEnum.PUBLISHED_STATUS);
    }

    getTitle(){
        return this.imageData.getElementValue(ImageDataEnum.TITLE);
    }

    getVisibility(){
        return this.imageData.getElementValue(ImageDataEnum.VISIBILITY);
    }

    setAllowComments(allowComments){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.ALLOW_COMMENTS, allowComments);
    }

    setAllowResale(allowResale){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.ALLOW_RESALE, allowResale);
    }

    setAltText(altText){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.ALT_TEXT, altText);
    }

    setCollectionName(collectionName){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.COLLECTION_NAME, collectionName);
    }

    setContentRating(contentRating){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.CONTENT_RATING, contentRating);
    }

    setCreationDatetime(datetime){

    }
    setDescription(description){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.DESCRIPTION, description);
    }

    setHashtags(hashtags){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.HASHTAGS, hashtags);
    }

    setLastEditDatetime(datetime){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.LAST_EDIT_TIMESTAMP, datetime);
    }
    setImageId(id){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.IMAGE_ID, id);
    }

    setImageFileData(fileData){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.IMAGE_DATA_FILE, fileData);
    }

    setImageFileUrl(fileUrl){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.IMAGE_FILE_URL, fileUrl);
    }

    setLanguageCode(languageCode){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.LANGUAGE_CODE, languageCode);
    }

    setPublishedStatus(publishedStatus){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.PUBLISHED_STATUS, publishedStatus);
    }

    setTitle(title){
        this.imageData.addOverrideNamedProperty(ImageDataEnum.TITLE, title);
    }

    /**
     * used internally to set the imageData values, but JS sucks and has no
     * idea what private or protected is
     * @param imageData
     */
    setValues(imageData){

        this.setAllowComments(Utils.getArrayValue(imageData, ImageDataEnum.ALLOW_COMMENTS, ''));
        this.setAllowResale(Utils.getArrayValue(imageData, ImageDataEnum.ALLOW_RESALE, ''));
        this.setAltText(Utils.getArrayValue(imageData, ImageDataEnum.ALT_TEXT, ''));
        this.setCollectionName(Utils.getArrayValue(imageData, ImageDataEnum.COLLECTION_NAME));
        this.setContentRating(Utils.getArrayValue(imageData, ImageDataEnum.CONTENT_RATING, ''));
        this.setCreationDatetime(Utils.getArrayValue(imageData, ImageDataEnum.CREATION_DATETIME, ''));
        this.setDescription(Utils.getArrayValue(imageData, ImageDataEnum.DESCRIPTION, ''));
        this.setHashtags(Utils.getArrayValue(imageData, ImageDataEnum.HASHTAGS, ''));
        this.setImageId(Utils.getArrayValue(imageData, ImageDataEnum.IMAGE_ID, ''));
        this.setImageFileData(Utils.getArrayValue(imageData, ImageDataEnum.IMAGE_DATA_FILE, ''));
        this.setImageFileUrl(Utils.getArrayValue(imageData,ImageDataEnum.IMAGE_FILE_URL, ''));
        this.setLanguageCode(Utils.getArrayValue(imageData, ImageDataEnum.LANGUAGE_CODE, ''));
        this.setLastEditDatetime(Utils.getArrayValue(imageData, ImageDataEnum.LAST_EDIT_TIMESTAMP, ''));
        this.setPublishedStatus(Utils.getArrayValue(imageData, ImageDataEnum.PUBLISHED_STATUS, ''));
        this.setTitle(Utils.getArrayValue(imageData, ImageDataEnum.TITLE, ''));
    }

}

export {ImageData};

Using the above I can request an images data from the server using AJAX. Then I pass the JSON Object, which was a PHP multidimensional array with keys and values converted to JSON, to this setValues() method.

I can then call the getters to get a value, it will either be a value or an empty string if nothing existed.

With this setup I can set default values when none exist. I also have enums which hold the values I will add as defaults to this later.

So as you can see this is highly useful. This way I can use the Enum anywhere I need a string and I don’t have to worry about spelling. If I do misspell something, I change it one time in one location and I am done. Otherwise I’d have to hunt down all the locations in the text etc. where I had hard coded a value.

But the limitation is Javascript is like WTF is this when you try to use them in Object literals. This is the only place I have found that they don’t work so far.

Dictating like…

It seems like the Javascript interpreter is trying to call a function when it sees the Enum constant reference inside an object literal.  But the rest of the time it works 100% fine. I only wanted this for testing so I’ll move on and not try using it inside object literals for testing LOL.

The work around

So I kept digging until I found a work around, a way to keep using my Fake enums and be able to build an object using the enums as the key names.

You need to use Object.defineProperty() to set a propertyusing the Enum and you get it back using Object.getOwnProperty(). I’ll update this article later when I have the time .

Links

Mozilla Developer Network Javascript class info

Mozilla Developer Network Javascript const info

Mozilla Developer Network javascript literal info

While working on this article I found this excellent article about using Enums in Javascript.

Categories
Web Development

Symfony 5+ how to include page specific javascript or css when using Webpack encore and SASS

In this article I will cover how to do this with Webpack in Symfony with CSS and SASS. This is slightly confusing.

Symfony has it’s own Webpack configuration called encore. Read that documentation article if you need more info, more links at the bottom of the page.

Webpack Config

First lets checkout the file app\webpack.config.js

Mine currently looks like this.


const Encore = require('@symfony/webpack-encore');

// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')

    /*
     * ENTRY CONFIG
     *
     * Add 1 entry for each "page" of your app
     * (including one that's included on every page - e.g. "app")
     *
     * Each entry will result in one JavaScript file (e.g. app.js)
     * and one CSS file (e.g. app.scss) if your JavaScript imports CSS.
     */
    .addEntry('app', './assets/js/app.js')
    .addEntry('modalAction', './assets/js/modal-call-to-action.js')
    .addEntry('registration', './assets/js/registration.js')
    .addEntry('featuredImage','./assets/js/featured-image.js')
    .addEntry('editAboutUser','./assets/js/edit-about-user.js')
    .addEntry('sogiDraw', './assets/js/sogi-draw.js')
    // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
    .splitEntryChunks()

    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()

    /*
     * FEATURE CONFIG
     *
     * Enable & configure other features below. For a full
     * list of features, see:
     * https://symfony.com/doc/current/frontend.html#adding-more-features
     */
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())

    // enables @babel/preset-env polyfills
    .configureBabelPresetEnv((config) => {
        config.useBuiltIns = 'usage';
        config.corejs = 3;
    })

    // enables Sass/SCSS support
    .enableSassLoader()

    // uncomment if you use TypeScript
    //.enableTypeScriptLoader()

    // uncomment to get integrity="..." attributes on your script & link tags
    // requires WebpackEncoreBundle 1.4 or higher
    //.enableIntegrityHashes(Encore.isProduction())

    // uncomment if you're having problems with a jQuery plugin
    .autoProvidejQuery()

    // uncomment if you use API Platform Admin (composer req api-admin)
    //.enableReactPreset()
    //.addEntry('admin', './assets/js/admin.js')
;

module.exports = Encore.getWebpackConfig();

See each of the addEntry() lines. Each one of those is including  the processing of the Javascript files. If you need some CSS for example on every page you would put a line like the following inside the app.js file at the top to import the CSS.

import {DrawingForm} from "../javascript/templates/drawing-form";
import {DrawingActions} from "../javascript/objects/DrawingActions";
import '/assets/css/dialog/styles.scss';

In the last line here I am including styles.scss with an import statement. The .scss indicates the file is a SASS file.

This mixing of JS and CSS imports gets to be REALLY confusing. You see just importing the CSS into the Javascript is not enough. Nope you must also include the CSS and the Matching Javascript files with tags.

If you just link to the Javascript that is not enough and you will see your JS elements don’t have any related CSS. Importing the CSS just tells Webpack to process it and create another file.

homer simpson meme
Many baby steps to get to your CSS

So the import statement inside the Javascript file for the .scss file tells webpack to take that import along with any other .scss imports it finds and combine them all into one .css file. If there are no .scss inports there will be no matching .css file created.

All of this is supposed to make web development easier.

Modern web development keeps getting easier by the day
Look at the SASS files

Now lets look inside the styles.css really quick to see what it is doing.

@import 'variables';
@import 'dialog-search-form';
//@import 'grids';
@import 'forms';
@import 'overlay';
@import 'dialog';
@import 'canvas-color-picker';
@import 'editor-canvas';
@import 'image-selector';

.content {
    margin: 55px auto 0 auto;
    background-color: #fff;
    width: 95%;
    height: auto;
}
.hidden {
    display: none;
}

.clear-fix:after {
    content: "";
    display: table;
    clear: both;
}

#drawingCanvas{
    background-color: #fff;

}
.canvas-border, #drawingCanvas {
    border: 2px solid #000;
    margin: 0px auto;
    display: block;
}
.center-image {
    text-align: center;
    margin: 0px auto;
}

Notice all of the import statements. This is because I am using SASS. Each of those files contains a small amount of SCSS and Webpack uses SASS to compile it all into a  single file in the end. The import statement lets Webpack do tree shaking and remove unused code.

borat nice meme
Very nice!!!

So to include a page specific CSS you should create a javascript file inside any folder, this is what gets confusing.( you can use an existing JS file) I keep mine in assets\css\ sometimes in further folders like Dialog or Overlay etc. Then inside that file you import the CSS file like this

import ‘/assets/css/dialog/styles.scss’;

replacing styles.scss with your stylesheet or SASS file name.

But that is not all. You still need to include the final css file that webpack creates and saves in public\build\ folder in your template.

If you imported anything in the .scss file webpack compiles it and includes it in the matching .css file. Webpack will create a matching JS and CSS file based on your addEntry() method calls and store them in the build folder.

So you never make changes, EVER to the files in public\build you store all of your files in the assets folder and add an entryPoint definition in the Webpack Encore file.

So to get your CSS working, (actually included in the page) you need to add a line like this in the template. In your templates head section add  a line to include the CSS file like this.

{{ encore_entry_link_tags('styles') }}

That line is in my base.html.twig file which is the main page template that all of my other page templates inherit from.  So public\build\app.css will be available to every page in my app. Notice it does not include the file type. Twig template knows what to do since encore_entry_link_tags is a twig function.

Page specific CSS only.

So to summarize. If you wanted a CSS file included in only one page of your app, you would do the following. This is a dialog I want to include in only one page of my application.

Step #1 create the css

Create a css file in app\assets\css\dialog\dialog.scss directory for example. In my case I will be using SASS so I will create app\assets\css\dialog\dialog.scss Inside there I import the other tiny SASS .scss files I need.

Step #2 create the javascript

Create a Javascript file or import the css in an existing Javascript file. This is just how Webpack works, you need to include your CSS in your JS files and it does everything else (tree shaking), at least for now. So I create the Javascript file app\assets\js\dialog.js Inside that file I import the css like this

import '/assets/css/dialog/dialog.scss';

Now with this in place, Webpack will compile both the css and the javascript from that file and save it in app\public\build\dialog.css and app\public\build\dialog.js These are the files we must include with template specific functions, not the .scss files. (show below) Webpack will take all .scss import calls and do tree shaking to only include the code that is used.

Step #3 register the Entry point

Now you must register dialog.js (whatever your Javascript file name is) inside the webpack.encore.js file shown above add a line like this.

.addEntry('my-page', './assets/js/dialog.js')

Now restart webpack. In my case I am running webpack with watch so I use the following after I ctrl + c to stop the current watch

yarn run encore dev --watch

Webpack Encore will now create the new Javascript and CSS imported in it to the matching file names inside the public\build\ directory.

Now you must include both the Javascript and the CSS files. This is what tripped me up. I was like WTF why is the related CSS not working but the javascript is?

Uhm, wait… what?

Step #4 include the CSS file

So this is one of the most important steps. You have to include the CSS file located in the public\build\ directory, in the head area of your Twig template like this.

{{ encore_entry_link_tags('dialog') }}

Notice there is no .css it is just the name of the file. also note encore_entry_link_tags is used for the CSS only and javascript has it’s own function covered next. This CSS file was created by Webpack thanks to it being imported in dialog.js. The addEntry() function will create the Javascript file plus a CSS file for each import statement it finds. It stores all the imported scss in a single file that matches the javscript file name but with .css tile type.

Step #5 include the Javascript

Now you must include the matching Javascript that uses the imported CSS file. This is the Javascript Webpack Encore created due to the addEntry() line in the Webpack configuration above.

{{ encore_entry_script_tags('dialog') }}

This should be placed in the section where you include all of your Javascripts, mine are included before the closing body tag. Notice this looks exactly the same as including the CSS, except for the function name is now encore_entry_script_tags

I only have a few page specific Javascripts.  Which is why I can never remember this process.

Next up “Don’t forget to call your templates parent” More fun I discovered while using Twig templates.

Links

Learn how to activate SASS in Symfony webpack encore.

Managing CSS and Javascript from Symfony Documentation.

How to activate CSS preprocessors in Symfony documentation.

Categories
Web Development

How to make AJAX requests to Symfony 5+ controllers

I couldn’t find anything in the documentation about this and there is little on the internet about it too.  There is probably a Bundle somewhere for this or some Symfony way, but I didn’t find anything.

I did find this SymfonyCast about submitting a whole form. If you need to test your Controller route read this. If you want to know more read this in the docs about Browserkit.

But what if you need to just send a simple AJAX request to a controller route? Well that is what this is about. This is very basic, there are settings you can add to the request for mime type for example and more.

How to send a simple AJAX request?

First you need the javascript to make a request to your controller endpoint. To do this you will need access to your routes, in your javascript. Read my article How to get URL Routes in your Javascript in Symfony 5+   to find out how.

Once you have your routing setup and you understand how that works you need to build your AJAX request. I use a custom class I built called EzAjax.js which is a wrapper around the jquery ajax function shown below.

The AJAX solution
import {Utils} from "./Utils";

class EZAjax {
    /**
     *
     * @param errorCallback
     * @param successCallback
     * @param requestUrl
     * @param data
     * @param ajaxOptions
     */
    constructor(errorCallback, successCallback, requestUrl, data, ajaxOptions = {}) {

        this.ajaxObj = null;
        this.ajaxOptionsObj = ajaxOptions;
        this.attempts = 0;
        this.delay = 300;
        this.errorCallback = errorCallback;
        this.successCallback = successCallback;
        //add the settings to the ajaxOptions
        this.ajaxOptionsObj.url = requestUrl;
        this.ajaxOptionsObj.data = data;
        this.ajaxOptionsObj.error = this.ajaxError.bind(this);
        this.ajaxOptionsObj.success = this.ajaxSuccess.bind(this);

    }

    ajaxError(jqXHR, errorString, errorThrown) {
        console.log('error string ' + errorString + ' error thrown ' + errorThrown);
        if (this.attempts <= 3) {
            console.log('attempts is ' + this.attempts + ' delay ' + this.delay);
            setTimeout(() => {
                this.performRequest();
            }, this.delay *= 2);
        } else {
            this.errorCallback(jqXHR, errorString, errorThrown);
        }
    }

    ajaxSuccess(response) {
        this.successCallback(response);
    }

    performRequest() {

        this.attempts++;
        console.log('performing request ' + this.attempts);
        //prevent multiple requests
        if (this.ajaxObj) {
            this.ajaxObj.abort();
        }
        console.log('the ajax options ' + JSON.stringify(this.ajaxOptionsObj));
        this.ajaxObj = $.ajax(this.ajaxOptionsObj);
    }

}

export {EZAjax};

This makes it easier to work with. I just create a simple fail and success function and pass the names in like this.

let ezAjax = new EZAjax(emailExistsError, emailExistsSuccess, checkEmailUrl, data);
ezAjax.performRequest();
Testing inside Javascript

The below code goes inside a class you place inside app\assets\javascript\ directory I called mine registration.js  Below is a snippet of the code for checking if the email exists.


//code for comparing email fields before submit and make sure the email doesn't exist
$(emailMatchField).focusout(function (event) {
    let email = $(emailField).val();
    let emailConfirm = $(emailMatchField).val();

    if (email !== emailConfirm) {

        $('#noEmailMatch').remove();
        $(emailMatchField).css('background-color', redBgColor);
        let matchHtml = '<small id=' + emailMatchErrorId + '" class="red-text" >Email fields must match</small>';
        $(emailMatchField).after(matchHtml);

    }
  if (email === emailConfirm) {
        let data = {"email" : email};
        // if the email fields match make sure the email does not exist in the system
        let checkEmailUrl = Routing.generate( 'email_exists');
        let ezAjax = new EZAjax(emailExistsError, emailExistsSuccess, checkEmailUrl, data);
        console.log('the route is ' + checkEmailUrl);
        ezAjax.performRequest();
        $('#noEmailMatch').remove();
        $(emailMatchField).css('background-color', whiteBgColor);
    }
});

The first if statement checks if the fields match if not it shows an error. The second if statement checks if the email exists only if both fields match first.

The emailExistsError function will open a dialog to inform the user the email they entered exists and give them a login link instead. emailExistsSuccess does nothing I could just put function() in it’s place if I wanted.

Notice this line though

let data = {"email" : email};

That is the data we send via the EzAJAX class. That is the value we need in the controller.

The Symfony controller

Now we need to be able to work with the values sent in the request.

public function checkEmailExists(Request $request)
{
    $json = array();

    if ($request->isXmlHttpRequest()) {
        //check if the email exists
        $email = $request->query->get('email');
        $email = DataSanitizer::sanitizeEmail($email);
        $exists = $this->getDoctrine()->getRepository(User::class)->checkEmailExists($email);
        if($exists){
            $loginUrl = $this->generateUrl('app_login');
            $linkAttr = array(
                AnchorTagAttributes::HREF_ATTRIBUTE => $loginUrl,
                GlobalHtmlAttributes::CLASS_ATTRIBUTE => 'h5'
            );
            $loginLink = AnchorTag::getAnchorTag('login', $linkAttr);
            $json['error'] = "A user with this email exists. Please $loginLink instead. Or use another email.";
        }
    } else {
        $json['error'] = $this->illegalRequest;
    }
    return new JsonResponse($json);
}

To get the value sent via AJAX in the controller I use:

$email = $request->query->get(’email’);

Which matches the value I sent in the data JSON object to this controller.

As you can see in the javascript I am passing a value for email and in the controller I am fetching that value. I didn’t add any mime type info to the request, so the default for jquery ajax is 'application/x-www-form-urlencoded; charset=UTF-8'. To change the MIME type set the contentType on the ajax method.

Also note that this way of working with an Ajax request in the controller is not secured. Anyone from any IP can send a request to this, which can flood the server or allow them access to things they shouldn’t be allowed to access.

Soon I will dig into how to best secure the Symfony Contoller routes and write an article about that.

Categories
Software Development Web Development

How to get URL Routes in your Javascript in Symfony 5+

This article has been recently updated to fix errors.

Do not miss the last step.

If you are using an EventSubscriber to store the last page the user visited or else Symfony redirects your users to the routes after they register.

Isn’t this exactly what you would expect???? Yeah me too.

In the Symfony docs it shows you how to get a URL to your javascript in your template. Basically what this is good for is it gives your Javascript code access to a url that you store in a javascript variable. You would place this at the bottom of your Twig template page, then you can access it with other Javascript code within that script tag area.

Another way to do it is to hide an element with an id in the template then use Javascript to fetch it.

This is all good if you only have a few routes.

But yet another way to get the routes is also mentioned in the Symfony Documentation, FosJsRoutingBundle. This allows you to skip the other ways and be able to just get the routes in your Javascript, when and where you need them. For example if your frontend is React, Vue etc. or you have a Javascript component that needs to use AJAX to interact with the backend.

Step 1.

Install FosJsRoutingBundle with this command

composer require friendsofsymfony/jsrouting-bundle

That command in version 5.1+ of Symfony installs and does pretty much everything you need. It even registers the bundle for you. Your config/bundles.yaml file should look something like this now. See FosJsRoutingBundle listed last.

<?php

return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
    Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
    Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
    Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
    Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
    Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
    Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
    Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
    Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
    DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
    Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
    Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
    SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
    FOS\JsRoutingBundle\FOSJsRoutingBundle::class => ['all' => true],
];

Step 2.

I am not sure what version Symfony started this but I know in 5+ you don’t have to register the bundle like step 2 of the old docs shows. This is done automatically for you in a new file named config/bundles.php which is where all bundles are automatically registered when you install them.

So all you do is this command now in the terminal

composer require friendsofsymfony/jsrouting-bundle

inside your apps root level. It is auto registered for you and you will see it listed inside composer.json file too.

Step 3.

You may or may not have to create this file. It may be created for you it might not.

So if the following file

When you install the bundle the new system, it might create a new file named app/config/routes/fos_js_routing.yaml for you and the required code is inside.
If not you must create this file and add the following.

fos_js_routing:
    resource: "@FOSJsRoutingBundle/Resources/config/routing/routing-sf4.xml"

 

For step 4 the required code is the same for version 5 as for version 4. I don’t know exactly what this does.

$ php bin/console assets:install --symlink public

But I know, you will also need to use this command or nothing will work. This is also how you update( see below)

php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json

That actually puts the file with the routes in the proper location so you can use it

Basically to install the bundle you just use the composer command and most of the stuff is now done for you.

Don’t forget to expose your routes

expose routes meme
Don’t forget to expose your routes.

I told you there are many baby steps.

In order for the routes to work you must add something new to every single route definition that you want to be able to generate a route for in your Javascript.

You must add this to the annotation or where ever you define your routes. I use annotations so I can just look to the controllers.

options={"expose"=true}

So for my menu route the definition in the controller looks like this.

@Route("/menu", name="menu", options={"expose"=true}, methods={"GET"})
</code.

Now how to use it?

yoda use the routes meme
Use the routes Luke.

So now that it is installed how do you use it? One more step. Now you must include the needed Javascript in your page with these tags. If you are not using Webpack with Symfony you can use these lines.

<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
<script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script>

Place those anywhere you want. What I did is put them in my base twig template in a section where I include my Javascript this way it is available on every page because I will probably be needing access to routes as I build the app.

If you are using Webpack with Symfony, place these at the top of the file where you need to access routes.

const routes = require('../../public/js/fos_js_routes.json');
import Routing from '/public/bundles/fosjsrouting/js/router.min.js';

If you don’t know, if you are using webpack then there will be an app.js file in your app\assets\js\app.js

Learn how to install webpack here in the Symfony Docs. Learn more about how to use Webpack with Symfony here.

You include this in your base template. Then anything you add to it will be included for other Javascript code you use with import statements. You must be using Webpack for this to work though. This is where I have included JQuery and Bootstrap and now FosJsRouting for example.

Once you have all of the above done you can use it like this


//set the routes for the Routing object
Routing.setRoutingData(routes);
//create a url
let testUrl = Routing.generate( 'menu');
//check to see it is working
console.log(testUrl); //outputs /menu

Read a lot more about generating URLs in the FosJsRouting documentation.

Route not found errors?

more errors meme
And.. Errors

Yes I got these too. You must call this setRoutingData() method first with the routes constant created above. I was like WTF too.

Routing.setRoutingData(routes);

The last step

What ever you do, do not forget this crucial step if you are using an EventSubscriber to save the users last page visited. Otherwise your users are shown your fos_js_routes.json file after Registering ONLY, NO OTHER TIME.

homer simpson meme
Not the action we expected now is it.

With the standard Symfony Authentication system created with the Maker bundles, your users will be returned to the page they last viewed when they login/register.
To have more control over this you can create an EventSubscriber like this.

There are more ways to do this than the one I suggest. But I suggest using a Subscriber class and extending

EventSubscriberInterface

The reason is, this works for both the Login and Registration forms and you don’t have to repeat code anywhere. Below is what my class looks like.


namespace App\EventSubscriber;


use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class TargetPathSubscriber implements EventSubscriberInterface
{

    use TargetPathTrait;

    private SessionInterface $session;

    public function __construct(SessionInterface $session)
    {
        $this->session = $session;
    }

    public function onKernelRequest(RequestEvent $event): void
    {

        $request = $event->getRequest();
        //don't save the path if the user has come to the login page first making a
        //never ending loop. Dont save AJAX requests either. Make sure this is a master
        //not a sub request, subs happen in forms
        $route = $request->attributes->get('_route');
        dump('Route is ' . $route);

        if (
            !$event->isMasterRequest()
            || $request->isXmlHttpRequest()
            || 'app_login' === $route
            || 'app_register' === $route
            || 'js/routing?callback=fos.Router.setData' === $route
            || 'menu'
            || 'alias_exists'
            || 'email_exists'
        ) {
            return;
        }

        $this->saveTargetPath($this->session, 'main', $request->getUri());

    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => ['onKernelRequest']
        ];
    }
}

You can read more about how this works in the Symfony documentation here. What the code inside onKernelRequest() does is it saves the targetPath aka the last page the user was viewing/tried to access. The problem is it saves things you don’t want like the login or registration routes.
What these lines of code are doing is excluding routes. What the Authentication system does is it looks for the _targetpath. You will need to make changes to your firewall too in order for this to work. More on that here.

Basically the code above only sets a targetpath if it is not one of the routes in the list. I had to add '/js/routing...' === $route so that the user would not be redirected to that, which is a script which outputs all of your routes you have exposed to FosJsRoutingBundle.

I set my firewall so that if nothing exists for targetpath, which will be the case for the routes in the list or if a user goes directly to yoursite.com What we need in that instance is a default page to redirect the user to.

This is what my firewall code looks like (inside config/packages/security.yaml


firewalls:
    dev:
      pattern: ^/(_(profiler|wdt)|css|images|js)/
      security: false
    main:
      lazy: true
      entry_point: form_login
      provider: app_user_provider
      form_login:
        login_path: app_login
        check_path: app_login
        #csrf_token_generator: security.csrf.token_manager
        enable_csrf: true
        default_target_path: list_articles
        username_parameter : 'email'
        password_parameter: 'password'

See this one magic line ( default_target_path: list_articles ) if nothing is set in my TargetPathSubscriber this is the route that is used.

The interesting and most confusing part of all of this was the fact that Symfony only displayed the FosJsRoutingBundle routes when a user registered not when they logged in. That was insanely confusing.

so much work meme
I told you getting routes in your symfony js is a lot of work

Updating your routes

Every time you add a new routes or change a routes name you must update the routes.json file or else you will get route not found errors. To update your routes use this command.

php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json
And now you know how to get routes in your Javascript in Symfony

View all the routes

You may also need or want to view which routes are now exposed after updating or out of curiosity. The following command will output all of your exposed routes.

 php bin/console fos:js-routing:debug

Links

More on stackoverflow about redirecting after registration.

An older SymfonyCast about integrating FosJsBundle

Here is the link to the FosJsRoutingBundle installation documentation on github which this blog basically explains.

Events and EventListeners in the Symfony Documentation.

More about using targetpath in this SymfonyCast

Here is a link to my favorite HTMLEntities converter if you write articles about programming you will need this.

 

Categories
Software Development Web Development

How to create and use a custom Javascript Event

You have probably used events in Javascript many times.  Especially if you have done any User Interface programming. There are many types of events provided by browsers and the Javascript engines.

Did you know you can create your own custom events with the CustomEvent() constructor? Here is a minimal example. In this article I will explain custom events and how to use them.

First off what would you use a custom event for? You use them to notify other objects in your app that actions have occurred such as “user clicked x” or “user closed dialog”. Using events prevents code coupling and reduces dependencies.

I came across this need when creating a dialog box where I wanted an overlay to show beneath it blocking out the page behind. I didn’t want my DialogBox to have to know about my Overlay.  I didn’t want them coupled. I didn’t want my DialogBox to have to have an Overlay object as an argument creating a dependency. I didn’t want my DialogBox to even know that an Overlay object existed.

The answer is for my DialogBox to emit/create/dispatch a custom Javascript Event and have a listener for that event to close the Overlay.

So lets look at some code.


import {Utils} from "./Utils";

class DialogBox {

    /**
     *
     * @param {string} dialogId
     * @param {string} dialogClass
     */
    constructor(dialogId = 'dialogId', dialogClass = '') {
        this.closeDialogId = 'closeDialog';
        this.dialogBoxBottomRowId = 'dialogBoxBottomRow';
        this.dialogBoxClass = 'dialogBox ' + dialogClass;
        this.contentContainerId = 'dialogContentContainer';
        this.dialogHtml = '';
        this.dialogBoxId = dialogId;
        this.closeHandler = null;
        this.divElement = null;
    }

    /**
     *
     * @param {string} rowContent : the content HTML etc to be added as a row
     * @param {string} cssClass : applied only if passed in
     * @returns {void}
     */
    bottomRow(rowContent = '', cssClass = 'dialog-bottom-row') {

        let bottomRow = document.getElementById(this.dialogBoxBottomRowId);

        //if the bottom row does not exist add it to the html
        if ( Utils.isEmpty(bottomRow)) {
            let rowHtml = '<div id="' + this.dialogBoxBottomRowId + '" ';
            rowHtml += ' class="' + cssClass + '" ';
            rowHtml += ' >' + rowContent + '</div>';
            this.dialogHtml += rowHtml;
        } else {
            //if a bottom row exists replace it
            bottomRow.className = cssClass;
            bottomRow.innerText = rowContent;
        }
    }
    /**
     * Centers the dialog vertically and horizontally in the parent element
     * @param {string} parentElementId
     */
    centerDialog(parentElementId = 'body') {

        let parentWidth = 0;
        let parentHeight = 0;
        //need the dialog boxes calculated width and height
        let dialogHeight = this.divElement.clientHeight;
        let dialogWidth = this.divElement.clientWidth;

        //must use two different ways to get the height and width
        if (parentElementId === 'body') {
            parentWidth = window.innerWidth;
            parentHeight = window.innerHeight;
        } else {
            let parentElement = document.getElementById(parentElementId);
            //make sure null or undefined were not returned
            if (!Utils.isEmpty(parentElement)) {
                parentHeight = parentElement.clientHeight;
                parentWidth = parentElement.clientWidth;
            }
        }
        let left = (parentWidth / 2) - (dialogWidth / 2);
        let top = (parentHeight / 2) - (dialogHeight / 2);
        //must add px or it doesn't work at all
        this.divElement.style.top = top + 'px';
        this.divElement.style.left = left + 'px';
    }
    /**
     *
     * @param {string} content
     * @param {string} containerClass
     */
    contentContainer(content, containerClass = 'dialog-content') {
        let contentDiv = document.getElementById(this.contentContainerId);

        //if the container exists replace it contents
        if (Utils.isEmpty(contentDiv)) {
            let contentHtml = '<div id="' + this.contentContainerId + '"';
            contentHtml += ' class="' + containerClass + '" >';
            contentHtml += content + '</div>';
            this.dialogHtml += contentHtml;
        } else {
            contentDiv.innerHTML = content;
            contentDiv.className = containerClass;
        }
    }
    /**
     * calls removeDialogBox which removes the dialog and event listeners
     */
    hideDialogBox() {
        this.removeDialogBox();
    }

    /**
     *
     * @param {string} menuText
     * @param {string} menuTextClass
     */
    menuBar(menuText, menuTextClass = '') {
        let menuTextId = 'dialog-menu-text';
        /*
         * if dialogMenuBar is present then the length will be non zero or true
         * if this is the case replace the content, this allows this method to be called
         * again later to change the value
         */
        let menuBarID = 'dialog-menu-bar';
        let menuTextDiv = document.getElementById(menuTextId);

        if (Utils.isEmpty(menuTextDiv)) {
            this.dialogHtml = '<div class="dialog-menu-bar" id="dialog-menu-bar" >';
            this.dialogHtml += '<div id="' + menuTextId + '" class="dialog-menu-text ';
            this.dialogHtml += menuTextClass + '" >' + menuText + '</div>';
            this.dialogHtml += '<div id="' + this.closeDialogId + '" class="close-dialog" >';
            this.dialogHtml += '<img src="/images/drawing/close-window.png" ';
            this.dialogHtml += 'alt="Close dialog" >';
            this.dialogHtml += '</div></div>';
        } else {
            menuTextDiv.innerText = menuText;
            menuTextDiv.className = menuTextClass;
        }
    }

    /**
     * displays the dialog box, you must call centerDialog to center it
     */
    showDialogBox() {
        //remove any existing dialog boxes first
        this.removeDialogBox();
        this.divElement = document.createElement("div");
        this.divElement.id = this.dialogBoxId;
        this.divElement.className = this.dialogBoxClass;
        this.divElement.innerHTML = this.dialogHtml;
        //position the dialog box now give it the highest z-index to be on top
        this.divElement.style.zIndex = Utils.getHighestZIndex() + 1;
        document.body.appendChild(this.divElement);
        //add the listener for when the user clicks to close

        this.closeHandler = function ( ) {
        this.removeDialogBox();
        }.bind(this);

        let close = document.getElementById(this.closeDialogId);
        close.addEventListener('click', this.closeHandler, false);
    }

    /**
     * removes the dialog html from the page and removes the close listener
     * dispatches event 'dialogClosed' to be used to close an overlay etc.
     */
    removeDialogBox() {
        let dialogElem = document.getElementById(this.dialogBoxId);
        //if a dialog box of the same id exists delete it first to prevent errors and issues
        if (dialogElem) {
            let close = document.getElementById(this.closeDialogId);
            close.removeEventListener('click', this.closeHandler, false);
            dialogElem.remove();
            const dialogEvent = new CustomEvent('dialogClosed');
            document.body.dispatchEvent(dialogEvent);
        }
    }
}

export {DialogBox}

That is a lot of code 169 lines to be exact. I am still in the process of converting this code, still going to add some Template literals instead of the old fashioned string concatenation technique.

There are several very import things to note here in this code. For example the way the addEventListener() and removeEventListener are used. These functions have to be passed THE EXACT SAME parameters or removeEventListener() fails to remove the event listener and that clutters your memory up because you will have listeners referring to elements that don’t exist.

That is why I have this code this.closeHandler


//add the listener for when the user clicks to close

        this.closeHandler = function ( ) {
        this.removeDialogBox();
        }.bind(this);

        let close = document.getElementById(this.closeDialogId);
        close.addEventListener('click', this.closeHandler, false);

See this.closeHandler = function  that stores the function to handle the click on the close button. Both the add and remove event listener functions have to be passed the exact same function.

Look at the removeDialogBox function closer.


let dialogElem = document.getElementById(this.dialogBoxId);
        //if a dialog box of the same id exists delete it first to prevent errors and issues
        if (dialogElem) {
            let close = document.getElementById(this.closeDialogId);
            close.removeEventListener('click', this.closeHandler, false);
            dialogElem.remove();
            const dialogEvent = new CustomEvent('dialogClosed');
            document.body.dispatchEvent(dialogEvent);
        }

Notice that the second argument to removeEventListener is this.closeHandler that is the same function passed to the addEventListener above.

If you use anonymous functions inside add and remove event listeners instead, then they wont be the same function and so your event listener won’t be removed and your memory fills up faster.

Another important note is that the this.handler function must use bind(this) like so

this.closeHandler = function ( ) { this.removeDialogBox(); }.bind(this);

If you don’t bind the function expression then you will get an error about this.removeDialogBox is not a function.
This is because you are storing the closeHandler in memory for later use.  At that later time the context will be different, the code won’t be executing within your class anymore, it will be in it’s own context. That means “this” that was alive in your class, no longer exists. Which means that function no longer exists You must bind “this” by using “.bind(this) at the end of the function.

And now about the Custom Event. You will see it at the bottom of the removeDialogbox() function

const dialogEvent = new CustomEvent('dialogClosed');
document.body.dispatchEvent(dialogEvent);

Those two lines is all it takes to create and dispatch your own Custom event.

Listening for custom events

This means you write code that listens for the custom “dialogClosed” event to be fired like this.

document.body.addEventListener('dialogClosed', function (){
overlay.hideOverlay();
});

Notice I am using document.body this is a very easy way to create the listener. This is using an anonymous function which is bad because this listener can’t be removed. It should be removed right below this. In order to do that you would need to create the handler function above it and pass it to both the add and remove event listeners.

Adding information to the event.

This is one of the most handy parts of custom events, the ability to pass information in the event. This can be any information, a full object even.
In this article it mentions adding custom data with “detail”.  Here is an example from the code above.

const dialogEvent = new CustomEvent('dialogClosed',{ detail: {
        id: this.dialogBoxId
    } });

Here I am passing id you can use this same format to pass many more values just add a comma to the end of each one. Then to access the extra information in your listener you do like this.

document.body.addEventListener( 'dialogClosed', function (event) {

let dialogId = event.detail.id
switch (dialogId) {
case mainDialogId :
mainOverlay.hideOverlay();
break;
case colorDialogId :
overlay2.hideOverlay();
break;
}

}, false);

More about addEventListener

Above I am using event.detail.id to get the value I stored in id in the detail of the custom event. Notice how I am using a switch statement to compare the id’s of the dialog that closed to close the correct overlay. There is no default behavior for this action, either one dialog closes or another. I could have 5 different dialogs if I wanted.

Links

The living DOM specification – covers javascript events and listeners and custom events etc. all in great detail. This is the language specification ie. “How things should work” Each browser implements it’s own version of this.

MDN addEventListener – great information about adding event listeners with lots of examples.

MDN creating and triggering events – great article about how to create and trigger custom events with examples.

MDN CustomEvent – basic documentation and good information about the CustomEvent object.