Skip to main content

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) with person.name
  • person.getAll(foaf.knows) with person.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>
&nbsp;has friend&nbsp;
<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>
);
};