Skip to main content

Connecting nodes

Now that we have a list of people, let's make it possible to connect any of those people as friends.

First, create another form which shows two separate <select> fields to select two people.

To avoid duplicate code, let's create a new functional component <SelectPerson /> which returns a <select> field with an <option> tag for each person in state.

Just like with printing the names, you can generate the <option> tags by looping over persons.map and using person.getValue(this.hasName).

Something like this:

<form>
<SelectPerson persons={persons} />
&nbsp;has friend&nbsp;
<SelectPerson persons={persons} />
<input type="button" value="Make connection" />
</form>
const SelectPerson = function ({persons}) {
return (
<select>
<option value={null}>---</option>
{persons.map((person) => (
<option key={person.toString()}>{person.getValue(hasName)}</option>
))}
</select>
);
};

Matching input back to a node

Let's store each of the selected persons in state, just like we did with the name of a new person.

let [person1, setPerson1] = useState<NamedNode>(null);
let [person2, setPerson2] = useState<NamedNode>(null);

However, a <select> field in react only works with a string value. So we need to convert our NamedNodes to a string, and then back again to a NamedNode once we've selected a value.

The <option> tags currently show the name of the person, so we could potentially use the names as values and then try to find back the right person from the selected name:

//DO NOT DO THIS
let selectedPerson = persons.find((person) => {
person.getValue(this.hasName) === name;
});

This gets problematic though when there are two people with the same name.

Better is to use the URI of the named node, as it's unique and allows us to quickly find the right node back. To do that we can use

NamedNode.getOrCreate(uri);

Note: Since URI's are unique globally, NamedNode.getOrCreate() gives you the previously created node, or creates the node if this is the first time its requested

So let's put that to action and use the URI of the named nodes as values:

  1. Let's change the <option> fields. Set the value of the option to the URI of the NamedNode (person.uri).
  2. Set the value of the <select> tags to the right strings, which are person1.uri and person2.uri
  3. Implement an onChange handler for each <select> tag. The URI of the selected person will be in e.currentTarget.value. Use that with NamedNode.getOrCreate() to update person1 or person2

Got that working? Great!

Finally, let's add a onClick handler to the button in the form.

First, add a new NamedNode in the top of the file called hasFriend, you will use this as the property/edge for the friendship connections.

In the handler, if person1 and person2 are set, simply use person1.set(hasFriend,person2) to create the friendship connection between the selected people.

Note that edges in Linked Data are directional. That means they only go one way. The example above only states that person1 has person2 as a friend. As the creator of your own app, it is up to you to decide how to model your data to match the world. There isn't always a perfect answer. If you think friendships should always be mutual, you can add another line to explicitly set person2.set(hasFriend,person1). Linked data does allow for reasoning, which could automatically infer this reverse connection, but that goes outside the scope of this tutorial.

Updating the view on graph changes

Now let's show these hasFriend connections we're making.

Go back to the where you print a list of all the names of people. After each name, print how many friends this person has, and what their names are.

For example: Joe has 2 friends: Veronica & Jack.

Note: LINCD uses Sets and Maps, which use size instead of length, so to get the amount of friends use person.getAll(this.hasFriend).size

To get the names of all the friends you can map over all the values of hasFriend (which are NamedNodes representing people), and return the name of each person. This creates an array of names of people. Which we can that turn into one string with .join()

{
person
.getAll(hasFriend)
.map((friend: NamedNode) => friend.getValue(hasName))
.join(', ');
}

Note that once you've made these changes you won't see the view updating when you make new connections, but it will update when you add a new person!

This is because when we add a new person we are changing reacts state, whereas when we create a new friendship connection we are only making a change in the graph.

LINCD introduces linkedComponents to automate this, amongst other things.

For this tutorial however, we'll introduce a little trick to force the update:

let [bool,setBool] = useState(false);
let forceUpdate = () => setBool(!bool);

Put this in the top of the Home component, and you can now call forceUpdate() after creating a new connection.

Congrats!

You now have a working Linked Data based interface!

Below is an example implementation of this step for reference.

import {useState} from 'react';
import {NamedNode} from 'lincd/lib/models';

//properties used as edges are also NamedNodes themselves!
let hasName = NamedNode.create();
let hasFriend = NamedNode.create();

export default function Home() {
let [newName, setNewName] = useState<string>('');
let [persons, setPersons] = useState<NamedNode[]>([]);
let [person1, setPerson1] = useState<NamedNode>(null);
let [person2, setPerson2] = useState<NamedNode>(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 = NamedNode.create();
//create a new edge from this new person to its own name, using the hasName property as the connecting edge
//to store a string in the graph, we use a Literal node.
newPerson.setValue(hasName, newName);

//add the person to the array and update the state
setPersons(persons.concat(newPerson));
};
let onSubmitConnection = function (e) {
if (person1 && person2) {
person1.set(hasFriend, 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.uri}>
{person.getValue(hasName)} has {person.getAll(hasFriend).size} friends:{' '}
{person
.getAll(hasFriend)
.map((friend: NamedNode) => friend.getValue(hasName))
.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>
);
}
const SelectPerson = function ({persons, setPerson, value}) {
return (
<select value={value?.uri} onChange={(v) => setPerson(NamedNode.getOrCreate(v.currentTarget.value))}>
<option value={null}>---</option>
{persons.map((person) => (
<option value={person.uri} key={person.uri}>
{person.getValue(hasName)}
</option>
))}
</select>
);
};