4 - Display to-dos

To display the to-dos we are going to use two additional components from solid-ui-react: the Table and TableColumn components.

The Table component has a required prop things, which is an array of objects containing each thing in the dataset and the dataset they belong to. It should look like this:

[{ dataset: myDataset, thing: thing1 }, { dataset: myDataset, thing: thing2 } ];

In our case, we already have the dataset (our to-do list), but now we need to extract the things from it and map them to obtain an array that looks like the above.

The place where we are fetching our to-dos is in the AddTodo component, but we are going to create a component called TodoList to display our table, so we are going to need to use the list there too. Let's move the useEffect to the App component, so we can pass todoList and setTodoList to the components that need them. We are adding a check to see if the user is logged out, in which case we exit the useEffect.

// App.js

import React, { useEffect, useState } from "react";
import {
  LoginButton,
  LogoutButton,
  Text,
  useSession,
  CombinedDataProvider,
} from "@inrupt/solid-ui-react";
import { getSolidDataset, getUrlAll, getThing } from "@inrupt/solid-client";
import AddTodo from "./components/AddTodo";
import TodoList from "./components/TodoList";
import { getOrCreateTodoList } from "./utils";

const STORAGE_PREDICATE = "http://www.w3.org/ns/pim/space#storage";

const authOptions = {
  clientName: "Solid Todo App",
};

function App() {
  const { session } = useSession();
  const [todoList, setTodoList] = useState();

  useEffect(() => {
    if (!session || !session.info.isLoggedIn) return; 
    (async () => {
      const profileDataset = await getSolidDataset(session.info.webId, {
        fetch: session.fetch,
      });
      const profileThing = getThing(profileDataset, session.info.webId);
      const podsUrls = getUrlAll(profileThing, STORAGE_PREDICATE);
      const pod = podsUrls[0];
      const containerUri = `${pod}todos/`;
      const list = await getOrCreateTodoList(containerUri, session.fetch);
      setTodoList(list);
    })();
  }, [session, session.info.isLoggedIn]);

  return (
    <div className="app-container">
      {session.info.isLoggedIn ? (
        <CombinedDataProvider
          datasetUrl={session.info.webId}
          thingUrl={session.info.webId}
        >
          <div className="message logged-in">
            <span>You are logged in as: </span>
            <Text
              properties={[
                "http://xmlns.com/foaf/0.1/name",
                "http://www.w3.org/2006/vcard/ns#fn",
              ]}
            />
            <LogoutButton />
          </div>
          <section>
            <AddTodo todoList={todoList} setTodoList={setTodoList} />
            <TodoList todoList={todoList} setTodoList={setTodoList} />
          </section>
        </CombinedDataProvider>
      ) : (
        <div className="message">
          <span>You are not logged in. </span>
          <LoginButton
            oidcIssuer="https://broker.pod.inrupt.com/"
            redirectUrl={window.location.href}
            authOptions={authOptions}
          />
        </div>
      )}
    </div>
  );
}

export default App;

And our AddTodo component will now look like this:

// components/AddTodo/index.jsx

import {
  addDatetime,
  addStringNoLocale,
  createThing,
  getSourceUrl,
  saveSolidDatasetAt,
  setThing,
} from "@inrupt/solid-client";
import { useSession } from "@inrupt/solid-ui-react";
import React, { useState } from "react";

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

function AddTodo({ todoList, setTodoList }) {
  const { session } = useSession();
  const [todoText, setTodoText] = useState("");

  const addTodo = async (text) => {
    const indexUrl = getSourceUrl(todoList);
    const todoWithText = addStringNoLocale(createThing(), TEXT_PREDICATE, text);
    const todoWithDate = addDatetime(
      todoWithText,
      CREATED_PREDICATE,
      new Date()
    );
    const updatedTodoList = setThing(todoList, todoWithDate);
    const updatedDataset = await saveSolidDatasetAt(indexUrl, updatedTodoList, {
      fetch: session.fetch,
    });
    setTodoList(updatedDataset);
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    addTodo(todoText);
    setTodoText("");
  };

  const handleChange = (e) => {
    e.preventDefault();
    setTodoText(e.target.value);
  };

  return (
      <form className="todo-form" onSubmit={handleSubmit}>
        <label htmlFor="todo-input">
          <input
            id="todo-input"
            type="text"
            value={todoText}
            onChange={handleChange}
          />
        </label>
        <button className="add-button" type="submit">Add Todo</button>
      </form>
  );
}

export default AddTodo;

Notice we added a line in handleSubmit to set the text to an empty string after we have added the to-do, so that the input box content is cleared.

