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;
2c00ffb