Creating a component event

I’ve been working on creating a calender component for perspective using FullCalender.
I was wondering how I could add component events, like the table has for example.

First, you’ll want to add the event to your schema.json, assuming you’re using one; provide an events array that has at least a name and description, and as much schema information as applicable, if you’re passing contextual information:

From there, it depends on which side you’re firing this event from. From the frontend, your component props will have a componentEvents, which is an instance of…ComponentEvents - that class has a fireComponentEvent function that accepts an event name and a payload.

If you want to fire a component event from the Java side, you’ll need to register a custom ModelDelegate - from there, the Component instance your delegate receives will have a fireEvent method that takes three arguments, one of which will be the static EventConfig.COMPONENT_EVENTS.

2 Likes

It seems we have no schema.json. I don’t really know what it is or does. Is it the same json file I set my components props in? Maybe it’s worth creating?
In any case, the event I would like to make has to do with creating “events” or “appointments” in the calendar. So an event to handle what happens when a date/time range has been selected. We’d use it to update the events prop I made to fill the calendar with events and also update the database with the new “events”.
Would it be better trying to make this event fire on the front-end or on the Java side?

We've been working on getting a calendar component into perspective using FullCalender, a popular web component.

It displays just fine, but we've been struggling with getting the functionality we want. The main one being a way to script what we want to happen after a date selection has been made. Either we have to make a new component event or find some way to use FullCalendars function and pass along the variables to Ignition.

We can't find any other example modules that use a schema.json, so we don't know where it should be and how it works.

We were unable to use the ComponentEvents class in our .tsx file where we create our component. This is because the class expects a parameter of the type AbstractUIElementStore.

Any help in making it possible to create a new component event that has access to the information gained in FullCalendars functions or a way to simply pass this information into the designer for use there is greatly appreciated!.

In case you're wondering what our component actually looks like in .tsx:

/**
 * Example of a component which displays an image, given a URL.
 */

import * as React from 'react';
// import { CSSProperties } from "react";

import { Component, ComponentMeta, ComponentProps, SizeObject} from '@inductiveautomation/perspective-client';
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';


// import bind from 'bind-decorator';

// the 'key' or 'id' for this component type.  Component must be registered with this EXACT key in the Java side as well
// as on the client side.  In the client, this is done in the index file where we import and register through the
// ComponentRegistry provided by the perspective-client API.
export const COMPONENT_TYPE = "at-display-calendar";

// this is the shape of the properties we get from the perspective 'props' property tree.
export interface ATCalendarProps {
    aspectRatio: string;
    events: Array<object>;
    startTime: string;
}


export class ATCalendar extends Component<ComponentProps, any> {
    calendar: Calendar;

    render() {

        if (this.calendar != null)
        {
            this.calendar.setOption('events', this.props.props.read('events'));
            this.calendar.refetchEvents();
            this.calendar.rerenderEvents();
        }
        return(
            // {/*<div style={myStyles}>*/}

            <div style="overflow-y: auto;" {...this.props.emit()} ><div {...this.props.emit()} id='calendar'/></div>


        );


    }

    componentDidMount()
    {
        let cal = this.calendar;
        let calendarEl: HTMLElement = document.getElementById('calendar')!;
        cal = new Calendar(calendarEl,
            {
                windowResize: function(view)
                {
                    alert('The calendar has adjusted to a window resize');
                },
                plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
                header:
                    {
                        left: 'dayGridMonth,timeGridWeek,timeGridDay',
                        center: 'title',
                        right: 'today, ,prev,next'
                    },
                events: this.props.props.read('events'),
                selectable: true,
                firstDay: 1,
                weekNumbers: true,
                weekNumbersWithinDays: true,
                nowIndicator: true,
                height: 'parent',
                contentHeight: "auto",
                defaultView: 'dayGridMonth',
                minTime: "07:00:00",
                maxTime: "19:00:00",
                select: function(info) {
                    alert('selected ' + info.startStr + ' to ' + info.endStr);

                    let eventInfo = {start: info.start, end: info.end};

                    if (!isNaN(info.start.valueOf())) {
                        cal.addEvent(eventInfo, cal.getEventSources()[0]);
                        alert('Great. Now, update your database...');
                    } else {
                        alert('Invalid date.');
                    }
                }
            });

        cal.render();

    }
}


