Creating a custom component
In this tutorial, you will learn how to extend Geogirafe by creating a custom component to draw, modify and delete map markers and their linked attributes. The main learning objectives are:
- Understanding how Geogirafe components are structured
- Understanding how Geogirafe components communicate and interact via the state manager (for example how to pass data from the map to a side panel and vice-versa)
- Getting familiar with the uhtml (micro µ html) templating syntax
- Getting familiar with the OpenLayers mapping library
If you are just starting with Geogirafe, you may want to check out the Architecture and Client setup pages before continuing.
The tutorial is divided into the following steps:
- Setting up the project
- Adding a button to the lateral menu bar
- Creating the component’s initial structure
- Adding markers on the map
- Adding select, draw and modify interactions to the map markers
- Using the state manager
- Creating a subcomponent in the side panel to display a list of chips
- Creating a subcomponent in the side panel to display and modify the selected marker's details
The complete code of this tutorial is available here:
Here’s a preview of what you will build:

Your custom component will be made of several parts:
- A button in the lateral menu bar to show/hide your custom hello world component.
- A side panel with the following elements:
- Control buttons (a button to toggle the marker draw/move mode and a button to delete all markers)
- A details box which displays the ID, position and colour of the currently selected marker
- A list of chips on which you can click to select a marker
- Map markers which can be selected and moved.
Setting up the project
Clone the Geogirafe repository (or fork it) locally:
git clone https://gitlab.com/geogirafe/gg-viewer.git
Build the application:
npm install
In this tutorial, we will use the preconfigured SITN demo configuration, you may however create your own configuration or use one of the other preconfigured demos. To use the SITN demo configuration:
- On Linux, run
npm run configure-demo sitn - On Windows, run
npm run configure-demo-win sitn
Make sure you work on a local drive on Windows, npm will fail on network drives.
Then, start the development server:
npm start
Adding a button to the lateral menu bar
At this step, you will add a new “Hello” button to the lateral menu bar as illustrated below:

The "Hello" button will allow users to show or hide the "Hello World" custom component that you will create.
First, add an entry to the graphical interface’s state definition in the state manager:
- Open
src/tools/state/graphicalInterface.ts - In the
GraphicalInterfaceclass, which has a list of properties (helpVisible, drawingPanel, etc) to control the status and visibility of elements in the graphical interface, Add a boolean property calledhelloPanelVisibleand set its value tofalse:
export default class GraphicalInterface {
isMobile = false;
helpVisible = false;
drawingPanelVisible = false;
printPanelVisible = false;
extLayerPanelVisible = false;
crossSectionPanelVisible = false;
helloPanelVisible = false;
editPanelVisible = false;
sharePanelVisible = false;
selectionComponentVisible = false;
selectionComponent = "";
layoutPanelVisible = false;
aboutPanelVisible = false;
userPreferencesPanelVisible = false;
infoWindowVisible = false;
searchComponentVisible = true;
basemapComponentVisible = true;
darkMapMode = false;
darkFrontendMode = systemIsInDarkMode() ?? false;
swipeupPanelMode: SwipeupPanelMode = "closed";
contactPanelVisible = false;
swipeupPanelContent:
| "selector"
| "features"
| "menu"
| "drawing"
| "offline"
| null = "selector";
}
Then, modify index.html (at the project’s root) to add the “hello” button to the lateral menu bar:
- Download and copy the
helloworld.svgfile to the project'ssrc/assets/icons/folder - Open
index.htmlin the root directory - In
index.html, add the following<button>element at the beginning of thebuttonbardiv:
<div id="buttonbar">
<button
slot="menu-content"
class="gg-icon-button gg-big"
onclick="document.geogirafe.state.interface.helloPanelVisible = !document.geogirafe.state.interface.helloPanelVisible"
>
<img alt="helloworld-icon" src="icons/helloworld.svg" />
</button>
<button
tip="drawing-panel"
slot="menu-content"
class="gg-icon-button gg-big"
onclick="document.geogirafe.state.interface.drawingPanelVisible = !document.geogirafe.state.interface.drawingPanelVisible"
>
<img alt="draw-icon" src="icons/draw.svg" />
</button>
</div>
- Make sure the development server is running (if it is not, launch it with
npm startin the terminal), open your web browser at the url indicated in the terminal (by default https://app.localhost:8080/). In the lateral button bar, you should see the newly added Hello button. If you click on it, nothing happens; that's normal because we haven't linked it to anything yet.
Creating the component’s initial structure
At this step, you will create a skeleton structure for your Hello World custom component. At the end of this step, you will be able to display a side panel and toggle it with the Hello button you created at the previous step, it should look like this:

-
Add a new
helloworld/folder insrc/components/ -
In the
helloworld/folder, create three new files:component.ts(which will contain the component's class definition)template.html(which will contain the component's HTML template to be rendered)style.css(which will contain the styling rules)
Importing styles into a component-
To import a single stylesheet, use the
styleUrlproperty with a single filepath, e.g.styleUrl = "./style.css"; -
To import multiple stylesheets, use the
styleUrlsproperty with an array of filepaths, e.g.styleUrls = ['./style.css', '../../styles/common.css'];
Geogirafe comes with a set of predefined styles, see the Styling documentation for more details.
-
In
helloworld/component.ts, copy the following code:import type { Callback } from "../../tools/state/statemanager";
import GirafeHTMLElement from "../../base/GirafeHTMLElement";
class HelloWorldComponent extends GirafeHTMLElement {
templateUrl = "./template.html";
styleUrls = ["./style.css", "../../styles/common.css"]; // we also import the common styles
private readonly eventsCallbacks: Callback[] = [];
darkFrontendMode: boolean = false;
visible: boolean = false;
constructor() {
super("hello");
}
// Renders the component or the empty component depending on visibility attribute
render(): void {
console.debug("Helloworld - render()");
if (this.visible) {
this.renderComponent();
} else {
this.renderEmptyComponent();
}
}
// Renders the component
private renderComponent() {
super.render();
super.girafeTranslate();
this.registerEvents();
}
// Hide the panel and removes listeners
private renderEmptyComponent() {
// Unregister events
this.unregisterEvents();
// Render empty component
this.renderEmpty();
}
// Register events
registerEvents(): void {
this.eventsCallbacks.push(
// Subscribe to dark mode toggle
this.subscribe(
"interface.darkFrontendMode",
(_oldValue: boolean, newValue: boolean) => {
this.darkFrontendMode = newValue;
}
)
);
}
// Unregister events
unregisterEvents(): void {
this.unsubscribe(this.eventsCallbacks);
this.eventsCallbacks.length = 0;
}
// Register visibility events
registerVisibilityEvents(): void {
console.debug("Helloworld - registerVisibilityEvents()");
// Hello World panel toggle
this.subscribe(
"interface.helloPanelVisible",
(_oldValue: boolean, newValue: boolean) => this.togglePanel(newValue)
);
}
// Toggle side panel display
private async togglePanel(visible: boolean): Promise<void> {
console.debug("Helloworld - toggle panel()");
this.visible = visible;
this.render();
}
/*
ConnectedCallback(): called each time the element is added to the document. The specification recommends that,
as far as possible, developers should implement custom element setup in this callback rather than the constructor.
*/
connectedCallback(): void {
super.connectedCallback();
this.registerVisibilityEvents();
}
}
export default HelloWorldComponent; -
In
template.html, add the following HTML:<div id="panel">
<div id="content">
<h1 i18n="hello">Hello Giraffe!</h1>
</div>
</div>i18n translationsGeogirafe uses the i18next internationalization framework to translate user interface elements.
The
i18nattribute in html elements is a reference to a variable in the i18n translation files located in thesrc/assets/i18n/folder. In the preconfigured instance we are using, there are 4 files with the core translations:de.json(German),en.json(English),fr.json(French),it.json(Italian). Additional local or remote translation files can be included as needed.To access translation variables (in component.ts or template.html), Geogirafe also provides a
I18nManagerclass with agetTranslation()method, e.g.:<div>${this.i18nManager.getTranslation('color_giraffe')}</div>The list of translations and source files must be configured in
public/config.json, e.g.:"languages": {
"translations": {
"de": ["i18n/de.json", "i18n/custom-translation-de.json"],
"en": ["i18n/en.json"],
"fr": ["i18n/fr.json", "https://sitn.ne.ch/static/dummy/fr.json"],
"it": ["i18n/it.json"]
},
"defaultLanguage": "en"
}See the documentation on Configuration and Translations for more details.
-
Create four new translation files the in the
src/assets/i18n/folder:helloworld-de.jsonfor German:{
"de": {
"draw_giraffe": "Zeichnen",
"draw_giraffe_help": "Zeichne eine Giraffe",
"delete_all_giraffes": "Alles löschen",
"delete_all_giraffes_help": "Lösche alle Giraffen",
"zoom_on_giraffe": "Zoomen",
"zoom_on_giraffe_help": "Zoomen Sie auf die Giraffe",
"color_giraffe": "Klicken Sie hier, um meine Farbe zu ändern!",
"hello": "Hallo Giraffe!",
"hello-panel": "Meine Komponente"
}
}helloworld-en.jsonfor English:{
"en": {
"draw_giraffe": "Draw",
"draw_giraffe_help": "Draw a giraffe",
"delete_all_giraffes": "Delete all",
"delete_all_giraffes_help": "Delete all giraffes",
"zoom_on_giraffe": "Zoom",
"zoom_on_giraffe_help": "Zoom on giraffe",
"color_giraffe": "Click to change my color!",
"hello": "Hello giraffe!",
"hello-panel": "My component"
}
}helloworld-fr.jsonfor French:{
"fr": {
"draw_giraffe": "Dessiner",
"draw_giraffe_help": "Dessiner une girafe",
"delete_all_giraffes": "Supprimer tout",
"delete_all_giraffes_help": "Supprimer toutes les girafes",
"zoom_on_giraffe": "Zoomer",
"zoom_on_giraffe_help": "Zoomer sur la girafe",
"color_giraffe": "Clique pour changer ma couleur!",
"hello": "Salut girafe!",
"hello-panel": "Mon composant"
}
}helloworld-it.jsonfor Italian:{
"it": {
"draw_giraffe": "Disegnare",
"draw_giraffe_help": "Disegna una giraffa",
"delete_all_giraffes": "Elimina tutte",
"delete_all_giraffes_help": "Elimina tutte le giraffe",
"zoom_on_giraffe": "Zoomer",
"zoom_on_giraffe_help": "Zoom sulla giraffa",
"color_giraffe": "Clicca per cambiare il mio colore!",
"hello": "Ciao giraffa!",
"hello-panel": "Il mio componente"
}
} -
Add the new translation files in the configuration (
public/config.json):"languages": {
"translations": {
"de": ["i18n/de.json", "i18n/helloworld-de.json"],
"en": ["i18n/en.json", "i18n/helloworld-en.json"],
"fr": ["i18n/fr.json", "i18n/helloworld-fr.json", "https://sitn.ne.ch/static/dummy/fr.json"],
"it": ["i18n/it.json", "i18n/helloworld-it.json"]
},
"defaultLanguage": "fr"
} -
In
style.css, add the following styling rules:#panel {
height: 100%;
overflow: auto;
min-width: 20rem;
}
#content {
background: var(--bkg-color);
padding: 1.5rem;
margin: 0;
}
#content .message {
text-align: center;
width: 100%;
}
#content .message * {
margin: 0.25rem;
}
#content div.option {
margin-top: 0.5rem;
display: flex;
justify-content: space-between;
}
.btn-group-flex {
display: flex;
flex-direction: row;
gap: 0.5rem;
margin: 0.5rem auto;
}
.btn-group-flex > button,
.btn-group-flex > select,
.btn-group-flex > input {
flex: 50%;
} -
In
src/tools/app/geogirafeapp.ts:Import the
HelloWorldComponentby adding the following line to the list of imports at the beginning of the file:import HelloWorldComponent from "./components/helloworld/component";Define the component's name (alias) by adding the following line to the list of definitions, in the
defineCoreComponents()method:customElements.define("girafe-helloworld", HelloWorldComponent); -
In
index.html, add thegirafe-helloworldcustom element you just defined to the right panel element<girafe-lr-panel>:
<girafe-lr-panel class="panel-right" dock="right">
<girafe-helloworld
slot="main"
data-toggle-path="interface.helloPanelVisible"
data-title="hello-panel"
></girafe-helloworld>
<girafe-drawing
slot="main"
data-toggle-path="interface.drawingPanelVisible"
data-title="drawing-panel"
></girafe-drawing>
<girafe-print
slot="main"
data-toggle-path="interface.printPanelVisible"
data-title="print-panel"
></girafe-print>
<girafe-ext-layer
slot="main"
data-toggle-path="interface.extLayerPanelVisible"
data-title="ext-layer-panel"
></girafe-ext-layer>
<girafe-cross-section-settings
slot="main"
data-toggle-path="interface.crossSectionPanelVisible"
data-title="cross-section-settings"
></girafe-cross-section-settings>
<girafe-edit
slot="main"
data-toggle-path="interface.editPanelVisible"
data-title="edit-panel"
></girafe-edit>
<girafe-layout
slot="main"
data-toggle-path="interface.layoutPanelVisible"
data-title="layout-panel"
></girafe-layout>
<girafe-about
slot="main"
data-toggle-path="interface.aboutPanelVisible"
data-title="about-panel"
></girafe-about>
<girafe-user-preferences
slot="main"
data-toggle-path="interface.userPreferencesPanelVisible"
data-title="user-preferences-panel"
></girafe-user-preferences>
<girafe-share
slot="main"
data-toggle-path="interface.sharePanelVisible"
data-title="share-panel"
></girafe-share>
<girafe-contact
slot="main"
data-toggle-path="interface.contactPanelVisible"
data-title="contact-panel"
></girafe-contact>
</girafe-lr-panel>
The data-title attribute in custom elements is a reference to a variable in the i18n translation files (in our example, the hello-panel variable). It is used to automatically translate the header at the top of the panel.
If you look in your browser and click on the Hello button, you should now see a side panel with a Salut girafe! title. If you try changing the language in the user settings menu, the default text will be automatically replaced by the translated text specified in the hello variable in the translation files.
Adding markers on the map
At this step, you will add a vector layer with markers to the map. The result should look like this (notice the three giraffe shaped markers on the map when the HelloWorld panel is open):

