JavaScript Idioms - sgml/signature GitHub Wiki

Overuse

Use Backbone as a model for a generic caching layer instead of library specific persistence methods (component props, slots, store APIs, etc.)

Use CSS for creating UI; do not depend on it (zindex esp)

Use URLs for tracking history

Use HTML for storing metadata

Use constants instead of calling endpoints for database values that do not change, such as statuses

Use callbacks <input onclick=foo()> even if templates can include inline logic <input :click="a + b > 0 ? go() : stay()">

Underuse

Use named functions

Use refactoring tools such as the JS Refactoring Assistant to move anonymous functions, inline functions, and ternary logic out of templates BEFORE doing any refactoring or wholesale framework migration

Use anything but fat arrow functions(farts)

Static File Generation

Use pnpm generate to test whether or not there are syntax errors during static file generation

Type Checking

Use multimethods or multiple-dispatch to encapsulate type checking of API request/response payloads

Reflection

Use reflection accept any data type as an arg, then to do different things based on the type, which is Postel's law:

/* Define mapping */
var app = {"emit": emit};

/* Define interface */
function emit(){}

function \u1000missing(object, method, fallback)
  {
  /* Existence check */
  if (/function/.test(object[method]) ) 
    {
    object[method]();
    }
  /* reify */
  else
    {
    object[fallback](method)
    }    
  }

\u1000missing(app,"isReady","emit")

Decoupling

Split out the following JavaScript constructs into files:

  1. Constants
  2. API Endpoints/GraphQL Queries
  3. Templating Logic
  4. JSON Schema Definition
  5. DOM Event Callback Functions (Load/Unload)
  6. AJAX Event Callback Functions (Request/Response)
  7. Persistence (LocalStorage/Caching/Store Libraries)

Patterns

try { set loading boolean to true, fetch data, set state } catch { throw errors } finally { reset loading boolean to false }

function foo(a,b,c) { return a + b + c > 3 } // instead of a && b && c

Names

Use generic schema.org names for JSON keys (Context, ID, Type):

{
"@context": {
        "name": "http://xmlns.com/foaf/0.1/name",
        "homepage": {
            "@id": "http://xmlns.com/foaf/0.1/workplaceHomepage",
            "@type": "@id"
        },
        "Person": "http://xmlns.com/foaf/0.1/Person"
    },
    "@id": "https://www.linkeddatatools.com/johndoe",
    "@type": "Person",
    "name": "John Doe",
    "homepage": "https://www.linkeddatatools.com/"
}

Use a custom JSON parser

function customReviver(key, value) {
    if (key === 'email' || key === 'phone') {
        // Modify the value as needed
        // For example, return a boolean value
        return value === 'true';
    }
    return value; // Pass through unmodified values
}

const jsonString = '"{\\"email\\": true, \\"phone\\": true}}"';
const parsedObject = JSON.parse(jsonString, customReviver);

console.log(parsedObject); // Outputs: { email: true, phone: true }

Use VBScript Naming Conventions:

Begin each separate word in a name with a capital letter, as in FindLastRecord and RedrawMyForm.

    Begin function and method names with a verb, as in InitNameArray or CloseDialog.

    Begin class, structure, module, and property names with a noun, as in EmployeeName or CarAccessory.

    Begin interface names with the prefix "I", followed by a noun or a noun phrase, like IComponent, or with an adjective describing the interface's behavior, like IPersistable. Do not use the underscore, and use abbreviations sparingly, because abbreviations can cause confusion.

    Begin event handler names with a noun describing the type of event followed by the "EventHandler" suffix, as in "MouseEventHandler".

    In names of event argument classes, include the "EventArgs" suffix.

    If an event has a concept of "before" or "after," use a suffix in present or past tense, as in "ControlAdd" or "ControlAdded".

    For long or frequently used terms, use abbreviations to keep name lengths reasonable, for example, "HTML", instead of "Hypertext Markup Language". In general, variable names greater than 32 characters are difficult to read on a monitor set to a low resolution. Also, make sure your abbreviations are consistent throughout the entire application. Randomly switching in a project between "HTML" and "Hypertext Markup Language" can lead to confusion.

    Avoid using names in an inner scope that are the same as names in an outer scope. Errors can result if the wrong variable is accessed. If a conflict occurs between a variable and the keyword of the same name, you must identify the keyword by preceding it with the appropriate type library. For example, if you have a variable called Date, you can use the intrinsic Date function only by calling [DateTime.Date](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.date).

