JavaScript Idioms - sgml/signature GitHub Wiki
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()">
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)
Use pnpm generate
to test whether or not there are syntax errors during static file generation
Use multimethods or multiple-dispatch to encapsulate type checking of API request/response payloads
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")
Split out the following JavaScript constructs into files:
- Constants
- API Endpoints/GraphQL Queries
- Templating Logic
- JSON Schema Definition
- DOM Event Callback Functions (Load/Unload)
- AJAX Event Callback Functions (Request/Response)
- Persistence (LocalStorage/Caching/Store Libraries)
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
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).
Use the async await pattern if calls are non-blocking; make sure you do not await a non-async function, it will fail silently
Run linting on each file before you commit it
- Use a constants file instead of string sprawl
- Use a custom Error object to handle map/filter/reduce errors
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
Use the pause on caught exceptions
feature in the developer tools to catch type mismatches
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
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);
}
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:
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(){})) }
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
}
]
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
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
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)
}
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);
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); }
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>
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
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
Use CLI commands to decouple HTML, JS, CSS, transpilers, and preprocessors
npx tailwindcss -i ./styles.css -o ./output.css --minify
References Vue Template Refs