Към съдържанието
wannacode

Списък от данни

Накратко: Рисуването от данни е една функция render(), която строи списъка в страницата от масив, така че всички места показват едно и също.

Проблем

Кошницата лъже

Купувачът добавя хляб и мляко. Редовете в кошницата са верни, сумата е вярна, а горе пише „Кошница (0)“. Натисни двата бутона „Добави“ и виж брояча.

HTML, CSS, JS

Конзолата е празна. Всяко място на страницата е обновено с отделен ред код в add, и за брояча такъв ред няма. Браузърът не знае, че трите места показват едно и също. Затова никой не забелязва грешката, освен купувача.

Идеята

От масив към списък

Продуктите идват като данни: масив от обекти с име и цена. Страницата трябва да ги покаже като елементи от списък. Пътят е един и същ за всеки списък:

function productItem(product) {
const li = document.createElement('li');
li.textContent = `${product.name}, ${money(product.price)}`;
return li;
}
list.replaceChildren(...products.map(productItem));
  • document.createElement('li') създава нов елемент. Той още не е в страницата.
  • textContent слага текста. Името на продукта е текст, а не HTML, затова никой символ в него не става таг.
  • products.map(productItem) прави от всеки продукт по един <li>: масив от елементи.
  • list.replaceChildren(...) маха старите деца на списъка и слага новите. ... разпъва масива на отделни аргументи.

Това се казва рисуване на списък от данни: данните са в JavaScript, а страницата се строи от тях с функция.

Виж го

Каталогът

HTML, CSS, JS

Смени цената на хляба в масива и натисни „Изпълни“. Не пипаш HTML-а: списъкът следва данните. Добави пети продукт в масива и той се появява като пети ред.

Пробвай

Само наличните

Покажи в списъка само продуктите, които са в наличност (stock е по-голямо от 0). Под списъка, в #count, напиши колко са: „Налични: 3 от 5“.

HTML, CSS, JS

Идеята

Едни данни, три места

Кошницата е масив cart. Страницата го показва три пъти: броя горе, редовете и сумата. При всяка промяна на cart трите места трябва да се сменят заедно.

На ръка това значи: всяка функция, която променя cart, пипа всяко място, което го показва. add пипа редовете и сумата. remove пипа редовете, сумата и брояча. Утре идва четвърто място, например „Безплатна доставка над 30 €“, и трябва да се сети всяка функция.

Когато едно място е забравено, данните и екранът се разминават: в cart има два продукта, а страницата казва нула. Грешка няма. Има страница, която показва нещо невярно.

Виж го

Виж разминаването

const cart = [];
function addByHand(product) {
cart.push(product);
cartList.append(cartItem(product));
total.textContent = `Общо: ${money(sum())}`;
}
function render() {
badge.textContent = `Кошница (${cart.length})`;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Общо: ${money(sum())}`;
}
function add(product) {
cart.push(product);
render();
}
Стъпка по стъпка, с думи
  1. Вляво са данните: масивът cart. Вдясно са трите места на страницата, които го показват: броячът, редовете и сумата.
  2. addByHand(хляб). Първо се сменят данните: масивът има един продукт.
  3. После функцията пипа екрана на ръка: нов ред и нова сума. Всяко място е отделен ред код.
  4. Ред за брояча никой не е написал. Данните казват 1, броячът казва 0. В конзолата няма грешка: страницата показва нещо невярно.
  5. Още един продукт, пак на ръка. Разликата расте: 2 срещу 0.
  6. Сега add(): тя само сменя данните. За миг целият екран е назад, защото още нищо не е нарисувано.
  7. render() рисува трите места наново от cart: брояча, всеки ред и сумата. Не я интересува какво е имало на екрана преди.
  8. Правилото: всяка промяна е първо данните, после render(). Екранът следва от данните и забравено място няма.

addByHand пипа две места от три. render пипа и трите, всеки път, от данните. Тя не пита какво е имало на екрана преди и затова не може да се обърка от него.

Идеята

Една render()

Правилото е кратко: функциите, които променят данните, не пипат екрана. Те сменят данните и викат render(). Само render() пише в страницата и пише всичко от данните:

function render() {
badge.textContent = `Кошница (${cart.length})`;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Общо: ${money(sum())}`;
}

