5 - Mark a to-do as "done"

Now that we can display our to-dos, we need a way to mark them as done. We will store this "done" state under "http://www.w3.org/2002/12/cal/ical#completed", with a date time as the object. Let's add a new column to our table.

// components/TodoList/index.jsx

const COMPLETED_PREDICATE = "http://www.w3.org/2002/12/cal/ical#completed";
// ...
<TableColumn
  property={COMPLETED_PREDICATE}
  dataType="datetime"
  header="Done"
  body={({ value }) => (
     <label>
       <input type="checkbox" />
      </label>
     )}
 />
// ...

For now this check box doesn't do anything. We need to add this property with a datetime value to our to-do thing when we click the checkbox. For that, we are going to need the URL for our to-do, so we can find it and add properties to it.

For this we are going to use the useThing hook from solid-ui-react.

We need to write a function that handles the adding of a completed property to our to-do thing. This function will take the to-do thing as an argument, add a completed property with a datetime value to it, set it in the dataset, and save the updated dataset.

// components/TodoList/index.jsx
import {
  addDatetime,
  getSourceUrl,
  saveSolidDatasetAt,
  setThing,
} from "@inrupt/solid-client";
import {
  Table,
  TableColumn,
  useSession,
} from "@inrupt/solid-ui-react";

function TodoList({ todoList, setTodoList }) {
  const { fetch } = useSession();
  // ...
  const handleCheck = async (todo) => {
	    const todosUrl = getSourceUrl(todoList);
	    const date = new Date();
	    const doneTodo = addDatetime(
	      todo,
	      "http://www.w3.org/2002/12/cal/ical#completed",
	      date
	    );
	    const updatedTodos = setThing(todoList, doneTodo, { fetch });
	    await saveSolidDatasetAt(todosUrl, updatedTodos, {
	      fetch,
	    });
	  };
  // ...
}

To access the to-do thing, we first need to create a custom body component for our TableColumn. It needs to be a proper component so that we can use the useThing hook, so let's put it outside the TodoList component but in the same file. We will also pass it a checked prop that we will use to set the checked property in the checkbox, and our handleCheck function.

// components/TodoList/index.jsx
import {
  Table,
  TableColumn,
  useThing,
  useSession,
} from "@inrupt/solid-ui-react";

function CompletedBody({ checked, handleCheck }) {
    const { thing } = useThing();
    return (
      <label>
        <input
          type="checkbox"
          checked={checked}
          onChange={() => handleCheck(thing)}
        />
      </label>
    );
  }

Now we can use this component in the body of our column:

// components/TodoList/index.jsx

<TableColumn
  property={COMPLETED_PREDICATE}
  dataType="datetime"
  header="Done"
  body={({ value }) => <CompletedBody checked={Boolean(value)} handleCheck={handleCheck} />}
 />

Now if you click on the checkbox, a property is added to the to-do. If you check the index.ttl file, you will see something like this:

<https://pod.inrupt.com/virginiabalseiro/todos/index.ttl#16089989748796144560745441174>
        <http://www.w3.org/2002/12/cal/ical#created>  "2020-12-26T16:09:34.880Z"^^xsd:dateTime ;
        <http://schema.org/text>  "Walk the dog" ;
        <http://www.w3.org/2002/12/cal/ical#completed>  "2020-12-26T16:09:39.853Z"^^xsd:dateTime .

We will also want to mark to-dos as "undone", so essentially removing this property from the to-do. For that we will need to modify our handleCheck function so that it removes the to-do if it was marked as done at the moment of clicking the checkbox, or add it if it was undone:

// components/TodoList/index.jsx
import {
  addDatetime,
  getDatetime,
  getSourceUrl,
  getThingAll,
  getUrl, 
  removeDatetime,
  saveSolidDatasetAt,
  setThing,
} from "@inrupt/solid-client";

const COMPLETED_PREDICATE = "http://www.w3.org/2002/12/cal/ical#completed";

function TodoList({ todoList, setTodoList }) {
const { fetch } = useSession();
// ...

const handleCheck = async (todo, checked) => {
    const todosUrl = getSourceUrl(todoList);
    let updatedTodos;
    let date;
    if (!checked) {
      date = new Date();
      const doneTodo = addDatetime(todo, COMPLETED_PREDICATE, date);
      updatedTodos = setThing(todoList, doneTodo, { fetch });
    } else {
      date = getDatetime(todo, COMPLETED_PREDICATE);
      const undoneTodo = removeDatetime(todo, COMPLETED_PREDICATE, date);
      updatedTodos = setThing(todoList, undoneTodo, { fetch });
    }
    const updatedList = await saveSolidDatasetAt(todosUrl, updatedTodos, {
      fetch,
    });
    setTodoList(updatedList);
  };
// ...
}

