<div id="todos">
<form method="post" action="/my-backend-script">
<input type="text" placeholder="What's next?" />
<input type="submit" value="Add" />
</form>
<pre></pre>
<ul></ul>
<label>
<input type="checkbox" />
Hide completed tasks
</label>
</div>
#todos {
font-family: sans-serif;
color: #444;
}
/**
* Todo List Input
*/
#todos form {
display: flex;
}
#todos form input[type="text"],
#todos form input[type="submit"] {
padding: 5px 10px;
border: 1px solid #666;
outline: none;
}
#todos form input[type="text"] {
flex: 1;
}
#todos form input[type="text"]:focus {
border-color: #369;
}
#todos form input[type="submit"] {
display: none;
text-transform: uppercase;
}
#todos form input[type="submit"]:active {
border-color: #369;
background-color: #369;
color: #fff;
}
/*
By adding a simple "is-valid" class to the
Todo's FORM - that wraps all the other
components - then we can easily fix the
style to display a "ready to sent" UI.
It's Javascript that implements the logic
to add or remove the "is-valid" class.
*/
#todos form.is-valid input[type="submit"] {
display: block;
}
#todos form.is-valid input[type="text"] {
margin-right: 10px;
}
/**
* Todos List
*/
#todos ul {
margin: 10px 0 0 0;
padding: 0;
border: 1px solid #ddd;
border-radius: 4px;
}
#todos ul li {
padding: 5px 10px;
border-bottom: 1px solid #ddd;
transition: background-color 500ms ease;
}
#todos ul li:last-child {
border-bottom: none;
}
#todos ul li:hover {
background-color: #cde;
}
#todos ul li.is-done {
font-style: italic;
text-decoration: line-through;
font-size: 10px;
background: #eee;
}
#todos label {
display: block;
border: none;
margin: 10px 0 0 0;
padding: 0;
color: #4ad;
outline: none;
font-size: 12px;
}
/**
* App's State
* As the features grow, we need a more
* flexible data structure for the
* app's state. Objects are great!
*/
const state = {
hideCompleted: false,
formIsValid: false,
todos: [
{
text: 'buy milk',
completed: true,
},
{
text: 'clean kitchen',
completed: false,
},
{
text: 'learn js',
completed: false,
},
],
}
/**
* =====================================
* Hande State Rendering
* =====================================
*
* This portion of the app has the single
* responsibility to sync the App's State
* with the final HTML.
*
* You can act on existing nodes by adding
* or removing classes, changing the inner
* text or value of UI components.
*
* A normal (but not optimal) approach is
* "destroy/create" so to avoid complex
* DOM updates.
*
* But in the end, it is up to you.
* Thing is, you don't have to worry with
* the App's logic here, you just have to
* be sure that the final HTML reflects the
* data in "state".
*/
const render = () => {
resetDOMList()
renderDOMList()
renderDOMFormStatus()
renderDOMHelperMessage()
}
/**
* A "data -> DOM" rendering approach makes
* it very easy to apply dynamic filters
* based on the app's state.
*
* This is a great example of a new feature
* that we could implement WITHOUT changing
* the main rendering logic :-)
*/
const getVisibleTodos = () =>
state.todos
.filter(item => state.hideCompleted
? !item.completed
: true
)
const resetDOMList = () =>
document
.querySelectorAll('#todos ul *')
.forEach($ => $.remove())
const renderDOMList = () =>
getVisibleTodos()
.forEach(renderDOMListItem)
const renderDOMListItem = (item) => {
/**
* NOTE: item is now an object!
* - item.text
* - item.completed
*/
const li = document.createElement('li')
// NOTE: the items's text is not a
// property of the object
const txt = document
.createTextNode(item.text)
li.appendChild(txt)
// NOTE: you can play easily with
// conditional classes applied to the
// list items
if (item.completed) {
li.className = 'is-done'
}
// NOTE: setting an item as completed is
// now trivial. Just change the data an
// trigger a new re-render!
li.addEventListener('click', () => {
item.completed = !item.completed
render()
})
document
.querySelector('#todos ul')
.appendChild(li)
}
const getHelperMessage = () =>
getVisibleTodos().length
? null
: 'Create your first task!'
const renderDOMHelperMessage = () =>
document
.querySelector('#todos pre')
.innerHTML = getHelperMessage()
const getFormClassName = () =>
state.formIsValid
? 'is-valid'
: ''
const renderDOMFormStatus = () =>
document
.querySelector('#todos form')
.className = getFormClassName()
/**
* =====================================
* Handle New Item
* =====================================
*/
const handleSubmit = (e) => {
e.preventDefault()
e.stopPropagation()
// reference the text input from the form
const textEl = e.target[0]
// NOTE: a simple return can do wonders
// in order to avoid to nest your code
// too much.
//
// This technicque has a name:
// return first
if (!textEl.value) return
// Update App's State
//
// NOTE: the new todo item now has to
// respect the app's data structure and
// implement some default values
state.todos.push({
completed: false,
text: textEl.value,
})
// Trigger re-rendering
render()
// reset input
textEl.value = ''
textEl.focus()
}
document
.querySelector('#todos form')
.addEventListener('submit', handleSubmit)
/**
* =====================================
* Handle Form Validation
* =====================================
*
* Ternary operator (inline if) comes in
* handy for quick decisions like a
* class name!
*/
const handleForm = (e) => {
// Update App's State
const len = e.target.value.length
state.formIsValid = len > 0
// Trigger re-rendering
render()
}
document
.querySelector('#todos [type="text"]')
.addEventListener('keyup', handleForm)
/**
* =====================================
* Handle Filter UI
* =====================================
*/
const handleHide = (e) => {
// Update App's State
state.hideCompleted = e
.target
.checked
// Trigger re-rendering
render()
}
document
.querySelector('[type="checkbox"]')
.addEventListener('click', handleHide)
/**
* Run the first rendering loop
* kinda "start the app" - kinda
*/
render()
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.