Usefull links
- Test latest JS features browser adoption and support: https://www.lambdatest.com/web-technologies/
- CJS vs AMD vs UMD vs ESM
import/exportstatements vsrequire/module.exports- getter/setter
- Class
- Generator
- Event management
- Date
fetchAPI- File's content management
- Forms
- iframe
- Encoding
- Crypto
- URL
- The various heights and their meaning
- Browser APIs
- Publishing a package
- Unit testing
- Building CLI tools
- Performances
- Service Worker API
- Security
- Printing
- Mobile keyboard (aka virtual keyboard) management
- Tips & Tricks
- Setting properties dynamically
- Dealing with unique items
- Passing variables to the
scripttag- Creating DOM from HTML string
- Detecting drag data in the dragover event
- Detecting the
dragendevent- Disabling all interactions inside a DOM
- Hashing content
- Compressing strings
- Converting string to DOM element(s)
- Troubleshooting
- FAQ
- How to use ES6 modules in Node?
- How to add support for ESM and CommonJS in a package?
- How to detect drag data in the dragover event?
- How to implement
pinch to zoomgesture?- How to implement two-fingers swipe gesture?
- How to disable two-fingers gestures?
- How to disable all interactions inside a DOM?
- How to load a local file's content into the browser?
- How to calculate a mobile's URL navigation bar's height?
- How to get the OS?
- How to asynchronously inject a external script and pass it data?
- CJS aka CommonJS is the NodeJS original pattern to break down and consume modules. Example:
const myMod = require('./myModules.js'). - AMD aka Asynchronous Modules is a pure front-end technology to asynchrously load modules.
- UMD aka Universal Modules is a verbose technology that makes modules work in both front-end and back-end.
- ESM aka ES6 Modules aims at merging all of the above qualities in a single standard that works both front-end and back-end. It also supports tree-shaking.
When a JS package is published to NPM, the default format is CJS (i.e., commonJS, aka require). The main code is the file that is defined in the package.js under the main property.
To publish a package using a different format, there is no other choice but to use a transpiler such as Webpack, Rollup or Microbundle. Those transpilers usually define a config file (e.g., webpack.config.js or rollup.config.js) where one or many destinations (one for each format) in an output property. Then, exposing each file via the package.json is what will expose the various format to NPM or the browser. For example:
package.json
"jsdelivr": "public/build/bundle.min.js",
"main": "src/main.js"When exposing an entry point on JSDelivr, JSDelivr chooses one of those three properties (ordered by priority):
jsdelivrbrowsermain
Dual packaging in both ES6 modules and CommonJS can be a pain. That's because there can be different use cases. This section covers how to use a native ES6 module package and add support for CommonJS using the rollup bundler.
- Convert all your
.jsfiles to.mjs. This change is required in order to get rid of the"type": "module"in thepackage.json. When set, the"type"property restricts the package to a single standard. The idea behind this change is to indicate which file uses which format based on the file's extension (.cjsfor CommonJS and.mjsfor ES6 module). - Make sure you have an
index.mjsthat combines all your modules into a single entrypoint. This is required to create aindex.cjsthatrequirecan use as a main entrypoint. For example, add a./src/index.mjssimilar to this:
export * as collection from './collection.mjs'
export * as converter from './converter.mjs'- Add rollup and configure it to convert your
.mjsfiles into.cjsfiles under a newdistfolder:- Install rollup with a plugin that can handle multiple inputs:
npm i -D [email protected] [email protected]Use those specific versions because the next versions fail.
- Add the following
rollup.config.jsin the project's root directory:
import multiInput from 'rollup-plugin-multi-input' export default { input: ['src/**/*.mjs'], // Thanks to 'rollup-plugin-multi-input', an array can be used instead of a single string. output: { dir: 'dist', format: 'cjs', chunkFileNames: '[name]-[hash].cjs', entryFileNames: '[name].cjs', exports: 'named' }, plugins:[multiInput()] }
- Add the new script in the
package.json:
{ "scripts": { "build": "rollup -c" } }
- Update the entry points in the
package.jsonto support both CommonJS and ES6 modules:
{
"main": "./dist/index.cjs",
"exports": {
".": "./dist/index.cjs",
"./collection": "./src/collection.mjs",
"./converter": "./src/converter.mjs"
}
}NOTICE:
- There should be no
"type": "module".- There MUST be both a
"main"property at the root and a"."property under the"exports". Those two properties MUST be set to the same value which MUST be a.cjsfile. This is what will allow the CommonJS to work.
- Optionally, use the
--ext .mjsoption in youreslintscript in yourpackage.jsonif you still want to lint your.mjsfiles:
{
"scripts": {
"lint": "eslint rollup.config.js src/ test/ --fix --ext .mjs"
}
}The issue with the example above is that it expects the ES6 modules and CommonJS version to not have the same default entry point (i.e., "."). If you wish to have the same, use the following:
{
"main": "./dist/index.cjs",
"exports": {
".": [{
"import": "./src/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.cjs"
},
"./dist/index.cjs"
],
"./collection": "./src/collection.mjs",
"./converter": "./src/converter.mjs"
}
}- Exporting a constructor
In an other module:
module.exports = function MyObject() { return this }
const MyObject = require('./myObject.js') const o = new MyObject()
- ES6 module native file extension is
.mjs(they can also be stored in normal.jsfiles). - To execute a an ES6 module in NodeJS, you must:
- Use NodeJS 12+
- Add the
"type": "module"configuration in thepackage.json. - If you're using NodeJS 12, you must use the
--experimental-modulesflag. For example:node --experimental-modules index.js - NodeJS 13+ supports modules without any flags need.
Update your package.json with a type and exports property as follow:
{
"name": "my-package",
"type": "module",
"exports": {
"./date": "./src/date.js",
"./math": "./src/math.js",
"./string": "./src/string.js"
"./package.json": "./package.json"
}
}Notice the
"./package.json": "./package.json". Some bundler (e.g., rollup) need this export.
In the client, this package would be used as follow:
import { doSometing } from 'my-package/math'For more details of why, in 2021, we should structure our code this way moving forward, please refer to the section.
With modules, one of the focus is to minimize the amount of code that is truly being used by any client. Because CommonJS was designed to run server-side, it did not really matter to save a few KBs, but on the clients, those few KBs can matter a lot.
Let's imagine a package made of 3 components called:
datemathstring
With both CommonJS and ES6, the JS package would be structured as follow:
my-package/
|__src/
| |__date.js
| |__math.js
| |__string.js
|
|__package.json
The CommonJS would typically use an extra index.js file to combine all the modules together:
my-package/
|__src/
| |__date.js
| |__math.js
| |__string.js
|
|__index.js
|__package.json
This file would be similar to this:
module.exports = {
date: require('./src/date'),
math: require('./src/math'),
string: require('./src/string')
}With CommonJS, the package.json would contain the following entry point:
{
"name": "my-package",
"main": "./index.js"
}Once published on NPM (or any other JS package manager), the client would use this package as follow:
const { math } = require('my-package')
math.doSomething()This could also be written as follow:
const myPck = require('my-package')
myPck.math.doSomething()Using the ES6 import API, this could also be written as follow:
import { math } from 'my-package'With CommonJS a bundler is not capable to remove the code that is not used. Though the example above only uses the math API, the entire package is bundled.
I use to think that packaging a library using ES6 would, out-of-the-box (i.e., without the help of any bundler), perform tree shacking so that:
import { math } from 'my-package'Results in smaller bundles than:
import { math, date, string } from 'my-package'But in reality ES6 pulls as much code in both cases (a bundler like webpack would perform tree-shacking and make the first example smaller than the second).
Let's see how we would use a more idiomatic ES6 design to our package. In the package.json add a new entry point called exports as follow:
{
"name": "my-package",
"main": "./index.js",
"type": "module",
"exports": {
"./date": "./src/date.js",
"./math": "./src/math.js",
"./string": "./src/string.js"
"./package.json": "./package.json"
}
}This package is now using the native ES6 APIs. The previous example can be rewritten as follow:
import { doSomething } from 'my-package/math'As you can see, only the my-package/math module is being used here. As we're getting close to full browser support for modules, bundler will become obsolete. Therefore, it worth structuring your package as above in order to only load the minimum amount of code into client applications.
For more details about this topic, please refer to this article: https://medium.com/@bluepnume/javascript-tree-shaking-like-a-pro-7bf96e139eb7
The import/export keywords were introduce in ES6 to replace the CommonJS require/module.exports keywords. The change of API seems pedantic at first, and to be honest, the CommonJS require/module.exports API is easier to grasp and use IMHO. However, this change is important, and it is the recommended way to produce and consume modules in Javascript. The two main benefits of this new native module system are universal code (i.e., runs both in NodeJS and the browser) and tree shaking. Tree shaking is the ability of a compiler/transpiler to eliminate dead code (i.e., unused bits of a library) in order to decrease the bundle size. This might not be critical for small apps, but can become vital for bigger ones.
// Previous way:
// const { func_01, func_02 } = require('./utils')
import { func_01, func_02 } from './utils.js'// Previous way:
// module.exports = { func_01, func_02 }
export {
func_01,
func_02
}Alternatively, you can also rewritte the above as follow:
export const func_01 = () => /*...*/
export const func_02 = () => /*...*/However, to write this:
import utils from './utils.js'You must write utils.js as follow:
export default {
func_01,
func_02
}or as follow:
export default function() {
// some code here
}Notice that you have to use the
functionkeyword instead of the arrow function in this case.
Otherwise, import using this:
import * as utils from './utils.js'WARNINGS: The benefits of tree shaking are wasted when using
export defaultas the entire object is exported. Try to be specific as much as possible.
If you wish to export an entire module to another one (typical example is the index.js case):
export { someApi } from './your-module'
export * as someOtherApi from './your-other-module'
export { default as someOtherApiAgain } from './your-other-module-again'Notice the subtle difference between export * as someOtherApi and export { default as someOtherApiAgain }. The last case is used when you want to export a default function.
Tips for writing ES modules in Node.js
const watch = {
name:'apple',
owner: '',
get time() {
return new Date()
},
set newOwner(owner) {
this.owner = owner
}
}
console.log(watch.name)
console.log(watch.owner)
console.log(watch.time)
watch.newOwner = 'Nic'
console.log(watch.owner)
setTimeout(() => console.log(watch.time), 1000)If you need to use getters and setters on a class that uses function:
function Watch() {
this.name = 'apple'
Object.defineProperty(this, 'time', {
get() {
return new Date()
}
})
return this
}const a = { hello: 'Baby' }
a['world'] = 0 // IMPORTANT: This line is not a mistake. Please refer to the note below.
Object.defineProperty(a, 'world', { get() { return 34 } })Notice the weird a['world'] = 0. This is important to explicitly declare the world property. Without this line, executing Object.keys(a) would return ['hello'] instead of ['hello', 'world'].
class Person {
constructor(fullName) {
const parts = (fullName || '').split(' ').filter(x => x)
this.firstName = parts[0]||''
this.lastName = parts.slice(-1)[0]||''
}
sayHi(msg) {
console.log(`${this.firstName} says: ${msg}`)
}
}
const james = new Person('James Bond')
console.log(james.firstName) // -> 'James'
console.log(james.lastName) // -> 'Bond'
console.log(james instanceof Person) // -> trueclass Employee extends Person {
constructor(fullName, jobTitle) {
super(fullName)
this.jobTitle = jobTitle
}
}
const fred = new Employee('Fred Stones', 'Bus driver')
console.log(fred.firstName) // -> 'Fred'
console.log(fred.lastName) // -> 'Stones'
console.log(fred.jobTitle) // -> 'Bus driver'
console.log(fred instanceof Employee) // -> true
console.log(fred instanceof Person) // -> trueThis will fail:
class Employee extends Person {
constructor(fullName, jobTitle) {
somePromise.then(() => {
super(fullName)
this.jobTitle = jobTitle
})
}
}Use this instead:
class Employee extends Person {
constructor(fullName, jobTitle) {
return somePromise.then(() => {
super(fullName)
this.jobTitle = jobTitle
return this
})
}
}Notice that, in order to work:
- You MUST return the promise.
- You MUST explicitly use
return this.
const a = [1,2,3]
const aGen = (function*(){yield* a})()
console.log(aGen.next()) // -> { value:1, done:false }
console.log(aGen.next()) // -> { value:2, done:false }
console.log(aGen.next()) // -> { value:3, done:false }
console.log(aGen.next()) // -> { value:undefined, done:true }
console.log(aGen.next()) // -> { value:undefined, done:true }To capture all keys, use keydown rather than keypress (keypress only works for alpha-numerical keys).
document.addEventListener('keydown', e => {
const isCtrl = e.key == 'Control'
const isCommand = e.key == 'Meta'
const isAlt = e.key == 'Alt'
})Javascript exposes two classes to emit events:
Eventhelps creating events with no data.CustomEventhelps creating events with data.
A typical use case where emitting custom events makes your life easier happens when you wish to leverage advanced tracking in Google Analytics or Facebook pixels. If you use Google Tag Manager, it would be nice if you could simply hook to special events rather than having to update your web page for each specific clicks.
To emit and catch events, use a snippet similar to the following:
// Emit the event
var event = new Event("hello", { bubbles: true });
yourDOMelement.dispatchEvent(event); // You can also replace `yourDOMelement` with `document`.
// Capture the event
document.addEventListener("hello", function(event) {
alert("Hello from " + event.target.tagName);
});Alternatively, to emit the event from within a listener callback or using inline JS-in-HTML:
document.getElementById('submit-resume-form').addEventListener('submit', function(event) {
var event = new Event("hello", { bubbles: true });
this.dispatchEvent(event);
// Rest of the code...
})<button onclick="this.dispatchEvent(new Event('add-to-cart', { bubbles: true }))">Add to cart</button>To emit events with data, use this API as follow:
var data = { message: "Hello world" }
var event = new CustomEvent("hello", { bubbles: true, detail: data });
document.dispatchEvent(event)
document.addEventListener("hello", function(event) {
alert("Message: " + event.detail.message);
});document.onreadystatechange = () => {
if (document.readyState === 'complete') {
// document ready
}
}Use
document.readyState === 'interactive'to detect when the DOM is ready.
or
document.addEventListener('readystatechange', () => {
if (document.readyState == 'complete') {
// document ready
}
})WARNING: When used in the
addEventListenerAPI, theonprefix must be dropped.
Use this function on onkeypress, onkeyup or onkeydown events:
const ignoreNumber = e => {
const keyCode = e.keyCode || e.which
const isNumber = keyCode > 47 && keyCode < 58
if (isNumber)
e.preventDefault()
}The difference between those 3 events:
- The execution order: 1st is
onkeydownthen 2nd isonkeyupthen 3rd isonkeypress. onkeydownandonkeyupcapture any key press, whileonkeypressonly capture the standard characters. For example, if you need to capture the arrow keys or the escape key, do not useonkeypress.
Main properties:
key: Value of the key pressed (e.g., 'a', 'b' ...).keyCode || which: Code that represent the underlying value.ctrlKey: true or false depending on whether the ctrl key is also pressed.metaKey: true or false depending on whether the Windows or Command key is also pressed.shiftKey: true or false depending on whether the shift key is also pressed.
At the time of the keypress event, the e.target.value is not updated yet. This means it contains the old value. Sometimes, you need to validate the upcoming new value (e.g., validating an email address).
NOTE: If you don't need to prevent the keypress, you can replace
onkeypresswithonkeyup. Withonkeyup,e.target.valuecontains the latest value.
const onKeyPress = e => {
const pos = e.target.selectionStart||0
const val = e.target.value || ''
const nextVal = val.substring(0,pos) + (e.key||'') + val.substring(pos,val.length)
if (!validateEmail(nextVal)) {
e.preventDefault()
return false
}
}const onPaste = e => {
const clipboardData = e.clipboardData || { getData:(e:any) => e }
const val = (clipboardData.getData('Text')||'').trim()
if (!validateEmail(val)) {
e.preventDefault()
return false
} else {
e.target.value = val
e.preventDefault()
}
}<!-- This is the item that we can drag. -->
<div draggable=true ondragstart="doSomethingWhenItemStartsDragging(event)"></div>
<!-- This is the item that is allowed to be dropped onto. -->
<div ondragover="allowThisDomToBeDroppedOnto(event)" ondrop="doSomethingWithTheDraggedItem(event)" ></div>const doSomethingWhenItemStartsDragging = ev => {
// Store any data as long as this is a string so that the DOM that receives this dragged
// item can have some context.
ev.dataTransfer.setData('text', ev.target.id)
}
const allowThisDomToBeDroppedOnto = ev => ev.preventDefault() // This is required to allow this DOM to be dropped onto
const doSomethingWithTheDraggedItem = ev => {
console.log(`Element ID ${ev.dataTransfer.getData('text')} has been dropped!`)
}Other useful tips:
Original article: Detecting multi-touch trackpad gestures in JavaScript WARNING: In Safari, the two-fingers pinch to zoom gesture is used to show all the tabs. Though you could spend time to hack it, my personal recommendation is to not support this gesture in Safari. After all, if Apple does not want its users to pinch to zoom in Safari, then so be it.
Two-fingers gestures are captured via the wheel event:
var ref = { scale:1, x:0, y:0 }
var childSurface = document.getElementById('child-surface')
document.getElementById('some-parent-surface').addEventListener('wheel', e => {
e.preventDefault()
let update = false
if (e.ctrlKey) { // pinch to zoom
const newScale = ref.scale - (e.deltaY * 0.01)
if (newScale >= 0.1 && newScale <= 10) {
gardenZoom.value = newScale
gardenDisplayValue.innerHTML = newScale.toFixed(2)
ref.scale = newScale
update = true
}
} else { // two-fingers move
ref.x -= e.deltaX
ref.y -= e.deltaY
update = true
}
if (update)
childSurface.style.transform = `translate(${ref.x}px, ${ref.y}px) scale(${ref.scale})`
})setTimeout(() => console.log('Hello'), 1000) // Schedule that function in 1 secondconst schedule_fn = setTimeout(() => console.log('Hello'), 1000) // Schedule that function in 1 second
clearTimeout(schedule_fn) // Cancel that scheduled functionThough MomentJS used to be the popular library to manage timezones, other libraries have emerged as more modern alternatives. One of them is Luxon, which was developed by a MomentJS maintainer to overcome some of MomentJS biggest shortcoming.
npm i luxon
const { DateTime } = require('luxon')
const tz = 'Australia/Sydney'
const dt = DateTime.fromISO((new Date()).toISOString()).setZone(tz) // Where dt is a Luxon DateTime object.
console.log(dt.toFormat('dd/MM/yyyy HH:mm'))
console.log(dt.toISO()) // Usual ISO format '2023-07-24T10:41:13.345+10:00'
console.log(dt.minus({ hour:1 }).toISO()) // Usual ISO format '2023-07-24T09:41:13.345+10:00'
console.log(dt.minus({ hour:1 }).setZone('UTC').toISO()) // Usual ISO format '2023-07-23T23:41:13.345Z'To do the inverse, converting a local string to UTC:
const convert_brisbane_date_str_to_utc_str = date_str => {
const d = DateTime.fromISO(date_str.replace(/Z$/,''), {zone: 'Australia/Brisbane'});
return d.toUTC().toISO()
}Full API doc at https://moment.github.io/luxon/api-docs/index.html
npm i geo-tz
const { find } = require('geo-tz')
const [tz] = find(-8.340539, 115.091949) || []The following POST passes data via query string and FormData (for the data serialized via URLSearchParams). The FormData might seem surprising as the URLSearchParams method is used, but this is how it works. Go figure...
fetch('https://example.com/login?client_id=12345&response_type=code&scope=email+openid+phone+profile&redirect_uri=https%3A%2F%2Fd12.cloudfront.net',{
method:'POST',
mode: 'cors',
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
body: new URLSearchParams({
username: '123456',
password: '********'
})
})
.then(resp => {
console.log('YES')
resp.json().then(console.log)
})
.catch(err => {
console.log('NO')
console.log(err)
})<input type="file" id="file-input" />
<button id="read-button">Read File</button>
<pre id="file-contents"></pre>
<script>
document.querySelector("#read-button").addEventListener('click', function() {
let file = document.querySelector("#file-input").files[0];
let reader = new FileReader();
reader.addEventListener('load', function(e) {
let text = e.target.result;
document.querySelector("#file-contents").textContent = text;
});
reader.readAsText(file);
});
</script>Prepend the file's path with ?raw:
import csv from '../data/my_data.csv?raw'<form onsubmit="updateForm(this, event)">
<textarea name="def" class="uk-textarea" rows="5" placeholder="Textarea"></textarea>
<input type="submit" class="uk-button uk-button-primary"></input>
</form>function updateForm(form, event) {
event.preventDefault()
var f = new FormData(form)
console.log(f.get('def')) // Gets the value stored in the textarea.
f.append('customField', 'Hello') // Add a custom field programmatically
console.log(f.get('customField')) // > 'Hello'
console.log(Object.fromEntries(f)) // converts the form data to a JSON object with a key value pair for each entry.
}NodeJS does not support the FormData object natively. To manipulate such object, install the form-data package. To use it with HTTP request, please refer to this example.
Testing viewport changes is usefull to preview a website with different screen sizes. To my knowledge, using an iframe is the only programmtic way to achieve this. To render https://example.com in mobile:
<iframe id="renderer" src="https://example.com" style="width:1000px;height:800px"></iframe>var iframe = document.getElementById('renderer')
iframe.style.width = '576px'
iframe.style.height = '900px'The contentWindow API exposes the iframe's content JS functions. Let's imagine that https://example.com exposes the following function:
<iframe id="renderer" src="https://example.com" style="width:1000px;height:800px"></iframe>var person = { name:'Peter', messages:[] }
window.saySomething = message => {
alert(`${person.name} says: "${message}"`)
person.messages.push(message)
return person
}Then, in the hosting page, the iframe's saySomething function can be invoked as follow:
var iframe = document.getElementById('renderer')
var person = iframe.contentWindow.saySomething('Hey folks! How y\'all doin\'?')
console.log(`${person.name} said something. This was their ${person.messages.length}th message.`)const str = 'Hello World!'
const enc = window.btoa(str)
const dec = window.atob(enc)
console.log(`Encoded String: ${enc} - Decoded String: ${dec}`)The simplest way is to use something similar to this:
Math.random().toString(36).substr(2, 9)Do not use this to create primary keys in RDBMS tables. The entropy of this output is not high enough.
If you're using NodeJS, a better way (i.e., higher entropy which means higher uniqueness) is:
const crypto = require('crypto')
crypto.randomBytes(50).toString('base64')In the browser:
const id = crypto.getRandomValues(new Uint32Array(1))[0].toString(16) // '496be336'
const uuid = crypto.randomUUID() // '69022058-dbbc-4000-ad62-1a0a3494d42e'To get the current URL, use the native window.location.href API. URL are broken down as follow:
[href] e.g., https://fonts.gstatic.com/s/flUhRq6tzZclQEJ.woff2?name=frank#footer
where href is broken down as:
[origin][pathname][search][hash]
where:
origin:https://fonts.gstatic.com, which can also be broken down as follow:protocol:https:host:fonts.gstatic.com, which can also br broken down as follow:hostname:fonts.gstatic.comport: null, which means80
pathname:/s/flUhRq6tzZclQEJ.woff2search:?name=frankhash:#footer
Simply use the native URL class. It is supported in both Node and the browser. Its properties are as follow:
- href: e.g.,
https://fonts.gstatic.com/s/materialicons/v70/flUhRq6tzZclQEJ.woff2?name=frank#footer - origin: e.g.,
https://fonts.gstatic.com - protocol: e.g.,
https: - host: e.g.,
fonts.gstatic.com - hostname: e.g.,
fonts.gstatic.com - port: e.g.,
'' - pathname: e.g.,
/s/materialicons/v70/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2 - search: e.g.,
?name=frank - searchParams:
URLSearchParams - hash: e.g.,
#footer
/**
* Breaks down a URL's parts. (e.g., getUrlParts(window.location.href))
*
* @param {String} url e.g., 'https://fonts.gstatic.com/s/materialicons/v70/flUhRq6tzZclQEJ.woff2'
*
* @return {[type]} parts.href 'https://fonts.gstatic.com/s/materialicons/v70/flUhRq6tzZclQEJ.woff2'
* @return {[type]} parts.origin 'https://fonts.gstatic.com'
* @return {[type]} parts.protocol 'https:'
* @return {[type]} parts.username ''
* @return {[type]} parts.password ''
* @return {[type]} parts.host 'fonts.gstatic.com'
* @return {[type]} parts.hostname 'fonts.gstatic.com'
* @return {[type]} parts.port ''
* @return {[type]} parts.pathname '/s/materialicons/v70/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2'
* @return {[type]} parts.search ''
* @return {[type]} parts.searchParams URLSearchParams {}
* @return {[type]} parts.hash ''
* @return {[type]} parts.ext '.woff2'
*/
const getUrlParts = url => {
const parts = new URL(url)
parts.ext = null
if (parts.pathname)
parts.ext = (parts.pathname.match(/\.([^.]*?)$/)||[])[0] || null
return parts
}To access the search params, use the URLSearchParams API as follow:
new URL('https://fonts.gstatic.com/s/flUhRq6tzZclQEJ.woff2?name=frank#footer').searchParams.get('name')To rebuild a URL string, use the toString() API:
let u = new URL('https://fonts.gstatic.com/s/flUhRq6tzZclQEJ.woff2?name=frank#footer')
u.hostname = 'hello.com'
u.hash = 'cool'
u.searchParams.set('name','carlos')
u.toString() // https://hello.com/s/flUhRq6tzZclQEJ.woff2?name=carlos#coolNowadays, most browsers support the history.pushState and history.replaceState Web APIs. They update the URL of the browser without reloading the page:
window.history.pushState(null, '', '/whateverpath/?name=something#somethingelse')The difference between history.pushState and history.replaceState is that the former pushes that new state in the browser's history, allowing the back button to work while the last only changes the URL.
This feature is not natively supported. What is needed is the ability to detect when the following APIs are fired:
window.history.pushStatewindow.history.replaceStatepopstateevent which occurs when the back button used.
To manually add support for this feature:
const getUrlParts = url => {
const parts = new URL(url)
parts.ext = null
if (parts.pathname)
parts.ext = (parts.pathname.match(/\.([^.]*?)$/)||[])[0] || null
return parts
}
const pathToUrl = path => {
if (!path)
return null
if (path[0] == '/')
return `http://hello${path}`
if (path[0] == 'h' && path[1] == 't' && path[2] == 't' && path[3] == 'p')
return path
return `http://hello/${path}`
}
(function(history) {
const pushState = history.pushState
const replaceState = history.replaceState
history.pushState = function(...args) {
window.dispatchEvent(new CustomEvent('locationchange', { bubbles: true, detail: { type:'pushstate', url:getUrlParts(pathToUrl(args[2])) } }))
return pushState.apply(history, arguments)
}
history.replaceState = function(...args) {
window.dispatchEvent(new CustomEvent('locationchange', { bubbles: true, detail: { type:'replacestate', url:getUrlParts(pathToUrl(args[2])) } }))
return replaceState.apply(history, arguments)
}
})(window.history)
const onUrlChange = handler => {
window.addEventListener('locationchange', handler)
window.addEventListener('popstate', handler)
}This is the full height of your browser's window, including:
- Tabs menu
- URL navigation bar
- Viewable screen
- Dev toolbar if it is displayed and located in the browser window
This is the viewable screen bit of the browser only. It includes the scrollbars if there are any. It does not include the dev toolbar if it is displayed and located in the browser window.
Same as window.innerHeight, but excludes scrollbars if there are any.
Height of your device's screen.
Same as window.screen.height minus dock's height, if there is any.
This is a special case, especially for iOS and Safari. On iOS Safari, the virtual keyboard does not trigger a screen resize. On any other devices, the virtual keyboard would affect window.screen.height and window.innerHeight, but not on freaking iOS. To manage the iOS case, use window.visualViewport.height.
As of November 2022, this still requires a hack:
- Create a non-visible and non-function DOM with the following CSS:
{
position: absolute;
width: 0px;
height: 100vh;
}- When the page is loaded, the mobile's URL navigation bar's height is the difference between this DOM's
clientHeightand thewindow.innerHeight. On desktop, or when the app is installed on the mobile's home screen, that difference is zero.
const urlNavBarHeight = document.querySelector('#control-height').clientHeight - window.innerHeighthttps://usefulangle.com/post/118/javascript-intersection-observer
This topic is outside the scope of this document. I wrote a complete guide about it in my NODEJS & NPM under the NPM publish section.
The simplest way to use import with Mocha is to use the esm package as follow:
- Install
esm:
npm i -D esm
- Require esm in your mocha command. In your package.json, update your test script as follow:
"scripts": {
"test": "mocha --require esm --exit"
}When the above is set up, you can write unit tests as follow:
import { assert } from 'chai'
/*
You unit tests here
*/The package.json allows to create symlinks to specific NodeJS executable files (those files must start with the shebang #!/usr/bin/env node). To set this up, use the bin property in the package.json using one of the following two approaches:
Option 1:
{
"name": "your-package-name",
"bin": "src/index.js"
}In this case, the symlink's name (i.e., the name of the command in your path that will execute src/index.js) is the name of your package. In this case, this is your-package-name.
Option 2:
{
"name": "your-package-name",
"bin": {
"my-command": "src/index.js"
}
}With the code above, the symlink's name is my-command instead of your-package-name. Beyond just allowing to specifiy a command name that differs from the package name, this approach also allows to set up more than one command.
- Create a new NPM project:
npm init
- Install the following tools:
npm i colors commander inquirer
colorscolors your terminal stdin/stdout,commandercaptures commands andinquirerhelps building questions and capturing answers after the command has been received.
- In your
package.json, adds abinproperty that points to the main file that can process commands:
{
"bin":{
"your-command-name": "index.js"
}
}- In the
index.js, add the content described in the file below calledcommander.js.
Must read article from Google: https://web.dev/fast/
HTML5 introduced those two new attributes to improve HTML page performances. With those attributes, the page rendering is not blocked by Javascript code. Both of them loads the JS file asynchronously.
The difference is that defer only execute the script when the page is entirely loaded, while async executes the script as soon as it is loaded.
https://developers.google.com/web/fundamentals/performance/rendering
https://developers.google.com/web/tools/chrome-devtools/memory-problems
Caching your JS files can significantly improve your page loading time. This is not something you can do on the client side (besides using a service worker to work offline). Instead, a caching policy must be configure on your file hosting server which must set the Cache-Control response header. To learn more about this topic, please refer to the Understanding the native Cache-Control HTTP header section of the AWS CLOUDFRONT GUIDE document.
The Web Animation API (WAAPI) is a Browser JS API that allows to create animations without using the Window.requestAnimationFrame() API (which yields poor performance). It does not expose new functionalities when compared to what you already do in CSS. The same CSS performance rules apply. Examples:
- Use
opacityandtransformas much as possible as they are rendered/re-rendered by the compositor which uses the GPU(1). - DO NOT USE CSS VARIABLES IN
transformORopacityTO ANIMATE THEM. This is a typical mistake. Updating CSS Variables triggers a reflow because they are inheritable. When you wish to animate a DOM, update thetransformORopacityproperty explicitly by targetting the exact DOM.
The biggest WAAPI advantage is its flexibility. CSS can be contrived when it comes to creating and reasoning about animation. Building animation in JS enables to use coding best practices (encapsulation, reusability, composition, ...).
As for Worklets, as of mid-2020, this is still an experimental feature. The core idea is to extend CSS as well as making CSS and JS able to communicate with each other (via hooks). In the context of animation, Worklets are similar to WAAPI with the following two advantages:
- It runs in a worker-thread. This means that the limitation on using
opacityandtransformis lifted (though it might still be a good idea to try to use them as only those two are GPU optimized). - It is not limited to time-based animation. For example, the animation can be tied to a scroll event.
(1) Most browsers compositing work is helped by the GPU. The most ubiquitous engine that uses this strategy is called Blink, which is a Google fork of Webkit.
Resources:
- CSS Animations vs Web Animations API
- Using Animation Worklet
- Google Chrome's doc about Houdini's Animation Worklet
Please refer to this CSS document instead CSS GUIDE - Performances best practices
The word "tabnapping" is a combination of the words "tab" and "kidnapping" (or "nabbing"). That attack works as follow:
- An attacker finds a way to embed a link to his nefarious website in your secured website (e.g., your bank website).
- Upon clicking clicking that link, a new tab opens to show the dangerous website which contains a link that triggers this piece of code:
// This code makes the referer website opened in another tab (e.g., your bank website) browse to
// https://event-more-dangerous-website.com
window.opener.location = 'https://event-more-dangerous-website.com'- Later, you browse back to the tab that used to contain your bank website, except that this tab is now https://event-more-dangerous-website.com. The attacker crafted a exact copy of your bank website's login page. The users that don't notice that the URL has now changed from https://mybank.com to https://event-more-dangerous-website.com, think that their session timed out and enter their secret credentials in the dangerous website.
What the bank should have done is to santize all its links by adding a noopener value in the rel attribute:
<a href="https://event-more-dangerous-website.com" target="_blank" rel="noopener"/><a href="https://event-more-dangerous-website.com" target="_blank" rel="noopener"/>This protects your website from Tabnapping attacks.
To learn more about other rel values, please refer to the HTML guide.
To print a web page, simply use:
window.print()By default, printing a web page will add:
- A white margin.
- A date and a title in the header.
- A file path and page number in the footer.
Remove all the above and make it look like the web page is taking the entire pdf page, add in the page CSS:
@page { size: auto; margin: 0mm; }Add in the page CSS:
@media print {
html, body {
height:100%;
margin: 0 !important;
padding: 0 !important;
overflow: hidden;
}
}- Prevent content from being hidden underneath the Virtual Keyboard by means of the VirtualKeyboard API
- VisualViewport API
const a = 'bla'
const b = { [a]:'Hello' }
console.log(b.bla) // 'hello'Set is a neat API to deal with collection of unique items. Instead of pushing items in an array that you need to clean later to arrange them by unique ID, you can use Set as follow:
const a = new Set([1,2,2,2,3]) // -> { 1, 2, 3 }
a.add(3) // -> { 1, 2, 3 }
a.add(4) // -> { 1, 2, 3, 4 }
console.log(Array.from(a)[0]) // 1
console.log(a.size) // 4In your HTML page, add this type of script:
<script defer src='/build/bundle.js' data-name="Nicolas"></script>Where data-name is the variable you wish to pass to your script.
NOTE: Prefixing with
data-is an HTML5 best practice to avoid conflicts and explicitely communicate that the attribute is a custom one.
In your bundle.js:
// This snippet is used in case document.currentScript is not natively supported
if (!document.currentScript)
document.currentScript = (function() {
var scripts = document.getElementsByTagName('script')
return scripts[scripts.length - 1]
})()
const name = document.currentScript.getAttribute('data-name') || null const doc = new DOMParser().parseFromString('<div><b>Hello!</b></div>', 'text/html')
document.getElementById('hello').appendChild(doc.body)NOTE:
DOMParseris not available in the NodeJS API. To use a similar API in NodeJS, usejsdomorcheerio.
For security reason, it is not possible to access the data stored in the dataTransfer inside the ondragover handler. To refresh our mind, remember that when an element is being dragged, data can be stored in the ondragstart event as follow:
const onDragStartHandler = ev => {
ev.dataTransfer.setData('text', 'Some valuable stringified data here')
}The only handler that can then read that data is the ondrop handler:
const onDropHandler = ev => {
console.log(ev.dataTransfer.getData('text'))
}However, there are cases where some logic in the ondragover handler requires a bit of context about that data. The hack is to leverage the data types of the dataTransfer object as follow:
const onDragStartHandler = ev => {
ev.dataTransfer.setData('text', 'Some valuable stringified data here')
ev.dataTransfer.setData('whatever you need here', '')
}
const onDragOverHandler = ev => {
console.log(ev.dataTransfer.types) // ["text/plain", "text/uri-list", "whatever you need here"]
}WARNING:
ev.dataTransfer.typesonly support lowercase characters.
In theory, detecting when an item stopped being dragged is supposed to be as easy as defining the ondragend attribute on the draggable DOM. In theory, this event is fired when the draggable DOM is dropped or when the drag is cancelled. However, in practice this does not work in many cases. Some of those frequent cases are:
- Firefox is buggy.
- If the draggable item is re-rendered while being dragged, the
dragendwill not fire.
The best alternative I've found so far is to add a mousemove listener on the window object. This listener is not fired when an item is dragged. Create a dragging variable that is set to true during the ondragstart event. When the drag stop, any mouse movements will trigger the mousemove event. If dragging is true, then set it back to false and emit your own custom event to signal that the drag ended.
Add the following CSS on the DOM:
pointer-events: none;const crypto = require('crypto')
console.log(crypto.createHash('sha1').update(JSON.stringify({hello:'world'})).digest('hex'))Use the shrink-string package.
const { compress, decompress } = require('shrink-string')
const main = async () => {
const c = await compress('hello world')
console.log(c)
const d = await decompress(c)
console.log(d)
}/**
* Converts a single node string to an HTML DOM element
*
* @param {String} HTML representing a single element
*
* @return {Element}
*/
const htmlToElement = html => {
var template = document.createElement('template')
html = html.trim() // Never return a text node of whitespace as the result
template.innerHTML = html
return template.content.firstChild
}
/**
* Converts a list of nodes string to an HTML DOM elements
*
* @param {String} HTML representing any number of sibling elements
* @return {NodeList}
*/
const htmlToElements = html => {
const template = document.createElement('template')
template.innerHTML = html
return template.content.childNodes
}This warning occurs when Rollup tries to draw your attention on the fact that a 3rd party library was not bundled. By default, Rollup only bundles local modules. To include 3rd party dependencies, you must use the @rollup/plugin-node-resolve plugin:
npm install @rollup/plugin-node-resolve --save-dev
Update the rollup.config.js as follow:
import { nodeResolve } from '@rollup/plugin-node-resolve'
export default {
input: ['src/**/*.mjs'], // Thanks to 'rollup-plugin-multi-input', an array can be used instead of a single string.
output: {
dir: 'dist',
format: 'cjs',
chunkFileNames: '[name]-[hash].cjs',
entryFileNames: '[name].cjs'
},
plugins:[
nodeResolve()
]
}This occurs at bundle time. Rollup is trying to bundle libraries that were originally created for CommonJS. Because they were not created for ESM, their are entirely exported via an API similar to this: import someCommonJsLib from 'someCommonJsLib'. This API is similar to an ESM module with a default export. However, the default export is not used in the case of the someCommonJsLib because that's a CommonJS library. Rollup throws this error because it cannot find the default export in the someCommonJsLib lib.
To fix this issue, you must tell Rollup that it should explicitly include that someCommonJsLib library in its bundle (no tree shaking anymore). To configure this, edit the rollup.config.js file as follow:
export default {
// Some other config
external: [
"someCommonJsLib"
]
}Please refer to the ES6 modules section.
This is called dual packaging. More about this topic under the Dual packaging section.
Please refer to the Detecting drag data in the dragover event section.
Please refer to the Two-fingers gestures section.
Please refer to the Two-fingers gestures section.
The following code disable pinch to zoom. The trickey part is the option passive: false. Without it, the code below would throw an Unable to preventDefault inside passive event listener error.
window.addEventListener('wheel', e => {
if (e.ctrlKey)
e.preventDefault()
}, { passive: false })Please refer to the Disabling all interactions inside a DOM section.
Please refer to the Creating an input allowing the user to load a file section.
Please refer to the Calculating a mobile browser's URL navigation bar's height? section.
const getOS = () => {
const nav:any = window.navigator
const userAgent = (nav.userAgent || '').toLowerCase()
const platform = (nav?.userAgentData?.platform || nav.platform || '').toLowerCase()
const macosPlatforms = ['macintosh', 'macintel', 'macppc', 'mac68k', 'macos']
const windowsPlatforms = ['win32', 'win64', 'windows', 'wince']
const iosPlatforms = ['iphone', 'ipad', 'ipod']
let os = null
if (macosPlatforms.indexOf(platform) !== -1)
os = 'Mac OS'
else if (iosPlatforms.indexOf(platform) !== -1)
os = 'iOS'
else if (windowsPlatforms.indexOf(platform) !== -1)
os = 'Windows'
else if (/android/.test(userAgent))
os = 'Android'
else if (/linux/.test(platform))
os = 'Linux'
return os
}Sample of code that you would use in Google Tag Manager to inject a custom script:
<script>(
function(s,a,t){
var el=document.createElement("script");
el.src=s;el.setAttribute("target",t);el.setAttribute("apikey",a);
el.defer="true";
document.head.appendChild(el);
})("https://example.com/[email protected]","fefefcewfdcwedcwqdw","bla");
</script>