Developing Blocks

Introduction

Blocks are discrete components that allow users to view, explore, or edit data.

The Block Protocol defines a standard for communication between blocks and the applications that embed them.

The protocol is split into a core specification setting out how applications and blocks communicate, and module specifications defining what applications and blocks communicate.

This guide helps get you set up and introduces some of the key features of the graph module specification, which deals with creating, reading and updating data records (or “entities”).

In practice, most block developers will not need to know the lower-level details of the specifications, as the libraries we provide implement them.

Choosing your approach

We provide three templates which allow you to define the entry point for your block in different ways:

  • custom-element: create a block defined as a custom element (also known as Web Components).
  • html: create a block defined as an HTML file, with JavaScript added via <script> tags.
  • react: create a block defined as a React component.

To create a new block, run npx create-block-app@canary block-name --template template@canary, replacing block-name with the name to give your block, and template with one of the template names listed above (keep the @canary on the end!)

I want to use a different technology

If you want to write blocks using other technologies or frameworks, you have several options:

  1. use a custom-element template and use different approaches when constructing the element-i.e. use a custom element as a wrapper for anything else you like.
  2. use an html template and import and use your additional libraries inside the <script> tag.
  3. use a language which can be transpiled to JavaScript. As an example, see this blog post on writing a block using F#. This block uses React – you can use transpiled JavaScript in blocks defined as HTML files or custom elements too.

I don’t want to use TypeScript

