Skip to content

Instantly share code, notes, and snippets.

@0xF5T9
Last active April 10, 2024 06:56
Show Gist options
  • Save 0xF5T9/db051553ac566a6cd7c8a10edb084d04 to your computer and use it in GitHub Desktop.
Save 0xF5T9/db051553ac566a6cd7c8a10edb084d04 to your computer and use it in GitHub Desktop.
Simple script that mimic Redux library.
<!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>
/**
* @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;
},
};
}
'use strict';
import { todo_app } from './todo.js';
console.log('Redux state: ', todo_app.getState());
* {
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;
}
}
'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
})">&times</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