// this is the actual thing that gets registered with the component registry
export class ATCalendarMeta implements ComponentMeta {

    getComponentType(): string {
        return COMPONENT_TYPE;
    }

    // the class or React Type that this component provides
    getViewClass(): React.ReactType {
        return ATCalendar;
    }

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

There are some schema JSON files in the perspective component example in the SDK (sorry, somewhat misleading; it doesn’t have to be literally called schema.json).

Those resource files are loaded into the ComponentDescriptor created for each component that’s going to be registered with the gateway: https://github.com/inductiveautomation/ignition-sdk-examples/blob/master/perspective-component/common/src/main/java/org/fakester/common/component/display/TagCounter.java

As for using componentEvents - you don’t need to create a new one; you already have one - this.props.componentEvents, just like you have access to your local property tree through this.props.props.
The Button component’s actionPerformed function is literally just:
this.props.componentEvents.fireComponentEvent("onActionPerformed", {});

Thanks, that’s a lot clearer.
Say I want to use actionPerformed, how do I define what the action is? The event “trigger” so to speak.

In what part of my schema.json would I need to put my events array?
The way I currently have it does not seem to work, it doesn’t show up in the designer at least. I can’t think of another logical place.

{
    "type": "object",
    "properties": {
        "events": {
            "type": "array",
            "description": "Data string of current drawing",
            "default": []
        },
		"defaultView": {
            "type": "string",
            "description": "The default view when first rendering the Calendar",
            "default": "dayGridMonth"
        },
		"firstDay": {
            "type": "integer",
            "description": "The first day of the week (0=sun, 1=mon, etc)",
            "default": 1
        },
		"minTime": {
            "type": "string",
            "description": "Inclusive minimum time at which the week and day views will start",
            "default": "00:00:00"
        },
		"maxTime": {
            "type": "string",
            "description": "Exclusive maximum time at which the week and day views will end",
            "default": "00:00:00"
        },
		"slotDuration": {
            "type": "string",
            "description": "Duration of the slots in week and day views",
            "default": "00:15:00"
        },
		"weekNumbers": {
            "type": "boolean",
            "description": "Show weekNumbers",
            "default": false
        },
		"weekNumbersWithinDays": {
            "type": "boolean",
            "description": "if weekNumbers are shown, show them within the days.",
            "default": false
        },
		"nowIndicator": {
            "type": "boolean",
            "description": "current time indicator for week and day views",
            "default": false
        },

        "style": {
            "$ref": "urn:ignition-schema:schemas/style-properties.schema.json",
            "default": {
                "classes": ""
            }
        }
    },
	"events": [
		{
			"name": "onActionPerformed",
			"description": "This event is fired when that 'action' of the component occurs.",
			"schema": {
				"type": "object"
			}
		}
	]
}

That looks accurate - it should be a top level key. Have you reinstalled your module and restarted the gateway?

As for the action trigger - it’s whatever you want it to be. Most likely from the frontend, you just emit an onActionPerformed event whenever that 'makes sense" for your component. With simple stuff like a checkbox or slider, it’s whenever the value changes; for something more complex, it’s really up to you.

Apart from restarting the gateway manually I’ve tried all of that.
Guessing me booting up my laptop this morning would’ve had the same effect as restarting the gateway though.

It still doesn’t seem to show up in the designer. Any ideas?
Are there no example components with events? The ones in the sdk examples don’t seem to have any after all.

Apologies - looks like (not sure why) our internal method of parsing the schema for events isn’t used by the public ComponetDescriptor.register hook. What you’ll want to do instead is add a manually constructed ComponentEventDescriptor to your schema; something like this: (just move the event key from the schema.json to its own resource file, for simplicity)

    public static JsonSchema EVENT_SCHEMA =
        JsonSchema.parse(RadComponents.class.getResourceAsStream("/radimage.event.json"));

