Created
July 15, 2020 05:21
-
-
Save VimalKumarS/757d396e3728fae48614676fe386888a to your computer and use it in GitHub Desktop.
sq soln
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
IMAGE REFERENCES | |
---------------- | |
Open these up in new tabs: | |
1. Image: https://s3-us-west-2.amazonaws.com/s.cdpn.io/t-640/todo-static.png | |
2. Animated GIF: https://s3-us-west-2.amazonaws.com/s.cdpn.io/t-640/todo-animated.gif | |
TASK LIST | |
--------- | |
Task #1 - Fix not being able to add todos | |
- Typing a todo in the input and pressing Enter should add it to the list. | |
Task #2 - Add "{numActiveTodos} item(s) left" status in footer | |
- The number is a count of to-dos that have not been completed. | |
- Text is left-aligned and vertically centered inside the footer. | |
Task #3 - Add "Clear completed" button in footer | |
- Clicking the button removes all the completed todos. | |
- Button is right-aligned and vertically centered inside the footer. | |
- Button is ONLY VISIBLE if there are completed todos, otherwise invisible. | |
- Style the button so it appears the same as in the static image. | |
BONUS | |
Task #4 - Add item filtering (e.g. all, active, completed) in footer | |
- Filters are placed in the footer, between the active item label on the left and the clear completed button on the right. | |
- Clicking a filter selects it and filters the list of todos. | |
- Style the filter buttons so they show a light gray border on hover and a light gray background color when selected. | |
Task #5 - Reset filter to "all" whenever todo list becomes empty | |
- If the last todo is deleted the filter should reset to "all". | |
- If Clear Completed is clicked and all the todos are removed the filter should reset to "all". | |
--> | |
<!-- TEMPLATES --> | |
<!-- APP --> | |
<script id="app-template" type="text/x-mustache-template"> | |
<div id="app"> | |
</div> | |
</script> | |
<!-- HEADER --> | |
<script id="header-template" type="text/x-mustache-template"> | |
<header class="header"> | |
<input | |
class="new-todo" | |
autocomplete="off" | |
spellcheck="false" | |
type="text" | |
placeholder="What needs to be done?" | |
> | |
</header> | |
</script> | |
<!-- LIST --> | |
<script id="list-template" type="text/x-mustache-template"> | |
<ul class="list"> | |
</ul> | |
</script> | |
<!-- TODO --> | |
<script id="todo-template" type="text/x-mustache-template"> | |
<li class="todo {{ isCompleted }} {{ class }}"> | |
<i class="far fa-check-circle toggle icon"> | |
</i> | |
<div class="name"> | |
{{ name }} | |
</div> | |
<i class="fas fa-times destroy icon"> | |
</i> | |
</li> | |
</script> | |
<!-- FOOTER --> | |
<script id="footer-template" type="text/x-mustache-template"> | |
<footer class="footer {{ hideFooter }}"> | |
<div><span class="bold">{{noOfItemsLeft}}</span> item(s) left</div> | |
<div class="radio-group"> | |
<div class="radio-option"> | |
<input id="all" type="radio" name = "filters" value="all" {{#filters.all.checked}}checked{{/filters.all.checked}}> | |
<label for="all"> All </label> | |
</div> | |
<div class="radio-option"> | |
<input id="active" type="radio" name = "filters" value="active" {{#filters.active.checked}}checked{{/filters.active.checked}}> | |
<label for="active"> Active </label> | |
</div> | |
<div class="radio-option"> | |
<input id="completed" type="radio" name = "filters" value="completed" {{#filters.completed.checked}}checked{{/filters.completed.checked}}> | |
<label for="completed"> Completed </label> | |
</div> | |
</div> | |
<button class="{{ showCompletedBtn }} clear-complete" text="clear completed" id="btncleartask"> Clear completed </button> | |
</footer> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
console.clear(); | |
// ---------------- | |
// STATE MANAGEMENT | |
// ---------------- | |
// INITIALIZING STATE | |
function defaultState() { | |
return { | |
todos: [ | |
{ | |
name: "example todo 1", | |
completed: true, | |
}, | |
{ | |
name: "example todo 2", | |
completed: false, | |
}, | |
], | |
}; | |
} | |
const store = { | |
// loadState calls defaultState internally | |
state: loadState(), | |
filter: { | |
value : 'all', | |
fn : () => true | |
}, | |
// MUTATING STATE | |
create(todo) { | |
const newTodo = { | |
name: "example todo", | |
completed: false, | |
...todo, | |
}; | |
this.state.todos.push(newTodo); | |
}, | |
destroy(todo) { | |
this.state.todos = this.state.todos.filter((item) => item !== todo); | |
}, | |
removecompletedtask() { | |
this.state.todos = this.state.todos.filter((item) => !item.completed); | |
}, | |
// filterTasks(fn) { | |
// return this.state.todos.filter((item) => fn.call(null,item)); | |
// }, | |
shouldShow(item) { | |
return this.filter.fn.call(null,item); | |
}, | |
// DERIVED STATE | |
hasTodos() { | |
return !!this.state.todos.length; | |
}, | |
}; | |
// ------------------------- | |
// RENDERING & EVENT BINDING | |
// ------------------------- | |
// RENDER APP | |
function renderApp() { | |
const id = "app-template"; | |
const app = templateToElement(id); | |
const header = renderHeader(); | |
addHeaderListeners(header); | |
app.appendChild(header); | |
const list = renderList(); | |
app.appendChild(list); | |
const footer = renderFooter(); | |
addFooterListeners(footer); | |
app.appendChild(footer); | |
return app; | |
} | |
// RENDER HEADER | |
function renderHeader() { | |
const id = "header-template"; | |
const header = templateToElement(id); | |
return header; | |
} | |
function addHeaderListeners(header) { | |
const listeners = {}; | |
listeners["keyup .new-todo"] = (e) => { | |
if (e.key === "Enter") { | |
const name = e.target.value.trim(); | |
if (!name) return; | |
store.create({ name }); | |
e.target.value = ""; | |
} | |
}; | |
return addListeners(header, listeners); | |
} | |
// RENDER LIST | |
function renderList() { | |
const id = "list-template"; | |
const list = templateToElement(id); | |
const todos = store.state.todos; | |
for (let todo of todos) { | |
let todoElement = renderTodo(todo); | |
addTodoListeners(todoElement, todo); | |
list.appendChild(todoElement); | |
} | |
return list; | |
} | |
// RENDER TODO | |
function renderTodo(todo) { | |
const id = "todo-template"; | |
const data = { | |
isCompleted: todo.completed ? "completed" : "", | |
class: store.shouldShow(todo) ? "" : "hidden", | |
...todo, | |
}; | |
const todoElement = templateToElement(id, data); | |
return todoElement; | |
} | |
function addTodoListeners(todoElement, todo) { | |
const listeners = {}; | |
listeners["click .toggle"] = (e) => { | |
todo.completed = !todo.completed; | |
}; | |
listeners["click .destroy"] = (e) => { | |
store.destroy(todo); | |
}; | |
return addListeners(todoElement, listeners); | |
} | |
// RENDER FOOTER | |
function renderFooter() { | |
const id = "footer-template"; | |
//console.log(store) | |
const data = { | |
hideFooter: !store.hasTodos() ? "hidden" : "", | |
noOfItemsLeft: store.state.todos.filter((todo) => !todo.completed).length, | |
showCompletedBtn: store.state.todos.filter((todo) => todo.completed).length > 0 ? '' : 'hide', | |
filters: { | |
all: { | |
label: 'All', | |
value: 'all', | |
checked: store.filter.value === 'all' | |
}, | |
active: { | |
label: 'Active', | |
value: 'active', | |
checked: store.filter.value === 'active' | |
}, | |
completed: { | |
label: 'Completed', | |
value: 'completed', | |
checked: store.filter.value === 'completed' | |
} | |
} | |
// TODO: Add footer template variables here | |
}; | |
const footer = templateToElement(id, data); | |
// addFooterListeners(footer); | |
return footer; | |
} | |
function addFooterListeners(footer) { | |
const listeners = {}; | |
listeners["click .clear-complete"] = (e) => { | |
store.removecompletedtask(); | |
}; | |
// TODO: Add footer event listeners here | |
listeners["change input[name='filters']"] = (e) => { | |
const val = e.target.value; | |
switch(val) { | |
case 'all': | |
store.filter = { | |
value : 'all', | |
fn : () => true | |
}; | |
break; | |
case 'completed': | |
store.filter = { | |
value : 'completed', | |
fn: (v) => v.completed | |
}; | |
break; | |
case 'active': | |
store.filter = { | |
value : 'active', | |
fn: (v) => !v.completed | |
}; | |
break; | |
} | |
updateApp(); | |
}; | |
return addListeners(footer, listeners); | |
} | |
/* | |
$$$$$$\ $$$$$$$$\ $$$$$$\ $$$$$$$\ $$\ | |
$$ __$$\\__$$ __|$$ __$$\ $$ __$$\ $$ | | |
$$ / \__| $$ | $$ / $$ |$$ | $$ |$$ | | |
\$$$$$$\ $$ | $$ | $$ |$$$$$$$ |$$ | | |
\____$$\ $$ | $$ | $$ |$$ ____/ \__| | |
$$\ $$ | $$ | $$ | $$ |$$ | | |
\$$$$$$ | $$ | $$$$$$ |$$ | $$\ | |
\______/ \__| \______/ \__| \__| | |
Everything below this comment is outside the scope | |
of the tech screen. If you're down here then you've | |
scrolled too far. | |
*/ | |
// 3 - render app & insert into DOM | |
updateApp(); | |
// 4 - template fetching & rendering | |
function getTemplate(id) { | |
return document.getElementById(id).innerHTML; | |
} | |
function stringToElement(string) { | |
return document.createRange().createContextualFragment(string).children[0]; | |
} | |
function templateToElement(template, data) { | |
const string = Mustache.render(getTemplate(template), data); | |
return stringToElement(string); | |
} | |
// 5 - persisting state between codepen iframe refreshes | |
function saveState() { | |
localStorage.setItem("appState", JSON.stringify(store.state)); | |
} | |
function loadState() { | |
const schema = defaultState(); | |
const newSchemaStr = JSON.stringify(schema); | |
const oldSchemaStr = localStorage.getItem("defaultState"); | |
if (newSchemaStr !== oldSchemaStr) { | |
localStorage.setItem("defaultState", newSchemaStr); | |
return schema; | |
} | |
const savedState = localStorage.getItem("appState"); | |
if (savedState) { | |
return JSON.parse(savedState); | |
} | |
return schema; | |
} | |
// 6 - updating the DOM with rendered app | |
function updateApp() { | |
let app = document.getElementById("app"); | |
if (!app) { | |
document.body.appendChild(stringToElement('<div id="app"></div>')); | |
app = document.getElementById("app"); | |
} | |
app.parentNode.replaceChild(renderApp(), app); | |
} | |
// 7 - "smart" event binding | |
function addListeners(element, map) { | |
Object.entries(map).forEach(([eventSelector, listener]) => { | |
const [event, ...selectorParts] = eventSelector.split(" "); | |
const selector = selectorParts.join(" "); | |
const nodes = element.querySelectorAll(selector); | |
nodes.forEach((node) => { | |
node.addEventListener(event, enhanceListener(listener)); | |
}); | |
}); | |
return element; | |
} | |
function enhanceListener(listener) { | |
return (e) => { | |
const preState = JSON.stringify(store.state); | |
listener(e); | |
const postState = JSON.stringify(store.state); | |
if (preState === postState) { | |
// no changes to state, skip re-rendering | |
return; | |
} | |
let newTodo = document.querySelector(".new-todo"); | |
let savedNewTodoVal = newTodo.value; | |
let refocusNewTodo = document.activeElement === newTodo; | |
updateApp(); | |
newTodo = document.querySelector(".new-todo"); | |
newTodo.value = savedNewTodoVal; | |
if (refocusNewTodo || !store.hasTodos()) { | |
newTodo.focus(); | |
const position = newTodo.value.length; | |
newTodo.setSelectionRange(position, position); | |
} | |
saveState(); | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/3.0.3/mustache.min.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:root { | |
--foreground: #fff; | |
--background: #f5f5f5; | |
--light-gray: #e6e6e6; | |
--medium-gray: #d9d9d9; | |
--heavy-gray: #737373; | |
--black: #313131; | |
--checked-green: #4bb543; | |
--destroy-red: #cc9a9a; | |
--destroy-red-hover: #af5b5b; | |
--selected-blue: #39c; | |
--border: 1px solid var(--light-gray); | |
--border-radius: 8px; | |
--button-padding: 6px 8px; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
outline: none; | |
} | |
body { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
font-family: Arial, sans-serif; | |
font-size: 16px; | |
background: var(--background); | |
color: var(--black); | |
user-select: none; | |
} | |
input, | |
button { | |
font-size: inherit; | |
font-family: inherit; | |
font-weight: inherit; | |
color: inherit; | |
background: none; | |
border: none; | |
} | |
ul { | |
list-style: none; | |
} | |
.icon { | |
width: 64px; | |
font-size: 20px; | |
cursor: pointer; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
flex-shrink: 0; | |
} | |
#app { | |
background: var(--foreground); | |
margin-top: 64px; | |
width: 640px; | |
border-radius: var(--border-radius); | |
box-shadow: 0 0 2px 0 rgba(30, 30, 30, 0.2), 0 2px 2px 0 rgba(30, 30, 30, 0.3); | |
} | |
.header { | |
display: flex; | |
} | |
.new-todo { | |
flex-grow: 1; | |
font-size: 24px; | |
padding: 16px 64px; | |
} | |
.new-todo::placeholder { | |
font-style: italic; | |
color: var(--light-gray); | |
} | |
.list { | |
border-top: var(--border); | |
} | |
.todo { | |
display: flex; | |
height: 64px; | |
align-items: stretch; | |
border-bottom: var(--border); | |
font-size: 24px; | |
} | |
.toggle { | |
font-size: 28px; | |
color: var(--light-gray); | |
padding-right: 8px; | |
} | |
.todo.completed .toggle { | |
color: var(--checked-green); | |
} | |
.name { | |
flex-grow: 1; | |
display: flex; | |
align-items: center; | |
} | |
.todo.completed .name { | |
font-style: italic; | |
text-decoration: line-through; | |
color: var(--medium-gray); | |
} | |
.destroy { | |
color: var(--destroy-red); | |
visibility: hidden; | |
} | |
.destroy:hover { | |
color: var(--destroy-red-hover); | |
} | |
.todo:hover .destroy { | |
visibility: visible; | |
} | |
.footer { | |
width: 100%; | |
min-height: 40px; | |
display: flex; | |
flex-direction: row; | |
align-items: center; | |
justify-content: space-between; | |
padding: 10px 15px; | |
} | |
.footer .bold { | |
font-weight: bold; | |
} | |
.footer .radio-group { | |
display: flex; | |
} | |
.footer .radio-option { | |
margin-right: 5px; | |
cursor: pointer; | |
} | |
/* .footer .radio-option:hover { | |
border: var(--border); | |
} */ | |
.footer .radio-option input { | |
display: none; | |
} | |
.footer .radio-option label { | |
color: var(--heavy-gray); | |
cursor: pointer; | |
} | |
/* .footer .radio-option.selected { | |
background-color: var(--light-gray); | |
} */ | |
input[name='filters']:checked + label { | |
background-color: var(--light-gray); | |
} | |
input[name='filters'] + label { | |
padding: var(--button-padding); | |
border-radius: var(--border-radius); | |
border: 1px solid transparent; | |
} | |
input[name='filters'] + label:hover{ | |
border: var(--border); | |
} | |
.clear-complete { | |
border: var(--border); | |
padding: var(--button-padding); | |
border-radius: var(--border-radius); | |
background-color: var(--foreground); | |
color: var(--heavy-gray); | |
cursor: pointer; | |
} | |
.clear-complete:hover { | |
background-color: var(--light-gray); | |
} | |
.clear-complete.hide { | |
display: none; | |
} | |
.invisible { | |
visibility: hidden; | |
} | |
.hidden { | |
display: none; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment