DEV Community

loading...

A(nother) todo: Version 1.1

jhechtf profile image Jim Burbridge ・5 min read

Note This post refers to this branch

Our initial version of the project works, but things are kind of ugly.

For one thing, the entirety of the application lives inside the main.js file, and while that may not be an issue now, when the app is relatively simple, it's going to end up being a nightmare later on down the line as we add more and more features. I've kept a good summary in the README.md, but I'll copy/paste it over so we can talk about what is going on.

This branch is meant to be "I've been working in this project for awhile now, and I would like to refactor it to do some things differently."

The things that are different here

  1. The application now does not all live inside the main.js file. Instead all of the functionality has been put into it's own files, with the main.js file simply serving as the aggregator of those events.
  2. The renderer and the store have been largely separated. The render function here is what would be called a "Pure" function -- it keeps no track of state and will render the todos based solely off their values into the DOM.
  3. Largely there are no direct interactions with the todo variable outside of the todo.store.js file with 1 exception. I could imagine that a slightly more knowledgeable engineer would want to persist information between browser refreshes, and as such takes advantage of the localStorage. However, trying to pull this data from storage and use our todos store API was likely a bit more complex. I imagine someone having had a bit of time to fiddle around with adding the items in a loop, but "this way was faster and just worked" is definitely something I could see myself in my earlier years saying.

However, this definitely has some downsides. The largest thing is that since we are no longer doing this all in the main.js file there are multiple places where we need to call document.querySelector('#todo-list-container'), which even for a relatively small app was rather annoying to write.

The question then is: how do we make it so that we only have to call the query selector once? There are a few ways, if you are starting off with Javascript and ESM think of a few and test them out.

Beyond that, I think a lot of developers after looking at this code will say something like "I just feel like there should be a way to get the rendering function to run when the data updates... there has to be some way to do that, right?"

Also at this point many people are probably feeling the itch of wanting to make the UI a bit prettier, with a navbar and things...

So let's look at what our src/ directory looks like now

Alt Text

Well, there's definitely more files!

Notice that each one of the files is a bit more specific. There are two files that start with todos, but one very clearly says it is the "store"(todos.store.js) while the other is the "render" (todos.render.js). The store now handles actions related to the actual array of todos. This means adding, removing, and marking todos as done is all stored in one logical place.

// todos.store.js
import renderTodos, { itemCache } from './todos.render';

/** @type {import('./todos.render').todo[]} */
const todos = [];
// Get an item from local storage
const stored = localStorage.getItem('todos__visited');

// if we do not have a value from our localStorage, add an example to the array.
if (!stored) {
  todos.push({
    title: 'Example todo',
    done: false,
    description: 'Extra details you may want the user to know.'
  });
  // make sure to set the storage so that other people do not see it again.
  localStorage.setItem('todos__visited', 'true');
}

export default todos;
/**
 * 
 * @param {string} title
 * @param {stirng} description
 * @returns {boolean} true if added / false if not.
 */
export function addTodo(title, description = null) {
  try {
    todos.push({ title, description: description || '', done: false });
    // render tick
    renderTodos(document.querySelector('#todo-list-container'), todos);
    return true;
  } catch (e) {
    return false;
  }

}

/**
 * 
 * @param {number} todo the index of the todo
 * @returns {-1 | 0 | 1} 1 for success, 0 for failure, -1 if the item could not be found. Helpful for debugging and not much else.
 */
export function removeTodo(todo) {
  // if we do not have this todo, don't try anything further
  if (!todos[todo]) return -1;
  // try to do this
  try {
    (itemCache.get(todos[todo]) || []).forEach(el => el && el.remove && el.remove());
    todos.splice(todo, 1);
    renderTodos(document.querySelector('#todo-list-container'), todos);
    return 1;
  } catch (e) {
    return 0;
  }
  // re-render
}
/**
 * 
 * @param {number} index 
 * @returns {boolean} True if toggled, false if there was an error.
 */
export function toggleTodo(index) {
  if (!todos[index]) return false;
  todos[index].done = !todos[index].done;
  renderTodos(document.querySelector('#todo-list-container'), todos);
}
Enter fullscreen mode Exit fullscreen mode

The .render. file focuses solely on taking the data and rendering it out to the provided <dl> element.

Other than that, there isn't really a whole lot different about this branch. It was just meant to serve as a logical thing, going from the "but it works!" to the "well, now things are a bit sectioned out and we've separated out concerns."

// todos.render.js
import styles from './styles.module.css';
/**
 * @typedef todo
 * @property {string} title
 * @property {boolean} done
 * @property {string} description
 */

/** @type {Map<todo, [HTMLElement, HTMLElement]} */
export const itemCache = new Map();

/**
 * @param {HTMLDListElement} todo
 * @param {todo[]} todos 
 * @returns {void} 
 */
export default function renderTodos(todo, todos) {
  const els = todos.flatMap((todo, index) => {
    // either we can grab this item from the cache OR we create a new one.
    const [dt, dd] = itemCache.get(todo) || [document.createElement('dt'), document.createElement('dd')];
    if (!itemCache.has(todo)) {
      // set the innerHTML, along with creating the delete span.
      const span = document.createElement('span');
      // Very generic close button
      span.innerHTML = '&times;'
      span.setAttribute('role', 'delete');
      dt.append(
        document.createTextNode(todo.title),
        span
      );

      // Add necessary classes.
      dt.classList.add(styles['todo-list-item']);
      dt.classList.toggle(styles['todo-list-complete'], todo.done);

      //update description element if we have a description
      if (todo.description) {
        dd.innerHTML = todo.description;
      }
      itemCache.set(todo, [dt, dd]);
    }

    dt.dataset.key = dd.dataset.key = index;
    dt.classList.toggle(styles['todo-list-complete'], todo.done);

    return [dt, dd];
  });
  // store the data so that it persists between sessions.
  localStorage.setItem('todos__data', JSON.stringify(todos));
  // append the items to the list.
  todo.append(...els);
}

Enter fullscreen mode Exit fullscreen mode

Please do view the gitlab repository. I am working on getting a live preview of the "app" up and going but the workflow here is a bit weird.

Discussion (0)

pic
Editor guide