Async

Use the async await pattern if calls are non-blocking; make sure you do not await a non-async function, it will fail silently

Linting

Run linting on each file before you commit it

Defensive Programming

  • Use a constants file instead of string sprawl
  • Use a custom Error object to handle map/filter/reduce errors

Debugging

Use a search to find missing key/value pairs as follows:

  • Lookup a string value
  • Copy the key name of the string value
  • Lookup the key name
  • Copy the method(s) that use the key name
  • Add if/else logic to fix the bug in question
  • Add/Update tests to verify the new logic
  • Verify tests in the CI/CD tool

Use test specs to debug your use of reusable components to make sure your assumptions are correct

Exceptions

Use the pause on caught exceptions feature in the developer tools to catch type mismatches

Comments

Add logging to every local variable and argument

Add logging to every function and loop

Point out code which has complex conditional logic or regex patterns for in-depth commenting

JSDoc

Use JSDoc annotations to do regular expression checking:

/**
 * @param {RegExp} regex - Regular expression to test
 * @param {string} input - Input string to match against
 * @returns {boolean} - Whether the input matches the regex
 */
function testRegex(regex, input) {
    return regex.test(input);
}

Defensive Coding

Use a dictionary to store a primary and secondary value for every constant since the value itself cannot change, but its children can:

  function bindingData() {
        const data = {}
        if (this.foo?.data?.length) {
            data.primary = this.foo.data
            } else {
               data.secondary = []
            }
 
            return data?.primary || data?.secondary
        }

