Pymeriq Crea una aplicación · Programa 3 ← Learn It
Tu misión
Tu JavaScript · corre dentro de tu appapp.js
Vista previa en vivo · tu app de verdad
" + head + "\n" + SHELL_BODY + "\n" + mocks +
"\n\nwindow.__error = null;\nwindow.addEventListener('error', function (e) { window.__error = e.message; });\nwindow.addEventListener('unhandledrejection', function (e) { window.__error = (e.reason && e.reason.message) ? e.reason.message : String(e.reason); });\n<\/scr" + "ipt>" +
"\n\ntry {\n" + safeCode +
"\n} catch (e) { window.__error = e.message; }\n<\/scr" + "ipt>\n";
}
// Documento del PROYECTO (el alumno escribe el documento completo): se
// inyectan los mocks al inicio del body para interceptar su código.
function projectDoc(code, opts) {
opts = opts || {};
var mocks = mockStorageScript(opts.seed) + (opts.real ? realFetchScript() : mockFetchScript(opts.fetchMode, opts.fraseMarca)) +
"window.__error = null; window.addEventListener('error', function (e) { window.__error = e.message; }); window.addEventListener('unhandledrejection', function (e) { window.__error = (e.reason && e.reason.message) ? e.reason.message : String(e.reason); });<\/scr" + "ipt>";
var c = String(code || "");
// Los mocks van ANTES de cualquier script del alumno (un script en
// no debe tocar el localStorage REAL de la plataforma), y el punto de
// inyección se busca IGNORANDO comentarios HTML (un "" comentado
// no debe tragarse los mocks).
var ciego = c.replace(//g, function (m) { return Array(m.length + 1).join(" "); });
var m = ciego.match(/]*>/i) || ciego.match(/]*>/i) || ciego.match(/]*>/i);
if (m) { var idx = m.index + m[0].length; return c.slice(0, idx) + mocks + c.slice(idx); }
return "" + mocks + c + "";
}
var gradeCounter = 0;
function grade(docStr, settle) {
return new Promise(function (resolve) {
var g = document.getElementById("grader");
g.onload = function () {
setTimeout(function () { resolve({ win: g.contentWindow, doc: g.contentDocument }); }, settle || 250);
};
gradeCounter += 1;
g.srcdoc = docStr + "";
});
}
// Para los estados: fotos de #estado en los instantes pedidos (ms tras la
// carga); resuelve tras la última. Cada foto llega con trim aplicado.
function gradeShots(docStr, times) {
return new Promise(function (resolve) {
var g = document.getElementById("grader");
g.onload = function () {
var shots = [];
times.forEach(function (t, i) {
setTimeout(function () {
shots[i] = ((g.contentDocument.querySelector("#estado") || {}).textContent || "").trim();
if (i === times.length - 1) resolve({ win: g.contentWindow, doc: g.contentDocument, shots: shots });
}, t);
});
};
gradeCounter += 1;
g.srcdoc = docStr + "";
});
}
function agrega(ctx, texto) {
var campo = ctx.doc.querySelector("#campo"), boton = ctx.doc.querySelector("#agregar");
if (!campo || !boton) return false;
campo.value = texto;
// Como al teclear de verdad: el campo también anuncia su evento 'input'.
try { campo.dispatchEvent(new ctx.win.Event("input", { bubbles: true })); } catch (e) {}
try { boton.click(); } catch (e) {}
return true;
}
function lis(ctx) { return ctx.doc.querySelectorAll("#lista li"); }
function memoria(ctx) { return (ctx.win && ctx.win.__mem) ? ctx.win.__mem : {}; }
function claveConTexto(mem, texto) {
var t = String(texto).toLowerCase();
for (var k in mem) { if (String(mem[k]).toLowerCase().indexOf(t) !== -1) return k; }
return null;
}
function enLista(ctx, texto) {
var l = lis(ctx), t = String(texto).toLowerCase();
for (var i = 0; i < l.length; i++) { if (l[i].textContent.toLowerCase().indexOf(t) !== -1) return true; }
return false;
}
// Texto impredecible: los mensajes de error no pueden regalar qué hardcodear.
function marca(base) { return base + "-" + Math.floor(100 + Math.random() * 900); }
// Al reportar un fallo, si el código lanzó un error DURANTE los clics, ese
// error es la causa real y es lo que el alumno necesita leer.
function fallaCon(ctx, msg) {
if (ctx.win && ctx.win.__error) return { ok: false, msg: "Hay un error: " + tag(ctx.win.__error) + "." };
return { ok: false, msg: msg };
}
// La app completa (lista + conductas heredadas) debe seguir viva
// cuando una lección SUMA cosas nuevas. Compartida por las lecciones 6 a 9.
async function listaViva(code) {
var ctx = await grade(buildDoc(code), 600);
if (ctx.win.__error) return { ok: false, msg: "Hay un error: " + tag(ctx.win.__error) + "." };
var t = marca("Leche");
agrega(ctx, "Pan"); agrega(ctx, t);
if (lis(ctx).length !== 2 || !enLista(ctx, "Pan") || !enLista(ctx, t)) return fallaCon(ctx, "Se rompió la lista al sumar lo nuevo: dos clics deben seguir dejando dos " + tag("
") + " con su texto.");
var l = lis(ctx);
for (var i = 0; i < l.length; i++) { if (!l[i].querySelector("button")) return fallaCon(ctx, "Se perdió el botón de borrar de la lección 5: cada " + tag("
") + " nuevo debe traerlo."); }
var campo = ctx.doc.querySelector("#campo");
if (campo && campo.value !== "") return fallaCon(ctx, "Se perdió la limpieza: tras agregar, el campo debe quedar limpio.");
agrega(ctx, "");
if (lis(ctx).length !== 2) return fallaCon(ctx, "Se perdió la protección: un clic con el campo vacío no debe agregar nada.");
var mem = memoria(ctx), okArr = false;
for (var k in mem) {
try {
var v = JSON.parse(mem[k]);
if (Array.isArray(v)) {
var j = JSON.stringify(v).toLowerCase();
if (j.indexOf("pan") !== -1 && j.indexOf(t.toLowerCase()) !== -1) okArr = true;
}
} catch (e) {}
}
if (!okArr) return fallaCon(ctx, "Se perdió el guardado: el array con lo agregado debe seguir yendo al cajón.");
return { ok: true };
}
var GRADOS_RE = /-?\d+([.,]\d+)?\s*(°|º|℃|℉|grados)/i;
function textoEstado(ctx) { return ((ctx.doc.querySelector("#estado") || {}).textContent || "").trim(); }
// Con una API real la red TARDA lo que tarda: sondear #estado hasta que
// aparezca una temperatura o se agote el tope (una foto fija a 2.6s
// culpaba al código por un internet lento).
async function esperaClima(ctx, tope) {
var inicio = Date.now();
while (Date.now() - inicio < tope) {
if (GRADOS_RE.test(textoEstado(ctx))) break;
await new Promise(function (r) { setTimeout(r, 400); });
}
return textoEstado(ctx);
}
// Pulsa el botón de borrar del
que contiene t1. Si el alumno le puso
// varios botones a sus elementos (editar, mover...), se prueba cada uno:
// basta con que ALGUNO borre correctamente.
async function borraEnProyecto(code, t1, t2) {
var ctx = null, sinBoton = false;
for (var bi = 0; bi < 3; bi++) {
ctx = await grade(projectDoc(code));
if (ctx.win.__error) return { ctx: ctx, borro: false };
agrega(ctx, t1); agrega(ctx, t2);
if (lis(ctx).length !== 2 || !enLista(ctx, t1) || !enLista(ctx, t2)) return { ctx: ctx, borro: false, agregaRoto: true };
var l = lis(ctx), li = null;
for (var i = 0; i < l.length; i++) { if (l[i].textContent.toLowerCase().indexOf(t1.toLowerCase()) !== -1) { li = l[i]; break; } }
var botones = li ? li.querySelectorAll("button") : [];
if (!botones.length) return { ctx: ctx, borro: false, sinBoton: true };
if (bi >= botones.length) break;
try { botones[bi].click(); } catch (e) {}
if (lis(ctx).length === 1 && enLista(ctx, t2) && !enLista(ctx, t1)) return { ctx: ctx, borro: true };
}
return { ctx: ctx, borro: false };
}
// Última temperatura que Open-Meteo respondió DE VERDAD en este corrector
// (grabada por realFetchScript), o null si la app nunca leyó la respuesta.
function tempReal(ctx) {
var datos = (ctx.win && ctx.win.__fetchData) || [], temp = null;
for (var i = 0; i < datos.length; i++) {
var d = datos[i];
if (String(d.url).indexOf("open-meteo") !== -1 && d.data && d.data.current_weather && typeof d.data.current_weather.temperature === "number") temp = d.data.current_weather.temperature;
}
return temp;
}
function noComments(code) { return String(code).replace(/\/\/[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " "); }
function noStrings(code) {
// Los backticks también se limitan a una línea: un backtick suelto en un
// comentario no debe tragarse el código hasta el próximo backtick.
return String(code).replace(/'(?:[^'\\\n]|\\.)*'/g, '""').replace(/"(?:[^"\\\n]|\\.)*"/g, '""').replace(/`(?:[^`\\\n]|\\.)*`/g, '""');
}
/* =================================================== LECCIONES */
var LESSONS = [
{
id: "reconexion",
eyebrow: "Lección 1 · Despierta tu mini-app",
title: "Reconexión: la lista que ya sabes hacer",
mode: "js",
teach:
'
En el Programa 1 cerraste JavaScript construyendo una mini-app que agrega elementos a una lista. Hoy despierta, y en este programa se convierte en una aplicación de verdad: una que recuerda y conversa con internet.
' +
'
Tu app es "Mi lista" — y el tema lo eliges TÚ en el proyecto final: tareas, recetas, libros, gastos... Al lado tienes su página; tu JavaScript le da vida.
' +
'
Escríbelo de memoria (ya lo hiciste una vez): al hacer clic en ' + tag("#agregar") + ', toma el texto de ' + tag("#campo") + ', crea un ' + tag("
") + ', agrégalo a ' + tag("#lista") + ' y limpia el campo. Y el detalle profesional: si el campo está vacío, no agregues nada.' +
'
¿Se te fue? No pasa nada: la última lección de la ruta de JavaScript del Programa 1 construyó exactamente esto — puedes ir a repasarla y volver. Y aquí la corrección te irá diciendo qué falta.
',
starter: function () { return ""; },
mission: [
{ key: "agrega", label: "Al hacer clic, el texto del campo entra a la lista.", check: async function (code) {
var ctx = await grade(buildDoc(code));
if (ctx.win.__error) return { ok: false, msg: "Hay un error: " + tag(ctx.win.__error) + "." };
agrega(ctx, "Pan");
if (lis(ctx).length !== 1 || !enLista(ctx, "Pan")) return fallaCon(ctx, "Al pulsar " + tag("#agregar") + " con texto en el campo, debe aparecer un " + tag("
") + " con ese texto en " + tag("#lista") + ".");
// Un segundo texto (impredecible): la lista debe ACUMULAR, con el texto real del campo.
var tok = marca("Leche");
agrega(ctx, tok);
if (lis(ctx).length !== 2 || !enLista(ctx, "Pan") || !enLista(ctx, tok)) return fallaCon(ctx, "Agregué \"Pan\" y luego \"" + tok + "\": la lista debe tener los DOS " + tag("
") + ", cada uno con el texto que estaba en el campo al hacer clic (sin reemplazar el anterior y sin textos fijos).");
return { ok: true };
} },
{ key: "limpia", label: "Después de agregar, el campo queda limpio.", check: async function (code) {
var ctx = await grade(buildDoc(code));
agrega(ctx, "Pan");
var campo = ctx.doc.querySelector("#campo");
if (campo && campo.value !== "") return fallaCon(ctx, "Tras agregar, limpia el campo: " + tag('campo.value = "";') + " — así queda listo para el siguiente.");
return { ok: true };
} },
{ key: "vacio", label: "Con el campo vacío, el clic no agrega nada.", check: async function (code) {
var ctx = await grade(buildDoc(code));
if (ctx.win.__error) return { ok: false, msg: "Hay un error: " + tag(ctx.win.__error) + "." };
var boton = ctx.doc.querySelector("#agregar");
try { boton.click(); } catch (e) {}
if (lis(ctx).length !== 0) return fallaCon(ctx, "Un clic con el campo vacío agregó un elemento vacío. Protégelo: " + tag('if (texto === "") return;') + ".");
return { ok: true };
} }
],
coach: function (code) { return []; },
winMsg: "Tu mini-app despertó, y con mejores modales que antes (nada de elementos vacíos). Pero tiene un problema que ya conoces: si recargas la página, TODO desaparece. Eso lo empezamos a arreglar ahora."
},
{
id: "guardar",
eyebrow: "Lección 2 · La app que no olvida",
title: "localStorage: guardar en el navegador",
mode: "js",
teach:
'
Tu navegador tiene un cajón donde las páginas pueden guardar datos que sobreviven al cerrar y volver: ' + tag("localStorage") + '. Es la primera mitad de lo que convierte una página en aplicación.
' +
'
Dos operaciones:
' +
'
localStorage.setItem("saludo", "hola"); // guardar\nconst valor = localStorage.getItem("saludo"); // leer ("hola")
' +
'
Cada dato vive bajo una clave (el nombre del cajoncito). Solo guarda texto — ese detalle importará en la próxima lección.
' +
'
Tu misión: cada vez que se agregue un elemento a la lista, guárdalo también en ' + tag("localStorage") + '. (Elige tú el nombre de la clave: es TU app.)
',
starter: function (prev) { return prev || ""; },
mission: [
{ key: "funciona", label: "La app sigue funcionando como en la lección 1 (no rompas lo anterior).", check: async function (code) {
var ctx = await grade(buildDoc(code));
if (ctx.win.__error) return { ok: false, msg: "Hay un error: " + tag(ctx.win.__error) + "." };
agrega(ctx, "Pan");
var tok = marca("Leche");
agrega(ctx, tok);
if (lis(ctx).length !== 2 || !enLista(ctx, "Pan") || !enLista(ctx, tok)) return fallaCon(ctx, "Se perdió lo de la lección 1: dos clics con textos distintos deben dejar dos " + tag("
") + ", cada uno con su texto.");
var campo = ctx.doc.querySelector("#campo");
if (campo && campo.value !== "") return fallaCon(ctx, "Se perdió la limpieza de la lección 1: tras agregar, el campo debe quedar limpio.");
agrega(ctx, "");
if (lis(ctx).length !== 2) return fallaCon(ctx, "Se perdió la protección de la lección 1: un clic con el campo vacío no debe agregar nada.");
return { ok: true };
} },
{ key: "guarda", label: "Lo agregado queda GUARDADO en localStorage (lo verificamos por dentro).", check: async function (code) {
var ctx = await grade(buildDoc(code));
if (ctx.win.__error) return { ok: false, msg: "Hay un error: " + tag(ctx.win.__error) + "." };
agrega(ctx, "Pan");
var tok = marca("Leche");
agrega(ctx, tok);
// El ÚLTIMO agregado debe estar guardado (sobrescribir una sola clave
// es válido aquí: la trampa de "muchos elementos" se resuelve en L3).
if (!claveConTexto(memoria(ctx), tok)) {
var mem = memoria(ctx), comoClave = false;
for (var k in mem) { if (k.toLowerCase().indexOf(tok.toLowerCase()) !== -1) comoClave = true; }
if (comoClave) return { ok: false, msg: "Guardaste el texto como CLAVE. La clave es el NOMBRE del cajoncito (elígelo tú y fíjalo); el texto del campo va como VALOR: " + tag("localStorage.setItem(tuClave, texto)") + "." };
return fallaCon(ctx, "Agregaste \"" + tok + "\" pero no quedó en el almacenamiento. Usa " + tag("localStorage.setItem(...)") + " con el texto del campo, dentro de tu manejador del clic.");
}
return { ok: true };
} },
{ key: "texto", label: "Usas " + tag("localStorage.setItem") + " en tu código.", check: async function (code) {
// Strings primero, comentarios después: un "//" dentro de un string
// (p.ej. "https://...") no debe tragarse el resto de la línea.
if (!/\.\s*setItem\s*\(/.test(noComments(noStrings(code)))) return { ok: false, msg: "No veo " + tag("localStorage.setItem(clave, valor)") + " en tu código." };
return { ok: true };
} }
],
coach: function (code) { return []; },
winMsg: "Tu app ya escribe en el cajón del navegador. Pero hay una trampa esperándote: guardaste UN texto... ¿y cuando sean muchos elementos? Para eso existe JSON."
},
{
id: "json",
eyebrow: "Lección 3 · El idioma de los datos",
title: "JSON: guardar la lista completa",
mode: "js",
teach:
'
' + tag("localStorage") + ' solo guarda texto, y tu lista es un array. El puente es JSON: el idioma en que los datos viajan y se guardan en toda la web.
' +
'
const lista = ["Pan", "Leche"];\nconst texto = JSON.stringify(lista); // \'["Pan","Leche"]\' — array a texto\nconst devuelta = JSON.parse(texto); // ["Pan", "Leche"] — texto a array
' +
'
El patrón profesional: mantén tus elementos en un array que vive FUERA de la función del clic, y tras CADA cambio guarda el array completo bajo una sola clave. Ensamblado se ve así:
' +
'
let cosas = []; // tu array — fuera del clic, para que no muera entre clics\n\n// y DENTRO de tu función del clic:\ncosas.push(texto);\nlocalStorage.setItem("mi-lista", JSON.stringify(cosas));
' +
'
Ojo: llámalo ' + tag("cosas") + ' (o como quieras, pero NO ' + tag("lista") + ' — ese nombre ya lo usa tu variable del ' + tag("
") + '). Reescribe tu guardado con ese patrón; la clave sigue siendo tuya.',
starter: function (prev) { return prev || ""; },
mission: [
{ key: "arreglo", label: "Con varios elementos agregados, el almacenamiento tiene UN array JSON con todos.", check: async function (code) {
var ctx = await grade(buildDoc(code));
if (ctx.win.__error) return { ok: false, msg: "Hay un error: " + tag(ctx.win.__error) + "." };
// Tres textos (dos impredecibles), y la clave debe ser LA MISMA:
// ni ventanas de 2, ni una clave nueva por clic.
var t1 = marca("Leche"), t2 = marca("Queso");
function arrCon(val, textos) {
try {
var v = JSON.parse(val);
if (!Array.isArray(v) || v.length !== textos.length) return false;
var j = JSON.stringify(v).toLowerCase();
for (var i = 0; i < textos.length; i++) { if (j.indexOf(textos[i].toLowerCase()) === -1) return false; }
return true;
} catch (e) { return false; }
}
agrega(ctx, "Pan");
var mem = memoria(ctx), clave = null;
for (var k in mem) { if (arrCon(mem[k], ["Pan"])) { clave = k; break; } }
agrega(ctx, t1); agrega(ctx, t2);
mem = memoria(ctx);
if (clave) {
if (!arrCon(mem[clave], ["Pan", t1, t2])) return fallaCon(ctx, "Agregué \"Pan\", \"" + t1 + "\" y \"" + t2 + "\": el array COMPLETO (con los tres) debe quedar bajo LA MISMA clave tras cada cambio.");
return { ok: true };
}
for (var k2 in mem) { if (arrCon(mem[k2], ["Pan", t1, t2])) return { ok: true }; }
return fallaCon(ctx, "Agregué \"Pan\", \"" + t1 + "\" y \"" + t2 + "\": el cajón debe tener UN array JSON con ESOS tres textos (guárdalo con " + tag("JSON.stringify(miArray)") + " tras cada cambio).");
} },
{ key: "stringify", label: "Usas " + tag("JSON.stringify") + " para guardar.", check: async function (code) {
if (!/\.\s*stringify\s*\(/.test(noComments(noStrings(code)))) return { ok: false, msg: "No veo " + tag("JSON.stringify(...)") + ": es lo que convierte tu array a texto guardable." };
return { ok: true };
} },
{ key: "sigue", label: "La app visible sigue funcionando como antes (no rompas lo anterior).", check: async function (code) {
var ctx = await grade(buildDoc(code));
if (ctx.win.__error) return { ok: false, msg: "Hay un error: " + tag(ctx.win.__error) + "." };
var tok = marca("Queso");
agrega(ctx, "Pan"); agrega(ctx, tok);
if (lis(ctx).length !== 2 || !enLista(ctx, "Pan") || !enLista(ctx, tok)) return fallaCon(ctx, "La lista visible debe mostrar los 2 elementos con su texto, además de guardarlos.");
var campo = ctx.doc.querySelector("#campo");
if (campo && campo.value !== "") return fallaCon(ctx, "Se perdió la limpieza: tras agregar, el campo debe quedar limpio.");
agrega(ctx, "");
if (lis(ctx).length !== 2) return fallaCon(ctx, "Se perdió la protección: un clic con el campo vacío no debe agregar nada.");
return { ok: true };
} }
],
coach: function (code) {
var tips = [];
if (/setItem\s*\([^)]*\+\s*/.test(noComments(code))) tips.push("Concatenar textos para 'acumular' funciona a medias y se rompe fácil. El array + " + tag("JSON.stringify") + " es el camino sólido.");
return tips;
},
winMsg: "Tu lista completa viaja a texto y vuelve intacta. Falta el momento mágico: que al ABRIR la app, lo guardado reaparezca."
},
{
id: "recordar",
eyebrow: "Lección 4 · El momento mágico",
title: "Recordar al arrancar",
mode: "js",
teach:
'
Todo lo guardado no sirve de nada si la app no lo LEE al arrancar. Hoy cierras el círculo: al cargar la página, tu app recupera su lista y la pinta.
' +
'
El patrón de arranque, al inicio de tu código (SUSTITUYE a tu ' + tag("let cosas = []") + ' de la lección pasada — si dejas los dos, chocan):
' +
'
let cosas = [];\nconst guardado = localStorage.getItem("mi-clave");\nif (guardado !== null) {\n cosas = JSON.parse(guardado);\n}\n// y ahora pinta cada elemento de `cosas` en la página
' +
'
' + tag("null") + ' es el valor con que JavaScript dice "aquí no hay nada": es lo que ' + tag("getItem") + ' devuelve la PRIMERA vez, cuando aún no has guardado. Por eso el ' + tag("if") + ': con el cajón vacío, tu app arranca con la lista vacía sin romperse.
' +
'
Cuando lo logres, haz la prueba sagrada en la vista previa: agrega cosas y pulsa ⟳ Reiniciar app — volverán cada vez que tu app despierte.
',
starter: function (prev) { return prev || ""; },
mission: [
{ key: "restaura", label: "Con datos ya guardados, la app los PINTA al arrancar (sin ningún clic).", check: async function (code) {
// La prueba sagrada: agregar tres textos impredecibles, capturar TODO
// lo que la propia app escribió, sembrarlo tal cual en una app recién
// nacida (como un reload real), y exigir que pinte cada uno en su
.
var toks = [marca("Pan"), marca("Leche"), marca("Queso")];
var sonda = await grade(buildDoc(code));
agrega(sonda, toks[0]); agrega(sonda, toks[1]); agrega(sonda, toks[2]);
var mem = memoria(sonda), hayArray = false;
for (var k in mem) {
try {
var v = JSON.parse(mem[k]);
if (Array.isArray(v)) {
var j = JSON.stringify(v).toLowerCase(), todos = true;
for (var i = 0; i < 3; i++) { if (j.indexOf(toks[i].toLowerCase()) === -1) todos = false; }
if (todos) hayArray = true;
}
} catch (e) {}
}
if (!hayArray) return fallaCon(sonda, "Primero tu app debe seguir guardando la lista COMPLETA como array JSON (lección 3): sin eso no hay nada que recordar.");
// Un reload real conserva TODO el cajón, no una sola clave.
var seed = {}; for (var kk in mem) seed[kk] = String(mem[kk]);
var ctx = await grade(buildDoc(code, { seed: seed }));
if (ctx.win.__error) return { ok: false, msg: "Hay un error al arrancar: " + tag(ctx.win.__error) + "." };
var l = lis(ctx), enSuLi = [false, false, false], juntos = false;
for (var a = 0; a < l.length; a++) {
var txt = l[a].textContent.toLowerCase(), c = 0;
for (var b = 0; b < 3; b++) { if (txt.indexOf(toks[b].toLowerCase()) !== -1) { c++; enSuLi[b] = true; } }
if (c > 1) juntos = true;
}
if (l.length !== 3 || juntos || !enSuLi[0] || !enSuLi[1] || !enSuLi[2]) return fallaCon(ctx, "Tu app guardó [\"" + toks[0] + "\", \"" + toks[1] + "\", \"" + toks[2] + "\"], la reiniciamos con ese cajón, y no pintó cada uno en su propio " + tag("
") + " al arrancar. Lee con " + tag("getItem") + " + " + tag("JSON.parse") + " al inicio y pinta cada elemento.");
return { ok: true };
} },
{ key: "primeravez", label: "La PRIMERA vez (nada guardado), arranca vacía y sin errores.", check: async function (code) {
var ctx = await grade(buildDoc(code));
if (ctx.win.__error) return { ok: false, msg: "Con el almacenamiento vacío tu app da error: " + tag(ctx.win.__error) + ". Protege el arranque (si " + tag("getItem") + " da " + tag("null") + ", empieza con " + tag("[]") + ")." };
if (lis(ctx).length !== 0) return { ok: false, msg: "Sin nada guardado, la lista debe arrancar vacía. (¿Quieres un aviso de \"lista vacía\"? Ponlo en un párrafo aparte, no como " + tag("
") + " — " + tag("#estado") + " lo usarás pronto para otra cosa.)" };
return { ok: true };
} },
{ key: "parse", label: "Usas " + tag("getItem") + " y " + tag("JSON.parse") + " en el arranque.", check: async function (code) {
var c = noComments(noStrings(code));
if (!/\.\s*getItem\s*\(/.test(c) || !/\.\s*parse\s*\(/.test(c)) return { ok: false, msg: "El arranque necesita leer (" + tag("getItem") + ") y traducir (" + tag("JSON.parse") + ")." };
return { ok: true };
} }
],
coach: function (code) { return []; },
winMsg: "Ese es EL momento: tu app se reinició y su lista volvió (pruébalo con ⟳ Reiniciar app). Tu página ya es una aplicación que recuerda. Pulamos el ciclo completo: también hay que poder borrar."
},
{
id: "borrar",
eyebrow: "Lección 5 · El ciclo completo",
title: "Borrar (y volver a guardar)",
mode: "js",
teach:
'
Una app de verdad deja quitar. Y la regla de oro de ayer aplica igual: tras CADA cambio — agregar O borrar — se vuelve a guardar el array completo. Si borras sin volver a guardar, al reiniciar lo borrado REVIVE.
' +
'
Tres herramientas nuevas: ' + tag("cosas.indexOf(texto)") + ' te dice en qué posición del array vive un valor (0 es la primera); ' + tag("cosas.splice(posicion, 1)") + ' quita 1 elemento en esa posición; y ' + tag("li.remove()") + ' hace que el elemento se quite a sí mismo de la página.
' +
'
const boton = document.createElement("button");\nboton.textContent = "x";\nboton.addEventListener("click", function () {\n const posicion = cosas.indexOf(texto); // ¿dónde vive en el array?\n cosas.splice(posicion, 1); // 1. quitar del array\n li.remove(); // 2. quitar el li de la página\n // 3. y aquí, como siempre: volver a guardar\n});\nli.appendChild(boton);
',
starter: function (prev) { return prev || ""; },
mission: [
{ key: "botonx", label: "Cada elemento nuevo trae su botón de borrar.", check: async function (code) {
var ctx = await grade(buildDoc(code));
if (ctx.win.__error) return { ok: false, msg: "Hay un error: " + tag(ctx.win.__error) + "." };
agrega(ctx, "Pan");
if (!ctx.doc.querySelector("#lista li button")) return fallaCon(ctx, "El " + tag("