1. با دارایی های مورد نیاز شروع کنید
برای اینکه چیدمان کمی منحصربفردتر شود، از چند تصویر SVG دست ساز و یک فونت سفارشی برگرفته از عناصر Envato.



شایان ذکر است که بیشتر این دارایی ها از یک آموزش قبلی می آیند. در واقع، ما همچنین از بسیاری از تکنیک های موقعیت یابی که در این آموزش یاد گرفتیم استفاده خواهیم کرد، بنابراین خواندن آن ارزش دارد.



2. با علامت گذاری صفحه ادامه دهید
ما با SVG و a شروع می کنیم div
ظرف:
<svg style="display:none;">...</svg> <div class="container">...</div>
SVG Sprites
همانطور که بارها در گذشته انجام داده ایم، به عنوان یک تمرین خوب، همه SVG ها را به عنوان ذخیره می کنیم symbol
s در یک ظرف SVG sprite. سپس، هر زمان که نیاز داشته باشیم، با تماس با آن، آنها را روی صفحه نمایش میدهیم use
عنصر
در اینجا نشانه گذاری برای SVG sprite آمده است:
<svg style="display:none;"> <symbol id="input" viewBox="0 0 345.27 56.51" preserveAspectRatio="none">...</symbol> <symbol id="checkbox_empty" viewBox="0 0 33.18 33.34">...</symbol> <symbol id="checkmark" viewBox="0 0 37.92 33.3" preserveAspectRatio="none">...</symbol> <symbol id="button" viewBox="0 0 256.6 60.02" preserveAspectRatio="none">...</symbol> <symbol id="close" viewBox="0 0 29.71 30.59">...</symbol> <symbol id="stats" viewBox="0 0 998.06 602.62" preserveAspectRatio="none">...</symbol> </svg>
توجه کنید preserveAspectRatio="none"
ویژگی که ما به بیشتر تصاویر پیوست کردیم. ما این کار را انجام دادهایم، همانطور که بعداً خواهیم دید، نمادهای ما مقیاس میشوند و ابعاد اولیه خود را از دست میدهند.
ظرف
ظرف شامل یک فرم، a div
عنصر و یک لیست مرتب شده خالی:
<div class="container"> <form class="todo-form">...</form> <div class="todo-stats">...</div> <ol class="todo-list"></ol> </div>
در داخل فرم، یک ورودی و یک دکمه ارسال به همراه SVGهای مرتبط با آنها خواهیم داشت:
<form class="todo-form"> <div class="form-wrapper"> <input type="text" name="name" autofocus> <svg> <use xlink:href="#input"></use> </svg> </div> <div class="form-wrapper"> <button type="submit">Add new task</button> <svg> <use xlink:href="#button"></use> </svg> </div> </form>
توجه کنید name
ویژگی ای که به فیلد ورودی اضافه کرده ایم. بعداً از این ویژگی برای دسترسی به مقدار ورودی پس از ارسال فرم استفاده خواهیم کرد.
توجه داشته باشید: در نسخه ی نمایشی ما، autofocus
ویژگی فیلد متن کار نخواهد کرد. در واقع، خطای زیر را ایجاد می کند که اگر کنسول مرورگر خود را باز کنید، می توانید ببینید:



