Component Reference

The following is an example of a custom component that has been created using the Geocortex Web SDK. In general, Geocortex Web components are functional React components extended with a few new behaviors and patterns.

import React, { useState } from "react";
import {
LayoutElement,
LayoutElementProperties,
} from "@vertigis/web/components";
import List from "@vertigis/web/ui/List";
import ListItem from "@vertigis/web/ui/ListItem";
import TitleBar from "@vertigis/web/ui/TitleBar";
import Button from "@vertigis/web/ui/Button";
import { useWatchAndRerender } from "@vertigis/web/ui";
import DialogActions from "@vertigis/web/ui/DialogActions";
import ExampleComponentModel from "./ExampleComponentModel";
export default function ExampleComponent(
props: LayoutElementProperties<ExampleComponentModel>
) {
const { model } = props;
const [hidden, setHidden] = useState(false);
useWatchAndRerender(model, "items");
return (
<LayoutElement {...props}>
<List>
<DialogActions key="dialog-actions">
<TitleBar text="Some Title"></TitleBar>
{hidden && (
<Button onClick={() => setHidden(false)}>
Show Component
</Button>
)}
{!hidden && (
<Button onClick={() => setHidden(true)}>
Hide Component
</Button>
)}
</DialogActions>
{!hidden &&
model.items
.map((item, idx) => (
<ListItem key={idx}>{item}</ListItem>
))
.toArray()}
</List>
</LayoutElement>
);
}

Component Registration

All components need to be registered with the Geocortex Web API in order to be used in a layout.