Use optional chaining to do existence checking when referencing nested data

        if (variables?.txa?.searchTerm) {

            const response = await this.$axios.$post('/GRAPHQL_URL/', {

            query,

            variables,

            })

Use recursion to loop through objects instead of dot notation and nested if/else, optional chaining foo?.bar, or guard operator foo && foo.bar logic:

Array Generics

Use a generic method to loop through arrays of objects:

function getArrayOfObjects(obj, objName) {
  let current

  for (current in obj) {
    if (getArrayOfObjects.id === undefined) {
      getArrayOfObjects.id = ""
    }
    if (!!obj[current] || obj[current] === "") {
      if (
        /Function|String|Object|Array/.test(String(obj[current].constructor))
      ) {
        if (getArrayOfObjects?.data) {
          getArrayOfObjects.data.push(objName + " => " + current)
        } else {
          getArrayOfObjects.data = []
        }
      }

      if (/String|Object|Function/.test(String(obj[current].constructor))) {
        getArrayOfObjects(obj[current], getArrayOfObjects.id + current)
      } else {
        console.log(obj[current]?.length)
        if (obj[current]?.length) {
          if (obj[current].length > 0) {
            getArrayOfObjects.id = obj[current]
          }
        }
      }
    }
  }
}

Use a generic function for parsing arrays instead of array specific methods:

function arrayChecker(arr) {
    console.assert([].every(function(){}))
}

Single Responsibility

Use dedicated functions instead of mixing templates with logic. Instead of this:

<div>
    <Foo />
<div :else>
    <Bar />
</div>

Do this:

<component :is="dyn">

<script>
import Foo
import Bar

function dyn(){
    if(a) {
       return 'foo'
    } else {
       return 'bar'
    }
}

Use let and simple if/else logic instead of const and inline ternary statements

  const locale = 'en'
  let foo = null
  let bar = null
  let baz = null

  try {
      if(locale === 'en'){
          foo = 'Arabic'
          bar = 'Phonetic'
          baz = 'Egyptian'
      } else {
          foo = 'Latin'
          bar = 'Etruscan'
          baz = 'Raetic'
      }
  } catch(e) {
      console.log('Error assigning value: ', e)
  } finally {
      console.log('foo: ', foo)
      console.log('bar: ', bar)
      console.log('baz: ', baz)
  }
      
  const summary = [
    {
      key: 'book',
      value: foo
    },
    {
      key: 'scroll',
      value: bar
    },
    {
      key: 'stone tablet',
      value: baz
    }
  ]

Simple Logic

Avoid ternary statements ?: as well as inline conditional expressions using && and ||.

Refactor the chain of boolean checks where there are more than two in the same statement, and update the API to return an aggregated response (sum, diff) instead of multiple values

Use try/catch/finally logic for code consistency, clarity and simplicity.

Use screenreader only classes to hide unconditional HTML for debugging purposes

Simple Forms

Use a subset of form controls (select, radio, checkbox), no default values, and no hardcoded options

Use an accordion to hide search boxes and other input fields by default to allow the JavaScript code to load before interaction is allowed

Simple Traceability

Use DOM event handlers instead of components for form controls (no button components please)

<div id="previous-page" @click="changePage">

changePage(event) {
  try {
      console.log('event: ', event)
      const bar = foo + (event.target.id == 'baz') ? 1 : -1
  } catch (e) {
      console.log(e)
  } finally {
      console.log(event)
  }

Modernization

const { parse, replace, generate } = require('abstract-syntax-tree');

// Read the old source code
const oldSourceCode = fs.readFileSync('old.js', 'utf8');

// Parse the source code into an AST
const ast = parse(oldSourceCode);

// Replace jQuery window statements with native JavaScript window statements
replace(ast, {
    enter(node) {
        if (node.type === 'MemberExpression' && node.object.name === '$' && node.property.name === 'window') {
            node.object.name = 'window';
        }
    }
});

// Generate the new source code from the AST
const newSourceCode = generate(ast);

// Write the new source code
fs.writeFileSync('new.js', newSourceCode);

Tracing

function traceMethodCalls(obj) {
    const handler = {
        get(target, propKey, receiver) {
            const targetValue = Reflect.get(target, propKey, receiver);
            if (typeof targetValue === 'function') {
                return function (...args) {
                    console.log('CALL', propKey, args);
                    return targetValue.apply(this, args); // (A)
                }
            } else {
                return targetValue;
            }
        }
    };
    return new Proxy(obj, handler);    
}

Testability

Add IDs and template refs for all buttons that handle events for testability and use the name of the method which handles the event as the value of the ID for ease of use

Use assert as a test runner:

// main.js
const obj = {};
obj.sum = (a, b) => {
    return a + b;
};
module.exports = obj;

// test.js
const main = require('./main.js');
const assert = require('assert');

const it = (desc, fn) => {
    try {
        fn();
        console.log('\x1b[32m%s\x1b[0m', `\u2714 ${desc}`);
    } catch (error) {
        console.log('\n');
        console.log('\x1b[31m%s\x1b[0m', `\u2718 ${desc}`);
        console.error(error);
    }
};

it('should return the sum of two numbers', () => {
    assert.strictEqual(main.sum(5, 10), 15);
});

And call it using a script tag:

// app.js
self.myapp = myapp; // All the methods in myapp will be exposed globally
myapp.sum = function(a, b) {
    return a + b;
};
 // test.html (your test runner for the front end)
<html>
    <body>
        <script src="app.js"></script>
        <script src="test.js"></script>
    </body>
</html>

Debugging

Use dev-mode to render reactive data in the UI for debugging purposes: https://github.com/fii-org/shared.web.vue.components/commit/6e0e5a51ca72235b81146ce4c86067e53a054cc9

Use the template output {{ }} to print values that need to be debugged. Wrap them in a div with an ID of hidden to make them easy to find via XPath in the developer tools

Mockups

Use web based editors to mockup things quickly:

Vue: https://www.tutorialspoint.com/online_vuejs_editor.php

Ember: https://ember-twiddle.com/

Buefy: https://codepen.io/tag/buefy

Bulma: https://codepen.io/tag/bulma

Tailwind: https://codepen.io/topic/tailwind/picks

CLI

Use CLI commands to decouple HTML, JS, CSS, transpilers, and preprocessors

Tailwind CLI

npx tailwindcss -i ./styles.css -o ./output.css --minify

References Vue Template Refs

Vue Dynamic Async Components

How to Use Typescript to Point to a Function

Is there a way to apply CSS style on a datalist

⚠️ **GitHub.com Fallback** ⚠️