And we need to update the CompletedBody component as well:

// components/TodoList/index.jsx

function CompletedBody({ checked, handleCheck }) {
    const { thing } = useThing();
    return (
      <label>
        <input
          type="checkbox"
          checked={checked}
          onChange={() => handleCheck(thing, checked)}
        />
      </label>
    );
  }

Notice we need to use setTodoList here to update the to-do list, which we are getting from the App component.

There is one little bug though, and it's that each time we check a to-do, our list gets rearranged.

To fix this, we can sort the things array after we extract the things from the to-do list dataset. We want them sorted by the date they were created:

// components/TodoList/index.jsx

const todoThings = todoList ? getThingAll(todoList) : [];
  todoThings.sort((a, b) => {
    return (
      getDatetime(a, CREATED_PREDICATE) - getDatetime(b, CREATED_PREDICATE)
    );
  });

In addition, with the TableColumn component we can sort items by property. If we pass a sortable prop to one of our columns, we can arrange our to-dos based on that property, so let's use the "Created At" column and the to-do content column to see how it works. Let's also add a "To do" header to the content column so we can see what criteria we are sorting by. Our (almost) finished TodoList component now looks like this:

// components/TodoList/index.jsx

import {
  addDatetime,
  getDatetime,
  getSourceUrl,
  getThingAll,
  getUrl,
  removeDatetime,
  saveSolidDatasetAt,
  setThing,
} from "@inrupt/solid-client";
import {
  Table,
  TableColumn,
  useThing,
  useSession,
} from "@inrupt/solid-ui-react";
import React from "react";

const TEXT_PREDICATE = "http://schema.org/text";
const CREATED_PREDICATE = "http://www.w3.org/2002/12/cal/ical#created";
const COMPLETED_PREDICATE = "http://www.w3.org/2002/12/cal/ical#completed";

function CompletedBody({ checked, handleCheck }) {
    const { thing } = useThing();
    return (
      <label>
        <input
          type="checkbox"
          checked={checked}
          onChange={() => handleCheck(thing, checked)}
        />
      </label>
    );
  }


function TodoList({ todoList, setTodoList }) {
  const todoThings = todoList ? getThingAll(todoList) : [];
  todoThings.sort((a, b) => {
    return (
      getDatetime(a, CREATED_PREDICATE) - getDatetime(b, CREATED_PREDICATE)
    );
  });

  const { fetch } = useSession();

  const handleCheck = async (todo, checked) => {
    const todosUrl = getSourceUrl(todoList);
    let updatedTodos;
    if (!checked) {
      const date = new Date();
      const doneTodo = addDatetime(todo, COMPLETED_PREDICATE, date);
      updatedTodos = setThing(todoList, doneTodo, { fetch });
    } else {
      const date = getDatetime(todo, COMPLETED_PREDICATE);
      const undoneTodo = removeDatetime(todo, COMPLETED_PREDICATE, date);
      updatedTodos = setThing(todoList, undoneTodo, { fetch });
    }
    const updatedList = await saveSolidDatasetAt(todosUrl, updatedTodos, {
      fetch,
    });
    setTodoList(updatedList);
  };

  const thingsArray = todoThings
    .filter(
      (t) =>
        getUrl(t, "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") !==
        "http://www.w3.org/ns/ldp#RDFSource"
    )
    .map((t) => {
      return { dataset: todoList, thing: t };
    });
  if (!thingsArray.length) return null;

  return (
    <div className="table-container">
      <span className="tasks-message">
        Your to-do list has {thingsArray.length} items
      </span>
      <Table className="table" things={thingsArray}>
        <TableColumn property={TEXT_PREDICATE} header="To Do" sortable />
        <TableColumn
          property={CREATED_PREDICATE}
          dataType="datetime"
          header="Created At"
          body={({ value }) => value.toDateString()}
          sortable
        />
        <TableColumn
          property={COMPLETED_PREDICATE}
          dataType="datetime"
          header="Done"
          body={({ value }) => <CompletedBody checked={Boolean(value)} handleCheck={handleCheck} />}
        />
      </Table>
    </div>
  );
}

export default TodoList;
💡
Commit: d96de67

6 - Delete a to-do