- In
helloworld/component.tsat the beginning, update the list of imports:
import type { Callback } from "../../tools/state/statemanager";
import GirafeHTMLElement from "../../base/GirafeHTMLElement";
import { Map } from "ol";
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import Feature from "ol/Feature";
import { Icon, Style } from "ol/style";
import { StyleLike } from "ol/style/Style";
import { Geometry } from "ol/geom";
import { Point } from "ol/geom";
- Then, modify the
HelloWorldComponentclass:
At the beginning, add the typed properties definitions for the vector source (helloSource) and vector layer (helloLayer), also add a getter method to access the map instance:
templateUrl = './template.html';
styleUrls = ['./style.css', '../../styles/common.css']; // we also import the common styles
private readonly eventsCallbacks: Callback[] = [];
darkFrontendMode: boolean = false;
visible: boolean = false;
private helloSource: VectorSource<Feature<Geometry>> | null = null;
private helloLayer: VectorLayer<VectorSource<Feature<Geometry>>> | null = null;
private get map(): Map {
return this.context.mapManager.getMap();
}
In the constructor, define the vector source (helloSource) and the vector layer (helloLayer) properties:
constructor() {
super('hello');
// Initialize a vector source containing three arbitrary point features
this.helloSource = new VectorSource({
features: [
new Feature({ geometry: new Point([2554842.0, 1204042.0]), color: '#FF0000', selected: false }),
new Feature({ geometry: new Point([2556508.3, 1195394.6]), color: '#00FF00', selected: false }),
new Feature({ geometry: new Point([2537187.2, 1186086.9]), color: '#0000FF', selected: false })
] as Feature<Geometry>[],
wrapX: false
});
// Initialize a vector layer
this.helloLayer = new VectorLayer({
source: this.helloSource as VectorSource<Feature<Geometry>>,
style: this.myStyleFunction as StyleLike
});
// Add a name property to the vector layer
this.helloLayer.set('name', 'helloworldLayer');
}
Before the constructor, add the following myStyleFunction() function to style the map markers (note that we use inline SVG to dynamically create giraffe shaped markers and vary their style depending on their selected property):
// Marker styling function
myStyleFunction(feature: Feature, _resolution: number): StyleLike {
let color = feature.get('color');
let strokeColor = '#000000';
let size = 60;
if (feature.get('selected')) {
color = feature.get('color');
strokeColor = '#FFFF00';
size = 80;
}
const svg = `<svg
width="90.222916mm"
height="90.222916mm"
viewBox="0 0 90.222916 90.222916"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
fill="${color}"
fill-opacity="1"
stroke="${strokeColor}"
stroke-width="1"
stroke-opacity="1"
stroke-miterlimit="4"
stroke-dasharray="none"
stroke-dashoffset="0"
d="m 62.512463,3.389974 -1.334285,0.4862752 2.282548,5.8911133 -1.043864,0.9234575 -21.672062,22.869405 -0.863513,3.21014 -17.622181,15.160832 -0.660941,16.018661 -1.653129,3.514514 0.173116,16.320967 h 2.157491 l 1.634009,-12.388391 5.305619,-13.132015 1.993677,-1.938899 9.680029,-2.298568 0.686263,1.187007 -1.583883,28.570866 1.981792,-0.05839 3.958414,-22.682853 5.985165,-11.072709 -1.228866,-7.364925 13.954704,-29.419393 6.652824,1.311031 2.458248,-0.95033 -8.586556,-8.1932982 z m -12.602848,58.001628 -2.718697,5.450313 5.543331,20.941874 1.930631,0.0015 z m -15.064713,0.05788 -3.948597,1.097608 -2.94659,7.007324 2.884062,18.124992 2.097029,0.0072 -0.646988,-11.811682 z"
/>
</svg>`;
return new Style({
image: new Icon({
anchor: [0.5, 0.5],
anchorXUnits: 'fraction',
anchorYUnits: 'fraction',
height: size,
width: size,
src: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`
})
});
}
In the renderComponent() method, add the following code to include the HelloLayer on the map:
// Renders the component
private renderComponent() {
super.render();
super.girafeTranslate();
this.registerEvents();
// Check if HelloLayer is already included in the map
const existsHelloLayer = this.map.getLayers().getArray().includes(this.helloLayer!);
if (!existsHelloLayer) {
this.map.addLayer(this.helloLayer!);
}
}
In the renderEmptyComponent() method, add the following code to remove the HelloLayer from the map:
// Hide the panel and removes listeners
private renderEmptyComponent() {
// Unregister events
this.unregisterEvents();
// Remove helloLayer from map
this.map.removeLayer(this.helloLayer!);
// Render empty component
this.renderEmpty();
}
Look in your browser and open the HelloWorld panel, you should now see three colored giraffes on the map. If you close the panel, the giraffes should dissapear.
Adding select, draw and modify interactions to the map markers
For more information about user interaction management see Managing user interactions See code for this part
At this step, you will add selection, drawing, modifying (moving) interactions to the map markers. You will also add two buttons to the HelloWorld panel:
- a button to toggle the drawing mode
- a button to delete all the markers.
The result will look like this:

- In
helloworld/component.tsat the beginning, update the list of imports:
import type { Callback } from "../../tools/state/statemanager";
import GirafeHTMLElement from "../../base/GirafeHTMLElement";
import { v4 as uuidv4 } from "uuid";
import { Map } from "ol";
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import Feature from "ol/Feature";
import { Icon, Style } from "ol/style";
import { StyleLike } from "ol/style/Style";
import { Point } from "ol/geom";
import { Geometry } from "ol/geom";
import { Select, Draw, Modify } from "ol/interaction";
import { SelectEvent } from "ol/interaction/Select";
import { DrawEvent } from "ol/interaction/Draw";
import { ModifyEvent } from "ol/interaction/Modify";
import { click, primaryAction, noModifierKeys } from "ol/events/condition";
import { ObjectEvent } from "ol/Object";
-
Then, modify the
HelloWorldComponentclass:Add the typed properties definitions for the interactions (
selectInteraction,drawInteraction,modifyInteraction) and for the drawing mode toogle (draw) at the beginning:class HelloWorldComponent extends GirafeHTMLElement {
templateUrl = './template.html';
styleUrls = ['./style.css', '../../styles/common.css']; // we also import the common styles
private readonly eventsCallbacks: Callback[] = [];
darkFrontendMode: boolean = false;
visible: boolean = false;
private helloSource: VectorSource<Feature<Geometry>> | null = null;
private helloLayer: VectorLayer<VectorSource<Feature<Geometry>>> | null = null;
selectInteraction: Select;
drawInteraction: Draw;
modifyInteraction: Modify;
draw: boolean = false;In the constructor, remove the three sample markers in the
helloSource, we won't need them anymore:// Initialize an empty vector source
this.helloSource = new VectorSource({
features: [] as Feature<Geometry>[],
wrapX: false,
});In the constructor, initialize a select interaction (which will allow you to select markers on the map) and add an event listener to detect when a selection occurs.
Note that interactions ask for permission before reacting to events via the
this.canExecutecheck in theconditionoption. This is to prevent multiple components interfering with each other. More about this further down.// Define selection interaction
this.selectInteraction = new Select({
condition: (e) => click(e) && this.canExecute("map.select"),
layers: [this.helloLayer], // Layers from which features should be selected
});
// Listen to select event
this.selectInteraction.on("select", (_e: SelectEvent) => {
console.log("a marker was selected");
// Set cursor style to default on selection
const target: HTMLElement = this.map.getTarget() as HTMLElement;
target.style.cursor = "default";
const selected: Feature[] = _e.selected;
// Set selected property to false for all features
this.helloSource!.forEachFeature((f: Feature<Geometry>) => {
f.set("selected", false);
});
// Set selected property to true for selected features
selected.forEach((f: Feature<Geometry>) => {
f.set("selected", true);
});
});In the constructor, initialize a draw interaction (which will allow you to draw new markers on the map) and add event listeners (
drawstartanddrawend) to detect when a drawing is made:// Define draw interaction
this.drawInteraction = new Draw({
source: this.helloSource!,
type: "Point",
condition: (e) => noModifierKeys(e) && this.canExecute("map.draw"),
});
// Listen to drawing start event
this.drawInteraction.on("drawstart", (_e: DrawEvent) => {
this.helloSource?.forEachFeature((f) => {
f.set("selected", false);
});
});
// Listen to drawing end event
this.drawInteraction.on("drawend", (_e: DrawEvent) => {
// NOTE: drawend event is dispatched before inserting the features
const color = randomColor();
_e.feature.setId(uuidv4()); // set marker ID
_e.feature.set("color", color); // set marker color (random)
_e.feature.set("selected", true); // set marker selected status
_e.feature.set("timestamp", Date.now()); // set marker timestamp
_e.feature.setStyle(this.myStyleFunction(_e.feature, 0)); // set marker style function
_e.feature.on("propertychange", (ev: ObjectEvent) => {
_e.feature.setStyle(this.myStyleFunction(ev.target as Feature, 0));
});
});
// Define draw interaction
this.drawInteraction = new Draw({
source: this.helloSource!,
type: "Point",
condition: (e) => noModifierKeys(e) && this.canExecute("map.draw"),
});
// Listen to drawing start event
this.drawInteraction.on("drawstart", (_e: DrawEvent) => {
this.helloSource?.forEachFeature((f) => {
f.set("selected", false);
});
});
// Listen to drawing end event
this.drawInteraction.on("drawend", (_e: DrawEvent) => {
// NOTE: drawend event is dispatched before inserting the features
const color = randomColor();
_e.feature.setId(uuidv4()); // set marker ID
_e.feature.set("color", color); // set marker color (random)
_e.feature.set("selected", true); // set marker selected status
_e.feature.set("timestamp", Date.now()); // set marker timestamp
_e.feature.setStyle(this.myStyleFunction(_e.feature, 0)); // set marker style function
_e.feature.on("propertychange", (ev: ObjectEvent) => {
_e.feature.setStyle(this.myStyleFunction(ev.target as Feature, 0));
});
});In the constructor, initialize a modify interaction (which will allow you to move the markers on the map) and add event listeners (
modifystartandmodifyend) to detect when a marker is moved:// Define modify interaction
this.modifyInteraction = new Modify({
hitDetection: this.helloLayer,
source: this.helloSource,
condition: (e) => primaryAction(e) && this.canExecute("map.modify"),
style: new Style(), // Use an empty style for the modification point or vertex
});
// Listen to modification start event
this.modifyInteraction.on("modifystart", (_e: ModifyEvent) => {
const target: HTMLElement = this.map.getTarget() as HTMLElement;
target.style.cursor = "grabbing"; // change cursor to grabbing when marker starts moving
});
// Listen to modification end event
this.modifyInteraction.on("modifyend", (_e: ModifyEvent) => {
const target: HTMLElement = this.map.getTarget() as HTMLElement;
target.style.cursor = "default"; // change cursor to default when marker stop moving
});In
renderComponent(), register the interaction listeners (for the interaction manager). TheregisterInteractionListener()method registers interactions in the state manager, a necessary step to guarantee that component interactions do not interfere which each other. By registering the listener as exclusive (second parameter true), any other feature editing interaction on the map becomes temporarily disabled. Unregistering will be done automatically by the hide() method of GirafeHTMLElement.// Renders the component
private renderComponent() {
super.render();
super.girafeTranslate();
// Register user interaction listeners
// Note: Unregistering will be done automatically by the hide() method of GirafeHTMLElement
this.registerInteractionListener('map.select', true);
this.registerInteractionListener('map.draw', true);
this.registerInteractionListener('map.modify', true);
this.map.addInteraction(this.selectInteraction);
this.registerEvents();
// Check if HelloLayer is already included in the map
const existsHelloLayer = this.map.getLayers().getArray().includes(this.helloLayer!);
if (!existsHelloLayer) {
this.map.addLayer(this.helloLayer!);
}
}In
renderEmptyComponent(), add disable the drawing mode:// Hide the panel and removes listeners
private renderEmptyComponent() {
// Turn off drawing mode
this.setDraw(false);
// Unregister events
this.unregisterEvents();
// Remove helloLayer from map
this.map.removeLayer(this.helloLayer!);
// Render empty component
this.renderEmpty();
}After the constructor, add a method to set the drawing mode:
// Set drawing mode
setDraw(val: boolean): void {
this.draw = val;
if (val) {
this.map.addInteraction(this.drawInteraction);
this.map.addInteraction(this.modifyInteraction);
} else {
this.map.removeInteraction(this.drawInteraction);
this.map.removeInteraction(this.modifyInteraction);
}
super.render();
}After the constructor, add the following method to delete all markers:
// Delete all features
deleteAllFeatures(): void {
this.helloSource?.clear();
}Create a new file
utils.tsin thehelloworld/directory and add the followingrandomColor()function to generate random colors (which will be used to color the newly drawn map markers):// Random color generator function
export function randomColor(): string {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}In
helloworld/component.html, import therandomColor()function fromutils.ts:import type { Callback } from "../../tools/state/statemanager";
import GirafeHTMLElement from "../../base/GirafeHTMLElement";
import { v4 as uuidv4 } from "uuid";
import { Map } from "ol";
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import Feature from "ol/Feature";
import { Icon, Style } from "ol/style";
import { StyleLike } from "ol/style/Style";
import { Geometry } from "ol/geom";
import { Select, Draw, Modify } from "ol/interaction";
import { SelectEvent } from "ol/interaction/Select";
import { DrawEvent } from "ol/interaction/Draw";
import { ModifyEvent } from "ol/interaction/Modify";
import { click, primaryAction, noModifierKeys } from "ol/events/condition";
import { ObjectEvent } from "ol/Object";
import { randomColor } from "./utils"; -
In the template (
helloworld/template.html) of the helloWorld component, add a button to toggle the drawing mode and another one to delete all markers:templating syntaxGeogirafe uses the µhtml templating engine to render HTML. You can dynamically change the content and attributes (style, title, etc) of an element by assigning variables or expressions with the curly brackets syntax
${}. For example, you can use:- the
classattribute to dynamically change the style of an element. - the
titleattribute to display a dynamic tooltip
You can also attach event listeners (and the associated callback function) to an element:
- the
onclickattribute is used to define a callback function that runs when the button is clicked.
For more details, please see the µhtml documentation.
<div id="panel">
<div id="content">
<h1 i18n="hello">Hello World!</h1>
<!-- Stringified list of markers from the state manager -->
<div>${JSON.stringify(this.helloworldState.markers)}</div>
<!-- Button group -->
<div class="btn-group-flex">
<!-- Draw button -->
<button
id="draw-btn"
class="${this.helloworldState.draw ? 'gg-button active' : 'gg-button'}"
title="${this.context.i18nManager.getTranslation('draw_giraffe_help')}"
onclick="${() => this.setDraw(!this.helloworldState.draw)}"
i18n="draw_giraffe"
>
Draw
</button>
<!-- Delete all button -->
<button
id="delete-points-btn"
class="gg-button"
title="${this.context.i18nManager.getTranslation('delete_all_giraffes_help')}"
onclick="${() => this.deleteAllFeatures()}"
i18n="delete_all_giraffes"
>
Delete
</button>
</div>
</div>
</div> - the
Look in your browser and open the HelloWorld panel, you should now see draw and delete buttons. If you click on the draw button, it will activate the drawing mode and you can then click on the map to add markers. You can also select and move markers on the map (check the console to see the select and modify event logs). When you select a marker, its style will be updated: it will be larger and have a yellow outline. If you click on the delete all button, all the markers on the map will be deleted.
Using the state manager
In this step, you will learn how to use the state manager to store the helloWorld component's state variables. The result will look like this:

Geogirafe uses a custom state manager called Brain based on Javascript Proxy objects.
Any component that extends the <GirafeHTMLElement> base class automatically inherits access to Geogirafe's state manager (via this.state which references this.context.stateManager.state). By convention, Geogirafe uses two locations to store a component's state variables:
this.statefor core components, e.g.this.state.themes,state.basemaps,state.mouseCoordinates, defined insrc/tools/state/state.ts.this.state.extendedStatefor custom/addon components, e.g.this.state.extendedState.helloworld, defined directly in custom components.
Observing changes on a state variable is done via the this.subscribe() method:
this.subscribe(
"path to variable in state manager or regular expression",
(_oldValue: any, _newValue: any, _parent: parentType) => {
// callback function executed when a change on the observed variable is detected
}
);
When a callback is triggered:
- The new value (
newValue) is always a Proxy, ensuring further modifications remain reactive. - The old value (
oldValue) is a deep-cloned and frozen snapshot. This guarantees it cannot be modified unintentionally.
In a class, private properties starting with _ are ignored (changes are not observed).
For example, to observe changes in the mouse coordinates variable in the core state (this.state.mouseCoordinates), we use:
this.subscribe(
"mouseCoordinates",
(_oldCoordinates: number[], _newCoordinates: number[]) => {
console.log(_newCoordinates);
}
);
The state manager can also detect deep changes in properties (i.e. multiple levels of nesting). If we wanted to monitor change only on a specific property of an an object in an array, you can use a regular expression with wildcards:
// Note that we use a regular expression with wildcards (*) here
this.subscribe(
/extendedState\.helloworld\.markers\..*\.myproperty/,
(_oldPropVal: boolean, _newPropVal: boolean, _parent: MyObjectType) => {
// callback function executed when a change on the observed variable is detected
console.log(
`myproperty on parent object ${_parent.id} changed from ${_oldPropVal} to ${_newPropVal}`
);
}
);
To observe changes on the length of an array (i.e. e.g. when an item is added or deleted), you can use:
// Note that we use a string containing the path to the marker property here
this.subscribe(
"extendedState.helloworld.myarray",
(
_oldPropVal: MyObjectType[],
_newPropVal: MyObjectType[],
_parent: HelloState
) => {
// callback function exectuted when a change on the observed variable is detected
}
);
Please see the State management documentation for more details.
We start by definining two typescript classes to represent the map markers (Marker) and the state (HelloState) of the helloWorld component:
-
In the
helloWorldcomponent's root folder, create a new file calledhellostate.ts:export class Marker {
id: string = "";
coordinates: number[] = [];
color: string = "";
selected: boolean = false;
}
export class HelloState {
draw: boolean = false;
markers: Marker[] = [];
} -
In
helloworld/component.ts, add an import for the OpenLayersVectorSourceEventclasse and for theHelloStateandMarkerclasses you just created:import type { Callback } from "../../tools/state/statemanager";
import GirafeHTMLElement from "../../base/GirafeHTMLElement";
import I18nManager from "../../tools/i18n/i18nmanager";
import MapManager from "../../tools/state/mapManager";
import { v4 as uuidv4 } from "uuid";
import Map from "ol/Map";
import VectorSource from "ol/source/Vector";
import { VectorSourceEvent } from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import Feature from "ol/Feature";
import { Icon, Style } from "ol/style";
import { StyleLike } from "ol/style/Style";
import { Point } from "ol/geom";
import { Geometry } from "ol/geom";
import { Select, Draw, Modify } from "ol/interaction";
import { SelectEvent } from "ol/interaction/Select";
import { DrawEvent } from "ol/interaction/Draw";
import { ModifyEvent } from "ol/interaction/Modify";
import { click } from "ol/events/condition";
import { ObjectEvent } from "ol/Object";
import { randomColor } from "./utils";
import { HelloState, Marker } from "./hellostate"; -
Then, modify the
HelloWorldComponentclass:Add a
helloworldStateproperty of typeHelloStateto the property definitions at the beggining ofHelloWorldComponentand remove thedrawproperty (we will move it to the component's state manager):class HelloWorldComponent extends GirafeHTMLElement {
templateUrl = './template.html';
styleUrls = ['./style.css', '../../styles/common.css']; // we also import the common styles
private readonly eventsCallbacks: Callback[] = [];
darkFrontendMode: boolean = false;
visible: boolean = false;
i18nManager: I18nManager;
private readonly map: Map;
private helloSource: VectorSource<Feature<Geometry>> | null = null;
private helloLayer: VectorLayer<VectorSource<Feature<Geometry>>> | null = null;
selectInteraction: Select;
drawInteraction: Draw;
modifyInteraction: Modify;
helloworldState: HelloState;In the constructor, initialize the
helloworldStateproperty with a reference to Geogirafe's extended state manager, remove the old draw (this.draw) property and instead initialize the draw property to false in the extended state manager:constructor() {
super('hello');
// Import the open layers map instance from Geogirafe's map manager
this.map = MapManager.getInstance().getMap();
// Initialize HelloState in Geogirafe's extended state manager
this.state.extendedState.helloworld = new HelloState();
// Reference the global state manager (this.state.extendedState.helloworld) from the local state manager (this.helloworldState)
this.helloworldState = this.state.extendedState.helloworld as HelloState;
// Initialize the drawing mode to off (false) in the extended state manager
this.helloworldState.draw = false; -
In the constructor, after the initialization of
helloSource, add the following event listeners onhelloSourceas following:// Define a vector source containing three arbitrary point features
this.helloSource = new VectorSource({
features: [] as Feature<Geometry>[],
wrapX: false,
});
// Listen to vector source feature adding
this.helloSource.on("addfeature", (_e: VectorSourceEvent) => {
const newFeature: Feature<Point> = _e.feature as Feature<Point>;
const newMarker = new Marker();
newMarker.id = newFeature.getId() as string;
newMarker.coordinates = newFeature.getGeometry()!.getCoordinates();
newMarker.color = newFeature.get("color") as string;
newMarker.selected = newFeature.get("selected") as boolean;
this.helloworldState.markers.push(newMarker);
super.render();
});
// Listen to vector source feature removal
this.helloSource.on("removefeature", (_e: VectorSourceEvent) => {
// Remove marker from list in state manager
const index = this.helloworldState.markers.findIndex(
(x) => x.id === _e.feature?.getId()
);
if (index > -1) {
this.helloworldState.markers.splice(index, 1);
}
super.render();
});
// Listen to vector source feature changes
this.helloSource.on("changefeature", (_e: VectorSourceEvent) => {
// Update marker attributes in state manager
for (let i = 0; i < this.helloworldState.markers.length; i++) {
const f = this.helloworldState.markers[i] as Marker;
// Check if feature exists in vector source
const olFeature = this.helloSource?.getFeatureById(
f.id
) as Feature<Point>;
// Update changed feature properties in the state manager
if (olFeature) {
f.color = olFeature.get("color");
f.coordinates = olFeature.getGeometry()!.getCoordinates();
f.selected = olFeature.get("selected");
}
}
super.render();
}); -
In the template (
helloworld/template.html) of the helloWorld component, add a stringified list of the markers stored in the extended state manager and replacethis.drawbythis.helloworldState.draw:<div id="panel">
<div id="content">
<h1 i18n="hello">Hello World!</h1>
<!-- Stringified list of markers from the state manager -->
<div>${JSON.stringify(this.helloworldState.markers)}</div>
<!-- Button group -->
<div class="btn-group-flex">
<!-- Draw button -->
<button
id="draw-btn"
class="${this.helloworldState.draw ? 'gg-button active' : 'gg-button'}"
title="${this.i18nManager.getTranslation('draw_giraffe_help')}"
id="draw-btn"
onclick="${() => this.setDraw(!this.helloworldState.draw)}"
i18n="draw_giraffe">
Draw
</button>
<!-- Delete all button -->
<button
id="delete-points-btn"
class="gg-button"
title="${this.i18nManager.getTranslation('delete_all_giraffes_help')}"
onclick="${() => this.deleteAllFeatures()}"
i18n="delete_all_giraffes">
Delete
</button>
</div>
</div>
</div> -
In the
setDraw()method, replace all calls tothis.drawbythis.helloworldState.draw:// Set drawing mode
setDraw(val: boolean): void {
this.helloworldState.draw = val;
if (val) {
this.map.addInteraction(this.drawInteraction);
this.map.addInteraction(this.modifyInteraction);
} else {
this.map.removeInteraction(this.drawInteraction);
this.map.removeInteraction(this.modifyInteraction);
}
super.render();
}
Look in your browser and open the HelloWorld panel. When you draw new markers, modify them or delete them (with the delete button), you should see the stringified state updated in the side panel.
Creating a subcomponent in the side panel to display a list of chips
In this step, we will create a subcomponent called "hellochip" and use it to display a list of chips in the HelloWorld side panel component. Each chip represents a map marker, as illustrated below:

-
Copy the
giraffe.svgfile to the project'ssrc/assets/icons/folder -
Add a new
hellochip/folder insrc/components/helloworld -
In the
hellochip/folder, create three new files:component.ts(which will contain the component's class definition)template.html(which will contain the component's HTML template to be rendered)style.css(which will contain the styling rules)
-
In
helloworld/hellochip/component.ts, copy the following code:import type { Callback } from "../../../tools/state/statemanager";
import GirafeHTMLElement from "../../../base/GirafeHTMLElement";
import { HelloState, Marker } from "../hellostate";
class HelloChipComponent extends GirafeHTMLElement {
templateUrl = "./template.html";
styleUrl = "./style.css";
private readonly eventsCallbacks: Callback[] = [];
fid: string = this.getAttribute("fid") as string;
color: string = this.getAttribute("color") as string;
mystyle: string = "";
title: string = "";
className: string = "chip";
helloworldState!: HelloState;
constructor() {
super("hellochip");
}
// Select a giraffe
select(): void {
console.log(`selected giraffe ${this.fid}`);
for (let i = 0; i < this.helloworldState.markers.length; i++) {
if (this.helloworldState.markers[i].id === this.fid) {
this.helloworldState.markers[i].selected = true;
} else {
this.helloworldState.markers[i].selected = false;
}
}
}
// Renders the component
private renderComponent(): void {
this.color = this.getAttribute("color") as string;
this.fid = this.getAttribute("fid") as string;
this.mystyle = `background-color:${this.color};`;
this.title = `Click to select giraffe ${this.fid}`;
super.render();
super.girafeTranslate();
}
// Register events
registerEvents() {
this.eventsCallbacks.push(
this.subscribe(
/extendedState\.helloworld\.markers\..*\..*/,
(
_oldSelected: boolean,
_newSelected: boolean,
featureProps: Marker
) => {
// update chip style when a map marker is selected
if (featureProps.id === this.fid) {
this.className = featureProps.selected
? "chip selected"
: "chip";
this.renderComponent();
}
}
)
);
// Instead of using the onclick attribute in the html template, you may also declare event listeners here
/*
this.addEventListener('click', (_e: Event) => {
this.select();
});
*/
}
/*
ConnectedCallback(): called each time the element is added to the document. The specification recommends that,
as far as possible, developers should implement custom element setup
in this callback rather than the constructor.
*/
connectedCallback() {
super.connectedCallback();
this.helloworldState = this.state.extendedState.helloworld as HelloState;
this.renderComponent();
this.registerEvents();
}
}
export default HelloChipComponent; -
In
helloworld/hellochip/template.html, copy the following code:<img src="icons/giraffe.svg" title="${this.title}" width="32" height="32" class="${this.className}" style="${this.mystyle}" onclick="${() => this.select()}"></img> -
In
helloworld/hellochip/style.css, copy the following code:.chip {
border: 2px rgba(0, 0, 0, 0.192) solid;
padding: 1px;
cursor: pointer;
box-shadow: 1px 1px 5px #00000042;
}
.chip:hover {
border: 2px rgb(255, 238, 0) solid;
box-shadow: 1px 1px 5px #e8f801;
}
.selected {
border: 2px rgb(255, 238, 0) solid !important;
} -
Modify
helloworld/template.html, as following, to render the list of chips:<div id="panel">
<div id="content">
<h1 i18n="hello">Hello World!</h1>
<!-- Stringified list of markers from the state manager -->
<div>${JSON.stringify(this.helloworldState.markers)}</div>
<!-- Button group -->
<div class="btn-group-flex">
<!-- Draw button -->
<button
id="draw-btn"
class="${this.helloworldState.draw ? 'gg-button active' : 'gg-button'}"
title="${this.i18nManager.getTranslation('draw_giraffe_help')}"
onclick="${() => this.setDraw(!this.helloworldState.draw)}"
i18n="draw_giraffe"
>
Draw
</button>
<!-- Delete all button -->
<button
id="delete-points-btn"
class="gg-button"
title="${this.i18nManager.getTranslation('delete_all_giraffes_help')}"
onclick="${() => this.deleteAllFeatures()}"
i18n="delete_all_giraffes"
>
Delete
</button>
</div>
<!-- List of chips -->
<div class="chip-container">
${ this.helloworldState.markers.map(f => uHtmlFor(f,
f.id)`<girafe-hello-chip
fid="${f.id}"
color="${f.color}"
></girafe-hello-chip
>`) }
</div>
</div>
</div> -
In
helloworld/component.ts, add the following subscriptions to theeventCallbacksarray in theregisterEvents()method. These subscriptions will listen to changes in the list of markers and their properties (color, selected) and run the callback functions:
// Register events
registerEvents(): void {
this.eventsCallbacks.push(
// Subscribe to dark mode toggle
this.subscribe('interface.darkFrontendMode', (_oldValue: boolean, newValue: boolean) => {
this.darkFrontendMode = newValue;
}),
// Subscribe to property changes (color, selected, etc) on the markers
this.subscribe(
/extendedState\.helloworld\.markers\..*\..*/,
(_oldSelected: boolean, _newSelected: boolean, _marker: Marker) => {
const mapFeatures = this.helloSource!.getFeatures();
// Update feature properties on map
if (mapFeatures) {
for (let i = 0; i < mapFeatures.length; i++) {
const target = this.helloworldState.markers.find((x) => x.id === mapFeatures[i].getId());
if (target) {
mapFeatures[i].set('color', target.color);
mapFeatures[i].set('selected', target.selected);
}
}
}
}
),
// Subscribe to changes on the list of markers (length change)
this.subscribe(
'extendedState.helloworld.markers',
(_oldMarkers: Marker[], _newMarkers: Marker[], _marker: Marker) => {
// Iterate over map features to check if they should be rendered
const mapFeatures = this.helloSource!.getFeatures();
const removeFeatures: Feature<Geometry>[] = [];
// Remove deleted features on map
if (mapFeatures) {
for (let i = 0; i < mapFeatures.length; i++) {
const target = this.helloworldState.markers.find((x) => x.id === mapFeatures[i].getId());
if (!target) {
removeFeatures.push(mapFeatures[i]);
}
}
}
this.helloSource?.removeFeatures(removeFeatures);
super.render();
}
)
);
}
-
In
src/tools/app/geogirafeapp.ts:Import the
HelloChipComponentby adding the following line to the list of imports at the beginning of the file:import HelloChipComponent from "../../components/helloworld/hellochip/component";Define the component's name (alias) by adding the following line to the list of definitions on the
customElementsvariable, in thedefineCoreComponents()method:customElements.define("girafe-hello-chip", HelloChipComponent);
Look in your browser and open the HelloWorld panel. When you draw new markers, modify them or delete them, you should see a list of colored chips rendered in the side panel.
Creating a subcomponent in the side panel to display and modify the selected marker's details
In this step, we will add a subcomponent called "hellodetails" to the HelloWorld side panel. This subcomponent will show an information box with details on the selected map marker and allow users to zoom a marker or delete it. The result is illustrated below:

-
Add a new
hellodetails/folder insrc/components/helloworld -
In the
hellodetails/folder, create three new files:component.ts(which will contain the component's class definition)template.html(which will contain the component's HTML template to be rendered)style.css(which will contain the styling rules)
-
In
helloworld/hellodetails/component.ts, copy the following code:import type { Callback } from "../../../tools/state/statemanager";
import GirafeHTMLElement from "../../../base/GirafeHTMLElement";
import Map from "ol/Map";
import { Feature } from "ol";
import { Geometry, Point } from "ol/geom";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { randomColor } from "../utils";
import { HelloState, Marker } from "../hellostate";
class HelloDetailsComponent extends GirafeHTMLElement {
templateUrl = "./template.html";
styleUrls = ["./style.css", "../../../styles/common.css"]; // we also import the common styles
private readonly eventsCallbacks: Callback[] = [];
fid: string = "";
color: string = "";
coordinates: number[] = [];
colorButton: HTMLElement;
zoomButton: HTMLButtonElement;
deleteButton: HTMLButtonElement;
helloworldState!: HelloState;
private get map(): Map {
return this.context.mapManager.getMap();
}
constructor() {
super("hellodetails");
this.colorButton = this.shadow.getElementById(
"giraffe-avatar"
) as HTMLElement;
this.zoomButton = this.shadow.getElementById(
"zoom-btn"
)! as HTMLButtonElement;
this.deleteButton = this.shadow.getElementById(
"delete-btn"
)! as HTMLButtonElement;
}
// Zoom to feature
zoomTo(id: string): void {
const layers = this.map.getLayers().getArray();
const helloLayer = layers.find(
(layer) => layer.get("name") == "helloworldLayer"
) as VectorLayer<VectorSource<Feature<Geometry>>>;
const source = helloLayer.getSource() as VectorSource;
const feature: Feature<Point> = source.getFeatureById(
id
) as Feature<Point>;
if (feature) {
this.state.position.center = feature.getGeometry()!.getCoordinates();
this.state.position.zoom = 7;
}
}
render(): void {
this.renderComponent();
}
// Render the component
private renderComponent(): void {
const target = this.helloworldState.markers.find((x) => x.selected);
if (target) {
this.color = target.color as string;
this.fid = target.id as string;
this.coordinates = target.coordinates;
} else {
this.color = "";
this.fid = "";
this.coordinates = [];
}
super.render();
super.girafeTranslate();
this.colorButton = this.shadow.getElementById(
"giraffe-avatar"
)! as HTMLElement;
this.zoomButton = this.shadow.getElementById(
"zoom-btn"
)! as HTMLButtonElement;
this.deleteButton = this.shadow.getElementById(
"delete-btn"
)! as HTMLButtonElement;
}
// Register events
registerEvents() {
this.eventsCallbacks.push(
this.subscribe(
"extendedState.helloworld.markers",
(
_oldSelected: boolean,
_newSelected: boolean,
_featureProps: Marker
) => {
this.render();
}
),
this.subscribe(
/extendedState\.helloworld\.markers\..*\..*/,
(
_oldSelected: boolean,
_newSelected: boolean,
_featureProps: Marker
) => {
this.fid = _featureProps.id;
this.color = _featureProps.color;
this.coordinates = _featureProps.coordinates;
this.render();
}
)
);
// Change giraffe color button event listener
this.colorButton.addEventListener("click", (_e) => {
const target = this.helloworldState.markers.find((x) => x.selected);
if (target) {
target.color = randomColor();
}
});
// Delete giraffe button event listener
this.zoomButton.addEventListener("click", (_e: Event) => {
this.zoomTo(this.fid);
});
// Delete giraffe button event listener
this.deleteButton.addEventListener("click", (_e: Event) => {
const index = this.helloworldState.markers.findIndex(
(x) => x.id === this.fid
);
if (index > -1) {
this.helloworldState.markers.splice(index, 1);
}
});
}
/*
ConnectedCallback(): called each time the element is added to the document. The specification recommends that,
as far as possible, developers should implement custom element setup
in this callback rather than the constructor.
*/
connectedCallback() {
super.connectedCallback();
this.helloworldState = this.state.extendedState.helloworld as HelloState;
this.renderComponent();
this.registerEvents();
}
}
export default HelloDetailsComponent; -
In
helloworld/hellodetails/template.html, copy the following code:<div id="hello-details">
<div class="centered">
<svg
id="giraffe-avatar"
width="240mm"
height="240mm"
viewBox="0 0 240 240"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
>
<g id="avatar-group" transform="translate(54.744042,15.699027)">
<g transform="translate(0,-5.0919451)">
<circle
id="circle-primitive"
fill="#FFFFFF"
fill-opacity="1"
stroke="none"
cx="65.255959"
cy="109.39291"
r="94.908058"
/>
<path
id="giraffe-path"
fill="${this.color}"
fill-opacity="1"
stroke="none"
d="m 78.96013,58.932266 c -3.314919,-0.130721 -6.576367,1.217866 -8.697661,3.821988 -1.54944,1.902081 -2.220341,3.551907 -2.491322,6.127273 -0.360918,3.430104 1.579676,7.499835 4.662765,9.779768 l 0.711068,0.526065 -0.866613,4.357873 c -0.476589,2.39681 -0.925256,4.795967 -0.997355,5.331457 l -0.131257,0.973585 0.911055,0.225824 c 2.647799,0.656543 3.537151,0.92333 4.865852,1.45986 0.80137,0.323583 1.550535,0.567854 1.665015,0.542602 0.11448,-0.02524 0.465997,-1.536988 0.781346,-3.359483 1.552062,-8.969976 1.420651,-8.410268 2.012281,-8.55245 1.16069,-0.278947 3.2742,-1.473287 4.358391,-2.4629 3.704989,-3.381751 4.61156,-8.618035 2.267561,-13.096356 -0.86038,-1.643785 -3.0377,-3.801761 -4.599718,-4.558896 -1.426929,-0.691647 -2.944627,-1.056793 -4.451408,-1.11621 z m -27.115658,0.06253 c -1.853499,0.0066 -2.399186,0.09148 -3.592545,0.556037 -1.776499,0.691555 -2.939674,1.490607 -4.233333,2.90835 -1.939102,2.125078 -2.789494,4.367665 -2.789494,7.356655 0,2.12751 0.541991,3.97087 1.659329,5.644102 1.531501,2.293438 3.657124,3.949724 5.948474,4.634857 l 0.842843,0.251667 0.360701,2.113047 c 0.683681,4.00508 1.596287,9.514157 1.596287,9.636622 0,0.153863 0.280961,0.06373 2.601391,-0.832508 1.03033,-0.397942 2.561188,-0.865222 3.401859,-1.038178 0.840669,-0.172966 1.58106,-0.366665 1.644862,-0.430466 0.138798,-0.138795 -0.09666,-1.521111 -1.081072,-6.349482 -0.40649,-1.99372 -0.738973,-3.823298 -0.738973,-4.065902 0,-0.270629 0.24434,-0.585637 0.632518,-0.814938 0.991741,-0.58583 2.634369,-2.411619 3.301609,-3.669543 3.21256,-6.056471 0.218561,-13.536525 -6.223911,-15.549954 -0.787148,-0.246002 -1.827815,-0.355867 -3.330545,-0.350366 z M 118.38356,78.4396 c -5.4613,0.01087 -10.83084,0.334656 -13.17129,0.794269 -8.3241,1.63468 -14.830472,6.177298 -19.081003,13.322183 -1.41655,2.381134 -1.443389,2.220246 0.62787,3.757909 l 1.796788,1.334286 0.325561,-0.45992 c 1.885112,-2.661536 3.461306,-4.224727 5.737635,-5.691124 2.05182,-1.321774 3.213278,-1.874636 5.775359,-2.749188 3.25365,-1.110612 5.79187,-1.558432 9.71052,-1.713073 l 3.41323,-0.134358 -0.14677,0.605131 c -0.22712,0.937305 -2.24839,4.87313 -3.37498,6.571691 -1.63554,2.465938 -5.22697,6.010124 -7.89099,7.787124 -2.122833,1.416 -5.099155,2.95806 -7.044536,3.65042 -0.443719,0.15792 -0.847719,0.39385 -0.89762,0.524 -0.04994,0.13015 0.257749,0.78161 0.683681,1.44746 0.910849,1.42388 2.374014,4.47857 2.374014,4.9568 0,0.48392 0.347564,0.42554 2.910931,-0.48886 5.55972,-1.98327 9.67439,-4.66415 13.96091,-9.09608 5.48334,-5.669343 8.59492,-12.340298 9.12761,-19.569868 0.22696,-3.0802 0.13785,-3.67811 -0.64544,-4.337203 -0.60562,-0.509278 -0.68029,-0.518698 -4.19148,-0.511599 z M 13.114536,78.4422 c -2.603428,-0.0031 -4.6648949,0.107204 -5.1201,0.360185 -0.8002799,0.44476 -0.9499785,1.401871 -0.7027994,4.497398 0.6171512,7.728879 3.7952414,14.125371 10.2019604,20.534657 4.00355,4.00515 7.663291,6.26847 13.744381,8.50026 1.41078,0.51776 2.081528,0.55543 2.081528,0.11627 0,-0.70954 2.621841,-5.7704 3.188951,-6.1557 0.107791,-0.0732 -1.432293,-0.92853 -3.422012,-1.90065 -1.98973,-0.97214 -4.452244,-2.36424 -5.472536,-3.09336 -4.48011,-3.201484 -8.269856,-8.088862 -10.335286,-13.328893 l -0.421161,-1.068152 3.391524,0.121441 c 3.97215,0.142071 6.170742,0.498425 9.203571,1.491898 5.26909,1.726026 8.813702,4.21721 12.006503,8.438761 l 0.541568,0.716235 1.643827,-1.233 c 0.903991,-0.678215 1.723361,-1.297845 1.820561,-1.377175 C 45.720137,94.85418 43.916309,91.48356 42.636249,89.776392 38.612699,84.410397 32.687916,80.732826 25.718934,79.275271 23.297647,78.768866 17.453586,78.44745 13.114536,78.442246 Z m 15.966469,14.394471 c -0.240599,0.01217 -0.409009,0.09103 -0.642339,0.243911 -0.684239,0.448302 -0.669992,0.916382 0.05788,1.920296 1.432581,1.975911 4.902044,4.72104 8.481653,6.710702 l 5.29e-4,5.3e-4 c 2.026822,1.12657 2.404464,1.22958 2.791044,0.76377 0.38286,-0.4613 0.149397,-1.05952 -1.234032,-3.159489 -2.11764,-3.21442 -5.35546,-5.657353 -8.379849,-6.322611 -0.521496,-0.11471 -0.834271,-0.169264 -1.07487,-0.157096 z m 72.257085,0.06666 c -2.007316,-0.07737 -6.279721,2.151388 -8.339026,4.827611 -1.7316,2.250345 -2.762355,4.453738 -2.300118,4.915978 0.226381,0.22637 0.435708,0.16996 1.876888,-0.50384 4.448961,-2.08007 9.848496,-6.643935 9.848496,-8.324561 0,-0.607115 -0.41713,-0.889397 -1.08624,-0.915188 z M 49.74331,103.49912 c -0.933826,-0.0436 -1.92105,0.0965 -2.925406,0.43873 -2.125009,0.72406 -3.923789,3.21232 -3.923789,5.42809 0,1.39971 0.343702,2.15024 1.139981,2.4908 1.112409,0.47575 1.968738,0.0897 2.616377,-1.17977 0.769692,-1.50877 2.116376,-2.10535 3.627686,-1.60714 0.644861,0.21257 1.015265,0.548 1.677935,1.52136 0.81571,1.19815 0.889728,1.25178 1.825728,1.31258 0.83435,0.0542 1.050367,-0.0176 1.491898,-0.49403 0.70113,-0.75653 0.840335,-1.69857 0.449586,-3.04271 -0.858174,-2.95199 -3.178522,-4.7372 -5.979996,-4.86791 z m 31.345888,0.0336 c -1.998729,-10e-4 -3.128489,0.43782 -4.595069,1.78439 -1.913938,1.75731 -2.589551,4.59993 -1.422652,5.98672 0.448681,0.53322 0.689603,0.64027 1.438672,0.64027 0.95336,0 1.4404,-0.39929 2.205551,-1.80816 0.47189,-0.86889 1.665536,-1.38929 2.797244,-1.21957 0.692282,0.10382 1.071491,0.32463 1.643311,0.95757 0.40876,0.45245 0.743109,0.91246 0.743109,1.02216 0,0.4401 0.936194,1.048 1.613852,1.048 0.921211,0 1.433375,-0.29189 1.818495,-1.03663 1.032631,-1.9969 -0.717997,-5.51018 -3.387908,-6.79959 -0.993378,-0.47974 -1.463786,-0.57453 -2.854605,-0.57516 z m -46.619893,13.43433 c -0.276352,-0.005 -0.555669,-0.002 -0.837676,0.0108 l -1.456756,0.0662 -0.462505,2.28927 c -1.563571,7.74074 -0.288655,15.69759 3.656624,22.81928 1.614911,2.91509 3.537109,5.39884 6.188771,7.99692 l 2.347142,2.29961 0.07855,25.41756 c 0.04639,15.07505 0.04993,21.36444 0.223242,24.0683 a 94.908057,94.908057 0 0 0 13.3072,2.02727 c 0.909749,-0.20503 1.446369,-0.55108 2.273763,-1.15704 0.712568,-0.52186 1.154285,-1.08547 1.665013,-2.12493 0.615881,-1.25334 0.694848,-1.60596 0.688848,-3.08508 -0.0058,-1.514 -0.07562,-1.79549 -0.745175,-3.01946 -1.327029,-2.42583 -3.701135,-3.71478 -6.382555,-3.46439 -0.746612,0.0697 -1.72126,0.31661 -2.16576,0.54829 -0.4445,0.23169 -0.852188,0.42116 -0.905888,0.42116 -0.144979,0 -0.118406,-33.42349 0.02687,-33.80207 0.111339,-0.2901 0.187372,-0.29124 0.727604,-0.0119 1.222769,0.63232 3.937537,1.53268 6.040456,2.0035 1.890281,0.42318 2.701491,0.48345 6.45542,0.47646 l 4.293277,-0.008 0.109038,1.98541 c 0.133612,2.42774 0.782759,4.41779 2.16421,6.63422 2.340951,3.75588 5.918588,5.86269 9.954947,5.86269 2.029489,0 3.586999,-0.42135 4.278808,-1.15755 l 0.508497,-0.54106 0.05736,-10.52028 0.05788,-10.52081 2.664436,-2.62826 c 2.899529,-2.86015 4.992249,-5.73789 6.754109,-9.28832 2.233512,-4.50088 3.462589,-9.69841 3.47059,-14.67456 0.0027,-1.74465 -0.522878,-6.0518 -0.990637,-8.11785 l -0.165365,-0.72864 -1.507918,-0.061 c -4.783659,-0.19443 -8.955752,2.39301 -11.072191,6.86677 -0.34141,0.72168 -0.621051,1.4125 -0.621152,1.53531 -9e-5,0.12281 0.290055,0.82187 0.644406,1.55339 1.31799,2.72084 1.874245,6.34028 1.473293,9.58443 -0.85606,6.92645 -6.157921,13.45791 -13.3227,16.41243 -5.78241,2.38449 -13.149373,2.18239 -18.597313,-0.51004 -5.78163,-2.85738 -10.019414,-7.77561 -11.655102,-13.52579 -0.39125,-1.37536 -0.473469,-2.1722 -0.47284,-4.57905 0.0011,-3.31386 0.273185,-4.64187 1.481045,-7.22437 0.848381,-1.81388 0.850151,-1.79921 -0.490408,-4.18217 -2.108401,-3.74786 -5.594162,-5.86426 -9.739458,-5.94641 z m 22.902995,6.65023 c -0.528376,0.01 -0.864489,0.1385 -1.458825,0.4868 -1.054891,0.61821 -1.520793,1.3503 -1.697054,2.66702 -0.22647,1.69181 1.400609,3.8858 3.35225,4.52014 0.898808,0.29214 2.352969,0.12191 2.989998,-0.34985 1.612371,-1.19404 1.880637,-2.73634 0.838708,-4.82141 -0.809191,-1.61935 -1.807162,-2.34294 -3.415813,-2.47634 -0.235627,-0.0195 -0.433139,-0.0296 -0.609264,-0.0263 z m 15.156698,0.0165 c -2.285825,0.0741 -4.032313,2.12408 -4.032313,4.85035 0,1.20002 0.444309,2.00284 1.436089,2.59416 1.746571,1.04132 4.770173,-0.17744 5.900415,-2.37815 1.173038,-2.28404 -0.263427,-4.8408 -2.840138,-5.05447 -0.156821,-0.013 -0.311666,-0.0168 -0.464053,-0.0119 z m 2.483051,11.21586 c -0.656468,0.008 -1.287205,0.58041 -1.85725,1.70532 -1.291241,2.54818 -4.130101,4.55892 -6.84661,4.84983 -3.611099,0.38671 -6.800913,-1.28571 -8.724532,-4.57492 -0.447072,-0.76445 -1.005451,-1.52501 -1.240753,-1.68981 -0.662358,-0.46394 -1.535462,-0.36725 -2.139402,0.23668 -0.73946,0.73944 -0.592905,1.90298 0.460436,3.65765 1.62333,2.70418 4.279106,4.76062 7.243487,5.60896 0.767919,0.21977 2.033558,0.37237 3.158979,0.38137 4.895789,0.0387 8.966806,-2.44771 11.117668,-6.79028 0.79294,-1.60101 0.797332,-2.0185 0.02997,-2.78588 -0.404895,-0.40489 -0.808112,-0.60352 -1.201995,-0.59892 z m -14.836303,37.70202 c -0.143047,-2e-5 -0.287245,0.007 -0.432015,0.0196 -2.50707,0.22733 -4.195265,1.92264 -4.503084,4.52169 -0.23368,1.97301 0.986602,4.00884 2.951242,4.92372 1.691079,0.78749 3.376216,0.56644 4.973855,-0.65215 1.644422,-1.25422 2.281523,-3.1966 1.702223,-5.18935 -0.630029,-2.1672 -2.546504,-3.62311 -4.692221,-3.62355 z m 20.363097,10.79986 c -0.807357,0.0171 -1.6291,0.11477 -2.417939,0.30076 -4.74466,1.11867 -8.179962,5.16078 -8.52351,10.02885 -0.161139,2.28329 0.287782,3.95948 1.59835,5.96707 1.30869,2.00471 2.928605,3.3361 4.902544,4.02353 a 94.908057,94.908057 0 0 0 10.506853,-1.80816 v -7.47654 c 0,-8.51825 -0.0089,-8.71429 -0.435634,-9.2568 -0.91263,-1.16021 -3.208586,-1.83016 -5.630664,-1.77871 z"
/>
<path
id="circle-path"
fill="none"
stroke="#000000"
stroke-width="1"
stroke-miterlimit="4"
stroke-dasharray="none"
stroke-opacity="1"
d="M 160.26051,109.39293 A 95.004552,95.004607 0 0 0 65.25596,14.388319 95.004552,95.004607 0 0 0 -29.74859,109.39293 95.004552,95.004607 0 0 0 65.25596,204.39752 95.004552,95.004607 0 0 0 160.26051,109.39293 Z"
/>
<text
font-size="17"
text-anchor="middle"
direction="ltr"
dy="20"
pointer-events="none"
style="user-select: none"
>
<textPath href="#circle-path" startOffset="70%">
${this.context.i18nManager.getTranslation('color_giraffe')}
</textPath>
</text>
</g>
</g>
</svg>
</div>
<div id="giraffe-details">
<div>ID: ${this.fid}</div>
<div>X: ${this.coordinates[0]?.toFixed(2)}</div>
<div>Y: ${this.coordinates[1]?.toFixed(2)}</div>
<div>Color: ${this.color}</div>
</div>
<div class="btn-group-flex">
<button id="zoom-btn" class="gg-button" title="" i18n="zoom_on_giraffe">
Zoom to
</button>
<button id="delete-btn" class="gg-button" title="" i18n="delete">
Delete
</button>
</div>
</div> -
In
helloworld/hellodetails/style.css, copy the following code:#hello-details {
background: #f2f2f2;
padding: 1.5rem;
margin: 10px 0px;
}
#giraffe-avatar {
width: 100%;
height: 100%;
max-width: 180px;
}
#avatar-group {
cursor: pointer;
}
#avatar-group:hover {
stroke: #2f4355;
}
#giraffe-details {
margin: 25px 0;
}
.centered {
display: flex;
align-items: center;
justify-content: center;
}
.btn-group-flex {
display: flex;
flex-direction: row;
gap: 0.5rem;
margin: 0.5rem auto;
}
.btn-group-flex > button,
.btn-group-flex > select,
.btn-group-flex > input {
flex: 50%;
} -
Modify
helloworld/template.html, as following to render the details box in the side panel (note we've removed the stringified list of markers):<div id="panel">
<div id="content">
<h1 i18n="hello">Hello World!</h1>
<!-- Button group -->
<div class="btn-group-flex">
<!-- Draw button -->
<button
id="draw-btn"
class="${this.helloworldState.draw ? 'gg-button active' : 'gg-button'}"
title="${this.context.i18nManager.getTranslation('draw_giraffe_help')}"
onclick="${() => this.setDraw(!this.helloworldState.draw)}"
i18n="draw_giraffe"
>
Draw
</button>
<!-- Delete all button -->
<button
id="delete-points-btn"
class="gg-button"
title="${this.context.i18nManager.getTranslation('delete_all_giraffes_help')}"
onclick="${() => this.deleteAllFeatures()}"
i18n="delete_all_giraffes"
>
Delete
</button>
</div>
<!-- Details box -->
<girafe-hello-details
id="girafe-details"
element=""
></girafe-hello-details>
<!-- List of chips -->
<div class="chip-container">
${ this.helloworldState.markers.map(f => uHtmlFor(f,
f.id)`<girafe-hello-chip
fid="${f.id}"
color="${f.color}"
></girafe-hello-chip
>`) }
</div>
</div>
</div> -
In
src/tools/app/geogirafeapp.ts:Import the
HelloDetailsComponentby adding the following line to the list of imports at the beginning of the file:import HelloDetailsComponent from "../../components/helloworld/hellodetails/component";Define the component's name (alias) by adding the following line to the list of definitions on the
customElementsvariable, in thedefineCoreComponents()method:customElements.define("girafe-hello-details", HelloDetailsComponent);
Look in your browser and open the HelloWorld panel. When you draw new markers, modify them or delete them (with the delete button), in addition to the colored chips, you should now see an information box displaying details about the selected marker. If you click on the giraffe avatar, it will change the marker's color. You can also delete the marker or zoom to it, if you click on the dedicated buttons. Congratulations! You have successfully finished this tutorial!