با این حال، اگر این برنامه را به صورت محلی (نه به عنوان پروژه Codepen) اجرا کنید، این مشکل وجود نخواهد داشت. یا می توانید فوکوس را از طریق جاوا اسکریپت تنظیم کنید.
درون div
، ما سه تو در تو قرار می دهیم div
s و SVG مرتبط. در این بخش، تعداد کل کارها (هم باقی مانده و هم تکمیل شده) را پیگیری می کنیم:
<div class="todo-stats"> <div class="total-tasks"> Total Tasks: <span>0</span> </div> <div class="completed-tasks"> Completed Tasks: <span>0</span> </div> <div class="remaining-tasks"> Remaining Tasks: <span>0</span> </div> <svg> <use xlink:href="#stats"></use> </svg> </div>
در نهایت آیتم های لیست سفارش داده شده به صورت پویا از طریق جاوا اسکریپت اضافه می شوند.
3. برخی از سبک های اساسی را تعریف کنید
با آماده شدن نشانهگذاری، با برخی از سبکهای بازنشانی ادامه میدهیم:
@font-face { font-family: "Summer"; src: url(SummerFont-Regular.woff); } @font-face { font-family: "Summer Bold"; src: url(SummerFont-Bold.woff); } :root { --white: #fff; } * { padding: 0; margin: 0; border: none; outline: none; box-sizing: border-box; } input, button { font-family: inherit; font-size: 100%; background: none; } [type="checkbox"] { position: absolute; left: -9999px; } button, label { cursor: pointer; } ol { list-style: none; } body { font: 28px/1.2 "Summer"; margin: 1.5rem 0; }
4. سبک های اصلی را تنظیم کنید
بیایید اکنون سبک های اصلی برنامه TODO خود را مورد بحث قرار دهیم.
سبک های ظرف
ظرف حداکثر عرض با محتوای مرکز افقی خواهد داشت:
.container { max-width: 700px; padding: 0 10px; margin: 0 auto; }
سبک های فرم
در صفحه های کوچک همه عناصر فرم روی هم چیده می شوند:



با این حال، در ویوپورت هایی با عرض 600 پیکسل و بالاتر، طرح بندی فرم به صورت زیر تغییر می کند:



بیایید به دو نکته توجه کنیم:
- در نماهای عریض، ورودی دو برابر اندازه دکمه خواهد بود.
- SVG ها عناصری کاملاً در موقعیت هستند و در زیر کنترل فرم مجاور خود قرار می گیرند. باز هم برای توضیح دقیق تر، نگاهی به این آموزش قبلی بیندازید.
در اینجا سبک های این بخش آمده است:
/*CUSTOM VARIABLES HERE*/ .todo-form .form-wrapper { position: relative; } .todo-form input, .todo-form button { position: relative; width: 100%; z-index: 1; padding: 15px; } .todo-form svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .todo-form button { color: var(--white); text-transform: uppercase; } @media screen and (min-width: 600px) { .todo-form { display: grid; grid-template-columns: 2fr 1fr; grid-column-gap: 5px; } }
سبک های آمار
در مرحله بعد، بیایید به نوار وضعیت نگاه کنیم که گزارشی سریع درباره تعداد کل کارها به ما می دهد.
در صفحه نمایش های کوچک ظاهر انباشته زیر را خواهد داشت:



با این حال، در ویوپورت هایی با عرض 600 پیکسل و بالاتر، باید به صورت زیر تغییر کند:



بیایید به دو نکته توجه کنیم:
- در نماهای گسترده، همه کودک
div
عرض عناصر برابر خواهد بود. - مشابه SVG های قبلی، این نیز کاملاً در موقعیت قرار می گیرد و به عنوان یک تصویر پس زمینه عمل می کند که کل بخش را پوشش می دهد.
سبک های مرتبط:
/*CUSTOM VARIABLES HERE*/ .todo-stats { position: relative; text-align: center; padding: 5px 10px; margin: 10px 0; color: var(--white); } .todo-stats > div { position: relative; z-index: 1; } .todo-stats svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } @media screen and (min-width: 600px) { .todo-stats { display: grid; grid-template-columns: repeat(3, 1fr); } }
سبک های وظیفه
طرحبندی وظایف، که به صورت پویا در بخش آینده ایجاد میکنیم، به شکل زیر خواهد بود:



هر وظیفه ای که با یک نشان داده می شود li
دو قسمت خواهد داشت



در قسمت اول، یک چک باکس همراه با نام کار مشاهده خواهید کرد. در قسمت دوم متوجه دکمه حذف برای حذف کار می شوید.
در اینجا سبک های مرتبط آورده شده است:
.todo-list li { display: grid; align-items: baseline; grid-template-columns: auto 20px; grid-column-gap: 10px; padding: 0 10px; } .todo-list li + li { margin-top: 10px; }
وقتی یک کار است ناقص، یک کادر خالی ظاهر می شود. از سوی دیگر، اگر وظیفه ای به عنوان علامت گذاری شود تکمیل شد، یک علامت چک ظاهر می شود. علاوه بر این، به نام آن 50 درصد کدورت و همچنین یک خط از طریق آن داده می شود.
در اینجا سبک های مسئول این رفتار آمده است:
.todo-list .checkbox-wrapper { display: flex; align-items: baseline; } .todo-list .checkbox-wrapper label { display: grid; margin-right: 10px; } .todo-list .checkbox-wrapper svg { grid-column: 1; grid-row: 1; width: 20px; height: 20px; } .todo-list .checkbox-wrapper .checkmark { display: none; } .todo-list [type="checkbox"]:checked + label .checkmark { display: block; } .todo-list [type="checkbox"]:checked ~ span { text-decoration: line-through; opacity: 0.5; }
در نهایت، در زیر استایل های دکمه حذف آمده است:
.todo-list .remove-task { display: flex; padding: 2px; } .todo-list .remove-task svg { width: 16px; height: 16px; }
5. جاوا اسکریپت را اضافه کنید
در این مرحله، ما آماده ساختن عملکرد اصلی برنامه لیست TODO خود هستیم. بیایید آن را انجام دهیم!
در ارسال فرم
هر بار که کاربر فرم را با فشار دادن دکمه ارسال می کند وارد کلید یا ارسال را فشار دهید، کارهای زیر را انجام خواهیم داد:
- از ارسال فرم جلوگیری کنید، در نتیجه از بارگذاری مجدد صفحه جلوگیری کنید.
- مقدار موجود در فیلد ورودی را بگیرید.
- با فرض خالی نبودن فیلد ورودی، یک شیء جدید به صورت واقعی ایجاد می کنیم که وظیفه را نشان می دهد. هر کار یک شناسه منحصر به فرد، یک نام دارد و به طور پیش فرض فعال (تکمیل نشده) خواهد بود.
- این وظیفه را به
tasks
آرایه. - آرایه را در حافظه محلی ذخیره کنید. ذخیره سازی محلی فقط رشته ها را پشتیبانی می کند، بنابراین برای انجام این کار، باید از آن استفاده کنیم
JSON.stringify()
روش تبدیل اشیاء داخل آرایه به رشته. - با … تماس بگیر
createTask()
عملکردی برای نمایش بصری کار بر روی صفحه نمایش. - فرم را پاک کنید.
- به قسمت ورودی تمرکز کنید.
این هم کد مربوطه:
const todoForm = document.querySelector(".todo-form"); let tasks = []; todoForm.addEventListener("submit", function(e) { // 1 e.preventDefault(); // 2 const input = this.name; const inputValue = input.value; if (inputValue != "") { // 3 const task = { id: new Date().getTime(), name: inputValue, isCompleted: false }; // 4 tasks.push(task); // 5 localStorage.setItem("tasks", JSON.stringify(tasks)); // 6 createTask(task); // 7 todoForm.reset(); } // 8 input.focus(); });
یک Task ایجاد کنید
را createTask()
تابع مسئول ایجاد نشانه گذاری کار خواهد بود.
برای مثال، ساختار تکلیف «برو پیاده روی» در اینجا آمده است:



دو چیز در اینجا مهم است:
- اگر کار تکمیل شود، علامت چک ظاهر می شود.
- اگر کار تکمیل نشده باشد،
span
عنصر را دریافت خواهد کردcontenteditable
صفت. این ویژگی به ما امکان ویرایش/بهروزرسانی نام آن را میدهد.
در زیر دستور این تابع آمده است:
function createTask(task) { const taskEl = document.createElement("li"); taskEl.setAttribute("id", task.id); const taskElMarkup = ` <div class="checkbox-wrapper"> <input type="checkbox" id="${task.name}-${task.id}" name="tasks" ${ task.isCompleted ? "checked" : "" }> <label for="${task.name}-${task.id}"> <svg class="checkbox-empty"> <use xlink:href="#checkbox_empty"></use> </svg> <svg class="checkmark"> <use xlink:href="#checkmark"></use> </svg> </label> <span ${!task.isCompleted ? "contenteditable" : ""}>${task.name}</span> </div> <button class="remove-task" title="Remove ${task.name} task"> <svg> <use xlink:href="#close"></use> </svg> </button> `; taskEl.innerHTML = taskElMarkup; todoList.appendChild(taskEl); countTasks(); }
یک کار را به روز کنید
یک کار را می توان به دو روش مختلف به روز کرد:
- با تغییر وضعیت آن از «ناقص» به «تمام» و بالعکس.
- با تغییر نام آن در صورتی که کار ناقص باشد. به یاد داشته باشید که در این مورد،
span
عنصر دارایcontenteditable
صفت.
برای پیگیری این تغییرات، ما از مزایای آن استفاده خواهیم کرد input
رویداد. این یک رویداد قابل قبول برای ما است زیرا در هر دو مورد صدق می کند input
عناصر و عناصر با contenteditable
فعال شد.
نکته دشوار این است که ما نمی توانیم مستقیماً این رویداد را به عناصر هدف متصل کنیم (چک باکس، span
) زیرا به صورت پویا ایجاد می شوند و در بارگذاری صفحه بخشی از DOM نیستند.
با تشکر از هیئت رویداد، ما ضمیمه می کنیم input
رویداد به لیست والد که بخشی از نشانه گذاری اولیه است. سپس، از طریق target
ویژگی آن رویداد، عناصری را که رویداد روی آنها رخ داده است بررسی می کنیم و آن را فراخوانی می کنیم updateTask()
عملکرد:
todoList.addEventListener("input", (e) => { const taskId = e.target.closest("li").id; updateTask(taskId, e.target); });
درون updateTask()
تابع، ما کارهای زیر را انجام خواهیم داد:
- کاری را که نیاز به به روز رسانی دارد، بگیرید.
- عنصری را که رویداد را آغاز کرده است بررسی کنید. اگر عنصر دارای
contenteditable
ویژگی (یعنی این استspan
عنصر)، نام کار را برابر با تعیین می کنیمspan
محتوای متن - در غیر این صورت (یعنی چک باکس است)، وضعیت کار و آن را تغییر می دهیم
checked
صفت. به علاوه، ما نیز آن را تغییر می دهیمcontenteditable
ویژگی مجاورspan
. - مقدار را به روز کنید
tasks
کلید در ذخیره سازی محلی - با … تماس بگیر
countTasks()
عملکرد.
در اینجا نحو این تابع آمده است:
function updateTask(taskId, el) { // 1 const task = tasks.find((task) => task.id === parseInt(taskId)); if (el.hasAttribute("contentEditable")) { // 2 task.name = el.textContent; } else { // 3 const span = el.nextElementSibling.nextElementSibling; task.isCompleted = !task.isCompleted; if (task.isCompleted) { span.removeAttribute("contenteditable"); el.setAttribute("checked", ""); } else { el.removeAttribute("checked"); span.setAttribute("contenteditable", ""); } } // 4 localStorage.setItem("tasks", JSON.stringify(tasks)); // 5 countTasks(); }
یک کار را حذف کنید
ما می توانیم یک کار را از طریق دکمه “بستن” حذف کنیم.



مشابه عملیات بهروزرسانی، نمیتوانیم رویدادی را مستقیماً به این دکمه متصل کنیم زیرا هنگام بارگیری صفحه در DOM نیست.
با تشکر مجدد از هیئت رویداد، ما یک را پیوست خواهیم کرد click
رویداد را در لیست والد قرار دهید و اقدامات زیر را انجام دهید:
- بررسی کنید که آیا عنصری که روی آن کلیک میشود دکمه «بستن» است یا SVG فرزند آن.
- اگر این اتفاق بیفتد، ما آن را می گیریم
id
مورد لیست والد - این را پاس کن
id
بهremoveTask()
عملکرد.
این هم کد مربوطه:
const todoList = document.querySelector(".todo-list"); todoList.addEventListener("click", (e) => { // 1 if ( e.target.classList.contains("remove-task") || e.target.parentElement.classList.contains("remove-task") ) { // 2 const taskId = e.target.closest("li").id; // 3 removeTask(taskId); } });
درون removeTask()
تابع، ما کارهای زیر را انجام خواهیم داد:
- حذف از
tasks
کار مرتبط را آرایه کنید. - مقدار را به روز کنید
tasks
کلید در ذخیره سازی محلی - مورد لیست مرتبط را حذف کنید.
- با … تماس بگیر
countTasks()
عملکرد.
در اینجا نحو این تابع آمده است:
function removeTask(taskId) { // 1 tasks = tasks.filter((task) => task.id !== parseInt(taskId)); // 2 localStorage.setItem("tasks", JSON.stringify(tasks)); // 3 document.getElementById(taskId).remove(); // 4 countTasks(); }
شمارش وظایف
همانطور که قبلاً بحث کردیم، بسیاری از توابع بالا شامل countTask()
عملکرد.
وظیفه آن نظارت بر وظایف برای تغییرات (افزودن، به روز رسانی، حذف) و به روز رسانی محتوای عناصر مرتبط است.



این هم امضای آن:
const totalTasks = document.querySelector(".total-tasks span"); const completedTasks = document.querySelector(".completed-tasks span"); const remainingTasks = document.querySelector(".remaining-tasks span"); function countTasks() { totalTasks.textContent = tasks.length; const completedTasksArray = tasks.filter((task) => task.isCompleted === true); completedTasks.textContent = completedTasksArray.length; remainingTasks.textContent = tasks.length - completedTasksArray.length; }
جلوگیری از افزودن خطوط جدید
هر بار که یک کاربر نام یک کار را به روز می کند، نباید با فشار دادن دکمه، خطوط جدیدی ایجاد کند وارد کلید



برای غیرفعال کردن این عملکرد، یک بار دیگر از امکان نمایندگی رویداد استفاده می کنیم و آن را پیوست می کنیم keydown
رویداد به لیست، مانند این:
todoList.addEventListener("keydown", function (e) { if (e.keyCode === 13) { e.preventDefault(); } });
توجه داشته باشید که در این سناریو فقط span
عناصر می توانند آن رویداد را تحریک کنند، بنابراین نیازی به بررسی اضافی مانند این نیست:
if (e.target.hasAttribute("contenteditable") && e.keyCode === 13) { e.preventDefault(); }
داده های ماندگار در بارگذاری صفحه
تا اینجا، اگر مرورگر را ببندیم و به پروژه دمو برویم، وظایف ما ناپدید می شوند.
اما، صبر کنید که 100٪ درست نیست! به یاد داشته باشید که هر بار که دستکاری کار را انجام می دهیم، آن را نیز ذخیره می کنیم tasks
آرایه در ذخیره سازی محلی به عنوان مثال، در کروم، برای دیدن کلیدها و مقادیر ذخیره سازی محلی، روی آن کلیک کنید کاربرد برگه سپس، را گسترش دهید محل ذخیره سازی منو و در نهایت روی یک دامنه کلیک کنید تا جفت های کلید-مقدار آن را مشاهده کنید.
در مورد من، در اینجا مقادیر برای tasks
کلید:



بنابراین، برای نمایش این وظایف، ابتدا باید آنها را از حافظه محلی بازیابی کنیم. برای انجام این کار، از JSON.parse()
روشی که رشته ها را دوباره به اشیاء جاوا اسکریپت تبدیل می کند.
در مرحله بعد، همه وظایف را در قسمت آشنا ذخیره می کنیم tasks
آرایه. به خاطر داشته باشید که اگر هیچ داده ای در حافظه محلی وجود نداشته باشد (به عنوان مثال اولین باری که از برنامه بازدید می کنیم)، این آرایه خالی است. سپس، باید از طریق آرایه تکرار کنیم و برای هر کار، آن را فراخوانی کنیم createTask()
عملکرد. و، این همه است!
قطعه کد مربوطه:
let tasks = JSON.parse(localStorage.getItem("tasks")) || []; if (localStorage.getItem("tasks")) { tasks.map((task) => { createTask(task); }); }
نتیجه
اوه! با تشکر از همراهی شما در این سفر طولانی. امیدوارم امروز دانش جدیدی کسب کرده باشید که بتوانید در پروژه های خود به کار ببرید.
بیایید به خود یادآوری کنیم که چه چیزی ساخته ایم:
بدون شک، ساخت چنین برنامهای با چارچوب جاوا اسکریپت ممکن است پایدارتر، آسانتر و کارآمدتر باشد (رنگآمیزی مجدد DOM گران است). با این حال، دانستن حل این نوع تمرین با جاوا اسکریپت ساده به شما کمک می کند تا درک کاملی از اصول آن داشته باشید و شما را به یک توسعه دهنده بهتر جاوا اسکریپت تبدیل کنید.
قبل از پایان، اجازه دهید دو ایده برای گسترش این تمرین پیشنهاد کنم:
- استفاده کنید HTML Drag and Drop API یا یک کتابخانه جاوا اسکریپت مانند Sortable.js برای ترتیب دادن مجدد وظایف
- داده ها (وظایف) را به جای مرورگر در فضای ابری ذخیره کنید. به عنوان مثال، ذخیره سازی محلی را با یک پایگاه داده بلادرنگ جایگزین کنید Firebase.
مثل همیشه، خیلی ممنون که خواندید!
بیشتر برنامه های Vanilla JavaScript
اگر می خواهید ساخت برنامه های کوچک با جاوا اسکریپت ساده را یاد بگیرید، آموزش های زیر را بررسی کنید: