Array Property Reverts to Default Value in Designer - Custom Perspective Component

Hi everyone,

I'm developing a custom Perspective component that includes a data property of type array, similar to the standard Table component. I'm running into an issue regarding property persistence and am hoping someone can point me in the right direction.

The Issue:

My component's data property is configured with a default array of 3 exemplary items. When I drag the component into a View in the Designer, it populates the 3 default items perfectly. However, if I delete one of the items, save the project, close the View, and reopen it—the data property reverts to the default 3 items, acting as if the deletion never happened.

Steps to Reproduce:

  1. Drag the custom component into a View.

  2. Delete one of the items from the data array in the Property Editor.

  3. Save the project.

  4. Close and reopen the View.

  5. The data property reverts to the original default state (the deleted item returns).

Component Schema:

Here is the JSON schema I am currently using:

JSON

{
    "type": "object",
    "properties": {
        "headerColor": {
            "type": "string",
            "description": "",
            "default": "#C0C0C0"
        },
        "data": {
            "type": "array",
            "description": "",
            "default": [
                {
                    "Item": "Laptop",
                    "Description": "A portable personal computer with a clamshell form factor, suitable for mobile use."
                },
                {
                    "Item": "Coffee Mug",
                    "Description": "A type of cup typically used for drinking hot beverages, such as coffee, hot chocolate, or tea."
                },
                {
                    "Item": "Mechanical Keyboard",
                    "Description": "A computer keyboard that uses individual mechanical switches for each key, known for tactile feedback."
                }
            ]
        },
        "visible": {
            "type": "boolean",
            "default": true,
            "description": ""
        }
    }
}

Has anyone run into this before, or is there something missing in my schema definition? I'm happy to share more details or the project source if needed.

Thanks in advance for any insights!

Best,

Muti

The schema is only used the very first time the component is instantiated, to my knowledge.
What is going on in your component's logic? Are you sure something there isn't automatically filling in these props?

I am sure. I have a dummy perspective-component project for this. May I send it for review?

Try using example instead of default in your schema. That's what we're using on the table component internally (to my surprise):

  "defaultMetaName": "Table",
  "schema": {
    "type": "object",
    "properties": {
      "data": {
        "type": ["array", "dataset"],
        "description": "Can be a dataset, an array of arrays, or an array of objects.",
        "items": {
          "type": ["object", "array"]
        },
        "example": [
          {
            "city": {
              "value": "Folsom",
              "editable": true,
              "style": {
                "backgroundColor": "#F7901D",
                "classes": "some-class"
              },
              "align": "center",
              "justify": "left"
            },
            "country": "United States",
            "population": 77271
          },
          {
            "city": "Helsinki",
            "country": "Finland",
            "population": 635591
          },
          {
            "city": "Jakarta",
            "country": "Indonesia",
            "population": 10187595
          },
          {
            "city": "Madrid",
            "country": "Spain",
            "population": 3233527
          },
          {
            "city": "Prague",
            "country": "Czech Republic",
            "population": 1241664
          },
          {
            "city": "San Diego",
            "country": "United States",
            "population": 1406630
          },
          {
            "city": "San Francisco",
            "country": "United States",
            "population": 884363
          },
          {
            "city": "Shanghai",
            "country": "China",
            "population": 24153000
          },
          {
            "city": "Tokyo",
            "country": "Japan",
            "population": 13617000
          },
          {
            "city": "Washington, DC",
            "country": "United States",
            "population": 658893
          },
          {
            "city": "Wellington",
            "country": "New Zealand",
            "population": 405000
          },
          {
            "city": "Delhi",
            "country": "India",
            "population": 11034555
          },
          {
            "city": "Dhaka",
            "country": "Bangladesh",
            "population": 14399000
          },
          {
            "city": "Lagos",
            "country": "Nigeria",
            "population": 16060303
          },
          {
            "city": "Karachi",
            "country": "Pakistan",
            "population": 14910352
          },
          {
            "city": "Istanbul",
            "country": "Turkey",
            "population": 14025000
          },
          {
            "city": "Cairo",
            "country": "Egypt",
            "population": 10230350
          },
          {
            "city": "Mexico City",
            "country": "Mexico",
            "population": 8974724
          },
          {
            "city": "London",
            "country": "United Kingdom",
            "population": 8825001
          },
          {
            "city": "New York City",
            "country": "United States",
            "population": 8622698
          },
          {
            "city": "Tehran",
            "country": "Iran",
            "population": 8154051
          },
          {
            "city": "Bogota",
            "country": "Colombia",
            "population": 7878783
          },
          {
            "city": "Rio de Janeiro",
            "country": "Brazil",
            "population": 6429923
          },
          {
            "city": "Riyadh",
            "country": "Saudi Arabia",
            "population": 5676621
          },
          {
            "city": "Singapore",
            "country": "Singapore",
            "population": 5535000
          },
          {
            "city": "Saint Petersburg",
            "country": "Russia",
            "population": 5191690
          },
          {
            "city": "Sydney",
            "country": "Australia",
            "population": 5248790
          },
          {
            "city": "Abidjan",
            "country": "Ivory Coast",
            "population": 4765000
          },
          {
            "city": "Dar es Salaam",
            "country": "Tanzania",
            "population": 4364541
          },
          {
            "city": "Wellington",
            "country": "New Zealand",
            "population": 405000
          },
          {
            "city": "Los Angeles",
            "country": "United States",
            "population": 3884307
          },
          {
            "city": "Berlin",
            "country": "Germany",
            "population": 3517424
          },
          {
            "city": "Jeddah",
            "country": "Saudi Arabia",
            "population": 3456259
          },
          {
            "city": "Kabul",
            "country": "Afghanistan",
            "population": 3414100
          },
          {
            "city": "Mashhad",
            "country": "Iran",
            "population": 3001184
          },
          {
            "city": "Milan",
            "country": "Italy",
            "population": 1359905
          },
          {
            "city": "Kiev",
            "country": "Ukraine",
            "population": 2908703
          },
          {
            "city": "Rome",
            "country": "Italy",
            "population": 2877215
          },
          {
            "city": "Chicago",
            "country": "United States",
            "population": 2695598
          },
          {
            "city": "Osaka",
            "country": "Japan",
            "population": 2691742
          },
          {
            "city": "Bandung",
            "country": "Indonesia",
            "population": 2575478
          },
          {
            "city": "Managua",
            "country": "Nicaragua",
            "population": 2560789
          },
          {
            "city": "Paris",
            "country": "France",
            "population": 2229621
          },
          {
            "city": "Shiraz",
            "country": "Iran",
            "population": 1869001
          },
          {
            "city": "Manila",
            "country": "Philippines",
            "population": 1780148
          },
          {
            "city": "Montreal",
            "country": "Canada",
            "population": 1649519
          },
          {
            "city": "Guadalajara",
            "country": "Mexico",
            "population": 1495189
          },
          {
            "city": "Dallas",
            "country": "United States",
            "population": 1317929
          },
          {
            "city": "Yerevan",
            "country": "Armenia",
            "population": 1060138
          },
          {
            "city": "Tunis",
            "country": "Tunisia",
            "population": 1056247
          }
        ]
      },

I tried example. It doesn't event load the items.

{
	"type": "object",
	"properties": {
		"headerColor": {
			"type": "string",
			"description": "",
			"default": "#C0C0C0"
		},
		"data": {
			"type": [
				"array",
				"dataset"
			],
			"description": "",
			"items": {
				"type": [
					"object",
					"array"
				]
			},
			"example": [
				{
					"Item": "Laptop",
					"Description": "A portable personal computer with a clamshell form factor, suitable for mobile use."
				},
				{
					"Item": "Coffee Mug",
					"Description": "A type of cup typically used for drinking hot beverages, such as coffee, hot chocolate, or tea."
				},
				{
					"Item": "Mechanical Keyboard",
					"Description": "A computer keyboard that uses individual mechanical switches for each key, known for tactile feedback."
				}
			]
		},
		"visible": {
			"type": "boolean",
			"default": true,
			"description": ""
		}
	}
}

Here's an example from my Chart.js component:

I tested with events. The test was successful. I guess it is working for primitive type items, but not with complex type items.

I removed the click value on the third position. It didn't revert after view close and open.

Any updates for complex types?

The weirdest thing happened. Kept repeatedly deleting middle item (index 2) from events. After a while I got the getPropsReducer( in the middle.

Please help!

Something has to be going in your components logic.

Repro.zip (132.0 KB)

I've attached a sample project demonstrating the issue. Any guidance or help pinpointing the solution would be much appreciated. Thank you.

Anybody tried the sample code?

Is anything wrong with this code?


import * as React from 'react';
import {
	Component,
	ComponentMeta,
	ComponentProps,
	PComponent,
	PropertyTree,
	SizeObject
} from '@inductiveautomation/perspective-client';

// this should match COMPONENT_ID value in the common subproject
export const COMPONENT_TYPE = "com.repro.MyComponent";

// props definition
export interface MyComponentProps {
	headerColor?: string;
	data?: object[];
	events?: string[];
	visible?: boolean;
}

// component
export class MyComponent extends Component<ComponentProps<MyComponentProps>, any> {

	// render the component
	render() {
		const { props: { headerColor, data, events, visible }, emit } = this.props;

		if (visible === false) {
			return null;
		}

		const _headerColor = headerColor ?? "#C0C0C0";
		const _data = data ?? [];

		return (
			<div class="repro-mycomponent-wrapper" {...emit()}>
				<table>
					<thead style={{ backgroundColor: _headerColor }}>
						<tr>
							<th>Item</th>
							<th>Description</th>
						</tr>
					</thead>
					<tbody>

						{_data.map((item: any, index) => (
							<tr key={index}>
								<td>{item["Item"]}</td>
								<td>{item["Description"]}</td>
							</tr>
						))}

						<tr>
							<td>Events</td>
							<td>{events ? events.join(", ") : ""}</td>
						</tr>

					</tbody>
				</table>
			</div>
		);
	}
};

// component metadata
export class MyComponentMeta implements ComponentMeta {

	getComponentType(): string {
		return COMPONENT_TYPE;
	}

	getViewComponent(): PComponent {
		return MyComponent as PComponent;
	}

	getDefaultSize(): SizeObject {
		return ({
			width: 750,
			height: 500
		});
	}

	getPropsReducer(tree: PropertyTree): MyComponentProps {
		return {
			headerColor: tree.readString("headerColor", ""),
			data: tree.readArray("data", []),
			events: tree.readArray("events", []),
			visible: tree.readBoolean("visible", true)
		};
	}
};



:person_shrugging:
I spent a while this morning trying to dig into this, and all I came away with is that our process to initialize Perspective components is way more complicated than I appreciated, possibly for good reasons (and possibly not).

I think you should be setting your default values within your palette entry, not your schema - and I think that the ComponentDescriptorBuilder may be doing something weird; it's not the code path we actually use internally. We use the ComponentDescriptor.fromJson` method exclusively on all our first party components, but we're honest about only calling the interface methods, so you should be able to achieve the same results as any of our stuff in a third party module.

There's a few different places default values are returned, but returning initial props on your palette entry seems to be the 'right' place:

case props:
    ComponentDescriptor c = getComponentRegistry().get().get(componentType);

    if (c == null) {
        log.errorf("Component descriptor for '%s' could not be found. "
            + " This component is not registered.", componentType);
        return new JsonObject().toString();
    }

    JsonObject initialProps = null;
    try {
        initialProps = c.getInitialProps(variantId);
    } catch (Exception e) {
        log.errorf("Attempt to get component property defaults"
                + " for '%s' component type was unsuccessful.",
            componentType);
    }

    if (forInit && initialProps != null) {
        return initialProps.toString();
    } else {
        return c.defaultProperties().toString();
    }

Where getInitialProps is a default method:

    @Nullable
    default JsonObject getInitialProps(String variantId) {
        return paletteEntries().stream()
            .filter(p -> Objects.equals(p.getVariantId(), variantId))
            .findFirst()
            .map(PaletteEntry::getInitialProps)
            .orElse(null);
    }

No matter what I tried, though, I either ended up with:

  1. No initial population of the starting values
  2. The default values overwriting the state of the props saved into the designer.

I honestly couldn't tell you exactly which layer is going wrong (debugging this stuff on a third party module is unpleasant) and I probably can't dedicate much more time to it.

Thank you very much Paul. Hopefully somebody will figure it out. Is there a way to escalate this?

Third party modules are explicitly "you are on you own" endeavors.

IA helps where they can, without commitments, and where it doesn't expose (much) of their secret sauce.

Instrument your code. Use lots of loggers, and switchable console.print() calls. Use reflection and logged exception traces to better understand where platform code calls your code.

I also have opinions on this :joy:

That code looks fine to me, everything looks totally normal.

Thank you Phill. I will look into logging more.

Or maybe this Java code?

MyComponent.java

package com.repro.common.components;

import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;

import com.inductiveautomation.ignition.common.jsonschema.JsonSchema;
import com.inductiveautomation.perspective.common.api.ComponentDescriptor;
import com.inductiveautomation.perspective.common.api.ComponentDescriptorImpl;
import com.inductiveautomation.perspective.common.api.ComponentEventDescriptor;

import com.repro.common.Constants;
import com.repro.common.Components;
// import com.repro.common.tools.Logger;

public class MyComponent {

	// this should match COMPONENT_TYPE value in the web subproject
	public static String COMPONENT_ID = "com.repro.MyComponent";

	private static final String COMPONENT_VARIANT_ID = "";

	private static final String COMPONENT_NAME = "MyComponent";
	private static final String COMPONENT_DEFAULT_NAME = "MyComponent";
	private static final String COMPONENT_DESCRIPTION = "Repro MyComponent";
	private static final String COMPONENT_PROPS_PATH = "/MyComponent.schema.json";
	private static final String COMPONENT_ICON_PATH = "/MyComponent.icon.png";
	private static final String COMPONENT_THUMBNAIL_PATH = "/MyComponent.thumbnail.png";

	public static ComponentDescriptor getComponentDescriptor() {

		ComponentDescriptor result = ComponentDescriptorImpl.ComponentBuilder
				.newBuilder()
				.setModuleId(Constants.MODULE_ID)
				.setId(COMPONENT_ID)
				.setName(COMPONENT_NAME)
				.setDefaultMetaName(COMPONENT_DEFAULT_NAME)
				.setSchema(getPropsSchema())
				.setEvents(getEventDescriptors())
				.setPaletteCategory(Constants.COMPONENT_CATEGORY)
				.addPaletteEntry(COMPONENT_VARIANT_ID, COMPONENT_NAME, COMPONENT_DESCRIPTION, getThumbnail(), null)
				.setIcon(getIcon())
				.setResources(Components.getBrowserResources())
				.build();

		com.repro.common.tools.Logger.info("MyComponent - getComponentDescriptor");

		return result;
	}

	public static List<ComponentEventDescriptor> getEventDescriptors() {

		return List.of(

		// register component events here
		// // // example: new ComponentEventDescriptor("onClick")

		);

	}

	private static JsonSchema getPropsSchema() {

		try {
			String path = COMPONENT_PROPS_PATH;
			if (!path.startsWith("/")) {
				path = "/" + path;
			}

			InputStream stream = Components.class.getResourceAsStream(path);
			JsonSchema schema = JsonSchema.parse(stream);

			com.repro.common.tools.Logger.info("MyComponent - getPropsSchema");

			return schema;
		} catch (Exception e) {
			com.repro.common.tools.Logger.error("MyComponent - getPropsSchema", e);
			return null;
		}
	}

	private static Icon getIcon() {
		try {
			String path = COMPONENT_ICON_PATH;
			if (!path.startsWith("/")) {
				path = "/" + path;
			}

			URL url = Components.class.getResource(path);
			ImageIcon icon = new ImageIcon(url);

			com.repro.common.tools.Logger.info("MyComponent - getIcon");

			return icon;
		} catch (Exception e) {
			com.repro.common.tools.Logger.error("MyComponent - getIcon", e);
			return null;
		}
	}

	private static BufferedImage getThumbnail() {
		try {
			String path = COMPONENT_THUMBNAIL_PATH;
			if (!path.startsWith("/")) {
				path = "/" + path;
			}

			InputStream stream = Components.class.getResourceAsStream(path);
			BufferedImage image = ImageIO.read(stream);

			com.repro.common.tools.Logger.info("MyComponent - getThumbnail");

			return image;
		} catch (Exception e) {

			com.repro.common.tools.Logger.error("MyComponent - getThumbnail", e);

			return null;
		}
	}

}

MyComponent.schema.json

{
	"type": "object",
	"properties": {
		"headerColor": {
			"type": "string",
			"description": "",
			"default": "#C0C0C0"
		},
		"data": {
			"type": [
				"array",
				"dataset"
			],
			"description": "",
			"items": {
				"type": [
					"object",
					"array"
				]
			},
			"default": [
				{
					"Item": "Laptop",
					"Description": "A portable personal computer with a clamshell form factor, suitable for mobile use."
				},
				{
					"Item": "Coffee Mug",
					"Description": "A type of cup typically used for drinking hot beverages, such as coffee, hot chocolate, or tea."
				},
				{
					"Item": "Mechanical Keyboard",
					"Description": "A computer keyboard that uses individual mechanical switches for each key, known for tactile feedback."
				}
			]
		},
		"events": {
			"type": "array",
			"items": "string",
			"description": "The events option defines the browser events that the chart should listen to for tooltips and hovering.",
			"default": [
				"mousemove",
				"mouseout",
				"click",
				"touchstart",
				"touchmove"
			]
		},
		"visible": {
			"type": "boolean",
			"default": true,
			"description": ""
		}
	}
}