Skip to main content

Creating linked components

Creating new linked components is not much different from creating any react component. You can use functional components or class-based components.

Single source vs a set of sources

The first thing to decide is whether your component visualises a single node or a set of nodes.

For example, aPersonProfile would use a single Person as its main source (even though it may show several properties and nodes related to this person).
While a PersonsGrid would use a set of Persons as its 'sources' and may for example display each of them as cards in a grid.

Single source

Most linked components will use a single source as their main data source. Even though it may display multiple nodes related to the single source. For example a PersonProfile can show the parents and children of a person, but its starting point is a single person.

Just like with plain React, you can choose to create your components as functions or as classes. Functional components are the preferred way to create linked components and are used for all examples below. In the bottom of the page you can find examples of components written as a class.

CLI command

To create a new linked component, you can use the following command from a linked package folder.

yarn lincd create-component [name]

This generates a template component that you can then link to your desired data structure. How you do that is explained below:

Consider this example of a linked component:

import {linkedComponent} from '../package';
import {Person} from 'lincd-foaf/lib/shapes/Person';
export const PersonView = linkedComponent<Person>(Person, ({source}) => {
//source is an instance of the Person shape
let person = source;
//get the name of the person from the graph through the Person shape
return <h1>Hello {person.name}!</h1>;
});

This example component is linked to the shape Person and displays the name of an instance of a Person.

The linkedComponent(dataRequest,component) method is used to link a data source to the component.

  1. The first argument declares which data the component requires to function (the data it is 'linked to'). Read more about data requests below.

  2. The second argument is the functional component itself, see also properties below.

  3. The type indication (in this case <Person>) is required to make typescript pickup the correct type of the source property.

You can import linkedComponent from package.ts which resides in the root of your LINCD package.

Data requests

With data requests you can declare exactly which data your component requires to function. This ensures that the data is automatically loaded before the component renders. It also ensures that only those nodes that match with the required data can be used with this component.

Requesting specific properties

Consider the following example:

export const PersonTag = linkedComponent<Person>(
Person.request((person) => ({
name: person.name,
homepage: person.homepage,
})),
({linkedData}) => {
let {name, homepage} = linkedData;
return <a href={homepage}>{name}</a>;
},
);

This example component states that it requires an instance of the Person as its source. And specifically, it wants access to person.name and person.profile, which the class Person makes accessible.

These required properties are then available as props.linkedData. Note that in this example the props argument is destructured straight into linkedData with {linkedData} and then further destructured into name and birthDate. The name and birthDate can then be used directly in the render method.

Shape.request()

Every class that extends Shape inherits the static method .request(). So you can link your component to a specific (for example Person) with Person.request()

This method takes a function as its parameter, which receives an instance of the shape as its first parameter. You can then access whatever property you need directly of this Shape. LINCD will track which properties you access and make sure they are available.

Whatever you return will be available as props.linkedData. This can be an array or an object. For example, the PersonView component above could also have returned an array for its required properties:

export const PersonTag = linkedComponent<Person>(
Person.request((person) => [person.name, person.homepage]),
({linkedData}) => {
//note: Person.request() returns an array, so linkedData is an array
let [name, homepage] = linkedData;
return <a href={homepage}>{name}</a>;
},
);

Linked child components

If your linked component renders other linked components, you need to declare this in your data request method. This way LINCD can make sure that it loads all the required data - that of your component plus the required data of the child components - at once.

To do this, you can use the static method .of(). Every linked component has it.

For example, say we have a Purchase shape which connects to a Person with purchase.customer and a Product with purchase.product, then we could implement a PurchaseView like this:

export const PurchaseView = linkedComponent<Purchase>(
Purchase.request((purchase) => ({
date: purchase.date,
Customer: () => PersonTag.of(purchase.customer),
})),
({linkedData}) => {
let {date, Customer} = linkedData;
return (
<div>
This purchase was made at {date} by <Customer />
</div>
);
},
);