export default function (registry: LibraryRegistry) {
// ... other item registrations
registry.registerComponent({
// The name used to identify the component in the layout
name: "example",
// The namespace used to identify the component in the layout.
namespace: "custom.abc123",
// The class corresponding to the React Functional Component
getComponentType: () => ExampleComponent,
// The model type this component is bound to.
itemType: "example-model",
title: "Example Component",
});

Components and Layout

Once a component has been registered, it can be used in a layout by referring it by name and namespace.

<?xml version="1.0" encoding="UTF-8"?>
<layout xmlns="https://geocortex.com/layout/v1" xmlns:custom="custom.abc123">
<custom:example/>
</layout>

Component Anatomy

At its core, each custom component is a React component that takes in props and renders a DOM element. However, custom components can implement a couple of patterns which are specific to Geocortex Web.

Rendering

Geocortex Web custom components return a DOM element, like any other React component, but with one restriction. The root DOM element must be a <LayoutElement/> node with all the layout attributes passed as props.

export default function ExampleComponent(
props: LayoutElementProperties<ExampleComponentModel>
) {
return (
<LayoutElement {...props}>
<p>Some Content</p>
</LayoutElement>
);
}

Props

Custom components take in a special type of props, LayoutElementProperties. These props include the React props but also include props specific to the SDK, such as;

  • The model associated with the component,
  • The ID of the component in the layout
  • The width and height, along with other presentation attributes in the layout

The LayoutElementProperties interface takes in ComponentModelBase type class corresponding to the model associated with the component. This will give the props.model property the correct type.

export default function ExampleComponent(
props: LayoutElementProperties<ExampleComponentModel>
) {
const { model, id, width, height, slot, stretch } = props;
...
}

State

Geocortex Web uses two patterns to manage state in a component. First, any purely local UI or presentation logic state should be captured using the React state hook pattern. For data that comes from configuration or other sources, like a service, the components model should be treated as the state.

export default function ExampleComponent(
props: LayoutElementProperties<ExampleComponentModel>
) {
// Presentation logic state
const [hidden, setHidden] = useState(false);
// Business Data / Objects
useWatchAndRerender(props.model, "modelProperty")
...
}

Models

Every component is bound to specific item type, and each item type is bound to a specific model. In this snippet, the ExampleComponent is bound to the example-model item type. The ExampleComponentModel is registered as the example-model item type, meaning that an ExampleComponentModel will be injected into the props of each ExampleComponent.

export default function (registry: LibraryRegistry) {
// ... other item registrations
registry.registerComponent({
name: "example",
namespace: LAYOUT_NAMESPACE,
getComponentType: () => ExampleComponent,
itemType: "example-model",
title: "Example Component",
});
registry.registerModel({
getModel: (config) => new ExampleComponentModel(config),
itemType: "example-model",
});
}

It's best practice to use the model to define configurable or persistent state, and use the component hooks to interact with the model. For UI specific transient state, like an active selection, it is recommended you use the React state hooks.

For example, the following component delegates the management of the list item content to its model. The component does not have to concern itself with whether those list items come from config, dynamic application events, or other sources.

tip

This example uses the useWatchAndRerender component hook in order to dynamically update the component when the model changes.

export default function ExampleComponent(
props: LayoutElementProperties<ExampleComponentModel>
) {
const { model } = props;
useWatchAndRerender(model, "items");
return (
<LayoutElement {...props}>
<List>
{model.items
.map((item, idx) => <ListItem key={idx}>{item}</ListItem>)
.toArray()}
</List>
</LayoutElement>
);
}

Configuration

Geocortex Web provides a suite of flexible, highly configurable components, and this flexibility is powered in a large part by the relationship between app config, models, and components. Each model has the potential to consume static app config, which it can deserialize and transform before providing the data to its associated component for rendering.

The following model defines a simple configurable property items.

  • The shape of this property in the app config JSON file is defined by the items property in the ExampleComponentModelProperties interface.
  • The shape of this property in the model is defined by the items property in the ExampleComponentModel class.

The ExampleComponentModel class consumes the configuration by extending the ComponentModelBase<ExampleComponentProperties> interface, using the @serializable class decorator, and then defining serialization logic in an override of the _getSerializableProperties() method. You can learn more about app config serialization in this article.

interface ExampleComponentModelProperties extends ComponentModelProperties {
items?: string[];
}
@serializable
export default class ExampleComponentModel extends ComponentModelBase<
ExampleComponentModelProperties
> {
items: Collection<string> = new Collection<string>();
protected _getSerializableProperties(): PropertyDefs<
ExampleComponentModelProperties
> {
const props = super._getSerializableProperties();
return {
...props,
items: {
serializeModes: ["initial"],
default: ["Default Value"],
serialize: () => this.items.toArray(),
deserialize: (items) => {
this.items.removeAll();
this.items.addMany(items);
},
},
};
}
}

Component Defaults

Most components support the config attribute in the layout, which links a component model to configuration in the app config JSON. However, many component models have default values they can supply for initialization instead of relying on configuration. This means that if you omit the config property for certain components, the component model will attempt to create itself with its default values. An example of defining these default values can be seen in this example. It's also the mechanism that powers the default map displayed by this layout.

<?xml version="1.0" encoding="utf-8" ?>
<layout xmlns="https://geocortex.com/layout/v1">
<map/> <!-- The map component is populated with values from the MapModel's `_getSerializableProperties` function. -->
</layout>

Initialization and Teardown

Component models have an initialization method, which can be used to perform asynchronous startup logic, and a teardown method, which can be used to free resources when a component is removed from the layout.

important

Always call super._onInitialize() when overriding the _onInitialize method, and super._onDestroy() when overriding the _onDestroy method.

export default class ExampleComponentModel extends ComponentModelBase {
items: Collection<string> = new Collection<string>();
protected async _onInitialize(): Promise<void> {
super._onInitialize();
this.items.add("1");
this.items.add("2");
this.items.add("3");
this.items.add("4");
// ...other initialization logic
}
protected async _onDestroy(): Promise<void> {
await super._onDestroy();
this.items.destroy();
// ...free up resources
}
}

Component Lifecycle

When a Geocortex Web Application boots up, the set of components which are initially active in the layout are created and initialized. Components like the Panel will only activate their first child. All initially inactive components will only be created when they are activated in the layout.

Activation and Deactivation

Custom code can listen and react to a components activation or deactivation by subscribing to the ui.activated or ui.deactivated event. The ui.* events contain various events relating to the component lifecycle.

tip

The UIService contains implements the commands ui.activate and ui.deactivate and exposes methods like getActiveChildComponentIds() which can be used to examine and manipulate the activation state of components.

@serializable
export default class ExampleComponentModel extends ComponentModelBase<
ExampleComponentModelProperties
> {
private readonly _handles: IHandle[] = [];
protected async _onInitialize(): Promise<void> {
super._onInitialize();
this._handles.push(
this.messages.events.ui.activated.subscribe(() => {
// ...on activated logic here
})
);
this._handles.push(
this.messages.events.ui.deactivated.subscribe(() => {
// ...on deactivated logic here
})
);
}
protected async _onDestroy(): Promise<void> {
await super._onDestroy();
// Make sure to clean up subscriptions when the model is destroyed.
this._handles.forEach((h) => h.remove());
}
}

Next Steps

Learn how to use Commands and Operations with Components

Learn how to run and implement commands and operations with custom components

Learn about Component Interactions

Learn about how components can interact with each other through their models

Create a Component with a Complex UI

Follow along with a more in depth component example

Create a Component that Consumes App Config

Learn more about writing components that consume configuration values.