Check out the Manifest Editor →
DevelopersResource creators

Resource creators

The primary functionality of the Manifest Editor is based around the ability to create, update and delete resources.

A resource creator is made up of a few different parts:

  • A form to create the resource, or custom UI
  • An asyncronous function to save the resource
  • Metadata about what it creators, and which fields

A simple example of a resource creator is the ImageUrlCreator.

The form is a simple text input, with a URL to an image hosted somewhere - and a button to submit the form.

import { FormEvent } from "react";
import { InputContainer, InputLabel, Input } from "@manifest-editor/editors";
import { CreatorContext } from "@manifest-editor/creator-api";
 
export function CreateImageUrlForm(props: CreatorContext) {
  const onSubmit = (e: FormEvent) => {
    e.preventDefault();
    const data = new FormData(e.target as HTMLFormElement);
    const formData = Object.fromEntries(data.entries()) as any;
 
    if (formData.url) {
      props.runCreate({ url: formData.url });
    }
  };
 
  return (
    <form onSubmit={onSubmit}>
      <InputContainer $wide>
        <InputLabel htmlFor="id">Link to Image</InputLabel>
        <Input id="url" name="url" defaultValue="" />
      </InputContainer>
 
      <Button type="submit">Create</Button>
    </form>
  );
}

The props passed to this component are provided by the CreatorContext, which includes the props.runCreate() which is also defined by the creator.

This is the creator function. This is used by the Form, but can also be used by other creators so you can compose them together to create more complex resources.

import { getImageDimensions, getFormat } from "@manifest-editor/shell";
import { CreatorFunctionContext } from "@manifest-editor/creator-api";
 
export interface CreateImageUrlPayload {
  url: string;
  format?: string;
  height?: number;
  width?: number;
}
 
export async function createImageUrl(
  data: CreateImageUrlPayload,
  ctx: CreatorFunctionContext
): Promise<CreatorResource> {
  if (!data.height || !data.width) {
    const dimensions = await getImageDimensions(data.url);
    if (dimensions) {
      data.height = dimensions.height;
      data.width = dimensions.width;
    }
  }
 
  return ctx.embed({
    id: data.url,
    type: "Image",
    format: data.format || (await getFormat(data.url)),
    height: data.height,
    width: data.width,
  });
}

Whatever you return from this function is what will be saved to the parent resource.

You can see what the parent resource is by looking at the CreatorContext along with some helpers for correctly creating references and linking them together in the Vault:

interface CreatorFunctionContext {
  options: {
    targetType: string;
    target?: Reference;
    parent?: CreatorParent;
    initialData?: any;
  };
  ref(idOrRef: string | Reference): ReferencedResource;
  embed(data: any): CreatorResource;
  create(definition: string, payload: any, options?: Partial<CreatorOptions>): Promise<CreatorResource>;
  generateId(type: string, parent?: Reference | ReferencedResource): string;
  getParent(): Reference | undefined;
  getTarget(): SpecificResource | Reference | undefined;
  getParentResource(): SpecificResource | undefined;
  getPreviewVault(): Vault;
}

This is required because of how the Vault stores resources as flat resources. In most cases you want to save embedded resources to the Vault and reference them by an ID and type.

There are some exceptions - such as service properties, in which case you can use the embed() helper.

There are a lot of examples in the GitHub repository of different ways to use these functions.

These functions are bound together in a CreatorDefinition:

import { CreatorDefinition } from "@manifest-editor/creator-api";
 
export const imageUrlCreator: CreatorDefinition = {
  id: "@manifest-editor/image-url-creator",
  create: createImageUrl,
  label: "Image",
  summary: "Image from a URL",
  icon: <AddImageIcon />,
  render(ctx) {
    return <CreateImageUrlForm {...ctx} />;
  },
  resourceType: "ContentResource",
  resourceFields: ["format"],
  supports: {
    parentFields: ["logo", "body", "thumbnail"],
  },
  staticFields: {
    type: "Image",
  },
};

This definition specifies some components and labels that will be displayed to users when they go to use your creator from the UI.

You can add new creators to your mapApp function when creating the Editor.

import { mapApp } from "@manifest-editor/shell";
import * as manifestEditorPreset from "@manifest-editor/manifest-preset";
import { myCustomCreator } from "./my-custom-creator";
 
const app = mapApp(manifestEditorPreset, (app) => ({
  ...app,
  creators: [
    //
    ...app.creators,
    myCustomCreator,
  ],
}));

Now in the Manifest editor, when the “create” icon is clicked in a valid context - based on the configuration you provided - they will see your icon and be able to click on it to use your Form component to create a new resource.