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.

By Robert jackson

This is my website. I write articles about programming and more. I've been programming 16 years now.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: