This document shows the steps needed for an application to contribute and use its own custom widgets to Form representations (for example to use in custom Details views).
We will use the example of a simple "Slider" widget to illustrate. The slider widget is an alternative UI to show and edit a numeric value inside a given range.
The first step is to create the core implementation of the new widget in the backend, and plug it inside the Form representation’s rendering process.
You will need the equivalent of all the following classes (these can found in org.eclipse.sirius.web.sample.slider
):
-
Slider.java
: a concrete subclass ofAbstractWidget
, which represents a concrete instance of your custom widget. In the case of the slider, it contains the fields representing the state of a specific instance (min, max and current value) and the JavaFunction
to be invoked to apply the slider’s only edition operation (setting a new value). -
SliderDescription.java
: a concrete subclass ofAbstractWidgetDescription
, which represents the configuration of your widget to appear inside aFormDescription
. -
SliderComponent.java
and the accompanyingIProps
: the component responsible to render aSliderDescription
into a concreteSlider
given a specific runtime context (represented by theVariableManager
). -
SliderWidgetDescriptor.java
: an implementation ofIWidgetDescriptor
which tells the generic Form rendering process how to use your new component. Note that thisIWidgetDescriptor
implementation must be a Spring@Component
so that it is correctly detected at runtime.
If your custom widget supports one or more mutations (edition operations), you will also need the equivalent of:
-
MutationEditSliderDataFetcher.java
: the GraphQL Data Fetcher implementation which corresponds to the edition operation. Must implementIDataFetcherWithFieldCoordinates
and be a@MutationDataFetcher
with the propertype
andfield
. -
Any custom
IFormInput
and/orIPayload
types needed. In the case of the Slider widget, we only haveEditSliderInput.java
which corresponds to the input data for theeditSlider
mutation. -
EditSliderValueEventHandler.java
: the actual implementation of the mutation, as anIFormEventHandler
. Must be a Spring@Service
to be properly detected.
Finally, you will also need to declare you new widget and its mutation(s) in the global GraphQL Schema.
This is done by simply providing a .graphqls
schema file.
Note that the file must be in a schema
folder in the classpath to be detected.
For the Slider widget we have /sirius-web-sample-application/src/main/resources/schema/slider.graphqls
with the following content:
type Slider implements Widget {
id: ID!
diagnostics: [Diagnostic!]!
label: String!
iconURL: String
minValue: Int!
maxValue: Int!
currentValue: Int!
}
extend type Mutation {
editSlider(input: EditSliderInput!): EditSliderPayload!
}
input EditSliderInput {
id: ID!
editingContextId: ID!
representationId: ID!
sliderId: ID!
newValue: Int!
}
union EditSliderPayload = SuccessPayload | ErrorPayload
Important
|
Make sure the type of the widget declared in the GraphQL Schema (type Slider above) matches the name of the Java class implementing the widget (Slider.java ).
Our GraphQL TypeResolver has this requirement in order to resolve the real type used for an interface easily.
|
Important
|
If your custom widget has a field with the same name as another widget but with another type, you will have to use a unique alias to perform the request.
For example, almost all of our widgets have a value field and we are thus using a dedicated alias for each kind booleaValue: value or stringValue: value .
|
Declare the GQLWidget
concrete sub-type for you new widget:
// File: packages/sirius-web/frontend/sirius-web/src/widgets/SliderFragment.types.ts
import { GQLWidget } from '@eclipse-sirius/sirius-components-forms';
export interface GQLSlider extends GQLWidget {
label: string;
minValue: number;
maxValue: number;
currentValue: number;
}
Implement the actual frontend React component for your new widget. The React component must match the signature expected for Property Sections.
// File: packages/sirius-web/frontend/sirius-web/src/widgets/SliderPropertySection.tsx
export const SliderPropertySection = ({
editingContextId,
formId,
widget,
subscribers,
readOnly,
}: PropertySectionComponentProps<GQLSlider>) => {
// ...
}
Finally, at the top-level of your application you must provide a PropertySectionContext
with a PropertySectionComponentRegistry
value which declare all you custom widgets:
// File: packages/sirius-web/frontend/sirius-web/src/index.tsx
const propertySectionsRegistry = {
getComponent: (widget: GQLWidget) => {
if (widget.__typename === 'Slider') {
return SliderPropertySection;
} else {
return (props: PropertySectionComponentProps<GQLWidget>) => null;
}
},
getWidgetContributions: () => {
const sliderWidgetContribution: WidgetContribution = {
name: 'Slider',
fields: `label iconURL minValue maxValue currentValue`,
icon: <LinearScaleOutlinedIcon />,
};
return [sliderWidgetContribution];
},
};
const propertySectionRegistryValue = {
propertySectionsRegistry,
};
ReactDOM.render(
// ...
<PropertySectionContext.Provider value={propertySectionRegistryValue}>
<App />
</PropertySectionContext.Provider>
// ...,
document.getElementById('root')
);
Note the sliderWidgetContribution
which gives the name and GraphQL type name and fields/section for the frontend to be able to retrieve the data for your custom widgets.
They must match the names you declared in the graphqls
file.
Although the Slider
example only defines direct attributes, the fields
can contain deeper definitions, e.g. id label iconURL container referenceValue { id label kind iconURL }
.
While technically optional, this step is highly recommended as it will allow widget(s) to be configurable using the View DSL like the rest of the core widgets.
First, create your own Ecore model.
It must define a sub-type of the WidgetDescription
EClass from view.ecore
(org.eclipse.sirius.components.view.WidgetDescription
) with the appropriate configuration attributes.
For example for the Slider widget:
When generating the Java implementation for your metamodel, make sure to enable the "Child Creation Extenders" flag in the GenModel.
This is needed for the core View DSL to be able to use your new sub-type(s) of WidgetDescription
.
Then you need to register your metamodel’s EPackage,
AdapterFactory
and a ChildExtenderProvider
so that the runtime knows about it.
For example in a Spring @Configuration
class:
@Bean
public EPackage sliderWidgetEPackage() {
return SliderWidgetPackage.eINSTANCE;
}
@Bean
public AdapterFactory sliderWidgetAdapterFactory() {
return new SliderWidgetItemProviderAdapterFactory();
}
@Bean
public ChildExtenderProvider sliderWidgetChildExtenderProvider() {
return new ChildExtenderProvider(ViewPackage.eNS_URI, SliderWidgetItemProviderAdapterFactory.ViewChildCreationExtender::new);
}
With these information, it becomes possible to create instances of your widget’s View-based description class (e.g. SliderDescription
) inside a View-based FormDescription.
The final step is to tell the system how to convert these modeled widget description into their corresponding core implementation.
This is done by declaring a IWidgetConverterProvider
:
@Service
public class SliderDescriptionConverterProvider implements IWidgetConverterProvider {
@Override
public Switch<AbstractWidgetDescription> getWidgetConverter(AQLInterpreter interpreter, IEditService editService, IObjectService objectService) {
return new SliderDescriptionConverterSwitch(interpreter, editService);
}
}
The actual conversion is delegated to a Switch<AbstractWidgetDescription>
implementation that you must provide.
It will be called during the View conversion process when the system finds an instance of your custom widget inside a View definition.
See the code for org.eclipse.sirius.web.sample.slider.SliderDescriptionConverterSwitch
for more details.
If you want your custom widget to be usable inside the visual Form Description Editor, some additional steps are needed.
On the backend you must provide a org.eclipse.sirius.components.formdescriptioneditors.IWidgetPreviewConverterProvider
.
Its role is similar to the IWidgetConverterProvider
, but it will be executed in the context of a Form Description Editor instead of on a concrete target model.
The AbstractWidgetDescription
it returns will be used to provide a "static preview" of your widget.
Depending on your case you may only provide a completely fixed widget description which will always render the same, or try to interpret statically some of the widget’s properties (e.g. colors) if possible.
See org.eclipse.sirius.web.sample.slider.SliderDescriptionPreviewConverterProvider
for an example.
On the frontend you must provide a component to render the widget preview in the context of the Form Description Editor.
It is very similar to the Property Section component but simpler as it does not support the actual behavior of the widget.
See sirius-web/frontend/sirius-web/src/widgets/SliderPreview.tsx
for an example.
Finally, you must register this preview component in the top-level regsitry’s getPreviewComponent
function:
const propertySectionsRegistry = {
getComponent: (widget: GQLWidget) => {
// ...
},
getPreviewComponent: (widget: GQLWidget) => {
if (widget.__typename === 'Slider') {
return SliderPreview;
}
},
getWidgetContributions: () => {
// ...
},
};