Solução do erro: Uncaught TypeError
TypeError não capturado: document.getElementById (...) é nulo
Tenho um único arquivo JavaScript que está conectado a várias páginas. O snippet a seguir é uma função usada para uma única página. O erro acima aparece quando o usuário navega para uma das páginas que não usa a função abaixo e faz referência à document.getElementById('button01').addEventListener('click', newThing);linha na parte inferior. Concluí que o erro ocorre devido ao fato de que button01não existe nessas páginas.
function newThing() {
const output = document.getElementsByTagName('output')[0];
if (!(document.forms.thingSelection2.type.value in options)) {
return false;
}
const list = options[document.forms.thingSelection2.type.value];
const method = document.forms.thingSelection1.mode.value + 'Item';
const item = list[method]();
output.innerHTML = item;
}
document.getElementById('button01').addEventListener('click', newThing);
Solução
Minha solução para isso é simples; coloque a linha em uma instrução if assim:
if(document.getElementById('button01')) {
document.getElementById('button01').addEventListener('click', newThing);
}
Isso remove o erro das páginas que não o usam.
Questões
Isso abre a possibilidade de algum comportamento de bug que eu não conheço? Existe uma maneira melhor de obter os mesmos resultados?
HTML
Agradeço o feedback que este post recebeu até agora. Como algumas das respostas contêm recomendações baseadas em suposições do meu HTML, decidi adicionar o respectivo HTML no trecho abaixo. Uma coisa a observar é que uso dois formulários. Isso é necessário para a funcionalidade do código. Não estou ciente de quaisquer bugs indesejados que isso possa causar. Se isso acontecer, por favor me avise abaixo.
<div><output></output></div>
<div><button id="button01">New Thing</button></div>
<div>
<form name="thingSelection1">
<input type="radio" name="mode" value="random" id="mode1"> Random
<br/><input type="radio" name="mode" value="forward" id="mode2"> Old - New
<br/><input type="radio" name="mode" value="reverse" id="mode3"> New - Old
</form>
</div>
<div>
<form name="thingSelection2">
Doodle <input type="radio" name="type" value="doodle" id="doodleCheck"><br/>
Video <input type="radio" name="type" value="video" id="videoCheck"><br/>
Audio <input type="radio" name="type" value="audio" id="audioCheck"><br/>
Photo <input type="radio" name="type" value="photo" id="photoCheck"><br/>
Text <input type="radio" name="type" value="text" id="textCheck">
</form>
</div>
Além disso, gostaria de salientar que o snippet original é apenas parte do meu JavaScript completo. Com base nas informações fornecidas por esta postagem , concluí que adicionar o JavaScript restante, embora possivelmente benéfico e relevante para algumas das respostas abaixo, pode estar fora do escopo do tópico original desta postagem. Por favor, observe sua opinião sobre isso nos comentários abaixo.
Respostas
Isso abre a possibilidade de algum comportamento de bug que eu não conheço?
Não é provável. A ifafirmação é boa, embora possa ser mais clara:
Em vez de selecionar o elemento duas vezes (uma para verificar se ele existe, outra vez para chamá
addEventListener-lo), salve-o em uma variável:const button = document.getElementById('button01'); if (button) { button.addEventListener('click', newThing); }Ou, se você estiver escrevendo JS que é transpilado para produção (o que, em um projeto profissional ou maior, você realmente deveria ser), use o encadeamento opcional:
document.getElementById('button01')?.addEventListener('click', newThing);
Mas a questão fundamental permanece - o layout HTML está completamente desconectado do JavaScript. Ter que verificar se existe um botão em primeiro lugar é algo que idealmente não deveria ser um problema para se preocupar, pelo menos em um projeto maior ou mais profissional. E se você não tivesse 1, mas 3 ou 5 ou 10 elementos com manipuladores em páginas diferentes, todos os quais podem ou não existir? A base de código seria mais difícil de manter do que deveria.
Existem algumas soluções para isso:
- Uma opção é ter um
<script>arquivo separado para as páginas com o formulário, por exemplo:
<form id="thingSelection2">
...
</form>
<script src="./thingSelection.js"></script>
onde thingSelection.jsadiciona o ouvinte de evento. Mas isso exigirá uma solicitação separada para o servidor, o que pode ser um problema em uma página grande em HTTP 1.1 - se você tiver muitos scripts diferentes como este, o grande número de solicitações paralelas pode retardar as coisas, especialmente para usuários com Alta latência. (O protocolo HTTP / 2 não tem problemas com conexões adicionais para o mesmo servidor IIRC)
(Você também pode embutir o script, por exemplo, </form><script>// etc</script>mas eu prefiro colocar os scripts em arquivos separados para permitir o cache)
- Mas se fosse eu, eu preferiria fortemente integrar totalmente a criação do HTML com os ouvintes de eventos para esse HTML, para torná-lo completamente inconcebível de um existir sem o outro, usando uma estrutura. Por exemplo, com React, você poderia fazer algo como:
const Things = () => {
const [item, setItem] = useState('');
const [selectedOption, setSelectedOption] = useState('foo');
const clickHandler = (e) => {
const fn = options[selectedOption];
if (fn) {
setItem(fn());
}
};
return (
<div>
<select value={selectedOption} onChange={e => setSelectedOption(e.currentTarget.value)}>
<option value="foo">foo</option>
<option value="bar">bar</option>
</select>
<button onClick={clickHandler}>click</button>
<output>{item}</output>
</div>
);
};
const options = {
foo: () => 'foo',
bar: () => 'bar',
};
const Things = () => {
const [item, setItem] = React.useState('');
const [selectedOption, setSelectedOption] = React.useState('foo');
const clickHandler = (e) => {
const fn = options[selectedOption];
if (fn) {
setItem(fn());
}
};
return (
<div>
<select value={selectedOption} onChange={e => setSelectedOption(e.currentTarget.value)}>
<option value="foo">foo</option>
<option value="bar">bar</option>
</select>
<button onClick={clickHandler}>click</button>
<output>{item}</output>
</div>
);
};
ReactDOM.render(<Things />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class="react"></div>
Então, quando você estiver em uma página onde as coisas são necessárias, basta renderizar <Things />. Com essa abordagem, o HTML e seus manipuladores JS associados são todos completamente autocontidos no Things. Não há necessidade de usar seletores que podem ter colisões não intencionais. (Por exemplo, indo com seu código original de document.getElementById('button01'), e se alguma outra seção completamente separada do HTML em uma das páginas tivesse um elemento que usou acidentalmente o mesmo ID? Então você terá problemas.)
Usar uma estrutura como essa é reconhecidamente muito para aprender, mas torna as bases de código significativamente mais fáceis de manter. Vale a pena para projetos de médio ou grande porte, IMO.
Por outro lado, seu código atual também pode ser um pouco melhorado:
Seletores de preferência As sequências de caracteres do seletor são geralmente mais fáceis de entender e mais flexíveis do que outros métodos de seleção de elementos (como getElementsByTagNamee document.forms.someFormName.someFormElement). A sequência de caracteres do seletor que corresponde a um elemento se alinhará com o seletor de CSS que estiliza o elemento. Por exemplo, eu substituiria:
document.forms.thingSelection2.type.value
com
document.querySelector('#thingSelection2 [name=type]').value
e
const output = document.getElementsByTagName('output')[0];
com
const output = document.querySelector('output');
(não há necessidade de selecionar uma coleção quando você só precisa de um único elemento correspondente)
Salve o valor Em vez de selecionar e extrair o valor duas vezes, escreva o código DRY; coloque-o em uma variável:
const { value } = document.querySelector('#thingSelection2 [name=type]');
if (value in options) { // Or, use `!options[value]
return false;
}
Parece que você incorporou alguns dos conselhos da minha resposta a uma de suas outras postagens - isso é bom.
A resposta da CertainPerformance tem ótimos conselhos. Seguindo com a essência disso, existem outras partes do código que " poderiam " ser problemáticas:
const output = document.getElementsByTagName('output')[0];
Embora deva haver um <output>elemento se o código HTML tiver um, se o JavaScript for executado antes que o DOM esteja pronto, isso pode levar a um erro, assim como o código para obter um elemento pelo atributo id :
const output = document.getElementsByTagName('output')[0];
console.log('output tagName:', output.innerHTML)
Seria melhor garantir que document.getElementsByTagName('output')tenha um comprimento diferente de zero antes de acessar o primeiro elemento.
Outra sugestão é retornar o mais cedo possível - por exemplo, a linha a ser atribuída outputnão afeta a condicional:
const output = document.getElementsByTagName('output')[0]; if (!(document.forms.thingSelection2.type.value in options)) { return false; }
Embora provavelmente não faça diferença para este código se o elemento de saída é obtido do DOM, reduzir os cálculos é um bom hábito a ser desenvolvido. Em outras situações, isso pode economizar um tempo valioso para os usuários - por exemplo, pegue o exemplo de uma solicitação do lado do servidor para buscar dados, o que pode levar alguns segundos.
Presumo que seu HTML seja um pouco assim:
<output></output>
<output></output>
<form>
<input name="mode" value="..." />
<input name="type" value="..." />
<button>Click!</button>
</form>
Minha recomendação principal seria não confiar nos IDs, pois eles são exclusivos por página. Em vez disso, considere confiar nos atributos de dados e nunca presumir que haja um determinado número deles. Seu javascript deve estar o mais desconectado possível do layout HTML .
Vamos modificar um pouco o que você tem. Vamos começar adicionando um atributo de dados que indica ao JavaScript que você tem um botão especial:
<button data-my-widget>Click!</button>
Isso permitiria a você atribuir um ouvinte de evento a todos os botões com o atributo determinado e permitiria que seus botões permanecessem progressivamente aprimorados:
const myThing = event => {
...
}
for (const button of document.querySelectorAll('[data-my-widget]')) {
button.addEventListener('click', newThing);
}
Você também está codificando a qual formulário cada botão se aplica - e os respectivos controles - no manipulador de eventos. Você pode tornar isso um pouco melhor usando o atributo form em a button, e fornecendo os ids dos elementos de destino como atributos de dados também:
<form>
<input name="mode" value="...." />
<input name="type" value="...." />
<button data-my-widget data-mode="mode" data-value="type">Click!</button>
</form>
Então você pode acessar este formulário dentro do manipulador:
const myThing = event => {
const outputs = document.getElementsByTagName('output');
if (outputs.length === 0) {
return;
}
const output = outputs[0];
const target = event.target;
const { form } = target;
if (form === undefined) {
return;
}
const { mode: modeId, value: valueId } = target.dataset;
const mode = form.elements[modeId]?.value;
const value = form.elements[valueId]?.value;
if (mode === undefined || value === undefined || value in options === false) {
return;
}
const list = options[value];
const f = list[`${mode}Item`];
if (f === undefined) {
return;
}
output.innerHTML = f();
}
Isso mantém seu javascript o mais inconsciente possível de sua estrutura HTML, embora eu ainda recomende substituir a outputsseção também, possivelmente usando atributos de dados, em vez de depender do índice do elemento:
<output id="output-1"></output>
<button ... data-output="output-1">Click!</button>
const myThing = event => {
...
const { mode: modeId, value: valueId, output: outputId } = target.dataset;
...
const output = document.getElementById(outputId);
output.innerHTML = ...;
}
Juntando tudo isso:
<output id="output-1"></output>
...
<output id="output-n"></output>
<form>
<input name="mode" value="...." />
<input name="type" value="...." />
<button data-my-widget data-mode="mode" data-value="type" data-output="output-1">Click!</button>
</form>
const handleClick = event => {
const { form } = event.target;
if (form === undefined) {
return;
}
const { mode: modeId, value: valueId, output: outputId } = target.dataset;
const mode = form.elements[modeId]?.value;
const value = form.elements[valueId]?.value;
const output = document.getElementById(outputId);
if (mode === undefined || value === undefined || output === undefined) {
return;
}
const list = options[value];
const f = list[`${mode}Item`];
if (f === undefined) {
return;
}
output.innerHTML = f();
}
for (const button of document.querySelectorAll('[data-my-widget]')) {
button.addEventListener('click', handleClick);
}