Глава 18Формы и поля форм
Я нынче ж на учёном кутеже
Твоё доверье службой завоюю,
Ты ж мне черкни расписку долговую,
Чтоб мне не сомневаться в платеже.
Формы были кратко представлены в предыдущей главе в качестве способа передачи информации, введённой пользователем, через HTTP. Они были разработаны в вебе до появления JavaScript, с тем расчётом, что взаимодействие с сервером происходит при переходе на другую страницу.
Но их элементы являются частями DOM, как и остальные части страницы, а элементы DOM, представляющие поля формы, поддерживают несколько свойств и событий, которых нет у других элементов. Это делает возможным просматривать и управлять полями ввода из программ JavaScript и добавлять функциональности к классическим формам или использовать формы и поля как основу для построения JavaScript приложения.
Поля
Веб-форма состоит из любого числа полей ввода, окружённых тегом <form>
. HTML предлагает много разных полей, от простых галочек со значениями вкл/выкл до выпадающих списков и полей для ввода текста. В этой книге не будут подробно обсуждаться все виды полей, но мы сделаем небольшой их обзор.
Много типов полей ввода используют тег <input>
. Его атрибут type
используется для выбора стиля поля. Вот несколько распространённых типов:
text |
Текстовое поле на одну строку |
password |
То же, что text , но прячет ввод |
checkbox |
Переключатель вкл/выкл |
radio |
Часть поля с возможностью выбора из нескольких вариантов |
file |
Позволяет пользователю вы��рать файл на его компьютере |
Поля форм не обязательно должны появляться внутри тега <form>
. Их можно разместить в любом месте страницы. Информацию из таких полей нельзя передавать на сервер (это возможно только для всей формы целиком), но когда мы делаем поля, которые обрабатывает JavaScript, нам обычно и не нужно передавать информацию из полей через submit
.
<p><input type="text" value="abc"> (text)</p> <p><input type="password" value="abc"> (password)</p> <p><input type="checkbox" checked> (checkbox)</p> <p><input type="radio" value="A" name="choice"> <input type="radio" value="B" name="choice" checked> <input type="radio" value="C" name="choice"> (radio)</p> <p><input type="file"> (file)</p>
Интерфейс JavaScript для таких элементов разнится в зависимости от типа. Мы рассмотрим каждый из них чуть позже.
У текстовых полей на несколько строк есть свой тег <textarea>
. В основном он нужен потому, что неудобно использовать атрибут для задания начального значения таких полей. У этого тега должен быть закрывающий тег </textarea>
, и он использует текст внутри этих тегов как начальный значение поля вместо использования атрибута value
.
<textarea> one two three </textarea>
Наконец, тег <select>
используется для создания поля, которое позволяет пользователю выбрать один из заданных вариантов.
<select> <option>Pancakes</option> <option>Pudding</option> <option>Ice cream</option> </select>
Когда значение поля изменяется, запускается событие "change"
.
Фокус
В отличие от большинства элементов документа HTML, поля форм могут получать фокус ввода клавиатуры. При щелчке или выборе их другим способом они становятся активными, т.е. главными приёмниками клавиатурного ввода.
Если в документе есть текстовое поле, то набираемый текст появится в нём, только если поле имеет фокус ввода. Другие поля по-разному реагируют на клавиатуру. К примеру, <select>
пытается перейти на вариант, содержащий текст, который вводит пользователь, а также отвечает на нажатия стрелок, передвигая выбор варианта вверх и вниз.
Управлять фокусом из JavaScript можно методами focus
и blur
. Первый перемещает фокус на элемент DOM, из которого он вызван, а второй убирает фокус. Значение document.activeElement
соответствует текущему элементу, получившему фокус.
<input type="text"> <script> document.querySelector("input").focus(); console.log(document.activeElement.tagName); // → INPUT document.querySelector("input").blur(); console.log(document.activeElement.tagName); // → BODY </script>
На некоторых страницах нужно, чтобы пользователь сразу начинал работу с какого-то из полей формы. При помощи JavaScript можно передать этому полю фокус при загрузке документа, но в HTML также есть атрибут autofocus
, который приводит к тому же результату, но сообщает браузеру о наших намерениях. В этом случае браузер может отменить это поведение в подходящих случаях, например когда пользователь перевёл фокус куда-то ещё.
<input type="text" autofocus>
Браузеры по традиции позволяют пользователю перемещать фокус клавишей Tab. Мы можем влиять на порядок перемещения через атрибут tabindex
. В примере документ будет переносить фокус с текстового поля на кнопку OK, вместо того, чтобы сначала пройти через ссылку help
:
<input type="text" tabindex=1> <a href=".">(help)</a> <button onclick="console.log('ok')" tabindex=2>OK</button>
По умолчанию, большинство типов элементов HTML не получают фокус. Но добавив tabindex
к элементу, вы сделаете возможным получение им фокуса.
Отключённые поля
Все поля формы можно отключить атрибутом disabled
, который существует и в виде свойства элемента объекта DOM.
<button>У меня всё хорошо</button> <button disabled>Я в отключке</button>
Отключённые поля не принимают фокус и не изменяются, и в отличие от активных, обычно выглядят серыми и выцветшими.
Когда программа находится в процессе обработки нажатия на кнопку или другой элемент, которое может потребовать общение с сервером и занять длительное время, неплохо отключать элемент до завершения действия. В этом случае, когда пользователь потеряет терпение и нажмёт на элемент ещё раз, действие не будет повторено лишний раз.
Форма в целом
Когда поле, содержится в элементе <form>
, у его элемента DOM будет свойство form
, которое будет ссылаться на форму. Элемент <form>
, в свою очередь, имеет свойство elements
, содержащее массивоподобную коллекцию полей.
Атрибут name
поля задаёт, как будет определено значение этого поля при передаче на сервер. Его также можно использовать как имя свойства при доступе к свойству формы elements
, который работает и как объект, похожий на массив (с доступом по номерам), так и map
(с доступом по имени).
<form action="example/submit.html"> Имя: <input type="text" name="name"><br> Пароль: <input type="password" name="password"><br> <button type="submit">Войти</button> </form> <script> var form = document.querySelector("form"); console.log(form.elements[1].type); // → password console.log(form.elements.password.type); // → password console.log(form.elements.name.form == form); // → true </script>
Кнопка с атрибутом type
равным submit
при нажатии отправляет форму. Нажатие клавиши Enter внутри поля формы имеет тот же эффект.
Отправка формы обычно означает, что браузер переходит на страницу, обозначенную в атрибуте формы action
, используя либо GET
, либо POST
запрос. Но перед этим запускается событие "submit"
. Его можно обработать в JavaScript, и обработчик может предотвратить поведение по умолчанию, вызвав на объекте event
метод preventDefault
.
<form action="example/submit.html"> Значение: <input type="text" name="value"> <button type="submit">Сохранить</button> </form> <script> var form = document.querySelector("form"); form.addEventListener("submit", function(event) { console.log("Saving value", form.elements.value.value); event.preventDefault(); }); </script>
Перехват событий "submit"
полезен в нескольких случаях. Мы можем написать код, проверяющий допустимость введённых значений и сразу же показать ошибку вместо передачи данных формы. Или мы можем отключить отправку формы по умолчанию, как в предыдущем примере, и дать программе возможность самой обработать ввод, например используя XMLHttpRequest
для отправки данных на сервер без перезагрузки страницы.
Текстовые поля
Поля с тегами <input>
и типами text
и password
, а также теги <textarea>
, имеют общий интерфейс. У их элементов DOM есть свойство value
, в котором содержится их текущее содержимое в виде строки текста. Присваивание этому свойству значения меняет содержимое поля.
Свойства текстовых полей selectionStart
и selectionEnd
содержат данные о положении курсора и выделения текста. Когда ничего не выделено, их значение одинаковое, и равно положению курсора. Например, 0 обозначает начало текста, 10 обозначает, что курсор находится после 10-м символа. Когда выделена часть поля, свойства имеют разные значения, а именно начало и конец выделенного текста. В эти поля также можно записывать значение.
К примеру, представьте, что вы пишете статью про Khasekhemwy, но затрудняетесь писать его имя правильно. Следующий код назначает тегу <textarea>
обработчик событий, который при нажатии F2 вставляет строку “Khasekhemwy”.
<textarea></textarea> <script> var textarea = document.querySelector("textarea"); textarea.addEventListener("keydown", function(event) { // The key code for F2 happens to be 113 if (event.keyCode == 113) { replaceSelection(textarea, "Khasekhemwy"); event.preventDefault(); } }); function replaceSelection(field, word) { var from = field.selectionStart, to = field.selectionEnd; field.value = field.value.slice(0, from) + word + field.value.slice(to); // Put the cursor after the word field.selectionStart = field.selectionEnd = from + word.length; } </script>
Функция replaceSelection
заменяет текущий выделенный текст заданным словом, и перемещает курсор на позицию после этого слова, чтобы можно было продолжать печатать.
Событие "change"
для текстового поля не срабатывает каждый раз при вводе одного символа. Оно срабатывает после потери полем фокуса, когда его значение было изменено. Чтобы мгновенно реагировать на изменение текстового поля нужно зарегистрировать событие "input"
, которое срабатывает каждый раз при вводе символа, удалении текста или других манипуляциях с содержимым поля.
В следующем примере мы видим текстовое поле и счётчик, показывающий текущую длину введённого текста:
<input type="text"> length: <span id="length">0</span> <script> var text = document.querySelector("input"); var output = document.querySelector("#length"); text.addEventListener("input", function() { output.textContent = text.value.length; }); </script>
Галочки и радиокнопки
Поле галочки – простой бинарный переключатель. Его значение можно извлечь или поменять через свойство checked
, содержащее булевскую величину.
<input type="checkbox" id="purple"> <label for="purple">Сделать страницу фиолетовой</label> <script> var checkbox = document.querySelector("#purple"); checkbox.addEventListener("change", function() { document.body.style.background = checkbox.checked ? "mediumpurple" : ""; }); </script>
Тег <label>
используется для связи куска текста с полем ввода. Атрибут for
должен совпадать с id
поля. Щелчок по метке label
включает поле ввода, оно получает фокус и меняет значение – если это галочка или радиокнопка.
Радиокнопка схожа с галочкой, но она связана с другими радиокнопками с тем же именем, так что только одна из них может быть выбрана.
Цвет: <input type="radio" name="color" value="mediumpurple"> Фиолетовый <input type="radio" name="color" value="lightgreen"> Зелёный <input type="radio" name="color" value="lightblue"> Голубой <script> var buttons = document.getElementsByName("color"); function setColor(event) { document.body.style.background = event.target.value; } for (var i = 0; i < buttons.length; i++) buttons[i].addEventListener("change", setColor); </script>
Метод document.getElementsByName
выдаёт все элементы с заданным атрибутом name
. Пример перебирает их (посредством обычного цикла for
, а не forEach
, потому что возвращаемая коллекция – не настоящий массив) и регистрирует обработчик события для каждого элемента. Помните, что у объектов событий есть свойство target
, относящееся к элементу, который запустил событие. Это полезно для создания обработчиков событий – наш обработчик может быть вызван разными элементами, и у него должен быть способ получить доступ к текущему элементу, который его вызвал.
Поля select
Поля select
похожи на радиокнопки – они также позволяют выбрать из нескольких вариантов. Но если радиокнопки позволяют нам контролировать раскладку вариантов, то вид тега <select>
определяет браузер.
У полей select
есть вариант, больше похожий на список галочек, чем на радиокнопки. При наличии атрибута multiple
тег <select>
позволит выбирать любое количество вариантов, а не один.
<select multiple> <option>Блины</option> <option>Запеканка</option> <option>Мороженка</option> </select>
В большинстве браузеров внешний вид такого поля будет отличаться от поля с единственным вариантом выбора (без свойства multiple
), которое обычно выглядит как выпадающее меню.
Атрибут size
тега <select>
используется для задания количества вариантов, которые видны одновременно – так вы можете влиять на внешний вид выпадающего списка. К примеру, назначив size
значение "3"
, вы увидите три строки одновременно, безотносительно того, присутствует ли опция multiple
.
У каждого тега <option>
есть значение. Его можно определить атрибутом value
, но если он не задан, то значение тега определяет текст, находящийся внутри этого тега. Свойство value
элемента <select>
отражает текущий выбранный вариант. Для поля с возможностью выбора нескольких вариантов это свойство не особо нужно, т.к. в нём будет содержаться только один из нескольких выбранных вариантов.
К тегам <option>
поля <select>
можно получить доступ как к массивоподобному объекту через свойство options
. У каждого варианта есть свойство selected
, показывающее, выбран ли сейчас этот вариант. Свойство также можно менять, чтобы вариант становился выбранным или не выбранным.
Следующий пример извлекает выбранные значения из поля select
с множественным выбором и использует их для создания двоичного числа из битов. Нажмите Ctrl (или Command на Маке), чтобы выбрать несколько значений сразу.
<select multiple> <option value="1">0001</option> <option value="2">0010</option> <option value="4">0100</option> <option value="8">1000</option> </select> = <span id="output">0</span> <script> var select = document.querySelector("select"); var output = document.querySelector("#output"); select.addEventListener("change", function() { var number = 0; for (var i = 0; i < select.options.length; i++) { var option = select.options[i]; if (option.selected) number += Number(option.value); } output.textContent = number; }); </script>
Файловое поле
Файловое поле изначально было предназначено для закачивания файлов с компьютера через форму. В современных браузерах они также позволяют читать файлы из JavaScript. Поле работает как охранник для файлов. Скрипт не может просто взять и открыть файл с компьютера пользователя, но если тот выб��ал файл в этом поле, браузер разрешает скрипту начать чтение файла.
Файловое поле обычно выглядит как кнопка с надписью вроде “Выберите файл”, с информацией про выбранный файл рядом с ней.
<input type="file"> <script> var input = document.querySelector("input"); input.addEventListener("change", function() { if (input.files.length > 0) { var file = input.files[0]; console.log("You chose", file.name); if (file.type) console.log("It has type", file.type); } }); </script>
Свойство files
элемента – массивоподобный объект (не настоящий массив), содержащий список выбранных файлов. Изначально он пуст. У элемента нет простого свойства file
, потому что пользователь может выбрать несколько файлов за раз при включённом атрибуте multiple
.
У объектов в свойстве files
есть свойства name
(имя файла), size
(размер файла в байтах) и type
(тип файла в смысле “media type” — text/plain
или image/jpeg
).
Чего у него нет, так это свойства, содержащего содержимое файла. Чтобы получить содержимое, приходится постараться. Так как чтение файла с диска занимает длительное время, интерфейс должен быть асинхронным, чтобы документ не замирал. Конструктор FileReader
можно представлять себе, как конструктор XMLHttpRequest
, только для файлов.
<input type="file" multiple> <script> var input = document.querySelector("input"); input.addEventListener("change", function() { Array.prototype.forEach.call(input.files, function(file) { var reader = new FileReader(); reader.addEventListener("load", function() { console.log("File", file.name, "starts with", reader.result.slice(0, 20)); }); reader.readAsText(file); }); }); </script>
Чтение файла происходит при помощи создания объекта FileReader
, регистрации обработчика события "load"
для него, и вызова его метода readAsText
с передачей тому файла. По окончании загрузки в свойстве result
сохраняется содержимое файла.
Пример использует Array.prototype.forEach
для прохода по массиву, так как в обычном цикле было бы неудобно получать нужные объекты file
и reader
от обработчика событий. Переменные были бы общими для всех итераций цикла.
Объекты FileReader
также инициируют событие "error"
, когда чтение файла не получается. Объект с ошибкой будет сохранён в свойстве error
объекта FileReader
. Если вы не хотите забивать голову ещё одной неудобной асинхронной схемой, вы можете обернуть её в обещание (см. главу 17):
function readFile(file) { return new Promise(function(succeed, fail) { var reader = new FileReader(); reader.addEventListener("load", function() { succeed(reader.result); }); reader.addEventListener("error", function() { fail(reader.error); }); reader.readAsText(file); }); }
Возможно читать только часть файла, вызывая slice
и передавая результат (т.н. объект blob) объекту reader
.
Хранение данных на стороне клиента
Простые HTML-странички с добавкой JavaScript могут выступать отличной основой для “мини-приложений” – небольших вспомогательных программ, автоматизирующих ежедневные дела. Присоединив к полям формы обработчики событий вы можете делать всё – от конвертации фаренгейтов в цельсии до генерации паролей из основного пароля и имени веб-сайта.
Когда такому приложению нужно сохранять информацию между сессиями, переменные JavaScript использовать не получится – их значения выбрасываются каждый раз при закрытии страницы. Можно было бы настроить сервер, подсоединить его к интернету и тогда приложение хранило бы ваши данные там. Это мы разберём в главе 20. Но это добавляет вам работы и сложности. Иногда достаточно хранить данные в своём браузере. Но как?
Можно хранить строковые данные так, что они переживут перезагрузку страниц — для этого надо положить их в объект localStorage
. Он разрешает хранить строковые данные под именами (которые тоже являются строками), как в этом примере:
localStorage.setItem("username", "marijn"); console.log(localStorage.getItem("username")); // → marijn localStorage.removeItem("username");
Значение в localStorage
хранится, пока его не перезапишут, удаляется при помощи removeItem
или очисткой локального хранилища пользователем.
У сайтов с разных доменов – разные отделения в этом хранилище. То есть, данные, сохранённые с вебсайта в localStorage
, могут быть прочтены или перезаписаны только скриптами с этого же сайта.
Также браузеры ог��аничивают объём хранимых данных, обычно в несколько мегабайт. Это ограничение, вкупе с тем фактом, что забивание жёстких дисков у людей не приносит прибыли, предотвращает отъедание места на диске.
Следующий код реализует простую программу для ведения заметок. Она хранит заметки в виде объекта, ассоциируя заголовки с содержимым. Он кодируется в JSON и хранится в localStorage
. Пользователь может выбрать записку через поле <select>
и поменять её текст в <textarea>
. Добавляется запись по нажатию на кнопку.
Заметки: <select id="list"></select> <button onclick="addNote()">новая</button><br> <textarea id="currentnote" style="width: 100%; height: 10em"> </textarea> <script> var list = document.querySelector("#list"); function addToList(name) { var option = document.createElement("option"); option.textContent = name; list.appendChild(option); } // Инициализируем список из локального хранилища var notes = JSON.parse(localStorage.getItem("notes")) || {"список покупок": ""}; for (var name in notes) if (notes.hasOwnProperty(name)) addToList(name); function saveToStorage() { localStorage.setItem("notes", JSON.stringify(notes)); } var current = document.querySelector("#currentnote"); current.value = notes[list.value]; list.addEventListener("change", function() { current.value = notes[list.value]; }); current.addEventListener("change", function() { notes[list.value] = current.value; saveToStorage(); }); function addNote() { var name = prompt("Имя записи", ""); if (!name) return; if (!notes.hasOwnProperty(name)) { notes[name] = ""; addToList(name); saveToStorage(); } list.value = name; current.value = notes[name]; } </script>
Скрипт инициализирует переменную notes
значением из localStorage
, а если его там нет – простым объектом с одной пустой записью "список покупок"
. Попытка прочесть отсутствующее поле из localStorage
вернёт null
. Передача null
в JSON.parse
заставит его проанализировать строку "null"
и вернуть null
обратно. Поэтому для значения по умолчанию можно использовать оператор ||
.
Когда данные в note
меняются (добавляется новая запись или меняется текущая), для обновления хранимого поля вызывается функция saveToStorage
. Если б мы рассчитывали, что у нас будут храниться тысячи записей, это было бы слишком накладно, и нам пришлось бы придумать более сложную процедуру для хранения – например, своё поле для каждой записи.
Когда пользователь добавляет запись, код должен явно обновить текстовое поле, хотя у поля <select>
и есть обработчик "change"
, который делает то же самое. Это нужно потому, что событие "change"
происходит, только когда пользователь меняет значение поля, а не когда это делает скрипт.
Есть ещё один похожий на localStorage
объект под названием sessionStorage
. Разница между ними в том, что содержимое sessionStorage
забывается по окончанию сессии, что для большинства браузеров означает момент закрытия.
Итог
HTML предоставляет множество различных типов полей формы – текстовые, галочки, множественного выбора, выбора файла.
Из JavaScript можно получать значение и манипулировать этими полями. По изменению они запускают событие "change"
, по вводу с клавиатуры – "input"
, и ещё много разных клавиатурных событий. Они помогают нам отловить момент, когда пользователь взаимодействует с полем ввода. Свойства вроде value
(для текстовых полей и select
) или checked
(для галочек и радиокнопок) используются для чтения и записи содержимого полей.
При передаче формы происходит событие "submit"
. Обработчик JavaScript затем может вызвать preventDefault
этого события, чтобы предотвратить передачу данных. Элементы формы не обязаны быть заключены в теги <form>
.
Когда пользователь выбрал файл в своей локальной файловой системе через поле выбора файла, интерфейс FileReader
позволит нам добраться до содержимого файла из программы JavaScript.
Объекты localStorage
и sessionStorage
можно использовать для хранения информации таким способом, который переживёт перезагрузку страницы. Первый сохраняет данные навсегда (ну или пока пользователь специально не сотрёт их), а второй – до закрытия браузера.
Упражнения
Верстак JavaScript
Сделайте интерфейс, позволяющий писать и исполнять кусочки кода JavaScript.
Сделайте кнопку рядом с <textarea>
, по нажатию которой конструктор Function
из главы 10 будет обёртывать введённый текст в функцию и вызывать его. Преобразуйте значение, возвращаемое функцией, или любую её ошибку, в строку, и выведите её после текстового поля.
Put a button next to a <textarea>
field, which, when pressed, uses the Function
constructor we saw in
Chapter 10 to wrap the text in a function
and call it. Convert the return value of the function, or any error it
raised, to a string and display it after the text field.
<textarea id="code">return "hi";</textarea> <button id="button">Run</button> <pre id="output"></pre> <script> // Ваш код. </script>
Use document.querySelector
or document.getElementById
to get access to the elements defined in
your HTML. An event handler for "click"
or "mousedown"
events on
the button can get the value
property of the text field and call
new Function
on it.
Make sure you wrap both the
call to new Function
and the call to its result in a try
block so
that you can catch exceptions that it produces. In this case, we
really don’t know what type of exception we are looking for, so catch
everything.
The textContent
property of the
output element can be used to fill it with a string message. Or, if
you want to keep the old content around, create a new text node using
document.createTextNode
and append it to the element. Remember to
add a newline character to the end so that not all output appears on
a single line.
Автодополнение
Дополните текстовое поле так, что при вводе текста под ним появлялся бы список вариантов. У вас есть массив возможных вариантов, и показывать нужно те из них, которые начинаются с вводимого текста. Когда пользователь щёлкает по предложенному варианту, он меняет содержимое поля на него.
<input type="text" id="field"> <div id="suggestions" style="cursor: pointer"></div> <script> // Строит массив из имён глобальных перменных, // типа 'alert', 'document', и 'scrollTo' var terms = []; for (var name in window) terms.push(name); // Ваш код. </script>
The best event for
updating the suggestion list is "input"
since that will fire
immediately when the content of the field is changed.
Then loop over the array
of terms and see whether they start with the given string. For example, you
could call indexOf
and see whether the result is zero. For each matching
string, add an element to the suggestions <div>
. You should probably
also empty that each time you start updating the suggestions, for
example by setting its textContent
to the empty string.
You could either add
a "click"
event handler to every suggestion element or add a single
one to the outer <div>
that holds them and look at the target
property of the event to find out which suggestion was clicked.
To get the suggestion text out of a DOM node, you could
look at its textContent
or set an attribute to explicitly store the
text when you create the element.
Игра “Жизнь” Конвея
Игра “Жизнь” Конвея - это простая симуляция “жизни” на прямоугольной решётке, каждый элемент которой живой или нет. Каждое поколение (шаг игры) применяются следующие правила:
Соседи – это все соседние клетки по горизонтали, вертикали и диагонали.
Обратите внимание, что правила применяются ко всей решётке одновременно, а не к каждой из клеток по очереди. То есть, подсчёт количества соседей происходит в один момент перед следующим шагом, и изменения, происходящие на соседних клетках, не влияют на новое состояние клетки.
Реализуйте игру, используя любые подходящие структуры данных. Используйте Math.random
для создания случайных начальных популяций. Выводите поле как решётку из галочек с кнопкой рядом, для перехода к следующему поколению. Когда пользователь включает или выключает галочки, эти изменения нужно учитывать при подсчёте следующего поколения.
<div id="grid"></div> <button id="next">Следующее поколение</button> <script> // Ваш код. </script>
To solve the problem of having the changes conceptually happen at the same time, try to see the computation of a generation as a pure function, which takes one grid and produces a new grid that represents the next turn.
Representing the grid can be done in any of the ways shown in Chapters 7 and 15. Counting live neighbors can be done with two nested loops, looping over adjacent coordinates. Take care not to count cells outside of the field and to ignore the cell in the center, whose neighbors we are counting.
Making changes to checkboxes take effect on the next generation can be done in two ways. An event handler could notice these changes and update the current grid to reflect them, or you could generate a fresh grid from the values in the checkboxes before computing the next turn.
If you choose to go with event handlers, you might want to attach attributes that identify the position that each checkbox corresponds to so that it is easy to find out which cell to change.
To draw the grid
of checkboxes, you either can use a <table>
element (see
Chapter 13) or simply put them all in
the same element and put <br>
(line break) elements between the
rows.