For our TodoList component, we are going to need the Table and TableColumn components from solid-ui-react. We're also going to use getThingAll from solid-client to extract the things from our dataset so we can create the array we need for the Table. For now let's just display the number of things our dataset contains:

// components/TodoList/index.jsx

import { getThingAll } from "@inrupt/solid-client";
import { Table, TableColumn } from "@inrupt/solid-ui-react";
import React, { useEffect, useState } from "react";

function TodoList({ todoList }) {
	const todoThings = todoList ? getThingAll(todoList) : [];

  return <div>Your to-do list has {todoThings.length} items</div>;
}

export default TodoList;

Once you add the TodoList component, you might need to stop and start your app again with npm start if you see any errors. To see if it works, try adding to-dos and see if the number of item changes. You will notice the length of the array indicates one item more than the number of to-dos you have created. This is because there is another item in the to-dos dataset that is not a to-do. We will fix that later.

To use the Table component, we need to create the array with the objects we need and pass it to the table:

// components/TodoList/index.jsx

function TodoList({ todoList }) {
// ...
const thingsArray = todoThings.map((t) => {
    return { dataset: todoList, thing: t };
  });
// ...
}

But to actually display anything we need to use the TableColumn component inside the Table. The TableColumn component needs a required prop property, which is the property we want to display. This means the predicate under which the data we want to show is stored. In the case of our to-dos, we have two properties: the text and the date in which the to-do was created, stored under http://schema.org/text and http://www.w3.org/2002/12/cal/ical#created respectively:

// ./components/TodoList/index.jsx

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

function TodoList({ todoList }) {
// ...
<div>
  Your to-do list has {todoThings.length} items
  <Table things={thingsArray}>
    <TableColumn property={TEXT_PREDICATE} />
    <TableColumn property={CREATED_PREDICATE} />
   </Table>
 </div>
// ...
}

You will notice two things: first, the headers. The TableColumn accepts an optional prop header, with which we can set the header of the column. If we don't pass this prop, the header will be the URL of the predicate for that property. You can also pass an empty string if you don't want headers. Let's do that for the text of our to-do, and pass "Created" for the date.

Second, there is nothing displayed for the created at column. This is because TableColumn also accepts an optional prop dataType, which defaults to 'string' if not set, but the data we have is not a string but a datetime, so we need to set it:

// components/TodoList/index.jsx

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

function TodoList({ todoList }) {
// ...
	<div className="table-container">
		<span className="tasks-message">
		  Your to-do list has {todoThings.length} items
		</span>
	  <Table className="table" things={thingsArray}>
	    <TableColumn property={TEXT_PREDICATE} header="" />
	     <TableColumn
	       property={CREATED_PREDICATE}
	       dataType="datetime"
	       header="Created At"
	      />
	   </Table>
	 </div>
// ...
}

Finally, it would be nice if we could format the date, though, so it could look like this: Sat Dec 26 2020, instead of a such a longer string. The body prop allows us to pass a custom body to the column, where we can format the value we get for each cell. This prop is super useful when we want to pass a custom component the cell, for instance a link, instead of the value as it comes from the dataset.

Before we do this though, we need to filter out the non-todo things we have in our dataset. If you look at the index.ttl file you will notice a line that looks like this:

<https://pod.inrupt.com/virginiabalseiro/todos/index.ttl>
        rdf:type  ldp:RDFSource .

That is automatically added by the server to identify what type of resource we're dealing with, but it will throw an error when we try to format the date, because it won't have a created property. This is also why we had an extra item in our to-dos count. So we need to filter out all the things containing a property type with the value RDFSource.

We will also switch from todoThing to thingsArray in the message displaying the number of items, since otherwise we are counting the type as well.

Our TodoList component now looks like this:

// ./components/TodoList/index.jsx

import React from "react";
import { getThingAll, getUrl } from "@inrupt/solid-client";
import { Table, TableColumn } from "@inrupt/solid-ui-react";

function TodoList({ todoList }) {
  const todoThings = todoList ? getThingAll(todoList) : [];

  const TEXT_PREDICATE = "http://schema.org/text";
  const CREATED_PREDICATE = "http://www.w3.org/2002/12/cal/ical#created";
  const TODO_TYPE_URL = "http://www.w3.org/2002/12/cal/ical#Vtodo";
  const TYPE_URL = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";

  const thingsArray = todoThings.filter((t) => getUrl(t, TYPE_URL) === TODO_TYPE_URL).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="" />
        <TableColumn
          property={CREATED_PREDICATE}
          dataType="datetime"
		      header="Created At"
          body={({ value }) => value.toDateString()}
        />
      </Table>
    </div>
  );
}

export default TodoList;

💡
Commit: 2c00ffb

5 - Mark a to-do as "done"