3

Given html

<div></div>
<div></div>

calling document.querySelector("div") returns the first div element, where .length is not a property of the return value.

Calling document.querySelectorAll() returns a NodeList having a .length property.

The difference between the two return values of .querySelector() and .querySelectorAll() is that the former is not an iterable; and error will be thrown when attempting to use the spread element to expand the element into an array.

In the following examples consider that either div or divs is a parameter received within the body of a functions call. Thus, as far as can gather, it is not possible to determine if the variable was defined as a result of Element.querySelector(), Element.querySelectorAll(), document.querySelector() or document.querySelectorAll(); further the difference between .querySelector() and .querySelectorAll() can only be checked using .length.

var div = document.querySelector("div");
for (let el of div) {
  console.log(".querySelector():", el)
}
<div></div>
<div></div>

logs

Uncaught TypeError: div[Symbol.iterator] is not a function

while

var div = document.querySelectorAll("div");
for (let el of div) {
  console.log(".querySelectorAll():", el)
}
<div></div>
<div></div>

returns expected result; that is, document.querySelectorAll("div") is expanded to fill the iterable array.

We can get the expected result at .querySelector() by setting div as an element of an Array

[div]

at for..of iterable parameter.

The closest have come to using same pattern for both or either .querySelector() or .querySelectorAll() is using callback of Array.from() and the .tagName of the variable, and spread element. Though this omits additional selectors that may have been called with .querySelector(), for example .querySelector("div.abc").

var div = document.querySelector("div");
var divs = document.querySelectorAll("div");
var elems = Array.from({length:div.length || 1}, function(_, i) {
  return [...div.parentElement.querySelectorAll(
    (div.tagName || div[0].tagName))
         ][i]
});

for (let el of elems) {
  console.log(".querySelector():", el)
}

elems = Array.from({length:divs.length || 1}, function(_, i) {
  return [...divs[0].parentElement.querySelectorAll(
    (divs.tagName || divs[0].tagName))
         ][i]
});

for (let el of elems) {
  console.log("querySelectorAll:", el)
}
<div></div>
<div></div>