Така всяко място е описано веднъж. Новото място се добавя на един ред в render() и важи за add, за remove и за всяка бъдеща функция. Редовете се рисуват наново целите: replaceChildren маха старите и слага нови от cart.map(cartItem).

Пробвай

Една render()

add и remove пипат екрана на ръка и всяка забравя по едно място. Напиши render() така, че да рисува брояча, редовете и сумата от cart, а add и remove само да сменят cart и да викат render().

HTML, CSS, JS

Пробвай

Намери грешката

Броячът винаги е с един продукт назад: след първия клик казва 0, след втория 1.

Избери реда с грешката.

Идеята

Цената на „всичко наново“

render() не знае какво се е променило, затова сменя всичко. Обикновено това е невидимо. Понякога не е. В кошницата долу всеки ред има поле за брой и при всяка промяна на полето се вика render(). Купувачът иска 15 млека: трие 2, пише 1 и 5.

HTML, CSS, JS

След цифрата 1 render() маха старото поле и слага ново със стойност 1. Фокусът беше на старото поле и вече го няма, затова 5 не стига никъде. Същото става с курсора в текстово поле, с позицията на превъртане в дълъг списък и с отворено падащо меню. А при 1000 реда браузърът строи 1000 нови елемента за всеки клик.

Двата пътя са лоши: на ръка забравяш места, а „всичко наново“ губи фокус и е бавно при голям списък. Решението е трето: пишеш една функция, която казва как изглежда екранът при тези данни, както render(). Някой друг сравнява новия резултат със стария и сменя в страницата само разликата: текста на брояча, един ред, нищо по полето. Това правят React и Vue, и това е следващият модул: компоненти.

По-точно, за любопитните

innerHTML вместо createElement. list.innerHTML = products.map((p) => `<li>${p.name}</li>`).join('') е по-кратко, но името на продукта влиза като HTML. Име с <img onerror=…> става код в страницата (XSS). textContent и createElement пазят текста като текст.

replaceChildren. Без аргументи изпразва елемента: list.replaceChildren(). Преди него се пишеше list.innerHTML = '' и после append.

Слушателите. Всеки нов бутон получава нов слушател в productItem. Старите бутони изчезват заедно със слушателите си. Друг път е делегирането: един слушател на <ul> и event.target.closest('li'), а в елемента пише кой продукт е (data-id).

hidden. element.hidden = true е като атрибута hidden в HTML: елементът не се показва. Ако CSS правило даде на елемента display, то печели над hidden, и елементът пак се вижда.

Да пазиш фокуса на ръка. Може да запомниш кое поле е било на фокус и къде е бил курсорът и след render() да ги върнеш. Работи, но е още едно място за забравяне. Библиотеките го правят вместо теб, защото не сменят полето изобщо.

Упражнение

Кошницата

Направи каталога и кошницата от данните. Всеки продукт е ред с бутон „Добави“ (името за екранния четец: „Добави Бял хляб“). Всеки ред в кошницата има бутон „Махни“ („Махни Бял хляб“). Броячът горе е „Кошница (2)“, сумата е „Общо: 10,00 €“, а „Кошницата е празна“ се вижда само когато кошницата е празна (свойството hidden).

HTML, CSS, JS

Тест

Провери се

1. Кошницата показва броя на продуктите на три места. При ръчното обновяване какво е най-честата грешка?
2. Какво прави list.replaceChildren(...items)?
3. Къде в add трябва да е render()?
4. Защо „рисувай всичко наново“ губи фокуса в поле от списъка?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·