@@ -1055,6 +1055,307 @@ defmodule ComponentsGuideWeb.ReactEditorController do
10551055 render_source ( conn , source )
10561056 end
10571057
1058+ def show ( conn , % { "id" => "todo-list-reducer" } ) do
1059+ source = ~s"""
1060+ function TodoItem({ item }) {
1061+ const domID = useId();
1062+ const descriptionID = `${domID}-description`;
1063+ const completedID = `${domID}-completed`;
1064+
1065+ return <fieldset class="flex items-center gap-3 py-2" data-id={item.id}>
1066+ <input name="completed[]" value={item.id} type="checkbox" checked={item.completed} id={completedID} class="w-5 h-5 text-purple-700 rounded" />
1067+ <label for={descriptionID} class="sr-only">Description</label>
1068+ <input name="description[]" id={descriptionID} type="text" value={item.description} class="flex-1 rounded" />
1069+ </fieldset>
1070+ }
1071+
1072+ const initialState = {
1073+ values: {
1074+ focusID: undefined,
1075+ items: [
1076+ {
1077+ id: crypto.randomUUID(),
1078+ description: "File taxes",
1079+ completed: false,
1080+ },
1081+ ],
1082+ },
1083+ errors: {},
1084+ }
1085+
1086+ function TodoList() {
1087+ const [{ values, errors }, dispatch] = useReducer(reducer, initialState)
1088+
1089+ useLayoutEffect(() => {
1090+ if (!values.focusID) return;
1091+
1092+ const el = document.querySelector(`[data-id="${values.focusID}"] input[type=text]`);
1093+ el?.focus();
1094+ }, [values.focusID]);
1095+
1096+ return <form className="w-full max-w-[40rem] mx-auto pb-16" onSubmit={(event) => {
1097+ event.preventDefault()
1098+ dispatch(event)
1099+ }}>
1100+ <div onChange={dispatch}>
1101+ {values.items.map((item, index) => (
1102+ <>
1103+ <TodoItem
1104+ key={item.id}
1105+ index={index}
1106+ item={item}
1107+ dispatch={dispatch}
1108+ />
1109+ </>
1110+ ))}
1111+ </div>
1112+ <div className="my-4" />
1113+ <button
1114+ type="button"
1115+ className="py-1 px-3 text-violet-50 bg-violet-800 rounded"
1116+ onClick={dispatch}
1117+ data-action="addItem"
1118+ >Add item</button>
1119+ <pre class="mt-8">{JSON.stringify(values, null, 2)}</pre>
1120+ </form>
1121+ }
1122+
1123+ function changed(state, event) {
1124+ const { form } = event.target
1125+ if (!form) {
1126+ return
1127+ }
1128+
1129+ const formData = new FormData(form)
1130+ const descriptions = formData.getAll("description[]").map(String)
1131+ const completeds = new Set(formData.getAll("completed[]").map(String))
1132+
1133+ for (const [index, item] of state.items.entries()) {
1134+ item.description = descriptions[index]
1135+ item.completed = completeds.has(item.id)
1136+ }
1137+ }
1138+
1139+ function clicked(state, event) {
1140+ // event.currentTarget should be the button. The first click it is, but the clicks after are null. Not sure why.
1141+
1142+ if (!(event.target instanceof Element)) {
1143+ return
1144+ }
1145+ const button = event.target.closest("button")
1146+ if (!button) {
1147+ return
1148+ }
1149+
1150+ const {
1151+ dataset: { action, payload },
1152+ } = button
1153+
1154+ if (action === "addItem") {
1155+ const id = crypto.randomUUID()
1156+ state.items.push({
1157+ id,
1158+ description: "",
1159+ completed: false,
1160+ })
1161+ state.focusID = id
1162+ } else if (action === "removeItem") {
1163+ const { id } = JSON.parse(payload) // TODO: use Zod?
1164+ const index = state.items.findIndex((q) => q.id === id)
1165+ state.items.splice(index, 1)
1166+ }
1167+ }
1168+
1169+ function reducer(state, event) {
1170+ state = structuredClone(state)
1171+
1172+ if (isInputChangeEvent(event)) {
1173+ changed(state.values, event)
1174+ } else if (isClickEvent(event)) {
1175+ clicked(state.values, event)
1176+ } else if (isSubmitEvent(event)) {
1177+ state.errors = validate(state.values)
1178+ }
1179+
1180+ return state
1181+ }
1182+
1183+ export function isInputChangeEvent(event) {
1184+ return event.type === "change" && event.target instanceof HTMLInputElement
1185+ }
1186+
1187+ export function isClickEvent(event) {
1188+ return event.type === "click"
1189+ }
1190+
1191+ export function isSubmitEvent(event) {
1192+ return event.type === "submit"
1193+ }
1194+
1195+ export default function App() {
1196+ return <TodoList />;
1197+ }
1198+ """
1199+
1200+ render_source ( conn , source )
1201+ end
1202+
1203+ def show ( conn , % { "id" => "todo-list-reducer-revisions" } ) do
1204+ source = ~s"""
1205+ function TodoItem({ item }) {
1206+ const domID = useId();
1207+ const descriptionID = `${domID}-description`;
1208+ const completedID = `${domID}-completed`;
1209+
1210+ return <fieldset class="flex items-center gap-3 py-2 after:content-[attr(data-revision)]" data-id={item.id}>
1211+ <input name="completed[]" value={item.id} type="checkbox" checked={item.completed} id={completedID} class="w-5 h-5 text-purple-700 rounded" />
1212+ <label for={descriptionID} class="sr-only">Description</label>
1213+ <input name="description[]" id={descriptionID} type="text" value={item.description} class="flex-1 rounded" />
1214+ </fieldset>
1215+ }
1216+
1217+ const initialState = {
1218+ values: {
1219+ focusID: undefined,
1220+ items: [
1221+ {
1222+ id: crypto.randomUUID(),
1223+ description: "File taxes",
1224+ completed: false,
1225+ },
1226+ ],
1227+ },
1228+ errors: {},
1229+ }
1230+
1231+ function TodoList() {
1232+ const [{ values, errors }, dispatch] = useReducer(reducer, initialState)
1233+
1234+ useLayoutEffect(() => {
1235+ if (!values.focusID) return;
1236+
1237+ const el = document.querySelector(`[data-id="${values.focusID}"] input[type=text]`);
1238+ el?.focus();
1239+ }, [values.focusID]);
1240+
1241+ return <form className="w-full max-w-[40rem] mx-auto pb-16" onSubmit={(event) => {
1242+ event.preventDefault()
1243+ dispatch(event)
1244+ }}>
1245+ <div onChange={dispatch}>
1246+ {values.items.map((item, index) => (
1247+ <>
1248+ <TodoItem
1249+ key={item.id}
1250+ index={index}
1251+ item={item}
1252+ dispatch={dispatch}
1253+ />
1254+ </>
1255+ ))}
1256+ </div>
1257+ <div className="my-4" />
1258+ <button
1259+ type="button"
1260+ className="py-1 px-3 text-violet-50 bg-violet-800 rounded"
1261+ onClick={dispatch}
1262+ data-action="addItem"
1263+ >Add item</button>
1264+ <pre class="mt-8">{JSON.stringify(values, null, 2)}</pre>
1265+ </form>;
1266+ }
1267+
1268+ function elChanged(el) {
1269+ const newRevision = parseInt(el.dataset.revision ?? "0") + 1;
1270+ el.dataset.revision = newRevision;
1271+ return newRevision;
1272+ }
1273+
1274+ function changed(state, event) {
1275+ const input = event.target;
1276+ const { form, dataset } = input;
1277+ if (!form) {
1278+ return;
1279+ }
1280+
1281+ const formRevision = elChanged(form);
1282+ dataset.revision = formRevision;
1283+ input.closest('fieldset').dataset.revision = formRevision;
1284+
1285+ const formData = new FormData(form);
1286+ const descriptions = formData.getAll("description[]").map(String);
1287+ const completeds = new Set(formData.getAll("completed[]").map(String));
1288+
1289+ for (const [index, item] of state.items.entries()) {
1290+ item.description = descriptions[index];
1291+ item.completed = completeds.has(item.id);
1292+ }
1293+ }
1294+
1295+ function clicked(state, event) {
1296+ // event.currentTarget should be the button. The first click it is, but the clicks after are null. Not sure why.
1297+
1298+ if (!(event.target instanceof Element)) {
1299+ return
1300+ }
1301+ const button = event.target.closest("button")
1302+ if (!button) {
1303+ return
1304+ }
1305+
1306+ const {
1307+ dataset: { action, payload },
1308+ } = button
1309+
1310+ if (action === "addItem") {
1311+ const id = crypto.randomUUID()
1312+ state.items.push({
1313+ id,
1314+ description: "",
1315+ completed: false,
1316+ })
1317+ state.focusID = id
1318+ } else if (action === "removeItem") {
1319+ const { id } = JSON.parse(payload) // TODO: use Zod?
1320+ const index = state.items.findIndex((q) => q.id === id)
1321+ state.items.splice(index, 1)
1322+ }
1323+ }
1324+
1325+ function reducer(state, event) {
1326+ state = structuredClone(state)
1327+
1328+ if (isInputChangeEvent(event)) {
1329+ changed(state.values, event)
1330+ } else if (isClickEvent(event)) {
1331+ clicked(state.values, event)
1332+ } else if (isSubmitEvent(event)) {
1333+ state.errors = validate(state.values)
1334+ }
1335+
1336+ return state
1337+ }
1338+
1339+ export function isInputChangeEvent(event) {
1340+ return event.type === "change" && event.target instanceof HTMLInputElement
1341+ }
1342+
1343+ export function isClickEvent(event) {
1344+ return event.type === "click"
1345+ }
1346+
1347+ export function isSubmitEvent(event) {
1348+ return event.type === "submit"
1349+ }
1350+
1351+ export default function App() {
1352+ return <TodoList />;
1353+ }
1354+ """
1355+
1356+ render_source ( conn , source )
1357+ end
1358+
10581359 def show ( conn , params ) do
10591360 source = ~s"""
10601361 export default function App() {
0 commit comments