This does not provide adequate accuracy for additional reasons; Element.querySelector() could have been originally passed to function, instead of document.querySelector(), similarly for .querySelectorAll(). Not sure if it is possible to retrieve the exact selector passed to.querySelector, All` without modifying the native function?

The desired pattern would accept the variable, and expand the contents of the iterable into an array if an .querySelectorAll() was used; which would treat .getElementsByTagName(), .getElementsByClassName(), .getElementsByTagName(), .getElementsByName() the same; or set the single value returned by .querySelector() as element of the array.

Note, the current working solution is

div.length ? div : [div]

which iterates div if div has a .length property, possibly an iterable, though simply have a .length property and not be an iterable; else set div as single element of an array, an iterable.

var div = document.querySelector("div");
var divs = document.querySelectorAll("div");
var elems = div.length ? div : [div];

for (let el of elems) {
  console.log(".querySelector():", el)
}

var elems = divs.length ? divs : [divs];

for (let el of elems) {
  console.log("querySelectorAll:", el)
}
<div></div>
<div></div>

Can this be achieved

  • without checking the .length of the variable?
  • without referencing the element three times on same line?

Can the approach of the working solution

  • be improved; that is should [Symbol.iterator] of div be checked instead of .length?
  • is there magic using .spread element or rest element which could allow omission of checking .length of object?
  • would using a Generator, Array.prototype.reduce() or other approach change the need to check the .length or [Symbol.iterator] property of a variable before expanding the element into an array?

Or, is the above the approach the briefest possible given the difference between objects which are iterable or not iterable?

guest271314
  • 1
  • 15
  • 104
  • 177
  • 2
    What are you trying to do here? – Ry- Oct 23 '16 at 02:00
  • Use the same pattern to expand a parameter passed to a function within the body of the function into an array; whether the variable is an iterable or is not an iterable. Determine if `div.length ? div : [div]` is the briefest possible pattern to use to check if parameter possibly has an iterable property. And if checking `div.length` is adequate way to check if an object is an `iterable`? Given parameter is one of, for example `.querySelector()` : not an iterable, `.querySelectorAll()` : an iterable. – guest271314 Oct 23 '16 at 02:02
  • 1
    Is this a long way of saying you want a function that turns an object into a one-element list if it’s not already iterable? – Ry- Oct 23 '16 at 02:06
  • @Ryan Yes. `div.length ? div : [div]` is relatively brief, and achieves this. Though is that correct approach? That is `{abc:123, length:3}` has a `.length` property, though is not necessarily an `iterable`, and not an array. – guest271314 Oct 23 '16 at 02:09
  • 3
    Does [this](http://stackoverflow.com/q/18884249/5647260) help? `isIterable(divs) ? divs : [divs]` – Andrew Li Oct 23 '16 at 02:10
  • It is not sufficient for every iterable, no. I’d do this: http://www.ecma-international.org/ecma-262/6.0/#sec-array.from but create a single-element array instead of one with no elements if length isn’t an integer after step 11. – Ry- Oct 23 '16 at 02:12
  • @AndrewLi Yes, that helps. Checking `.length` is not adequate. – guest271314 Oct 23 '16 at 02:12
  • @Ryan Tried using `Array.from()` , `Array.of()`, `Array.fill()` to create an array before, then fill with either element that does not have `.length` property or element that does have `.length` property, though they return different results when `spread element` `...` is used to expand the array; `array[0]` if set with both iterable and non-iterable as a single element; `.querySelector()` the single element, `.querySelectorAll()` a `NodeList`. Where `for..of` is expecting an array of elements, not a `NodeList`. Can you post an Answer illustrating the approach you are describing? – guest271314 Oct 23 '16 at 02:20
  • Note, the Answers at linked Question each return a `Boolean`. They do not provide a solution for converting non-iterable to an iterable or array and returning the iterable or array. OP of linked Question did not ask how to convert a non-iterable to an iterable or array, and return the converted value. – guest271314 Oct 23 '16 at 03:04

1 Answers1

2

I’d do more or less what Array.from does, but check the type of length instead of always converting it:

const itemsOrSingle = items => {
    const iteratorFn = items[Symbol.iterator]

    if (iteratorFn) {
        return Array.from(iteratorFn.call(items))
    }

    const length = items.length

    if (typeof length !== 'number') {
        return [items]
    }

    const result = []

    for (let i = 0; i < length; i++) {
        result.push(items[i])
    }

    return result
}
Ry-
  • 218,210
  • 55
  • 464
  • 476
  • What is purpose of `if (typeof length !== 'number') { return [items] }`? – guest271314 Oct 23 '16 at 02:34
  • @guest271314: If it's not a number, it doesn't make a lot of sense to loop from 0 to it. You could change that check to `length === undefined`, though. There's really no right way to do duck typing here. – Ry- Oct 23 '16 at 02:39
  • If `items` is iterable, could `items` be returned without call to `Array.from()`? What is purpose of using `Array.from()` in that particular `if` statement block? – guest271314 Oct 23 '16 at 02:42
  • 1
    @guest271314: Sure, you can do that if you want an iterable instead of an array. – Ry- Oct 23 '16 at 02:48
  • fwiw cobbled together an approach which utilizes `generator function`, `yield*` expression, `try..catch` to review caught error if parameter passed to generator function is not an iterable object, `Array.from()` to yield an iterable or array from passed parameter http://stackoverflow.com/a/40199709/ – guest271314 Oct 23 '16 at 05:04
  • Implemented at [jQuery alternative to nth-child selector that supports classes](http://stackoverflow.com/questions/40187310/jquery-alternative-to-nth-child-selector-that-supports-classes) – guest271314 Oct 23 '16 at 05:16