Register New Document Types With a Corresponding Editor
Introduction
This how-to describes how to introduce a new document type and set up a custom editor that allows users to edit documents of that type.
Prerequisites
Before starting this how-to, make sure you have completed the following prerequisites:
Custom Document Model
Studio Pro allows you to extend its metamodel by adding custom document types. These documents can store arbitrary data that can be serialized as strings. If you register an editor (a user-defined UI component) for a specific document type, documents of that type will be displayed in the UI alongside any other built-in document type, such as constants, Java Actions, and pages. Specifically, they will appear in the New Document and Find Advanced dialogs, context menus for adding documents, the App Explorer, and other UI elements that show Studio Pro documents. You can register custom editors to appear either as tabs or as modal dialogs.
Registering a New Document Type
To register a new document type, follow the steps below:
-
Generate a new extension named
myextensionas described in the Get Started guide. -
Replace the contents of
src/main/index.tswith the code below:import { IComponent, getStudioProApi } from "@mendix/extensions-api"; import { personDarkThemeIcon, personDocumentType, personLightThemeIcon } from "../model/constants"; import { PersonInfo } from "../model/PersonInfo"; export const component: IComponent = { async loaded(componentContext) { const studioPro = getStudioProApi(componentContext); await studioPro.app.model.customBlobDocuments.registerDocumentType<PersonInfo>({ type: personDocumentType, readableTypeName: 'Person', defaultContent: { firstName: '', lastName: '', age: 0, email: '' } }); await studioPro.ui.editors.registerEditorForCustomDocument({ documentType: personDocumentType, editorEntryPoint: 'editor', editorKind: 'tab', iconLight: personLightThemeIcon, iconDark: personDarkThemeIcon }) } } -
Add a new file
src/model/constants.tswith the following contents:export const personDocumentType = 'myextension.Person'; export const personLightThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAABKElEQVRIDd2Vyw3CMBBEAxIUAWVQBxIcKIBiuNAAFVAIV2iAA2cKoAGYF9nIctaxscIBRhrZ2Z3d9T9N8++YaoIb8ShexYcjfWz40FRhraib+MwQDdpijKXci7nEsZ8YYrOoSe6LEdsLpurFtW1yudiskjXPFSaHufGciFxwqZ9cLcJNWXnjAK2Zi7NdOsKcjlwdcIlygaV+crUIl8jbwnauj5F4CY2ujw0fmiTCAndDtXC2g+HzNq8JJVau9m2Jl+CkKAYxEbfi2ZE+Nnxo4jjeqQ5Sx3QnZThTH4gNX5yc7/cx9WLavovGKJfizJG+NXKSJy+afO2raI3oE1vyqaAA+OpjRwHWtqYIMdZekdMEUy15/NBkl8WsICMbz4ng2A3+y1TOH8ALNqHxhf/P+xwAAAAASUVORK5CYII='; export const personDarkThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAWdJREFUSIm1ljFuwkAQRd/giFTkABS5gMsolBRcIFBwCOTGNUfgDtDRJ9yDioaCKlJ8B0dYmyLjZGLtrh0Jj7SyNPP3f894dtbinHP0aIM+yQHuYkERuQdegDnwBIw1VABH4BV4c86VQRIXMGABXADXsi7AIsjjIR4AG0NwAnIgBUa6UvWdDG4DDLoI1OQlkAFJJMtEMWUtEhXQstTksxCxR2hmRP6UCwMamppnXcnN/sx8k6FPYGlqHixLRCAx32RZ++05mOtz65y7Btsu3I1XYNvgwmZwJty1XbNINYOzL4MxgIg8/Pftjb1bLmgZFSJSiAgiMvHEJhorYhxWoAY+Gt9RnyvP3lUDY/f+ipr67fmuX258U6ACPoEd8Kxrp74KmBp8rhz7H58JetsUWCtRcwZVwLqtTTsdNM3kAHzoOtg3V0z8oCmov1FhwP0NO93U77g2Qje5cETJvHaLKzMqcAvr/a/iC+JcVEP5CMhEAAAAAElFTkSuQmCC'; -
Add another file
src/model/PersonInfo.tsin the same directory:export type PersonInfo = { firstName: string; lastName: string; age: number; email: string; } -
Rename the
src/ui/index.tsxfile tosrc/ui/editor.tsxand paste the following contents into it:import React, { StrictMode, useCallback, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import { getStudioProApi, IComponent, StudioProApi } from "@mendix/extensions-api"; import type { PersonInfo } from "../model/PersonInfo"; function PersonEditor(input : { studioPro: StudioProApi, documentId: string }) { const {studioPro,documentId} = input; const [person, setPerson] = useState<PersonInfo>({ firstName: "", lastName: "", age: 0, email: "", }); const [documentVersion, setDocumentVersion] = useState(0); // Used to trigger re-fetching the document useEffect(() => { studioPro.app.model.customBlobDocuments.addEventListener("documentsChanged", ({ documents }) => { if (documents.some(doc => doc.id === documentId)) { setDocumentVersion(v => v + 1); // Trigger re-fetch of the document } }); }, [studioPro, documentId]); useEffect(() => { studioPro.app.model.customBlobDocuments .getDocumentById<PersonInfo>(documentId) .then(documentFromModel => { if (documentFromModel && !("error" in documentFromModel)) { setPerson(documentFromModel.document.contents); } }) .catch(err => { studioPro.ui.messageBoxes.show("error", "Error loading document", "Details: " + err?.message || err); }); }, [studioPro, documentId, documentVersion]); const savePerson = useCallback(async () => { try { await studioPro.app.model.customBlobDocuments.updateDocumentContent<PersonInfo>(documentId, person) } catch (error) { studioPro.ui.messageBoxes.show("error", "Error saving document", "Details: " + ((error as {message?: string})?.message || error)); } }, [studioPro, documentId, person]); const labelStyle = { display: 'inline-block', width: '300px' }; return ( <div style={{ backgroundColor: 'white', padding: '20px' }}> <h2>Person Editor</h2> <div> <label style={labelStyle}> First name: <input type="text" value={person.firstName} onChange={e => setPerson({ ...person, firstName: e.target.value })} /> </label> </div> <div> <label style={labelStyle}> Last name: <input type="text" value={person.lastName} onChange={e => setPerson({ ...person, lastName: e.target.value })} /> </label> </div> <div> <label style={labelStyle}> Age: <input type="number" value={person.age} onChange={e => setPerson({ ...person, age: Number(e.target.value) })} /> </label> </div> <div> <label style={labelStyle}> Email: <input type="email" value={person.email} onChange={e => setPerson({ ...person, email: e.target.value })} /> </label> </div> <div style={{ marginTop: 8 }}> <button onClick={savePerson}>Save</button> </div> </div> ); } export const component: IComponent = { async loaded(componentContext, args: { documentId: string; }) { const studioPro = getStudioProApi(componentContext); createRoot(document.getElementById("root")!).render( <StrictMode> <PersonEditor studioPro={studioPro} documentId={args.documentId} /> </StrictMode> ); } }; -
Update the build instructions by replacing the contents of
build-extension.mjswith the code below:import * as esbuild from 'esbuild' import {copyToAppPlugin, copyManifestPlugin, commonConfig} from "./build.helpers.mjs" import parseArgs from "minimist" const outDir = `dist/myextension` const appDir = "/Users/petar.vukmirovic/Mendix/App-Guide-Test-Blob" const extensionDirectoryName = "extensions" const entryPoints = [ { in: 'src/main/index.ts', out: 'main' } ] entryPoints.push({ in: 'src/ui/editor.tsx', out: 'editor' }) const args = parseArgs(process.argv.slice(2)) const buildContext = await esbuild.context({ ...commonConfig, outdir: outDir, plugins: [copyManifestPlugin(outDir), copyToAppPlugin(appDir, outDir, extensionDirectoryName)], entryPoints }) if('watch' in args) { await buildContext.watch(); } else { await buildContext.rebuild(); await buildContext.dispose(); } -
Replace the contents of
manifest.jsonwith the code below:{ "mendixComponent": { "entryPoints": { "main": "main.js", "ui": { "editor": "editor.js" } } } }
Explanation
Register the Document Type
In src/main/index.ts, you begin by registering a new document type. Once registered, you can perform all CRUD operations on it. However, it will not appear in the UI until an editor is also registered.
Optionally, you can provide a readableTypeName to display a user-friendly name in logs and the Studio Pro UI instead of the full type name. You can also customize serialization of the document contents to a string. By default, the API uses JSON.stringify for serialization and JSON.parse for deserialization.
Register the Editor
The next call to the Studio Pro API in src/main/index.ts registers an editor for the document type. This does the following:
- It registers the
editorentry point of the extension to the document type, so the editor is shown when users interacts with the document in Studio Pro (for example, through the App Explorer or Find Results). - This editor is shown as a tab, but you can also configure it to be shown as a modal dialog.
- Icons for both the light and dark themes are registered; these icons appear wherever a document icon is needed.
- Note that this editor will behave like editors for other, built-in document types. For example, the
studioPro.ui.editors.editDocumentcall will open the registered editor for custom documents.
Changes in the Editor
In src/ui/editor.tsx, the first highlighted block of code listens for changes to documents to ensure the most recent version of the currently active document is shown. Note that the document can be changed outside the currently open editor, either by calls to the custom blob document API, or by Studio Pro operations like Undo.
In the next highlighted block, document contents are fetched whenever a new document is opened or an existing document is updated.
We then provide a way to save changes.
Update Build and Manifest Files
Highlighted text in build-extension.mjs and manifest.json shows changes necessary to ensure the editor entry point is built and loaded properly.
Extensibility Feedback
If you would like to provide us with additional feedback, you can complete a small survey.
Any feedback is appreciated.