guest271314 guest271314 - 1 month ago 8
Javascript Question

Expand an iterable element or non-iterable element into an array without checking element .length

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
?

Answer

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
}
Comments