Last active
April 10, 2024 06:56
-
-
Save 0xF5T9/db051553ac566a6cd7c8a10edb084d04 to your computer and use it in GitHub Desktop.
Simple script that mimic Redux library.
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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<link rel="stylesheet" href="style.css" /> | |
<title>TODO List</title> | |
</head> | |
<body> | |
<div id="app"></div> | |
</body> | |
<script type="module" src="script.js"></script> | |
</html> |
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
/** | |
* @file redux.js | |
* @description Simple script that mimic Redux library. | |
*/ | |
/** | |
* Creates a Redux store that holds the complete state tree of your app. | |
* There should only be a single store in your app. | |
* @param {Function} reducer A root reducer function that returns the next state tree, | |
* given the current state tree and an action to handle. | |
*/ | |
export function createStore(reducer) { | |
let state = reducer(), | |
subscribers = []; | |
return { | |
/** | |
* Get the current state. | |
*/ | |
getState() { | |
return state; | |
}, | |
/** | |
* Dispatch an action. | |
* @param {String} action Specifies the action to be dispatched. | |
* @param {...*} args Specifies the action arguments. | |
*/ | |
dispatch(action, ...args) { | |
state = reducer(state, action, ...args); | |
subscribers.forEach((callback) => callback()); | |
}, | |
/** | |
* Add a subscriber callback function. | |
* @param {Function} callbackSpecifies the callback function. | |
* @returns {Boolean} Returns true if the subscriber function is successfully added, otherwise returns false. | |
* @note The subscriber functions are invoked after an action is dispatched. | |
*/ | |
addSubscriber(callback) { | |
let are_all_operation_success = false, | |
error_message = ''; | |
while (!are_all_operation_success) { | |
if (!(typeof callback === 'function')) { | |
error_message = `The '${callback}' argument is not a function.`; | |
break; | |
} | |
if (!callback.name) { | |
error_message = `The function must have a name.`; | |
break; | |
} | |
let is_function_name_duplicate = false; | |
for (const subscriber of subscribers) { | |
if (callback.name === subscriber.name) { | |
is_function_name_duplicate = true; | |
break; | |
} | |
} | |
if (is_function_name_duplicate) { | |
error_message = `A function with the same name is already exists.`; | |
break; | |
} | |
subscribers.push(callback); | |
are_all_operation_success = true; | |
} | |
if (!are_all_operation_success) console.error(error_message); | |
return are_all_operation_success; | |
}, | |
/** | |
* Remove a subscriber callback function. | |
* @param {String} callbackName Specifies the subscriber function name. | |
* @returns {Boolean} Returns true if the subscriber function is successfully removed, otherwise returns false. | |
*/ | |
removeSubscriber(callbackName) { | |
for (const index in subscribers) { | |
if (subscribers[index].name === callbackName) { | |
subscribers.splice(index, 1); | |
return true; | |
} | |
} | |
return false; | |
}, | |
}; | |
} |
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
'use strict'; | |
import { todo_app } from './todo.js'; | |
console.log('Redux state: ', todo_app.getState()); |
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
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
html { | |
font-size: 1rem; | |
} | |
#app { | |
display: inline-flex; | |
flex-flow: column nowrap; | |
margin: 20px; | |
& > *:not(:first-child) { | |
margin-top: 10px; | |
} | |
& .todo-item-edit, | |
& .todo-item-delete { | |
cursor: pointer; | |
} | |
& .todo-button { | |
padding: 4px; | |
} | |
& .todo-filter-text { | |
text-transform: capitalize; | |
} | |
} |
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
'use strict'; | |
import { createStore } from './redux.js'; | |
// Todo item class. | |
export class TodoItem { | |
id; | |
text; | |
isChecked; | |
constructor(id, text, isChecked = false) { | |
this.id = id; | |
this.text = text; | |
this.isChecked = isChecked; | |
} | |
} | |
// The default state that will be used if there is none on the local storage. | |
const init_state = { | |
// The todo items. | |
todos: [ | |
new TodoItem(1, 'Todo 1', true), | |
new TodoItem(2, 'Todo 2', false), | |
new TodoItem(3, 'Todo 3', true), | |
new TodoItem(4, 'Todo 4', true), | |
new TodoItem(5, 'Todo 5', false), | |
], | |
filter: 'all', // Appropriated filters: 'all' | 'checked' | 'unchecked' | |
}; | |
// The variable that saves and indicates the last dispatched action. | |
var last_action; | |
// The reducer function that will be passed when creating the redux store. | |
function reducer(state = init_state, action, ...args) { | |
// Save the last action. | |
last_action = action; | |
// Process the action. | |
switch (action) { | |
// Initialize the application, this action should be the first dispatched action. | |
case 'init': { | |
// Check if a valid state is available in the local storage. If so, load it. | |
const local_storage_todo_app = | |
window.localStorage.getItem('todo_app'); | |
if (local_storage_todo_app) | |
return JSON.parse(local_storage_todo_app); | |
// If no valid state available from the local storage, | |
// save the current state to the local storage. | |
window.localStorage.setItem('todo_app', JSON.stringify(state)); | |
return state; | |
} | |
// Set render filter. | |
case 'setfilter': { | |
const valid_filters = ['all', 'checked', 'unchecked'], // The valid filters. | |
[filter] = args; | |
state.filter = valid_filters.includes(filter) ? filter : 'all'; | |
window.localStorage.setItem('todo_app', JSON.stringify(state)); | |
return state; | |
} | |
// Update a todo item's checked status. | |
case 'updatestatus': { | |
const [id, element] = args, | |
todo = state.todos.find((element) => element.id == id); | |
todo.isChecked = element.checked; | |
window.localStorage.setItem('todo_app', JSON.stringify(state)); | |
return state; | |
} | |
// Add a new todo item. | |
case 'add': { | |
const [todo_text, todo_ischecked] = args, | |
new_id = | |
state.todos.reduce((acc, element) => { | |
return acc > element.id ? acc : element.id; | |
}, 0) + 1; // New id = biggest existing id number + 1 | |
// The todo text must be not a empty string. | |
if (todo_text != '') { | |
state.todos.push({ | |
id: new_id, | |
text: todo_text, | |
isChecked: todo_ischecked, | |
}); | |
window.localStorage.setItem('todo_app', JSON.stringify(state)); | |
} | |
return state; | |
} | |
// Edit todo item's text. | |
case 'edit': { | |
const [id, new_todo_text] = args; | |
state.todos.every((element, index, array) => { | |
if (element.id == id) { | |
array[index].text = new_todo_text; | |
return false; | |
} | |
return true; | |
}); | |
window.localStorage.setItem('todo_app', JSON.stringify(state)); | |
return state; | |
} | |
// Remove a todo item. | |
case 'remove': { | |
const [id] = args; | |
state.todos.every((element, index, array) => { | |
if (element.id == id) { | |
array.splice(index, 1); | |
return false; | |
} | |
return true; | |
}); | |
window.localStorage.setItem('todo_app', JSON.stringify(state)); | |
return state; | |
} | |
// Check all todo items. | |
case 'checkall': { | |
state.todos.forEach((element, index, array) => { | |
if (!element.isChecked) array[index].isChecked = true; | |
}); | |
window.localStorage.setItem('todo_app', JSON.stringify(state)); | |
return state; | |
} | |
// Uncheck all todo items. | |
case 'uncheckall': { | |
state.todos.forEach((element, index, array) => { | |
if (element.isChecked) array[index].isChecked = false; | |
}); | |
window.localStorage.setItem('todo_app', JSON.stringify(state)); | |
return state; | |
} | |
default: | |
return state; | |
} | |
} | |
// Ignore rendering after certain actions. | |
const render_ignored_actions = ['updatestatus']; | |
// The render function will be passed to the redux storage as a subscriber function. | |
function render() { | |
// Use the 'render_ignored_actions' and 'last_action' variables to check if rendering is necessary. | |
if (render_ignored_actions.includes(last_action)) return; | |
const state = todo_app.getState(), | |
filter = state.filter, | |
todos = state.todos.filter((element) => { | |
switch (filter) { | |
case 'all': | |
return true; | |
case 'checked': { | |
if (element.isChecked) return true; | |
break; | |
} | |
case 'unchecked': { | |
if (!element.isChecked) return true; | |
break; | |
} | |
} | |
return false; | |
}); | |
document.querySelector('#app').innerHTML = ` | |
<h2 class="todo-heading-text">TODO List (${ | |
state.todos.length | |
} - <span class="todo-filter-text">${state.filter}</span>):</h2> | |
<div class="todo-option-buttons"> | |
<button class="todo-button" onclick="todo_app.dispatch('setfilter', 'all')">Filter: All</button> | |
<button class="todo-button" onclick="todo_app.dispatch('setfilter', 'checked')">Filter: Checked</button> | |
<button class="todo-button" onclick="todo_app.dispatch('setfilter', 'unchecked')">Filter: Unchecked</button> | |
<button class="todo-button" onclick="let todo_text = prompt('Enter the todo text'); if (todo_text) todo_app.dispatch('add', todo_text)">Add TODO</button> | |
</div> | |
${todos | |
.map( | |
(element) => ` | |
<div class="todo-item"> | |
<input id="td-${ | |
element.id | |
}" type="checkbox" onchange="todo_app.dispatch('updatestatus', ${ | |
element.id | |
}, this)" ${ | |
element.isChecked ? 'checked' : '' | |
}> <label for="td-${element.id}">${element.text}</label> | |
<span class="todo-item-edit" onclick="let new_todo_text = prompt('Enter the new todo text'); if(new_todo_text) todo_app.dispatch('edit', ${ | |
element.id | |
}, new_todo_text)">✎</span> | |
<span class="todo-item-delete" onclick="todo_app.dispatch('remove', ${ | |
element.id | |
})">×</span> | |
</div> | |
` | |
) | |
.join('')} | |
`; | |
} | |
export const todo_app = createStore(reducer); // Create the redux storage. | |
window.todo_app = todo_app; // Store a reference 'todo_app' to the global scope. | |
todo_app.addSubscriber(render); // Add the render function to the subscribers. | |
todo_app.dispatch('init'); // Initialize the application. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment