Introducing Shapes
LINCD introduces Shapes to make writing and accessing data from the graph easier. Shapes also help with data validation.
For the purpose of this tutorial we'll just use an existing shape to see how it simplifies our code.
Shapes can be made for specific classes. For example lincd-foaf
already exports a shape for foaf:Person
.
You can import it with
import {Person} from 'lincd-foaf/lib/shapes/Person';
This specific Person shape helps you to create new persons and to set and get their properties without needing to know which Classes and Properties to use from the foaf
ontology.
Let's replace our array of NamedNodes
with Persons
.
Change
let [persons, setPersons] = useState<NamedNode[]>([]);
to
let [persons, setPersons] = useState<Person[]>([]);
After doing this, typescript will immediately guide us what needs changing next. The output of your yarn start
command should now show two errors.
The first one points us to
setPersons(persons.concat(newPerson));
We can no longer add a NamedNode
to persons.
Makes sense! Instead of creating a new NamedNode
, let's create a new Person
. We can use a normal constructor for that:
So replace:
let newPerson = NamedNode.create();
with
let newPerson = new Person();
You should now have one error left.
But whilst we're here, let's make two more changes:
First, new Person()
automatically creates a new NamedNode for us, and set's the property rdf:type
to foaf:Person
, so we don't need to do that ourselves anymore.
So you can remove this line:
newPerson.set(rdf.type, foaf.Person);
For the second change we'll use property accessors:
Property accessors
Shapes come with helpful methods to get/set certain properties of the node.
In this case Person implements get/set name
, which gets/sets the property foaf:name
.
So we can replace
newPerson.setValue(foaf.name, newName);
with
newPerson.name = newName;
Accessing the node of a shapes
Ok, now let's look at that last error.
Property 'uri' does not exist on type 'Person'.
This makes sense as well. Before we accessed the URI of the NamedNode, but now we have a Person shape instance.
You can think of Shapes as handy objects that wrap a Node. When we created a new Person()
this created a new NamedNode
for us.
You can access the node of a shape with shape.namedNode
.
So in this case we can replace {person.uri}
with {person.namedNode.uri}
Note that our app now builds without errors, even though we still have many other bits of unchanged code that now work with Person instances instead of NamedNodes.
This is because Shapes expose most of the same functionality for getting and setting properties as NamedNodes.
For example:
person.getAll(foaf.name)
works if person
is a NamedNode
, but also works if person
is an instance of the Person
shape.
Using typescript interfaces
This is handy, however, some parts of our app are not working even though typescript does not complain. That's because there are places where we didn't specify any types, so typescript has no idea.
This would be a good point to create some extra clarity so that typescript can help us identify what else to change.
Go to the definition of SelectPerson
and change the first line to this:
interface SelectPersonProps
{
persons: Person[],
setPerson: (person: Person) => void,
value: Person
}
const SelectPerson = function({ persons,setPerson,value }: SelectPersonProps) {
//...
This states that we're expecting the types of our props to match with the newly defined SelectPersonProps
interface.
Specifically, this states that all these props expect instance(s) Persons
.
This causes a bunch of new errors.
Let's start with the SelectPerson
component itself.
If you scan the errors you will see some similar errors to before, stating that person
has no property uri
.
So first replace person.uri
with person.namedNode.uri
within SelectPerson
Also, for the onChange
handler of the <select>
tag, we now need to supply a Person
instance to setPerson()
instead of a NamedNode
.
You can get an instance of Person
from a specific NamedNode
with new Person(namedNode)
So in this case for the onChange handler you can use
setPerson(new Person(NamedNode.getOrCreate(v.currentTarget.value)))
Or you can use the shapes own shorthand function getFromURI()
:
setPerson(Person.getFromURI(v.currentTarget.value))
If you look at the remaining errors you will see that there are all related to lines that use the SelectPerson
component.
Specifically, it's not happy with the properties it receives.
This is because the states person1
and person2
are still NamedNodes, not Persons. So let's update those definitions.
let [person1, setPerson1] = useState<Person>(null);
let [person2, setPerson2] = useState<Person>(null);
That solves all the previous errors, but the compiler will now show a new error.
person1.set(foaf.knows, person2);
is no longer valid, because the value needs to be Literal
or NamedNode
, not a Person
.
You can replace person2
with person2.namedNode
to solve this error.
But Person
also has several helper methods to maintain who a person 'knows'.
So when creating the connection between 2 people, you can replace
person1.set(foaf.knows, person2.namedNode);
with
person1.addKnows(person2);
Cleaning up
Finally, we are still accessing foaf properties manually in a few places.
Search Home.tsx
for foaf.
and see for each result how you can simplify it.
You can replace every occurrence of
person.getValue(foaf.name)
withperson.name
person.getAll(foaf.knows)
withperson.knows
You may get one more error now.
Since person.knows
returns a set of Persons, you'll have to replace
{person.knows.map((friend: NamedNode) =>
with
{person.knows.map((friend: Person) =>
Only shapes, no more ontologies!
When you're done you may see that the foaf
import in the top of your file has grayed out, since it is no longer used. You can now remove the import of foaf
.
This is great news since it means the code of your component no longer needs to know exactly which properties and classes of foaf
to use. That responsibility is now handled by the Person
shape.
When you build Linked Data apps with LINCD, you will typically use existing Shapes and use their methods. This means you don't have to think much about ontologies & properties.
In the next and final step we will make our graph persistent across page refreshes.
Example code for this step
import {useState} from 'react';
import {loadData} from 'lincd-foaf/lib/ontologies/foaf';
import {Person} from 'lincd-foaf/lib/shapes/Person';
loadData();
export default function Home() {
let [newName, setNewName] = useState<string>('');
let [persons, setPersons] = useState<Person[]>([]);
let [person1, setPerson1] = useState<Person>(null);
let [person2, setPerson2] = useState<Person>(null);
let [bool, setBool] = useState(false);
let forceUpdate = () => setBool(!bool);
let createNewPerson = function (e) {
//create a new named node for the new person
let newPerson = new Person();
//create a new edge from this new person to its own name, using the foaf.name property as the connecting edge
//to store a string in the graph, we use a Literal node.
newPerson.name = newName;
//add the person to the array and update the state
setPersons(persons.concat(newPerson));
};
let onSubmitConnection = function (e) {
if (person1 && person2) {
person1.addKnows(person2);
forceUpdate();
}
};
return (
<div>
<h1>Persons</h1>
<form>
<input value={newName} onChange={(e) => setNewName(e.currentTarget.value)} type="text" placeholder="name" />
<input type="button" value="Add new person" onClick={createNewPerson} />
</form>
<hr />
<ul>
{persons.map((person) => {
return (
<li key={person.namedNode.uri}>
{person.name} has {person.knows.size} friends:{' '}
{person.knows.map((friend: Person) => friend.name).join(', ')}{' '}
</li>
);
})}
</ul>
{persons.length > 1 && (
<form>
<SelectPerson persons={persons} setPerson={setPerson1} value={person1}></SelectPerson>
has friend
<SelectPerson persons={persons} setPerson={setPerson2} value={person2}></SelectPerson>
<input type="button" value="Make connection" onClick={onSubmitConnection} />
</form>
)}
</div>
);
}
interface SelectPersonProps {
persons: Person[];
setPerson: (person: Person) => void;
value: Person;
}
const SelectPerson = function ({persons, setPerson, value}: SelectPersonProps) {
return (
<select value={value?.namedNode.uri} onChange={(v) => setPerson(Person.getFromURI(v.currentTarget.value))}>
<option value={null}>---</option>
{persons.map((person) => (
<option value={person.namedNode.uri} key={person.namedNode.uri}>
{person.name}
</option>
))}
</select>
);
};