In the above example PersonTag is linked to purchase.customer and stored under the key Customer. The result is available as linkedData.Customer. This variable will be a component that you can use directly. It will automatically receive the right data (the customer) as its source. You can also pass any other properties that you would otherwise pass to PersonTag.

Note: you need to use a function () => ... that returns the child component and its linked property.

This is correct:

() => ChildComponent.of(shape.property)

This is not correct:

ChildComponent.of(shape.property)`

Requesting no properties

If a component doesn't require any data besides the nodes themselves then you can pass a Shape as the first argument of linkedData instead of a full data request. This will still ensure that only instances of the right shape can be used as the source of this component.

Note that the linkedData prop will not be available in this case.

Props

linkedData

props.linkedData is available if you make a data request. It will be an array or an object that exactly matches what you've returned in the data request.

source

props.source is always available. It will be an instance of the Shape you linked your component to. Though you may not need to use it if you're using the linkedData property.

Registering functional components

Internally, all linked components need to be registered with a name.

Since functional components may not have a name, you can use registerPackageModule from your packages' package.ts to register all the linked components in that file with their export name:

import {registerPackageModule} from './package';
//register all functional components in this file (module is a global variable)
registerPackageModule(module);

Alternatively, you can also choose to name the function and the export:

export const PersonView = linkedComponent<Person>(Person, function PersonView({source, sourceShape}) {
//..
});

Both work fine.

Components written as a class automatically register with their name and do not require this

Multiple sources (Set components)

A component that renders a set of sources is called a SetComponent. An example of a set component would be a PersonOverview that renders a set of persons in a Grid as profile cards. Or a list of products that are rendered as product cards.

When to use a single source instead

Note that set components display an independent set of sources. Not a set of sources coming from a single primary data source. For example an OrderOverview in a web shop could show the details of an order, including the list of ordered products. But all these products are related to the order. In this case the OrderOverview component itself has a single source (the order), whilst it may use a set component (like a ProductList) to displays the set of ordered products.

3 different types

Before you can start to implement your set component, ask your self: how does your component handle the rendering of each item in the set? (the children)

  • Controlled children - you control how each item is rendered (most common)
  • Configurable children - those who use your component can chose how each item is rendered
  • Pass on sources - you simply pass on the set of sources to another set component

Each of these is implemented in a slightly different way.

As we go through each of them below, we'll be using the example of a PersonOverview which renders a set of persons in a grid. And how those persons are rendered in the grid differs in each setup.

Controlled children

Use this setup if you want to retrieve a set of shapes and decide yourself how you want to render each item. This is probably the most common way to implement set components.

Your component may still be configurable with props, but you ultimately control how each item is rendered (and crucially, with which components)

The PersonOverview example below takes a set of persons and explicitly renders them with the PersonProfileCard component.

export const PersonOverview = linkedSetComponent<Person>(
Person.requestForEachInSet((person) => () => PersonProfileCard.of(person)),
({sources, getLinkedData}) => {
return (
<div className={style.PersonOverview}>
{sources.map((source) => {
let Profile = getLinkedData(source) as any;
return <Profile key={source.toString()} className={style.gridItem} />;
})}
</div>
);
},
);

Note that in this case the component would implement the grid layout with its own styling.

Shape.requestForEachInSet()

Set components with controlled children should use Shape.requestForEachInSet() to declare which data or component it will use for each item in the set. Shape.requestForEachInSet works the same as Shape.request in the sense that you provide it with a function that receives a single instance of the requested Shape, and returns a data request.

See the documentation on data request for single sources for further details.

Note that the example above returns a single bound child component, which is why the result of getLinkedData(source) can be used as a component directly.

Props

Set components with controlled children can use the following 2 props:

PropDescription
sourcesprops.sources is a Set of instances of the requested shape. It is available to any set component, but especially required for controlled set components.
getLinkedDataprops.getLinkedData will return the result of your data request passed to Shape.requestForEachInSet() for a specific item in the set.

Configurable children

Use this setup if you want to control the layout of the children, while allowing users of the component to configure how each item should be rendered. This makes your component more generic and applicable to more use cases. This is set up is also used for example by Grid.

The PersonOverview example below takes a set of persons and renders each item with a component that is defined by the code that uses the component:

export const PersonOverview = linkedSetComponent(Person, ({sources, ChildComponent}) => {
return (
<div className={style.PersonOverview}>
{sources.map((source) => {
return <ChildComponent of={source} key={source.node.toString()} />;
})}
</div>
);
});

Note that in this case the component would implement the grid layout with the styling of its top level <div>, without controlling the style of each child component.

Using configurable set components

Configurable set components like the one above can be used in the data request of other components, by passing the child component as the second argument passed to .of():

PersonOverview.of(persons, PersonProfileCard);

Here's an example of a TeamView that uses this syntax to shows all the persons in a team as profile cards:

export const TeamView = linkedComponent(
Team.request((team) => ({
name: team.name,
Members: PersonOverview.of(team.members, PersonProfileCard),
})),
({linkedData: {name, Members}}) => {
return (
<div>
<h1>{name}</h1>
<Members />
</div>
);
},
);

By providing the child component (PersonProfileCard) in the data request, this TeamView ensures that all the data it requires to render (including the data of each person to render as a PersonProfileCard) will be loaded before it renders.

In regular react components, configurable set components can be used with the 'as' attribute:

<PersonOverview of={persons} as={PersonProfileCard} />

Or with a render function passed as a single child.

<PersonOverview of={persons} />
{(person) => <PersonProfileCard key={person.toString()} />}
</PersonOverview>;

For more details on this, see using linked components

props

PropDescription
sourcesprops.sources is a Set of instances of the requested shape. It is available to any set component, but especially required for controlled set components.
getLinkedDataprops.ChildComponent is a React component that can be used to render each item in the set of sources. You would usually use it like this: sources.map((source) => <ChildComponent of={source} key={source.toString()} />);. You can pass properties to it, or wrap it in other JSX elements as you wish.

Passing on sources

Sometimes you really just want to use another existing SetComponent but in a specific way, for a specific type of data.

So far the example implementations of PersonOverview above have (had to) implement a grid layout themselves. But what if we could use a generic SetComponent for that called Grid?

The example below takes a set of persons, and passes them to Grid, whilst stating that the grid should render each person as a PersonProfileCard

Example

export const PersonOverview = linkedSetComponent<Person>(
Person.requestSet((persons) => ({
PersonGrid: () => Grid.of(persons, PersonProfileCard),
})),
({linkedData: {PersonGrid}}) => {
return (
<div className={style.PersonOverview}>
<PersonGrid />
</div>
);
},
);

Note that the props passed to the component are destructured twice in one line (({linkedData: {PersonGrid}}) => ...). It could also be written like this:

(props) => {
let PersonGrid = props.linkedData.PersonGrid;
};

Shape.requestSet()

For this use case you should use Shape.requestSet() and provide it with a data request function that receives a set of instances as its only argument:

//general form:
Shape.requestSet(instances => ...)

Do not use this pattern if you need to access specific properties of each item in the set in this component. For that, use the controlled children setup instead.

Props

PropDescription
linkedDataprops.linkedData will contain the returned value of requestSet(). Just like other data requests, it can be an array, object or a single bound child component.

Class based components

Though generally functional components are preferred, You can also choose to create class based components.

For this you can extend the helper class LinkedComponentClass and use the @linkedComponentClass decorator, like so:

import {React} from 'react';
import {linkedComponentClass} from '../module';
import {LinkedComponentClass} from 'lincd/lib/utils/LinkedComponentClass';
import {Person} from '../shapes/Person';
@linkedComponentClass(Person)
export class PersonView extends LinkedComponentClass<Person> {
render() {
//typescript knows that person is of type Person
let person = this.props.source;

//get the name of the person from the graph
return <h1>Hello {person.name}!</h1>;
}
}

Note here that the same shape needs to be provided to the decorator and as the type parameter of LinkedComponentClass

Set components as class components

Not yet implemented. Feel free to let us know if you would like to use this!