# Perspective Custom Container

**URL:** <https://forum.inductiveautomation.com/t/perspective-custom-container/40193>\
**Category:** Module Development\
**Created:** [November 1, 2020, 12:04am UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193 "2020-11-01T00:04:07Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![dillonu](https://avatars.discourse-cdn.com/v4/letter/d/278dde/32.png) [@dillonu](https://forum.inductiveautomation.com/u/dillonu)\
**Post date:** [November 1, 2020, 12:04am UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/1 "2020-11-01T00:04:07Z")

</div>

Hi, I’ve been looking through the SDK documentation and examples, and have been developing perspective modules for some time. We’ve gotten to the point where we’d like to create our own perspective container to handle client-side events like zooming and mouse dragging. How can we create a custom container? The examples and documentation don’t seem to cover this topic.

Thanks,  
Dillon

---

<div class="post-metadata">

**Author:** ![nader.chinichian](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nader.chinichian/32/9261_2.png) [@nader.chinichian](https://forum.inductiveautomation.com/u/nader.chinichian)\
**Post date:** [November 1, 2020, 9:57am UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/2 "2020-11-01T09:57:21Z")

</div>

Hi  
Are you trying to create cluttering and decluttering effect?

---

<div class="post-metadata">

**Author:** ![dillonu](https://avatars.discourse-cdn.com/v4/letter/d/278dde/32.png) [@dillonu](https://forum.inductiveautomation.com/u/dillonu)\
**Post date:** [November 3, 2020, 6:20am UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/3 "2020-11-03T06:20:07Z")

</div>

As a starting point, we’re trying to create a custom container for pan and zoom controls. Eventually we hope to create a custom container to optimize certain groupings of custom components, while still taking advantage of Ignition Designer’s tools (so not storing the components as a property list).

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [November 3, 2020, 4:56pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/4 "2020-11-03T16:56:14Z")

</div>

As far as I can tell from a quick skim of the implementation, containers just are components. The main thing you have to do is, in the designer scope, register a `ContainerDesignDelegate` (on the TS side) with the `InteractionRegistry` - then your component is in charge of implementing `addNewComponents`.

---

<div class="post-metadata">

**Author:** ![dillonu](https://avatars.discourse-cdn.com/v4/letter/d/278dde/32.png) [@dillonu](https://forum.inductiveautomation.com/u/dillonu)\
**Post date:** [November 4, 2020, 10:17pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/5 "2020-11-04T22:17:51Z")

</div>

Thanks for the response! So would that ContainerDesignDelegate be registered in the `web\packages\designer\typescript\*` context? Do we need to implement anything outside of the web folder?

Do I need to mess with the properties in `ComponentMeta` on the client side? I noticed there is a isContainer.

Any publically available examples?

Thanks again!

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [November 4, 2020, 10:53pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/6 "2020-11-04T22:53:40Z")

</div>

I’ll delegate here, since I’m well out of my depth 🙂  
@ynejati @PerryAJ

---

<div class="post-metadata">

**Author:** ![ynejati](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/ynejati/32/6093_2.png) [@ynejati](https://forum.inductiveautomation.com/u/ynejati)\
**Post date:** [November 5, 2020, 7:01pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/7 "2020-11-05T19:01:35Z")

</div>

We think it’s great that you’re experimenting with building your own container component. This is arguably one of the most challenging things you can do in an Ignition module. We do not have any publicly available examples at the moment, nor do we have any current plans to release any due to the complexity and time requirements of the task. However, we will gladly try and provide some useful information that may help.

Paul is correct, the main thing you need to do is to register your container component with the InteractionRegistry. The container is merely a component, but a component with a special design implementation as well as rules of interaction and layout.

Typically you will have:

1. A Designer version of your container component (DesignerCustomContainer)
2. A design delegate (CustomContainerDesignDelegate extends ContainerDesignDelegate) which implements designer type behaviors and interactions. You will want to set the `isContainer` member as true.
3. Your delegate registered with the InteractionRegistry (InteractionRegistry.registerInteractionDelegates)
4. `isContainer = true` in ComponentMeta set to true to aid in some Designer selection behavior to say the least.

Hopefully that helps.

-Yousuf

---

<div class="post-metadata">

**Author:** ![YF129701](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/yf129701/32/60498_2.png) [@YF129701](https://forum.inductiveautomation.com/u/YF129701)\
**Post date:** [August 30, 2022, 5:36pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/8 "2022-08-30T17:36:15Z")

</div>

Hi @dillonu did you ever make progress on this? I’m looking at implementing pan and zoom functionality natively in Perspective and have a working demo EXCEPT for the fact that the event `onMouseUp` doesn’t fire correctly about 5% of the time and its a bug that makes me want to rip my hair out.

---

<div class="post-metadata">

**Author:** ![dillonu](https://avatars.discourse-cdn.com/v4/letter/d/278dde/32.png) [@dillonu](https://forum.inductiveautomation.com/u/dillonu)\
**Post date:** [August 31, 2022, 4:09pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/9 "2022-08-31T16:09:32Z")

</div>

We never managed to make our own container, but we created a couple of components that make the parent container pan and zoom. It’s a bit hacky (and probably messes up a few event handlers within Ignition that we don’t use), but works for our use case.  
We do sometimes have issues with onMouseUp when swapping applications, which we use onBlur and other window-events to handle.

---

<div class="post-metadata">

**Author:** ![YF129701](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/yf129701/32/60498_2.png) [@YF129701](https://forum.inductiveautomation.com/u/YF129701)\
**Post date:** [August 31, 2022, 4:52pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/10 "2022-08-31T16:52:27Z")

</div>

It sounds like there’s a [ticket](https://ideas.inductiveautomation.com/ignition-features-and-ideas/p/perspective-zoom-and-pan-for-large-view) in progress for a pan and zoom container, but I believe it’s been in that state since April '22. I wish this was available right now - I have a project that’s about to close that uses the pan and zoom container that doesn’t work 100% of the time and I wish I could replace it with a native Perspective component.  
@ynejati Has there been progress made since the specification and prototyping phase (April 2022)?

---

<div class="post-metadata">

**Author:** ![Mats\_Sommervold](https://avatars.discourse-cdn.com/v4/letter/m/eb9ed0/32.png) [@Mats\_Sommervold](https://forum.inductiveautomation.com/u/Mats_Sommervold)\
**Post date:** [July 23, 2024, 11:44am UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/11 "2024-07-23T11:44:17Z")

</div>

I tried to reverse-engineer this a bit. As of now I am trying to make a similar container to the flex-container. I have managed to implement and register it as a regular component, and I have created a custom ContainerDesignDelegate implementation and registered with the ComponentDesignDelegateRegistry in the DesignerHook.java. This works and I am able to customize the Perspective Property Editor.

Where I struggle, is with the client side. I can't find any examples of how the react implementaiton of this would look like. It does not seem the `@inductiveautomation/perspective-client` package is public so i am importing all components blindly from the window object and guessing their implementations.

Here is my client implementation. I have added some comments with the issues I am facing and where I am unsure of what to do. The actual react component implementation is from MUI. This is supposed to be a perspective wrapper around it:

```python
import React from "react";
import Stack from "@mui/material/Stack";

// @ts-ignore
const {Component, ContainerDesignDelegate, ComponentRegistry, InteractionRegistry } = window.PerspectiveClient;

class StackMeta {

    getComponentType() {
        return "com.example.mylibrary.stack";
    }
    getViewComponent() {
        return MyStack;
    }

    isContainer() {
        return true;
    }

    getDefaultSize() {
        return ({
            width: 360,
            height: 360
        });
    }

    getPropsReducer(tree: any) {
        // This is my own reverse engineered way of getting the props. Probably not the ideal way.

        const propsMap = tree.root.value.value;
        const props: Record<string, any> = {};

        for (const [key, value] of propsMap.entries()) {
            props[key] = value.value.value;
        }

        return props;
    }
}

// ContainerDesignDelegate is undefined here
class StackContainerDesignDelegate extends ContainerDesignDelegate {

    addNewComponents() {
        // How do i implement this?
        // Am i supposed to handle the mounting and unmounting of the components myself?
    }
}

class MyStack extends Component {
    private props: any;

    render() {
        const { children, ...props } = this.props;

        return (
            <Stack {...props}>
                {/* It would be natural to me that the child components will be on the children prop. Is this the case? */}
                {children}
            </Stack>
        );
    }
}

ComponentRegistry.register(new StackMeta());

// InteractionRegistry is undefined here
InteractionRegistry.registerInteractionDelegates("com.example.mylibrary.stack", new StackContainerDesignDelegate());

```

Would appreciate any guidence

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [July 23, 2024, 4:53pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/12 "2024-07-23T16:53:30Z")

</div>

> [@Mats\_Sommervold](#):
>
> ```python
> getPropsReducer(tree: any) {
> 
> ```

This will be delivered a `PropertyTree` and is expected to return your container's property shape, e.g.:

```typescript
    getPropsReducer(tree: PropertyTree): ColumnContainerProps {
        return {
            breakpoints: tree.readArray("breakpoints", []),
            gutters: tree.readObject("gutters", {})
        };
    }

```

> [@Mats\_Sommervold](#):
>
> ```python
> addNewComponents() {
> // How do i implement this?
> // Am i supposed to handle the mounting and unmounting of the components myself?
> }
> 
> ```

The signature is:

```ts
addNewComponents(compDefs: Array<ComponentDefinition>, selection: SelectionStore, dropContainer: DesignerComponentStore, preferredLoc?: PreferredLocation): Array<ComponentInstanceDef> {

```

`ComponentInstanceDef` relies on you using the (supposedly non-public) `_addComponent` API directly on `dropContainer`:

```ts
            return {
                component: compDef,
                addressPath: dropContainer._addComponent(compDef)
            };

```

I don't see any way around that, and all of our components do it, so as long as you're comfortable with that relatively minor risk you can use the same approach I think.

> [@Mats\_Sommervold](#):
>
> ```python
> {/* It would be natural to me that the child components will be on the children prop. Is this the case? */}
> 
> ```

The children are available at `this.props.store.children;`, as far as I can tell. This will be an array of `ComponentStore`, automatically? `ComponentStore` has a `getComponent()` method that returns the actual `React` component, and you can render that however you want.

---

<div class="post-metadata">

**Author:** ![Mats\_Sommervold](https://avatars.discourse-cdn.com/v4/letter/m/eb9ed0/32.png) [@Mats\_Sommervold](https://forum.inductiveautomation.com/u/Mats_Sommervold)\
**Post date:** [July 24, 2024, 7:21am UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/13 "2024-07-24T07:21:38Z")

</div>

Thanks for the reply 🙂

I managed to find and install the `@inductiveautomation/perspective-client` library. Now I have the typescript models to go after. I still cant find the InteractionRegistry or the ContainerDesignDelegate in the typescript library. I am using version `2.1.3` Has this changed since the original post in this thread 2 years ago?

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [July 24, 2024, 3:28pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/14 "2024-07-24T15:28:18Z")

</div>

I think the designer stuff might be in a different top level library. Possibly as simple as `perspective-designer`?

---

<div class="post-metadata">

**Author:** ![Mats\_Sommervold](https://avatars.discourse-cdn.com/v4/letter/m/eb9ed0/32.png) [@Mats\_Sommervold](https://forum.inductiveautomation.com/u/Mats_Sommervold)\
**Post date:** [July 25, 2024, 9:38am UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/15 "2024-07-25T09:38:52Z")

</div>

Thank you! I I found the `perspective-designer` npm package.

I am now rendering the components like I show bellow. Everyting now works when in the browser. However, when working with the component inside of the designer, I get an `undefined` error.

I have added my own custom error boundary that catches the error. It seems to happen with every single component that is a child of my container. I believe I might be missing a prop that the `<Component />` expects?

How I render the component:

```python
{props.store.children.map((componentStore, index) => {
                const Component = componentStore.getComponent()

                return (
                     <MyErrorBoundary key={index}>
                         <Component />
                     </MyErrorBoundary>
                );
})}

```

The error message:

```python
TypeError: Cannot read properties of undefined (reading \'style\')

```

Stack trace from the perspective console (when removing the custom error boundary):

```python
10:09:57.847 [Browser Thread: 52c5f2e0-9750-42de-99a9-89b5a66bb0f9] ERROR Perspective.Designer.Workspace - level: LEVEL_ERROR
message: "ui.ErrorBoundary: Component error caught in error boundary: {\"componentStack\":\"
    at <anonymous> (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:139938)\\n at div\\n at http://localhost:8088/res/boostkit/js/index.js:2969:23
   at Grid (http://localhost:8088/res/boostkit/js/index.js:6139:24)
    at BoostStack (http://localhost:8088/res/boostkit/js/index.js:9021:22)
    at u (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:713329)
    at n (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:514924)
    at x (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:165760)
    at <anonymous> (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:139938)
    at div
    at u (http://localhost:8088/res/perspective/js/PerspectiveDesignerComponents.0bb8d494d9684c2484bc.js:2:152059)
    at u (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:713329)
    at n (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:514924)
    at x (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:165760)
    at <anonymous> (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:139938)
    at q (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:376303)
    at DefaultPropsProvider (http://localhost:8088/res/boostkit/js/index.js:5076:3)
    at RtlProvider (http://localhost:8088/res/boostkit/js/index.js:5950:7)
    at ThemeProvider$3 (http://localhost:8088/res/boostkit/js/index.js:5929:5)
    at ThemeProvider$2 (http://localhost:8088/res/boostkit/js/index.js:5993:5)
    at ThemeProvider$1 (http://localhost:8088/res/boostkit/js/index.js:6177:14)
    at BoostKitThemeProvider (http://localhost:8088/res/boostkit/js/index.js:8947:34)
    at ThemeProvider (http://localhost:8088/res/boostkit/js/index.js:8978:1)
    at u (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:713329)
    at n (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:514924)
    at x (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:165760)
    at <anonymous> (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:139938)
    at div\\n at div
    at g (http://localhost:8088/res/perspective/js/PerspectiveDesignerComponents.0bb8d494d9684c2484bc.js:2:120615)
    at u (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:713329)
    at n (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:514924)
    at x (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:165760)
    at <anonymous> (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:139938)
    at q (http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js:2:376303)
    at div\\n at t (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:46733)
    at div\\n at P (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:306018)
    at div\\n at o (http://localhost:8088/res/perspective/js/PerspectiveDesigner.5f4bf915c726742fd4fe.js:2:72542)\"}"
line_number: 2
source: "http://localhost:8088/res/perspective/js/PerspectiveClient.48795d90aca914295681.js"

```

It is difficult to debug this as the error happens outside of my code and in the designer where I can't set breakpoints. I noticed that the debug session in the tools menu disconnects upon opening it. If I could get that working it might be easier to debug. If you know of anything that might help, it is greatly appreciated.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [July 25, 2024, 6:41pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/16 "2024-07-25T18:41:40Z")

</div>

I'm officially out of my depth, but it looks like you might be missing this `layoutCallbackCreator` reference?

```ts
const ComponentClass: any = child.getComponent();

const buildLayout: LayoutBuilder = () => {
    if (display) {
        return {};
    } else {
        return {
            display: "none"
        };
    }
};
const layout = layoutCallbackCreator.forStyle(buildLayout);

return <ComponentClass key={ `component-${index}` } layout={layout} />;

```

---

<div class="post-metadata">

**Author:** ![Mats\_Sommervold](https://avatars.discourse-cdn.com/v4/letter/m/eb9ed0/32.png) [@Mats\_Sommervold](https://forum.inductiveautomation.com/u/Mats_Sommervold)\
**Post date:** [July 28, 2024, 8:27pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/17 "2024-07-28T20:27:22Z")

</div>

That solved it! Thank you for your patience and help!

---

<div class="post-metadata">

**Author:** ![victordcq](https://avatars.discourse-cdn.com/v4/letter/v/f6c823/32.png) [@victordcq](https://forum.inductiveautomation.com/u/victordcq)\
**Post date:** [July 29, 2024, 6:38am UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/18 "2024-07-29T06:38:39Z")

</div>

Could you share the working codes:)

---

<div class="post-metadata">

**Author:** ![Mats\_Sommervold](https://avatars.discourse-cdn.com/v4/letter/m/eb9ed0/32.png) [@Mats\_Sommervold](https://forum.inductiveautomation.com/u/Mats_Sommervold)\
**Post date:** [July 29, 2024, 11:06pm UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/19 "2024-07-29T23:06:55Z")

</div>

Here is the steps I took for a working solution:

**1. Correct Registry**  
Make sure you install the perspective npm packages from the correct registry. Add this to your `.npmrc` file. Either create one in your local repository or in the home directory for your user.

**.npmrc file**

```python
@inductiveautomation:registry=https://nexus.inductiveautomation.com/repository/node-packages/

```

**2. Install dependencies**  
Run the following command to install the client and designer npm package. _NB_ DO NOT SKIP adding version numbers when installing. In my experience, the repository will respond with `not found` if a version number is absent. Since I am running ignition `8.1.33` I downloaded the perspective client and designer `2.1.33`. In other words, the patch and minor version should be equal to the ignition version, while the major version is 2 (at least for the newer version of ignition).

```python
npm install @inductiveautomation/perspective-client@2.1.33 @inductiveautomation/perspective-designer@2.1.33`

```

**3. Create regular component**  
Create your component and component meta. The component can be a react class or function component.

```tsx
import {ComponentRegistry, PComponent, ComponentMeta, PropertyTree} from '@inductiveautomation/perspective-client`

// Create the meta class
class MyContainerMeta implements ComponentMeta {
    isContainer = true; // Make sure to mark it as a container component for later
    isDeepSelectable = true; // Also make it deep selectable in the designer

    getComponentType() {
        // Give it a unique "id"
        return "com.yourcompany.yourlibraryname.mycontainercomponent"
    }
    
    getViewComponent() {
        return MyContainerComponent;
    }

    getDefaultSize() {
        /* 
         * This is required and will be added to your component
         * when your do {...props.emit()}. I have chosen to avoid
         * setting a default width as you can see in the code in the
         * component bellow.
         */
        return {
            width: 360,
            height: 360
        };
    }

    getPropsReducer(propertyTree: PropertyTree) {
        /*
         * Resolve your custom props (that you will define in json later)
         * by using the functions readString, readNumber, readArray etc..
         * on the propertyTree
         */
        return {
            direction: propertyTree.readString("direction", "column"),
            spacing: propertyTree.readNumber("spacing", 0)
        }
    }
}

// Function component
const MyContainerCompnent: PComponent = (props) => {
    /* I am here using destructuring to remove the style prop
     * from the emmited props. The style prop comes with ignition 
     * default styles like default height and width. 
     * I do not want those on my components. 
     * 
     * You can do:
     * <div {...props.emit()} />
     * directly in your jsx/tsx component if you want to keep them.
     */
    const { style, ...emittedProps } = props.emit();

    return (
        // Your component
        <div {...emittedProps} {...props.props}> // spread emitted props and your custom props that live under props.props
        </ div>
    );
}

// You should register your component meta with the component registry:
ComponentRegistry.register(MyContainerComponentMeta());

```

**4. Define custom props**  
Now you have a regular non-container component. Before we add the container specialties, we'll create the `props.json` file for defining what props can be passed to our component from the designer. This file should live in the resources folder in the java common scope. There is multiple examples of these custom props [here](https://github.com/inductiveautomation/ignition-sdk-examples/blob/276f89239073e2108253716bce7f4ff70d20698d/perspective-component-minimal/common/src/main/resources/image.props.json).

```python
{
    "type": "object",
    "additionalProperties": false,
    "required": [
        "direction",
        "spacing"
    ],
    "properties": {
        "direction": {
            "type": "string",
            "enum": [
                "row",
                "column"
            ],
            "description": "Direction of the stack component",
            "default": "row"
        },
        "spacing": {
            "type": "number",
            "description": "Spacing between children. Number will be converted with the themes spacing scale",
            "default": 0
        }
    }
}

```

**5. Compile frontend tsx/jsx**  
There are described methods of compiling react code with gradle other places on the forum and in the [ignition sdk examples](https://github.com/inductiveautomation/ignition-sdk-examples/blob/276f89239073e2108253716bce7f4ff70d20698d/perspective-component/README.md) repository. We are using maven and found that making our own build system works best for now. However you do it, make sure that the final js file(s) are located in the gateway resource folder, we prefer a single file at `/resources/mounted/js/index.js`.

**6. Register your component**  
There are multiple examples of how to register your component and mount your js in the [ignition sdk examples](https://github.com/inductiveautomation/ignition-sdk-examples/blob/276f89239073e2108253716bce7f4ff70d20698d/perspective-component/README.md) repository. We don't need to do anything special for container components in this part.

**6. Create DesignDelegate**  
To make our component a container, we need to add behaviour for when to add child components. This can be done with adding and registering a DesignDelegate from the `perspective-designer` npm package.

```tsx
class MyContainerComponentDesignDelegate implements ContainerDesignDelegate {
    // Use the exact same component id as earlier in the component
    type: string = "com.yourcompany.yourlibrary.mycontainercomponent";
    isContainer = true; // Mark it as a container

    // Implement addNewComponents like this
    addNewComponents(
        compDefs: Perspective.ComponentDefinition[],
        selection: SelectionStore,
        dropContainer: DesignerComponentStore,
        preferredLocation?: PreferredLocation | undefined
    ): Perspective.ComponentInstanceDef[] {

        return compDefs.map((compDef) => {
            return {
                component: compDef,
                addressPath: dropContainer._addComponent(compDef)
            };
        });
    }
}

/* 
 * Finally, register the DesignDelegate only if the PerspectiveDesigner global
 * exists in the browser context. This will only exist in an Ignition designer and
 * not when the code is run in a browser. 
 * You will get an error if you don't do this.
 */
if (window.hasOwnProperty("PerspectiveDesigner")) {
    InteractionRegistry.registerInteractionDelegates(new StackContainerDesignDelegate())
}

```

**7. Display child components**  
Your container will now work as a conainer in the designer, but all the components you add to it will not show up. That's because you need to render them within your component. This is how you can do it:

```tsx
        <div {...emittedProps} {...props.props}>
            {props.store.children && props.store.children.length === 0 && (
                <div>
                   {/* fallback content if the container is empty (optional) */}
                    <p>Drop components here</p>
                </div>
            )}
            {/* Loop over all children */}
            {props.store.children.map((componentStore, index) => {
                // Get the react component
                const Component = componentStore.getComponent()

                /* 
                 * Not quite sure how this LayoutBuilder works, but
                 * we currently just return an empty object to not alter
                 * any layout styles.
                 */
                const buildLayout: LayoutBuilder = () => ({})
                const layout = layoutCallbackCreator.forStyle(buildLayout);

                return (
                    {/* Render the component with the layout and key prop */}
                    <Component key={index} layout={layout} />
                );
            })}
        </div>

```

Congratulations, You have now successfully created a container component!  
Feel free to ask me if anything is unclear. I'll leave you with the full finished tsx file:

```tsx
import React from "react";
import Perspective, {
    ComponentMeta,
    ComponentRegistry,
    PComponent,
    PropertyTree,
    ComponentStoreDelegate,
    AbstractUIElementStore, LayoutBuilder, layoutCallbackCreator
} from '@inductiveautomation/perspective-client';
import {
    ContainerDesignDelegate,
    DesignerComponentStore,
    InteractionRegistry,
    PreferredLocation,
    SelectionStore
} from "@inductiveautomation/perspective-designer";

class MyContainerMeta implements ComponentMeta {
    isContainer = true;
    isDeepSelectable = true;

    getComponentType() {
        return "com.yourcompany.yourlibrary.mycontainercomponent";
    }

    getViewComponent() {
        return MyContainerComponent;
    }

    getDefaultSize() {
        return ({
            width: 360,
            height: 360
        });
    }

    getPropsReducer(propertyTree: PropertyTree) {
        return {
            direction: propertyTree.readString("direction", "column"),
            spacing: propertyTree.readNumber("spacing", 0)
        }
    }
}

const MyContainerComponent: PComponent = (props) => {
    const { style, ...emittedProps } = props.emit();

    return (
        <div {...emittedProps} {...props.props}>
            {props.store.children && props.store.children.length === 0 && (
                <div>
                    <p>Drop components here</p>
                </div>
            )}
            {props.store.children.map((componentStore, index) => {
                const Component = componentStore.getComponent()

                const buildLayout: LayoutBuilder = () => ({})
                const layout = layoutCallbackCreator.forStyle(buildLayout);

                return (
                    <Component key={index} layout={layout} />
                );
            })}
        </div>
    );
}

ComponentRegistry.register(new MyContainerComponent());

class MyContainerComponentDesignDelegate implements ContainerDesignDelegate {
    type: string = "com.yourcompany.yourlibrary.mycontainercomponent";
    isContainer = true;
    addNewComponents(
        compDefs: Perspective.ComponentDefinition[],
        selection: SelectionStore,
        dropContainer: DesignerComponentStore,
        preferredLocation?: PreferredLocation | undefined
    ): Perspective.ComponentInstanceDef[] {

        return compDefs.map((compDef) => {
            return {
                component: compDef,
                addressPath: dropContainer._addComponent(compDef)
            };
        });
    }
}

if (window.hasOwnProperty("PerspectiveDesigner")) {
    InteractionRegistry.registerInteractionDelegates(new MyContainerComponentDesignDelegate())
}

```

**NB:** Be careful copying this code. I have changed some names and handwritten some code in the forum editor. Chances is that I have made some typos along the way. Also, please read the comments in the code. They may be important.

---

<div class="post-metadata">

**Author:** ![Benjamin\_Furlani](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/benjamin_furlani/32/66084_2.png) [@Benjamin\_Furlani](https://forum.inductiveautomation.com/u/Benjamin_Furlani)\
**Post date:** [August 27, 2024, 10:54am UTC](https://forum.inductiveautomation.com/t/perspective-custom-container/40193/20 "2024-08-27T10:54:14Z")

</div>

Where are you registering the Container Design Delate? Inside of the client or designer workspace?

[Next page](https://forum.inductiveautomation.com/t/perspective-custom-container/40193.md?page=2)