You can write your block in regular JavaScript using the methods described above - just rename your files from *.tsx/*.ts to *.jsx/*.js, remove the types, and get coding.

Creating a block

  1. Move to a folder where you want to create your block.
  2. Run npx create-block-app@canary your-block-name --template react@canary (or --template custom-element@canary).
  3. Switch to your new folder: cd [your-block-name].
  4. Run yarn install && yarn dev or npm install && npm run dev.
  5. Open http://localhost:63212 in your browser to see the starter template.

The development environment

The create-block-app package and provides everything you need to develop a block.

  • src/app.tsx or src/app.ts contains your block’s code.
    • You can include dependencies in your block but bear in mind that the more dependencies you add, the bigger your block’s download size will be. Common dependencies which you can reasonably expect an embedding application to provide (e.g. React) can be defined as peerDependencies in package.json.
  • yarn dev or npm run dev will run your block in development mode, serving it locally with hot reloading at http://localhost:63212.
    • This uses the file at src/dev.tsx to render your block within a mock embedding application called MockBlockDock.
    • By default, dev mode will also show you the properties that are being passed to your block and the contents of the mock datastore. Remove debug from MockBlockDock to turn this off, or toggle it via the provided switch in the UI.
  • yarn build or npm run build will:
    • Bundle the component into a single source file (without any dependencies listed as peerDependencies).
    • Generate a block-metadata.json file which:
      • points to the bundled source file.
      • brings in metadata from package.json, such as the description.
      • brings in anything in the blockprotocol object in package.json, e.g.
        • blockType: the type of block this is.
        • displayName: a friendly display name
        • examples: an array of example data structures your block would accept and use
        • image: a preview image showing your block in action (in place of public/block-preview.png)
        • icon: an icon to be associated with your block (in place of public/omega.svg)
        • name: a slugified name for your block (which may differ to the package name in package.json); it can be defined as blockname or @namespace/blockname, where namespace must be your username on blockprotocol.org if you intend to publish it there
      • list the externals, which are generated from peerDependencies in package.json.

Updating the block schema

A key part of the Block Protocol is the use of types to describe the data your block will work with.

Your block should be associated with an “entity type” which will be used by embedding applications to understand what sorts of entities can be sent to it (e.g. what properties do they have?).

When an embedding application loads your block, it should send an entity which complies with the structure of the block's declared entity type. We call such an entity the 'block entity'.

See working with types for more information on the type system, or jump straight to your dashboard to create a type.

Once you have created the type representing the data your block needs, copy its URL, and update the schema property in the blockprotocol object in package.json. In TypeScript block templates, you can then run yarn schema to regenerate the types for your block.

Lifecycle of a block

When a block is loaded into an embedding application:

The embedding application parses its block-metadata.json file and:

  • provides any external dependencies which the block needs.
  • sets up message handling as described in the core specification.
  • loads the block with the appropriate strategy for its blockType.

The block then receives any data which the embedder can provide straight away, for example as part of the graph module:

  • custom-element and react-type blocks will be sent initial data as properties/props.
  • html-type blocks will be sent messages containing the initial data.
  • the block can then do whatever it chooses to do with those properties.

At any time after this initialization, the block may send further messages via a Module for specific purposes, such as reading and writing data within the embedding application.

The starter blocks created by create-block-app implement a simple example of this:

  1. the Thing entity type referred to in blockprotocol.schema in package.json, for which types are found in src/types.gen.ts, defines the properties expected for the block entity.
  2. mock values for the block entity are passed to MockBlockDock in dev.tsx, including the properties it expects
  3. the block receives the data for blockEntitySubgraph (described below):
  • the react and custom-element blocks extract the block entity from the blockEntitySubgraph
  • the html block registers a callback for the blockEntitySubgraph message.
  • each block then accesses the properties from the block entity and uses the name property to render its Hello, World! message.

Using the Graph Module

The Graph Module describes how entities can be created, queried, updated, and linked together, including the block entity. It enables your block to create, read, update, and delete data in the embedding application.

The Graph Module is available via the graphModule property in each starter template. It has a number of methods corresponding to the messages defined in the specification.

Using these methods in combination, you can create complex graphs from within a block without having to know anything about the implementation details of the application embedding it.

Each message payload is the same: an object containing data and errors keys.

The graph properties object

The graph properties object is sent in properties/props for custom-element and react-type blocks, and as a message for html-type blocks.

It contains data sent from the embedding application to the block related to the graph module. Importantly:

  • readonly: a boolean indicating whether the block is in a read-only context. This typically means that the embedding application will reject any requests to update data, and the block should alter its UI and behaviour accordingly.

  • blockEntitySubgraph: this contains the 'block entity' and any entities immediately linked to it. It is a graph of entities rooted at the block entity.

We provide helper tools for extracting the key information from blockEntitySubgraph:

  • in the react template, a useEntitySubgraph hook can return the rootEntity (the block entity) and any linkedEntities
  • in the custom-element template, this.getBlockEntity() and this.getLinkedEntities() are available
  • in the html template (and everywhere), you can use functions available in @blockprotocol/graph/stdlib, for example:
    • getRoots to get the roots from a subgraph (for a blockEntitySubgraph, there should only be one)
    • getOutgoingLinkAndTargetEntities to get the entities linked from a given entity (the 'target' entities), and the links themselves (N.B this is equivalent to linkedEntities in the other templates, which are for outgoing links from the block entity onl)
    • getIncomingLinkAndTargetEntities to get the entities linking to a given entity, and the links

Many of the messages sent from the application to the block as part of the graph module return a Subgraph. You shouldn't have to worry about the internal workings of a Subgraph, but it is worth knowing that a given subgraph represents the result of a query starting with a given entity (or entities) and following links from it to other entities. The links are also entities (they may have properties and relationships of their own), known as 'link entities'.

The four components of a Subgraph are:

  • roots: the entities which were the starting point of the query (e.g. only the 'block entity' in the case of blockEntitySubgraph)
  • depths: which edges were followed from the roots when resolving the query, and how far
  • edges: connections between things in the graph. For example, a 'link entity' connects it and two other entities via hasRightEntity and hasLeftEntity edges (conceptually, the link entity is in the middle with an entity on its left and on its right)
  • vertices: each thing (vertex, or node) which was encountered when starting from the roots and following the specified edges to the specifieddepths`.

A Subgraph may also be rooted at types, and it may also contain types in its vertices, as well as have edges which are type-related connections between vertices – but when writing a block you will mostly be working with entities. Requesting types can be important for validating input when the type of data is not known ahead of time (e.g. for a generic table block). If you know that your block accepts or generates a specific data structure, you already know its type – but by defining its type, you are helping applications and end-users of your block choose what data can be sent to it.

Again, you probably don't need to worry about this when getting started – but if you start to work with complex data networks made up of many entities with different relationships, the Subgraph and knowing how to query it becomes a powerful tool.

Updating the block entity

A common use for the Graph Module is to update the block entity – to update the properties that are sent to the block.

Each block template includes a demonstration of calling graphModule.updateEntity to update the block entity.

To do this, you need to call updateEntity using the entityId of the blockEntity:

// Update the block entity, and receive the updated entity in return
const { data, errors } = await graphModule.updateEntity({
  data: {
    entityId: blockEntity.metadata.recordId.entityId,
    entityTypeId: blockEntity.metadata.entityTypeId,
    properties: {
      "https://blockprotocol.org/types/property-type/name/": "Bob",
    },
  },
});

How you get a reference to blockEntity depends on the type of block, as described above and demonstrated in each template.

As soon as the updateEntity call is processed, for react and custom-element blocks your block will be re-rendered with the updated properties. You could therefore omit the { data, errors } from the above snippet and rely on the updated properties when the block is re-rendered.

If you’re using the custom-element template, you have a helper method to achieve the above:

this.updateSelfProperties({
  "https://blockprotocol.org/types/property-type/name/": "Bob",
});

Because properties are identified by URIs, you may wish to alias them in your code if used in multiple places. For example:

const nameKey = "https://blockprotocol.org/types/property-type/name/";

if (blockEntity.properties[nameKey] !== "Bob") {
  await graphModule.updateEntity({
    data: {
      entityId: blockEntity.metadata.recordId.entityId,
      entityTypeId: blockEntity.metadata.entityTypeId,
      properties: { [nameKey]: "Bob" },
    },
  });
}

You can read more about how types are described on the working with types page.

Creating new entities

You can create new entities using the createEntity method.

New entities should be assigned starting properties and an entityTypeId (a URI pointing to their entity type).

Linking entities together

Because links between entities are just a special kind of entity, you call createEntity to create them, specifying additional linkData to indicate which entities are on the 'left' and 'right' of the link. For now you can think of the 'left' entity as the source of the link, and the 'right' entity as the target or destination.

For example, to link an entity to the block entity:

// link the 'blockEntity' to some 'otherEntity' you have a reference to (e.g. if you have newly created it)
graphModule.createEntity({
  data: {
    entityTypeId: "https://blockprotocol.org/types/entity-type/friend/v/1",
    linkData: {
      leftEntity: blockEntity.metadata.recordId.entityId,
      destinationEntityId: otherEntity.metadata.recordId.entityId,
    },
    properties: {}, // this can contain metadata about the link, if you wish
  },
});

You can define the type of relationships between your block entity and other entities when defining its type, and the entityTypeId of the relevant relationship will then be available in the generated types (after running yarn schema)/

Any entities linked directly from the block will appear in the blockEntitySubgraph property.

You can also link other entities together, but whether or not they appear in blockEntitySubgraph will depend on whether they are connected to the block entity at all, and far away they are (what depths are required to reach them).

Exploring the data store

There are messages for exploring the data available in the embedding application:

  • aggregateEntities allows you to request a list of available entities.

You can browse the available entities rder to display them, or create links between them.

If you want to retrieve more detailed information on a specific entity, you can call getEntity.

Going further with the graph module

If you are using TypeScript, the types for methods available on graphModule (as defined in the @blockprotocol/graph package) should help you understand what methods are available and how they operate.

Build

Once you’ve finished writing your block, run yarn build or npm run build.

This will produce a compiled version of your code in the dist folder, along with a metadata file describing your block (block-metadata.json).

It is worth updating the blockprotocol object in package.json to include your own icon, image, and examples for your block. These will automatically be included in the block-metadata.json produced after running yarn build or npm run build.

You now have a block package that you can provide to apps to use, by publishing it on the Hub.

Publish

Once you've built a block, you can add it to the Hub, so that your block will have an instant online demo playground, and will be searchable via our block API.

To publish a block on the Hub take the following steps.

Pre-requisite: obtain an API Key

  • If you don't yet already have an account, click the sign up button in the top-right corner of any page on the Þ website.
  • Once signed in, create an API key and copy it to your clipboard.

Publish from your terminal

  • Run npm run build or yarn build to create a production build of your block (it will appear in the dist folder)
  • run npx blockprotocol@canary publish to generate a .blockprotocolrc file when prompted
  • Replace the placeholder key in that file with your API key
  • now run npx blockprotocol@canary publish again
  • See your block on the Hub!

Updating your block

You can update your published block at any time by running npm run build && npx blockprotocol@canary publish or yarn build && npx blockprotocol@canary publish

Changing the Hub preview

  • Take a screenshot of your block in action and replace public/block-preview.png
  • update the description in package.json
  • add an icon to the public folder and update blockprotocol.icon in package.json
  • add a preview image to the public folder and update blockprotocol.image in package.json
  • update your block’s README.md – it will appear below your block on its hub page if you change it from the default
  • update the example properties object in blockprotocol.examples in package.json