0

I am delegating a key input event from the document onto the several textareas on the page. Because the operations I need to perform on the key input are complex, repetitive, and tied to the text contents of that textarea - I find it better to cache that information in a TextareaState object (something like {wasChanged: true, penultimateTextContents: "string", etc.}) for each <textarea>. It would also make sense to store this object permanently, rather than recomputing it every time an input event was fired (recomputing would erase the important previous results anyway).

At first I thought of using a key-value pair object, but DOM elements cannot be substituted as object keys. I could instead try to generate a unique CSS selector for each DOM element and then use that selector as the object key, but this approach seems very expensive.

My third and final approach was to use two arrays (var textareaElements = [], TextareaStateObjects = []) and then every time an input handler was fired, I could do TextareaStateObjects[textareaElements.indexOf(event.target)] to get the corresponding cached object. However, that still seems pretty expensive, considering that .indexOf runs at every input event, and is going to be very costly for larger array lengths.

Is there a more efficient solution here?

Gaurang Tandon
  • 6,504
  • 11
  • 47
  • 84
  • This may be slightly irrelevant but this sounds like a perfect use case for an observables library like Knockout.js – James Gould Jul 12 '18 at 11:05

2 Answers2

2

Is there a more efficient solution here?

Yes, either of your first two solutions. :-) Details:

At first I thought of using a key-value pair object, but DOM elements cannot be substituted as object keys.

That's true, but they can be Map object keys. They can even be WeakMap object keys if you need to prevent the Map from keeping the element in memory.

You do need to target environments that have Map and WeakMap (all current major ones do), although there are some really powerful polyfills out there doing cool things to emulate Map behavior in a clever, well-performing way.

Example:

// See https://stackoverflow.com/questions/46929157/foreach-on-queryselectorall-not-working-in-recent-microsoft-browsers/46929259#46929259
if (typeof NodeList !== "undefined" && NodeList.prototype && !NodeList.prototype.forEach) {
    // Yes, there's really no need for `Object.defineProperty` here
    NodeList.prototype.forEach = Array.prototype.forEach;
}

const map = new WeakMap();
document.querySelectorAll("textarea").forEach((element, index) => {
  ++index;
  map.set(element, {name: "TextArea #" + index});
});

document.addEventListener("click", event => {
  const entry = map.get(event.target);
  if (entry) {
    console.log("The name for the textarea you clicked is " + entry.name);
  }
});
<div>Click in each of these text areas:</div>
<textarea>one</textarea>
<textarea>two</textarea>

I could instead try to generate a unique CSS selector for each DOM element and then use that selector as the object key, but this approach seems very expensive.

It isn't if you use id for that: Give yourself a unique prefix and then following it with an ever-increasing number. (And if you need to go from the ID to the element, getElementById is blindingly fast.)

Example:

// See https://stackoverflow.com/questions/46929157/foreach-on-queryselectorall-not-working-in-recent-microsoft-browsers/46929259#46929259
if (typeof NodeList !== "undefined" && NodeList.prototype && !NodeList.prototype.forEach) {
    // Yes, there's really no need for `Object.defineProperty` here
    NodeList.prototype.forEach = Array.prototype.forEach;
}

let nextIdNum = 1;
const pseudoMap = Object.create(null);
document.querySelectorAll("textarea").forEach((element, index) => {
  if (!element.id) {
    element.id = "__x_" + nextIdNum++;
  }
  ++index;
  pseudoMap[element.id] = {name: "TextArea #" + index};
});

document.addEventListener("click", event => {
  const entry = pseudoMap[event.target.id];
  if (entry) {
    console.log("The name for the textarea you clicked is " + entry.name);
  }
});
<div>Click in each of these text areas:</div>
<textarea>one</textarea>
<textarea>two</textarea>

If you use id for other purposes, it can be a data-* attribute instead. (And if you need to go from the data-* attribute value to the element, querySelector on a data-* attribute isn't all that expensive.)

Example:

// See https://stackoverflow.com/questions/46929157/foreach-on-queryselectorall-not-working-in-recent-microsoft-browsers/46929259#46929259
if (typeof NodeList !== "undefined" && NodeList.prototype && !NodeList.prototype.forEach) {
    // Yes, there's really no need for `Object.defineProperty` here
    NodeList.prototype.forEach = Array.prototype.forEach;
}

let nextIdNum = 1;
const pseudoMap = Object.create(null);
document.querySelectorAll("textarea").forEach((element, index) => {
  const id = "__x_" + nextIdNum++;
  element.setAttribute("data-id", id);
  pseudoMap[id] = {name: "TextArea #" + index};
});

document.addEventListener("click", event => {
  const entry = pseudoMap[event.target.getAttribute("data-id")];
  if (entry) {
    console.log("The name for the textarea you clicked is " + entry.name);
  }
});
<div>Click in each of these text areas:</div>
<textarea>one</textarea>
<textarea>two</textarea>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Is there really an advantage to `element.setAttribute("data-id", id);` as compared to the shorter `element.dataset.id = id;`? – Gaurang Tandon Jul 12 '18 at 11:38
  • @GaurangTandon - Broader support ([link](https://caniuse.com/#feat=dataset)), but even IE11 had `dataset` so...not really, unless you need to support IE8 (which some people do, poor sods). :-) – T.J. Crowder Jul 12 '18 at 11:43
1

You could assign a unique id on each input and then create an array where the index of each key is used to access the cached version of your data.

var cachedData=[];
cachedData[someElement.id] = someValue;

This would be an O(1) operation to read/write the indexed value.

MKougiouris
  • 2,821
  • 1
  • 16
  • 19
  • Hmm, good solution. I would still need to generate the IDs though, I guess `const getRandomID => "APP" + Math.random();` would work for that. Would it? || Ah, maybe the ever-increasing number would work better as suggested by Crowder. – Gaurang Tandon Jul 12 '18 at 11:07
  • If the elements are not dynamicaly added/removed from the dom you can even do a simple forEach on the elements and assigne their index as the key, this would mean that you do not even need to set a key or generate anything. Only if nothing messes up your dom thought, this is not a good idea in general .. too fragile tie between obejcts – MKougiouris Jul 12 '18 at 11:12
  • I agree. My input elements are dynamic though :/ – Gaurang Tandon Jul 12 '18 at 11:14