Skip to content

Instantly share code, notes, and snippets.

@VimalKumarS
Created July 15, 2020 05:21
Show Gist options
  • Save VimalKumarS/757d396e3728fae48614676fe386888a to your computer and use it in GitHub Desktop.
Save VimalKumarS/757d396e3728fae48614676fe386888a to your computer and use it in GitHub Desktop.
sq soln
<!--
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>
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();
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/3.0.3/mustache.min.js"></script>
: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