    public static ComponentEventDescriptor EVENT_DESCRIPTOR = new ComponentEventDescriptor("onActionPerformed", "Fired when an action occurs", EVENT_SCHEMA);

    /**
     * Components register with the Java side ComponentRegistry but providing a ComponentDescriptor.  Here we
     * build the descriptor for this one component. Icons on the component palette are optional.
     */
    public static ComponentDescriptor DESCRIPTOR = ComponentDescriptorImpl.ComponentBuilder.newBuilder()
        .withPaletteCategory(RadComponents.COMPONENT_CATEGORY)
        .withPaletteDescription("A simple image component.")
        .withId(COMPONENT_ID)
        .withModuleId(RadComponents.MODULE_ID)
        .withSchema(SCHEMA) //  this could alternatively be created purely in Java if desired
        .withPaletteName("Rad Image")
        .withDefaultMetaName("radImage")
        .withEvents(List.of(EVENT_DESCRIPTOR))
        .shouldAddToPalette(true)
        .withResources(RadComponents.BROWSER_RESOURCES)
        .build();

Thank you! That’s done it.
Now I might be able to figure this out on my own, but it never hurts to ask. (edit: I haven’t managed it so far)
How do I define the attributes of the event object used in the designer? Table has event.row for example

That would go into the event object’s schema, under the events array. The extension function editor will automatically parse it into a docstring comment. See this example from our table component:

        {
            "name": "onSubviewCollapse",
            "description": "Fired when a rows subview is collapsed.",
            "schema": {
                "type": "object",
                "properties": {
                    "row": {
                        "type": "integer",
                        "description": "The unique row index as it is represented in the source data. Also known as the row ID."
                    },
                    "rowIndex": {
                        "type": "integer",
                        "description": "The row index as it is represented in the current visible data."
                    },
                    "value": {
                        "type": "object",
                        "description": "The row's value as a JSON object."
                    }
                }
            }
        }

Yes, that’s what I thought, but I couldn’t figure out what key to use for what I now see is properties;
Although it doesn’t seem to show up in the designer’s event object description, even after copying the schema from your example.

Do I need to do anything special in the typescript where I fire the event? Or maybe even with the EVENT_DESCRIPTOR?

edit: I am able to acces the values I put into the event object, even though the script editor doesn’t seem to pick up their names or descriptions.

edit 2: Does the fireComponentEvent function return any values? Anything I could use to know the script executed succesfully so I can use that info in a condition.
One of the events is supposed to update the database with a new event based on a selection in a calender. I then need to rerender the events, but I only want to do that if my script actually fired correctly and finished.

No, the frontend doesn’t block waiting for results of a scripting call - all frontend/backend communication is asynchronous. To accurately report status, you’d really have to dive into the internals - you might need to send your own messaging back and forth, and fire your function via a model delegate on the backend.

Fair enough.

Well, that’s about solved the problems I had. Though it would be nice to know how to have the event properties descriptions to show up in the script editor.

Thank you!

How are you constructing your ComponentEventDescriptor? If you use the 2 or 3 arg constructor, the script editor should be pulling in the description automatically. Or could you PM me a build of the module? I can take a look at the internal code that tries to parse the schema into a docstring.

For posterity; posting the solution to the issue with the script editor:

As I vaguely alluded to, you're not able to (unfortunately) get the automatic parsing of schemas out of a single file that we use internally - which means that the main issue with your module at present is the single event object schema being supplied for all four component event descriptors. So, to start off, you'll want to move them into separate files, and also flatten them - you don't need separate name, description (we'll get back to the description in a minute), or schema keys - just put everything in the schema key on the top level:


Then you'll want to individually create each ComponentEventDescriptor in ATCalendar.java - I made a simple wrapper class to fetch the individual schema resources more easily:

Now, as for the description, which you can't specify now? If you bump your SDK version from 8.0.3 to 8.0.5+, you'll be able to specify a 3-arg constructor for ComponentEventDescriptor - String name, String desc, JsonSchema schema - and that description will be automatically added in the rendered docstring.