A opinião de que o JavaScript não sabe como interagir com o sistema de arquivos não é totalmente correta. Em vez disso, o ponto é que essa interação é significativamente limitada em comparação com linguagens de programação do lado do servidor, como Node.js ou PHP. No entanto, JavaScript é capaz de receber (receber) e criar alguns tipos de arquivos e processá-los nativamente com sucesso.
Neste artigo, criaremos três pequenos projetos:
- Implementamos recebimento e processamento de imagens, áudio, vídeo e texto em formato txt e pdf
- Vamos criar um gerador de arquivo JSON
- Vamos escrever dois programas: um formará perguntas (em formato JSON) e o outro as usará para criar um teste
Se você estiver interessado, por favor me siga.
Código do projeto no GitHub .
Recebemos e processamos arquivos
Primeiro, vamos criar um diretório onde nossos projetos serão armazenados. Vamos chamá-lo de "Trabalhar com arquivos em JavaScript" ou o que você quiser.
Neste diretório, crie uma pasta para o primeiro projeto. Vamos chamá-lo de "Leitor de Arquivos".
Crie um arquivo "index.html" nele com o seguinte conteúdo:
<div>+</div>
<input type="file">
Aqui temos um receptor de arquivo contêiner e uma entrada com o tipo "arquivo" (para obter um arquivo; trabalharemos com arquivos únicos; para obter vários arquivos, a entrada deve ser adicionada com o atributo "múltiplo"), que ficará oculto sob o contêiner.
Os estilos podem ser incluídos em um arquivo separado ou na tag "style" dentro do cabeçalho:
body {
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
max-width: 768px;
background: radial-gradient(circle, skyblue, steelblue);
color: #222;
}
div {
width: 150px;
height: 150px;
display: flex;
justify-content: center;
align-items: center;
font-size: 10em;
font-weight: bold;
border: 6px solid;
border-radius: 8px;
user-select: none;
cursor: pointer;
}
input {
display: none;
}
img,
audio,
video {
max-width: 80vw;
max-height: 80vh;
}
Você pode fazer o design ao seu gosto.
Não se esqueça de incluir o script no cabeçalho com o atributo "defer" (precisamos esperar que o DOM seja desenhado (renderizado); você pode, é claro, fazer isso no script manipulando o evento "load" ou "DOMContentLoaded" do objeto "window", mas o adiamento é muito mais curto) , ou antes da tag de fechamento “body” (nenhum atributo ou manipulador é necessário). Eu pessoalmente prefiro a primeira opção.
Vamos abrir index.html em um navegador:
Antes de continuar a escrever o script, precisamos preparar os arquivos para a aplicação: precisamos de uma imagem, áudio, vídeo, texto em txt, pdf e qualquer outro formato, por exemplo, doc. Você pode usar minha coleção ou construir a sua própria.
Freqüentemente, temos que acessar os objetos "document" e "document.body" e também enviar os resultados para o console várias vezes, então sugiro envolver nosso código neste IIFE (isso é opcional):
;((D, B, log = arg => console.log(arg)) => {
//
// document document.body D B,
// log = arg => console.log(arg) -
// console.log log
})(document, document.body)
Em primeiro lugar, declaramos variáveis para o receptor do arquivo, entrada e arquivo (não inicializamos o último, pois seu valor depende do método de transferência - clicando na entrada ou soltando no receptor do arquivo):
const dropZone = D.querySelector('div')
const input = D.querySelector('input')
let file
Desative a manipulação dos eventos arrastar e soltar pelo navegador:
D.addEventListener('dragover', ev => ev.preventDefault())
D.addEventListener('drop', ev => ev.preventDefault())
Para entender por que fizemos isso, tente transferir uma imagem ou outro arquivo para o navegador e veja o que acontece. E há processamento automático de arquivos, ou seja, o que vamos implementar por conta própria para fins educacionais.
Lidamos com o lançamento de um arquivo no receptor de arquivo:
dropZone.addEventListener('drop', ev => {
//
ev.preventDefault()
// ,
log(ev.dataTransfer)
// ( )
/*
DataTransfer {dropEffect: "none", effectAllowed: "all", items: DataTransferItemList, types: Array(1), files: FileList}
dropEffect: "none"
effectAllowed: "all"
=> files: FileList
length: 0
__proto__: FileList
items: DataTransferItemList {length: 0}
types: []
__proto__: DataTransfer
*/
// (File) "files" "DataTransfer"
//
file = ev.dataTransfer.files[0]
//
log(file)
/*
File {name: "image.png", lastModified: 1593246425244, lastModifiedDate: Sat Jun 27 2020 13:27:05 GMT+0500 (, ), webkitRelativePath: "", size: 208474, …}
lastModified: 1593246425244
lastModifiedDate: Sat Jun 27 2020 13:27:05 GMT+0500 (, ) {}
name: "image.png"
size: 208474
type: "image/png"
webkitRelativePath: ""
__proto__: File
*/
//
handleFile(file)
})
Acabamos de implementar o mecanismo dran'n'drop mais simples.
Processamos o clique no receptor do arquivo (delegamos o clique à entrada):
dropZone.addEventListener('click', () => {
//
input.click()
//
input.addEventListener('change', () => {
// ,
log(input.files)
// ( )
/*
FileList {0: File, length: 1}
=> 0: File
lastModified: 1593246425244
lastModifiedDate: Sat Jun 27 2020 13:27:05 GMT+0500 (, ) {}
name: "image.png"
size: 208474
type: "image/png"
webkitRelativePath: ""
__proto__: File
length: 1
__proto__: FileList
*/
// File
file = input.files[0]
//
log(file)
//
handleFile(file)
})
})
Vamos começar a processar o arquivo:
const handleFile = file => {
//
}
Excluímos o receptor do arquivo e inserimos:
dropZone.remove()
input.remove()
A maneira como um arquivo é processado depende de seu tipo:
log(file.type)
//
// image/png
Não trabalharemos com arquivos html, css e js, portanto, proibimos seu processamento:
if (file.type === 'text/html' ||
file.type === 'text/css' ||
file.type === 'text/javascript')
return;
Também não trabalharemos com arquivos MS (com o tipo MIME "application / msword", "application / vnd.ms-excel", etc.), pois eles não podem ser processados por meios nativos. Todos os métodos de processamento de tais arquivos, oferecidos no StackOverflow e outros recursos, são reduzidos à conversão para outros formatos usando várias bibliotecas ou ao uso de visualizadores do Google e da Microsoft, que não querem trabalhar com o sistema de arquivos e localhost. Ao mesmo tempo, o tipo de arquivos PDF também começa com "aplicativo", portanto, processaremos esses arquivos separadamente:
if (file.type === 'application/pdf') {
createIframe(file)
return;
}
Para o restante dos arquivos, obtemos seu tipo de "grupo":
// ,
const type = file.type.replace(/\/.+/, '')
//
log(type)
//
// image
Usando switch..case, definimos uma função de processamento de arquivo específica:
switch (type) {
//
case 'image':
createImage(file)
break;
//
case 'audio':
createAudio(file)
break;
//
case 'video':
createVideo(file)
break;
//
case 'text':
createText(file)
break;
// , ,
//
default:
B.innerHTML = `<h3>Unknown File Format!</h3>`
const timer = setTimeout(() => {
location.reload()
clearTimeout(timer)
}, 2000)
break;
}
Função de processamento de imagem:
const createImage = image => {
// "img"
const imageEl = D.createElement('img')
//
imageEl.src = URL.createObjectURL(image)
//
log(imageEl)
//
B.append(imageEl)
//
URL.revokeObjectURL(image)
}
Função de processamento de áudio:
const createAudio = audio => {
// "audio"
const audioEl = D.createElement('audio')
//
audioEl.setAttribute('controls', '')
//
audioEl.src = URL.createObjectURL(audio)
//
log(audioEl)
//
B.append(audioEl)
//
audioEl.play()
//
URL.revokeObjectURL(audio)
}
Função de processamento de vídeo:
const createVideo = video => {
// "video"
const videoEl = D.createElement('video')
//
videoEl.setAttribute('controls', '')
//
videoEl.setAttribute('loop', 'true')
//
videoEl.src = URL.createObjectURL(video)
//
log(videoEl)
//
B.append(videoEl)
//
videoEl.play()
//
URL.revokeObjectURL(video)
}
Função de processamento de texto:
const createText = text => {
// "FileReader"
const reader = new FileReader()
//
//
// - utf-8,
//
reader.readAsText(text, 'windows-1251')
//
//
reader.onload = () => B.innerHTML = `<p><pre>${reader.result}</pre></p>`
}
Por último, mas não menos importante, função de processamento de PDF:
const createIframe = pdf => {
// "iframe"
const iframe = D.createElement('iframe')
//
iframe.src = URL.createObjectURL(pdf)
//
iframe.width = innerWidth
iframe.height = innerHeight
//
log(iframe)
//
B.append(iframe)
//
URL.revokeObjectURL(pdf)
}
Resultado:
Crie um arquivo JSON
Para o segundo projeto, crie uma pasta "Create-JSON" no diretório raiz (Work-With-Files-in-JavaScript).
Crie um arquivo "index.html" com o seguinte conteúdo:
<!-- head -->
<!-- materialize css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<!-- material icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- body -->
<h3>Create JSON</h3>
<!-- -->
<div class="row main">
<h3>Create JSON</h3>
<form class="col s12">
<!-- "-" -->
<div class="row">
<div class="input-field col s5">
<label>key</label>
<input type="text" value="1" required>
</div>
<div class="input-field col s2">
<p>:</p>
</div>
<div class="input-field col s5">
<label>value</label>
<input type="text" value="foo" required>
</div>
</div>
<!-- -->
<div class="row">
<div class="input-field col s5">
<label>key</label>
<input type="text" value="2" required>
</div>
<div class="input-field col s2">
<p>:</p>
</div>
<div class="input-field col s5">
<label>value</label>
<input type="text" value="bar" required>
</div>
</div>
<!-- -->
<div class="row">
<div class="input-field col s5">
<label>key</label>
<input type="text" value="3" required>
</div>
<div class="input-field col s2">
<p>:</p>
</div>
<div class="input-field col s5">
<label>value</label>
<input type="text" value="baz" required>
</div>
</div>
<!-- -->
<div class="row">
<button class="btn waves-effect waves-light create-json">create json
<i class="material-icons right">send</i>
</button>
<a class="waves-effect waves-light btn get-data"><i class="material-icons right">cloud</i>get data</a>
</div>
</form>
</div>
Materializar é usado para estilizar .
Adicione alguns estilos personalizados:
body {
max-width: 512px;
margin: 0 auto;
text-align: center;
}
input {
text-align: center;
}
.get-data {
margin-left: 1em;
}
Obtemos o seguinte: Os
arquivos JSON têm o seguinte formato:
{
"": "",
"": "",
...
}
Entradas ímpares do tipo "texto" são chaves, mesmo as que são valores. Atribuímos valores padrão às entradas (os valores podem ser qualquer um). Um botão com a classe "create-json" é usado para obter os valores inseridos pelo usuário e criar um arquivo. Botão das classes "get-data" - para obter dados.
Vamos prosseguir para o script:
// "create-json"
document.querySelector('.create-json').addEventListener('click', ev => {
// "submit" , ..
//
// ,
ev.preventDefault()
//
const inputs = document.querySelectorAll('input')
// ,
//
//
// "chunk" "lodash"
// (, ) -
//
// (, ) -
//
const arr = []
for (let i = 0; i < inputs.length; ++i) {
arr.push([inputs[i].value, inputs[++i].value])
}
// ,
console.log(arr)
/*
[
["1", "foo"]
["2", "bar"]
["3", "baz"]
]
*/
//
const data = Object.fromEntries(arr)
//
console.log(data)
/*
{
1: "foo"
2: "bar"
3: "baz"
}
*/
//
const file = new Blob(
//
[JSON.stringify(data)], {
type: 'application/json'
}
)
//
console.log(file)
/*
{
"1": "foo",
"2": "bar",
"3": "baz"
}
*/
// ,
// "a"
const link = document.createElement('a')
// "href" "a"
link.setAttribute('href', URL.createObjectURL(file))
// "download" ,
// -
link.setAttribute('download', 'data.json')
//
link.textContent = 'DOWNLOAD DATA'
// "main"
document.querySelector('.main').append(link)
//
URL.revokeObjectURL(file)
// { once: true }
//
}, { once: true })
Ao clicar no botão "CRIAR JSON", o arquivo "data.json" é gerado, o link "BAIXAR DADOS" aparece para baixar este arquivo.
O que podemos fazer com este arquivo? Baixe-o e coloque-o na pasta "Create-JSON".
Nós temos:
// ( ) "get-data"
document.querySelector('.get-data').addEventListener('click', () => {
// IIFE async..await
(async () => {
const response = await fetch('data.json')
// ()
const data = await response.json()
console.table(data)
})()
})
Resultado:
Crie um gerador de perguntas e testador
Gerador de perguntas
Para o terceiro projeto, vamos criar uma pasta “Test-Maker” no diretório raiz.
Crie um arquivo "createTest.html" com o seguinte conteúdo:
<!-- head -->
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<!-- body -->
<!-- -->
<div class="container">
<h3>Create Test</h3>
<form id="questions-box">
<!-- -->
<div class="question-box">
<br><hr>
<h4 class="title"></h4>
<!-- -->
<div class="row">
<input type="text" class="form-control col-11 question-text" value="first question" >
<!-- -->
<button class="btn btn-danger col remove-question-btn">X</button>
</div>
<hr>
<h4>Answers:</h4>
<!-- -->
<div class="row answers-box">
<!-- -->
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="radio" checked name="answer">
</div>
</div>
<input class="form-control answer-text" type="text" value="foo" >
<!-- -->
<div class="input-group-append">
<button class="btn btn-outline-danger remove-answer-btn">X</button>
</div>
</div>
<!-- -->
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="radio" name="answer">
</div>
</div>
<input class="form-control answer-text" type="text" value="bar" >
<div class="input-group-append">
<button class="btn btn-outline-danger remove-answer-btn">X</button>
</div>
</div>
<!-- -->
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="radio" name="answer">
</div>
</div>
<input class="form-control answer-text" type="text" value="baz" >
<div class="input-group-append">
<button class="btn btn-outline-danger remove-answer-btn">X</button>
</div>
</div>
</div>
<br>
<!-- -->
<button class="btn btn-primary add-answer-btn">Add answer</button>
<hr>
<h4>Explanation:</h4>
<!-- -->
<div class="row explanation-box">
<input type="text" value="first explanation" class="form-control explanation-text" >
</div>
</div>
</form>
<br>
<!-- -->
<button class="btn btn-primary" id="add-question-btn">Add question</button>
<button class="btn btn-primary" id="create-test-btn">Create test</button>
</div>
Desta vez, o Bootstrap é usado para estilizar . Não estamos usando os atributos "obrigatórios", pois estaremos validando o formulário em JS (com obrigatórios, o comportamento de um formulário consistindo de vários campos obrigatórios se torna irritante).
Adicione alguns estilos personalizados:
body {
max-width: 512px;
margin: 0 auto;
text-align: center;
}
input[type="radio"] {
cursor: pointer;
}
Obtemos o seguinte: Temos um modelo de pergunta. Proponho movê-lo para um arquivo separado para uso como um componente usando importação dinâmica. Crie um arquivo "Question.js" com o seguinte conteúdo:
export default (name = Date.now()) => `
<div class="question-box">
<br><hr>
<h4 class="title"></h4>
<div class="row">
<input type="text" class="form-control col-11 question-text">
<button class="btn btn-danger col remove-question-btn">X</button>
</div>
<hr>
<h4>Answers:</h4>
<div class="row answers-box">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="radio" checked name="${name}">
</div>
</div>
<input class="form-control answer-text" type="text" >
<div class="input-group-append">
<button class="btn btn-outline-danger remove-answer-btn">X</button>
</div>
</div>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="radio" name="${name}">
</div>
</div>
<input class="form-control answer-text" type="text" >
<div class="input-group-append">
<button class="btn btn-outline-danger remove-answer-btn">X</button>
</div>
</div>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="radio" name="${name}">
</div>
</div>
<input class="form-control answer-text" type="text" >
<div class="input-group-append">
<button class="btn btn-outline-danger remove-answer-btn">X</button>
</div>
</div>
</div>
<br>
<button class="btn btn-primary add-answer-btn">Add answer</button>
<hr>
<h4>Explanation:</h4>
<div class="row explanation-box">
<input type="text" class="form-control explanation-text">
</div>
</div>
`
Aqui temos tudo igual a createTest.html, exceto que removemos os valores padrão das entradas e passamos o argumento "nome" como o valor do atributo de mesmo nome (este atributo deve ser único para cada pergunta - isso torna possível mude as opções de resposta, escolha uma de várias). O valor padrão para nome é o tempo em milissegundos desde 1º de janeiro de 1970 - uma alternativa simples para os geradores de valor aleatório Nanoid usados para obter um identificador exclusivo (o usuário provavelmente não terá tempo para criar duas perguntas em 1 ms).
Vamos passar para o script principal.
Vou criar algumas funções auxiliares (fábrica), mas isso não é necessário.
Funções secundárias:
//
const findOne = (element, selector) => element.querySelector(selector)
//
const findAll = (element, selector) => element.querySelectorAll(selector)
//
const addHandler = (element, event, callback) => element.addEventListener(event, callback)
//
// Bootstrap ,
// DOM
// - (),
// 1
const findParent = (element, depth = 1) => {
// ,
// ,
let parentEl = element.parentElement
// , ..
while (depth > 1) {
//
parentEl = findParent(parentEl)
//
depth--
}
//
return parentEl
}
No nosso caso, em busca do elemento pai, chegaremos ao terceiro nível de aninhamento. Como sabemos o número exato desses níveis, poderíamos ter usado if..else if ou switch..case, mas a opção de recursão é mais versátil.
Mais uma vez: não é necessário introduzir funções de fábrica, você pode facilmente usar a funcionalidade padrão.
Encontre o contêiner principal e o contêiner para perguntas e também desative o envio do formulário:
const C = findOne(document.body, '.container')
// const C = document.body.querySelector('.container')
const Q = findOne(C, '#questions-box')
addHandler(Q, 'submit', ev => ev.preventDefault())
// Q.addEventListener('submit', ev => ev.preventDefault())
Função de inicialização do botão para excluir a pergunta:
//
const initRemoveQuestionBtn = q => {
const removeQuestionBtn = findOne(q, '.remove-question-btn')
addHandler(removeQuestionBtn, 'click', ev => {
//
/*
=> <div class="question-box">
<br><hr>
<h4 class="title"></h4>
=> <div class="row">
<input type="text" class="form-control col-11 question-text" value="first question" >
=> <button class="btn btn-danger col remove-question-btn">X</button>
</div>
...
*/
findParent(ev.target, 2).remove()
// ev.target.parentElement.parentElement.remove()
//
initTitles()
}, {
//
once: true
})
}
Função de inicialização do botão para excluir opção de resposta:
const initRemoveAnswerBtns = q => {
const removeAnswerBtns = findAll(q, '.remove-answer-btn')
// const removeAnswerBtns = q.querySelectorAll('.remove-answer-btn')
removeAnswerBtns.forEach(btn => addHandler(btn, 'click', ev => {
/*
=> <div class="input-group">
...
=> <div class="input-group-append">
=> <button class="btn btn-outline-danger remove-answer-btn">X</button>
</div>
</div>
*/
findParent(ev.target, 2).remove()
}, {
once: true
}))
}
Função de inicialização do botão para adicionar opção de resposta:
const initAddAnswerBtns = q => {
const addAnswerBtns = findAll(q, '.add-answer-btn')
addAnswerBtns.forEach(btn => addHandler(btn, 'click', ev => {
//
const answers = findOne(findParent(ev.target), '.answers-box')
// const answers = ev.target.parentElement.querySelector('.answers-box')
// "name"
let name
answers.children.length > 0
? name = findOne(answers, 'input[type="radio"]').name
: name = Date.now()
//
const template = `
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="radio" name="${name}">
</div>
</div>
<input class="form-control answer-text" type="text" value="">
<div class="input-group-append">
<button class="btn btn-outline-danger remove-answer-btn">X</button>
</div>
</div>
`
//
answers.insertAdjacentHTML('beforeend', template)
//
initRemoveAnswerBtns(q)
}))
}
Combinamos as funções de botões de inicialização em um:
const initBtns = q => {
initRemoveQuestionBtn(q)
initRemoveAnswerBtns(q)
initAddAnswerBtns(q)
}
Função para inicializar cabeçalhos de perguntas:
const initTitles = () => {
//
const questions = Array.from(findAll(Q, '.question-box'))
//
questions.map(q => {
const title = findOne(q, '.title')
// - + 1
title.textContent = `Question ${questions.indexOf(q) + 1}`
})
}
Vamos inicializar os botões e o título da pergunta:
initBtns(findOne(Q, '.question-box'))
initTitles()
Adicionar função de pergunta:
//
const addQuestionBtn = findOne(C, '#add-question-btn')
addHandler(addQuestionBtn, 'click', ev => {
// IIFE async..await
//
//
//
(async () => {
const data = await import('./Question.js')
const template = await data.default()
await Q.insertAdjacentHTML('beforeend', template)
const question = findOne(Q, '.question-box:last-child')
initBtns(question)
initTitles()
})()
})
Função de criação de teste:
//
addHandler(findOne(C, '#create-test-btn'), 'click', () => createTest())
const createTest = () => {
//
const obj = {}
//
const questions = findAll(Q, '.question-box')
//
//
const isEmpty = (...args) => {
//
args.map(arg => {
//
//
arg = arg.replace(/\s+/g, '').trim()
//
if (arg === '') {
//
alert('Some field is empty!')
//
throw new Error()
}
})
}
//
questions.forEach(q => {
//
const questionText = findOne(q, '.question-text').value
//
//
const answersText = []
findAll(q, '.answer-text').forEach(text => answersText.push(text.value))
// - "checked" "answer-text"
/*
=> <div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
=> <input type="radio" checked name="answer">
</div>
</div>
=> <input class="form-control answer-text" type="text" value="foo" >
...
*/
const rightAnswerText = findOne(findParent(findOne(q, 'input:checked'), 3), '.answer-text').value
//
const explanationText = findOne(q, '.explanation-text').value
//
isEmpty(questionText, ...answersText, explanationText)
// " "
obj[questions.indexOf(q)] = {
question: questionText,
answers: answersText,
rightAnswer: rightAnswerText,
explanation: explanationText
}
})
//
console.table(obj)
//
const data = new Blob(
[JSON.stringify(obj)], {
type: 'application/json'
}
)
//
//
if (findOne(C, 'a') !== null) {
findOne(C, 'a').remove()
}
//
const link = document.createElement('a')
link.setAttribute('href', URL.createObjectURL(data))
link.setAttribute('download', 'data.json')
link.className = 'btn btn-success'
link.textContent = 'Download data'
C.append(link)
URL.revokeObjectURL(data)
}
Resultado:
Usando dados de um arquivo
Usando o gerador de perguntas, crie um arquivo como este:
{
"0": {
"question": "first question",
"answers": ["foo", "bar", "baz"],
"rightAnswer": "foo",
"explanation": "first explanation"
},
"1": {
"question": "second question",
"answers": ["foo", "bar", "baz"],
"rightAnswer": "bar",
"explanation": "second explanation"
},
"2": {
"question": "third question",
"answers": ["foo", "bar", "baz"],
"rightAnswer": "baz",
"explanation": "third explanation"
}
}
Coloque este arquivo (data.json) na pasta "Test-Maker".
Crie um arquivo "useData.html" com o seguinte conteúdo:
<!-- head -->
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<!-- body -->
<h1>Use data</h1>
Adicione alguns estilos personalizados:
body {
max-width: 512px;
margin: 0 auto;
text-align: center;
}
section *:not(h3) {
text-align: left;
}
input,
button {
margin: .4em;
}
label,
input {
cursor: pointer;
}
.right-answer,
.explanation {
display: none;
}
Roteiro:
//
const getData = async url => {
const response = await fetch(url)
const data = await response.json()
return data
}
//
getData('data.json')
.then(data => {
//
console.table(data)
//
createTest(data)
})
// name
let name = Date.now()
//
const createTest = data => {
// data -
//
for (const item in data) {
//
console.log(data[item])
// ,
// , ,
const {
question,
answers,
rightAnswer,
explanation
} = data[item]
// name
name++
//
const questionTemplate = `
<hr>
<section>
<h3>Question ${item}: ${question}</h3>
<form>
<legend>Answers</legend>
${answers.reduce((html, ans) => html += `<label><input type="radio" name="${name}">${ans}</label><br>`, '')}
</form>
<p class="right-answer">Right answer: ${rightAnswer}</p>
<p class="explanation">Explanation: ${explanation}</p>
</section>
`
//
document.body.insertAdjacentHTML('beforeend', questionTemplate)
})
//
const forms = document.querySelectorAll('form')
//
forms.forEach(form => {
const input = form.querySelector('input')
input.click()
})
//
//
const btn = document.createElement('button')
btn.className = 'btn btn-primary'
btn.textContent = 'Check answers'
document.body.append(btn)
//
btn.addEventListener('click', () => {
//
const answers = []
//
forms.forEach(form => {
// ()
const chosenAnswer = form.querySelector('input:checked').parentElement.textContent
//
const rightAnswer = form.nextElementSibling.textContent.replace('Right answer: ', '')
//
answers.push([chosenAnswer, rightAnswer])
})
console.log(answers)
//
// ,
/*
Array(3)
0: (2) ["foo", "foo"]
1: (2) ["bar", "bar"]
2: (2) ["foo", "baz"]
*/
//
checkAnswers(answers)
})
// ()
const checkAnswers = answers => {
//
let rightAnswers = 0
let wrongAnswers = 0
// ,
// - () ,
// -
for (const answer of answers) {
//
if (answer[0] === answer[1]) {
//
rightAnswers++
//
} else {
//
wrongAnswers++
//
const wrongSection = forms[answers.indexOf(answer)].parentElement
//
wrongSection.querySelector('.right-answer').style.display = 'block'
wrongSection.querySelector('.explanation').style.display = 'block'
}
}
//
const percent = parseInt(rightAnswers / answers.length * 100)
// -
let result = ''
//
// result
if (percent >= 80) {
result = 'Great job, super genius!'
} else if (percent > 50) {
result = 'Not bad, but you can do it better!'
} else {
result = 'Very bad, try again!'
}
//
const resultTemplate = `
<h3>Your result</h3>
<p>Right answers: ${rightAnswers}</p>
<p>Wrong answers: ${wrongAnswers}</p>
<p>Percentage of correct answers: ${percent}</p>
<p>${result}</p>
`
//
document.body.insertAdjacentHTML('beforeend', resultTemplate)
}
}
Resultado (caso a resposta à terceira pergunta esteja errada):
Bônus. Gravando dados para CloudFlare
Acesse cloudflare.com , cadastre-se, clique em Trabalhadores à direita e, em seguida, clique no botão "Criar um trabalhador".
Altere o nome do trabalhador para "dados" (isso é opcional). No campo "{} Script", insira o seguinte código e clique no botão "Salvar e implantar":
//
addEventListener('fetch', event => {
event.respondWith(
new Response(
//
`{
"0": {
"question": "first question",
"answers": ["foo", "bar", "baz"],
"rightAnswer": "foo",
"explanation": "first explanation"
},
"1": {
"question": "second question",
"answers": ["foo", "bar", "baz"],
"rightAnswer": "bar",
"explanation": "second explanation"
},
"2": {
"question": "third question",
"answers": ["foo", "bar", "baz"],
"rightAnswer": "baz",
"explanation": "third explanation"
}
}`,
{
status: 200,
// CORS
headers: new Headers({'Access-Control-Allow-Origin': '*'})
})
)
})
Agora podemos receber dados do CloudFlare. Para fazer isso, você só precisa especificar a URL do trabalhador em vez de 'data.json' na função "getData". No meu caso, é assim: getData ('https://data.aio350.workers.dev/') .then (...).
Um longo artigo acabou. Espero que você tenha encontrado algo útil nele.
Obrigado pela atenção.