When a Materialize Select component is initialized with a "static" element (i.e. included in an HTML markup or previously inserted into DOM), everything works as expected:
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('select');
var instances = M.FormSelect.init(elems);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" rel="stylesheet" />
<div class="input-field col s12">
<select>
<option value="" disabled selected>Choose your option</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
<label>Materialize Select</label>
</div>
However, when creating the component dynamically, the initialization fails:
const makeOption = (maybeOption) => {
const {
text,
value,
selected
} = typeof maybeOption === "string" ? {
text: maybeOption,
value: maybeOption
} :
maybeOption;
const opt = document.createElement("option");
opt.innerText = text;
opt.value = value;
opt.selected = selected;
return opt;
};
const makeSelectInput = ({
id,
name,
empty = true,
label,
options = [],
parent,
placeholder,
value,
classes = []
}) => {
const wrap = document.createElement("div");
wrap.classList.add("input-field", ...classes);
const sel = document.createElement("select");
sel.id = id || name;
sel.name = name;
const lbl = document.createElement("label");
lbl.htmlFor = id || name;
lbl.innerText = label;
if (empty) {
sel.options.add(makeOption({
text: placeholder || "",
value: ""
}));
}
options.map(makeOption).forEach((o) => sel.options.add(o));
wrap.append(sel, lbl);
parent.append(wrap);
const elems = wrap.querySelectorAll(`#${id || name}`);
M.FormSelect.init(elems);
return sel;
};
window.addEventListener("DOMContentLoaded", () => {
const parent = document.createElement("div");
try {
makeSelectInput({
parent,
options: ["Apples", "Oranges"],
name: "fruits",
label: "Fruits"
});
}
catch({ message }) {
console.log(message);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" rel="stylesheet" />
The error message thrown is as follows:
"TypeError: Cannot set property 'tabIndex' of null"
I understand that the root cause lies in a failure to select (or create) a supplementary element during the initialization of the component, but what exactly causes the error to occur in the second instance described above, but not in the first?
There are similar Q&As dealing with the error when using VueJS, React, or Angular (also this) frameworks, but none are concerned with DOM manipulation with vanilla JavaScript. That said, to summarize, there is a couple of possible solutions to the issue at hand:
- A mysterious one involving switching from a class selector to an id selector - unfortunately, I fail to grasp what would change unless the class selector was malformed and resulted in the root element being passed in as
null
. - Initializing in the
componentDidMount
lifecycle hook - also explaining the "how", but not the "why" part of the issue. But usage of the hook at the very least highlights that the issue has something to do with insertion into DOM. - Making sure the DOM finished loading, but as you can see from the second code snippet, the component is initialized after the
DOMContentLoaded
event fires.