diff --git a/content/en/docs/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md b/content/en/docs/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md index 4e9fe259c8d..1fbebf17791 100644 --- a/content/en/docs/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md +++ b/content/en/docs/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md @@ -12,11 +12,11 @@ Pluggable web widgets are the new generation of custom-built widgets. These widg This how-to teaches you how to do the following: -* Generate a widget structure -* Create a basic text input widget -* Add a label +- Generate a widget structure +- Create a basic text input widget +- Add a label -**Are you in a hurry?** +**Are you in a hurry?** Clone this [code sample](https://github.com/mendix/text-box-sample) from GitHub with the basic and advanced features already implemented. @@ -24,9 +24,9 @@ Clone this [code sample](https://github.com/mendix/text-box-sample) from GitHub Before starting this how-to, make sure you have completed the following prerequisites: -* Install the LTS version of [Node.js](https://nodejs.org/en/download/). -* Install an integrated development environment (IDE) of your choice (Mendix recommends [Microsoft Visual Studio Code](https://code.visualstudio.com/)) -* Have a basic understanding of [TypeScript](https://www.typescriptlang.org/) +- Install the LTS version of [Node.js](https://nodejs.org/en/download/). +- Install an integrated development environment (IDE) of your choice (Mendix recommends [Microsoft Visual Studio Code](https://code.visualstudio.com/)) +- Have a basic understanding of [TypeScript](https://www.typescriptlang.org/) ## Creating a TextBox Input Widget @@ -36,29 +36,30 @@ The following steps teach you how to build a pluggable input widget, and show yo 1. Open Studio Pro and create a new test app by selecting **File > New App** from the top menu bar and then **Blank App**. 2. Create a test case for the new widget:
- 1. In the domain model of **MyFirstModule**, add a new entity.
- 2. Add a new attribute of type **String**.
- 3. Open **MyFirstModule’s** **Home** page.
- 4. There, add a **Data view** widget, double-click the widget, and give it a data source microflow by selecting **Data source** > **Type** > **Microflow**.
- 5. Next to the microflow field click the **Select** button, and click **New**.
- 6. Provide the name *DSS_CreateTestObject* to this new microflow.
- 7. Click the **Show** button. This will open the microflow editor. Then click the **OK** button to close the dialog box.
- 8. Add a new **Create object** action on your microflow. + + 1. In the domain model of **MyFirstModule**, add a new entity.
+ 2. Add a new attribute of type **String**.
+ 3. Open **MyFirstModule’s** **Home** page.
+ 4. There, add a **Data view** widget, double-click the widget, and give it a data source microflow by selecting **Data source** > **Type** > **Microflow**.
+ 5. Next to the microflow field click the **Select** button, and click **New**.
+ 6. Provide the name _DSS_CreateTestObject_ to this new microflow.
+ 7. Click the **Show** button. This will open the microflow editor. Then click the **OK** button to close the dialog box.
+ 8. Add a new **Create object** action on your microflow. 3. Open the new **Create object** action's properties by double-clicking it. For its **Entity**, click the **Select** button and choose the entity you created above. Then click **OK** to close the dialog box. 4. Right-click the **Create Entity** activity, then click **Set $NewEntity as Return Value**. 5. Go back to the home page, open the **Add Widget** menu, and then add a **TextBox** widget inside the data view. 6. Open the Textbox's properties and select the **Datasource Attribute (path)** string attribute you created above. Then click the **OK** button to close the dialog box. The end result should look like this: - {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/createtestobject.png" alt="The page editor in Studio Pro in structure mode. The page contains a Data View that has a Microflow as its datasource. It contains a Text Input widget referencing the text attribute created in this section." class="no-border" >}} + {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/createtestobject.png" alt="The page editor in Studio Pro in structure mode. The page contains a Data View that has a Microflow as its datasource. It contains a Text Input widget referencing the text attribute created in this section." class="no-border" >}} - {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/microflowcreateentity.png" alt="A Microflow with a single Create Entity Action. The Action creates an instance of the entity created in the steps above. The Microflow ends with returning the new entity." class="no-border" >}} + {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/microflowcreateentity.png" alt="A Microflow with a single Create Entity Action. The Action creates an instance of the entity created in the steps above. The Microflow ends with returning the new entity." class="no-border" >}} ### Scaffolding the Widget The Pluggable Widget Generator is the quickest way to start developing a widget. It creates a widget’s recommended folder structure and files. -Using a terminal or command line, navigate to your new Mendix app's folder, create a new folder named *myPluggableWidgets*, and start the generator using: +Using a terminal or command line, navigate to your new Mendix app's folder, create a new folder named _myPluggableWidgets_, and start the generator using: ```shell mkdir myPluggableWidgets @@ -68,23 +69,23 @@ npx @mendix/generator-widget@latest TextBox The generator will ask you a few questions during setup. Answer the questions by specifying the following information: -* Widget name: *{Your widget name}* -* Widget Description: *{Your widget description}* -* Organization Name: *{Your organization name}* -* Copyright: *{Your copyright date}* -* License: *{Your license}* -* Initial Version:*{Your initial version number}* -* Author: *{Your author name}* -* Mendix App path: **../../** -* Programming language: **TypeScript** -* Which type of components do you want to use? **Function Components** -* Widget type: **For web and hybrid mobile apps** -* Widget template: **Empty widget (recommended for more experienced developers)** -* Unit tests: **No** -* End-to-end tests: **No** +- Widget name: _{Your widget name}_ +- Widget Description: _{Your widget description}_ +- Organization Name: _{Your organization name}_ +- Copyright: _{Your copyright date}_ +- License: _{Your license}_ +- Initial Version:_{Your initial version number}_ +- Author: _{Your author name}_ +- Mendix App path: **../../** +- Programming language: **TypeScript** +- Which type of components do you want to use? **Function Components** +- Widget type: **For web and hybrid mobile apps** +- Widget template: **Empty widget (recommended for more experienced developers)** +- Unit tests: **No** +- End-to-end tests: **No** {{% alert color="warning" %}} -Currently, **Organization Name** cannot include a dash "-" character. Dashes in the organization name will result in an error when running your app, asking to check if the widgets "were generated with the latest version of the pluggable-widgets-tools and are ES6 modules." +Currently, **Organization Name** cannot include a dash "-" character. Dashes in the organization name will result in an error when running your app, asking to check if the widgets "were generated with the latest version of the pluggable-widgets-tools and are ES6 modules." To fix this in an existing widget, modify the `packagePath` property of its **package.json** file and rebuild the widget. {{% /alert %}} @@ -105,164 +106,176 @@ When the build script completes it will package your widget as a `.mpk` file and 1. Drag the Text Box widget to the Data View added in [Creating a Test Project](#creating-a-test-project). 1. Run your app locally and open it in the browser. The homepage should now display Hello World below the text widget: - {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/hello-world.png" alt="A live mendix app displaying a text field and below it the pluggable widget with the text Hello World." class="no-border" >}} + {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/hello-world.png" alt="A live mendix app displaying a text field and below it the pluggable widget with the text Hello World." class="no-border" >}} ### Adding the Attribute -Open the *(YourMendixApp)/myPluggableWidgets/textBox* folder in your IDE of choice. From now on, all file references will be relative to this path. To set up your new widget, first you must use an attribute of the context object and display that attribute in an input field: +Open the _(YourMendixApp)/myPluggableWidgets/textBox_ folder in your IDE of choice. From now on, all file references will be relative to this path. To set up your new widget, first you must use an attribute of the context object and display that attribute in an input field: -1. Remove the file *src/components/HelloWorldSample.tsx*. Errors in *TextBox.editorPreview.tsx* will be dealt with in step 6 below. +1. Remove the file _src/components/HelloWorldSample.tsx_. Errors in _TextBox.editorPreview.tsx_ will be dealt with in step 6 below. 2. The generator creates the widget definition file `src/TextBox.xml` with preset properties. Replace the `sampleText` property following this snippet: - ```xml {hl_lines=["7-15"]} - - - Text Box - Edit text input - - - - - Attribute (path) - - - - - - - - - ``` - - Explaining the code: - - * *TextBox.xml* is the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition) used in Studio Pro which reads the widget's capabilities - * The property `pluginWidget=true` will make the widget work with the [Pluggable Widgets API](/apidocs-mxsdk/apidocs/pluggable-widgets/) - * The property `needsEntityContext=true` is set up to allow the attribute to be taken from context - * The property of the [type attribute](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/#attribute) only allows the selection of string attributes from the domain model + ```xml {hl_lines=["7-15"]} + + + Text Box + Edit text input + + + + + Attribute (path) + + + + + + + + + ``` + + Explaining the code: + + - _TextBox.xml_ is the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition) used in Studio Pro which reads the widget's capabilities + - The property `pluginWidget=true` will make the widget work with the [Pluggable Widgets API](/apidocs-mxsdk/apidocs/pluggable-widgets/) + - The property `needsEntityContext=true` is set up to allow the attribute to be taken from context + - The property of the [type attribute](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/#attribute) only allows the selection of string attributes from the domain model 3. The generator includes scripts which assist with building and packaging your widget. Use `npm start` to run the watcher, it will generate updated Typescript types based on the widget definition file. - When done generating the types can be found in `typings/TextBoxProps.d.ts` + When done generating the types can be found in `typings/TextBoxProps.d.ts` - {{% alert color="info" %}}The console will display an error along the lines of `HelloWorldSample.tsx could not be found`. We will address this in the section [Labeling the Input](#label-input) of this how-to. It can be ignored for now.{{% /alert %}} + {{% alert color="info" %}}The console will display an error along the lines of `HelloWorldSample.tsx could not be found`. We will address this in the section [Labeling the Input](#label-input) of this how-to. It can be ignored for now.{{% /alert %}} 4. Create a new file, `src/components/TextInput.tsx`. This will be the display component. A display component is a regular React component and does not interact with Mendix APIs. It can be re-used in any React application. - Paste the following [React function component](https://react.dev/learn/your-first-component) into the newly create `TextInput.tsx` file. + Paste the following [React function component](https://react.dev/learn/your-first-component) into the newly create `TextInput.tsx` file. + + ```tsx + import { ReactElement } from "react"; - ```tsx - import { ReactElement } from "react"; + export interface TextInputProps { + value: string; + } - export interface TextInputProps { - value: string; - } + export function TextInput({ value }: TextInputProps): ReactElement { + return ; + } + ``` - export function TextInput({ value }: TextInputProps): ReactElement { - return ; - } - ``` - - In short, the component receives an input object, called props, containing a string property named `value`. In turn the component returns a [React input element](https://react.dev/reference/react-dom/components/input) with its value set to what the `TextInput` component received in `props.value`. While the syntax looks like HTML, [it actually is JavaScript](https://react.dev/learn/writing-markup-with-jsx). + In short, the component receives an input object, called props, containing a string property named `value`. In turn the component returns a [React input element](https://react.dev/reference/react-dom/components/input) with its value set to what the `TextInput` component received in `props.value`. While the syntax looks like HTML, [it actually is JavaScript](https://react.dev/learn/writing-markup-with-jsx). -5. The container component *src/TextBox.tsx* receives the properties in the runtime, and forwards the data to the display component. The container works like glue between the Mendix application and the display component. In *TextBox.tsx* update the component to look like this: +5. The container component _src/TextBox.tsx_ receives the properties in the runtime, and forwards the data to the display component. The container works like glue between the Mendix application and the display component. In _TextBox.tsx_ update the component to look like this: - ```tsx - import { ReactElement } from "react"; - import { TextBoxContainerProps } from "../typings/TextBoxProps"; - import { TextInput } from "./components/TextInput"; + ```tsx + import { ReactElement } from "react"; + import { TextBoxContainerProps } from "../typings/TextBoxProps"; + import { TextInput } from "./components/TextInput"; - import "./ui/TextBox.css"; + import "./ui/TextBox.css"; - export function TextBox(props: TextBoxContainerProps): ReactElement { - const value = props.textAttribute.value ?? ""; - return ; - } - ``` + export function TextBox(props: TextBoxContainerProps): ReactElement { + const value = props.textAttribute.value ?? ""; + return ; + } + ``` - Explaining the code: + Explaining the code: - * Line 2 imports the generated types for the widget. The types were generated when we ran `npm start` in the previous section and its properties are based on the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition). - * The `textAttribute` is a data access object provided by the Pluggable Widgets API. It not only provides the widget with the latest value of the attribute, but also offers methods for updating the value and performing validation. React will re-render the component when the props change. - * On line 8 we provide a fallback value for the textAttribute value. This is necessary, as its value may be `undefined` and our TextInput display component only accepts `string`. The OR operator offers [short-circuit evaluation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR#short-circuit_evaluation), which will return the value on the left if it evaluates to truthy. Otherwise it will return the value provided on the right side. + - Line 2 imports the generated types for the widget. The types were generated when we ran `npm start` in the previous section and its properties are based on the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition). + - The `textAttribute` is a data access object provided by the Pluggable Widgets API. It not only provides the widget with the latest value of the attribute, but also offers methods for updating the value and performing validation. React will re-render the component when the props change. + - On line 8 we provide a fallback value for the textAttribute value. This is necessary, as its value may be `undefined` and our TextInput display component only accepts `string`. The OR operator offers [short-circuit evaluation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR#short-circuit_evaluation), which will return the value on the left if it evaluates to truthy. Otherwise it will return the value provided on the right side. -6. Pluggable Widgets also have a Preview component, which is used in the design mode of the Studio Pro page editor. Update *src/TextBox.editorPreview.tsx* such that the deleted `HelloWorldSample` component is replaced by our `TextInput` component. This will resolve the errors thrown by `npm start`. +6. Pluggable Widgets also have a Preview component, which is used in the design mode of the Studio Pro page editor. Update _src/TextBox.editorPreview.tsx_ such that the deleted `HelloWorldSample` component is replaced by our `TextInput` component. This will resolve the errors thrown by `npm start`. - ```tsx - import { ReactElement } from "react"; - import { TextBoxPreviewProps } from "../typings/TextBoxProps"; - import { TextInput } from "./components/TextInput"; + ```tsx + import { ReactElement } from "react"; + import { TextBoxPreviewProps } from "../typings/TextBoxProps"; + import { TextInput } from "./components/TextInput"; - export function preview({ textAttribute }: TextBoxPreviewProps): ReactElement { - return ; - } - ``` + export function preview({ + textAttribute, + }: TextBoxPreviewProps): ReactElement { + return ; + } + ``` - Unlike the Container component, the Preview component receives mocked values for the widget attributes. In this case `textAttribute` always receives a string. Thanks to this it is not necessary to deal with a possible `undefined` value. + Unlike the Container component, the Preview component receives mocked values for the widget attributes. In this case `textAttribute` always receives a string. Thanks to this it is not necessary to deal with a possible `undefined` value. -7. Once the watcher is done building your widget, go to Studio Pro and **synchronize** your project with F4. Synchronizing from the file system is a required step after changing the widget definition file. For other changes this is not necessary. +7. Once the watcher is done building your widget, go to Studio Pro and **synchronize** your project with F4. Synchronizing from the file system is a required step after changing the widget definition file. For other changes this is not necessary. - The widget now displays a red border indicating that it needs to be updated. Open its context menu with a right click and select **Update all widgets**. + The widget now displays a red border indicating that it needs to be updated. Open its context menu with a right click and select **Update all widgets**. - {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/updateallwidgets.png" alt="A demonstration of opening the context menu of a widget in the page editor to reveal the Update all widgets command." class="no-border" >}} + {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/updateallwidgets.png" alt="A demonstration of opening the context menu of a widget in the page editor to reveal the Update all widgets command." class="no-border" >}} 8. Open the widget properties. In the **Data source** tab **select** the **text attribute** created in [section 3.1](#creating-a-test-project). 9. **Run** the app locally to see the results, the new widget is already functional. The first text box is a standard Text box widget and the second is your pluggable web widget. Select the first text box and enter some text, unfocus the text box and the pluggable widget will now display the same data. - {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/twotextwidgets.png" alt="A live Mendix App featuring two text fields displaying the identical text: test." class="no-border" >}} + {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/twotextwidgets.png" alt="A live Mendix App featuring two text fields displaying the identical text: test." class="no-border" >}} ### Adding Style The input works, but the styling could be improved. In the next code snippets, you will add the default styling to make your TextBox widget look like a Mendix widget. All pluggable widgets receive [standard properties](/apidocs-mxsdk/apidocs/pluggable-widgets-client-apis/#standard-properties). To allow users of your widget to style it like any other Mendix widget you will need to apply the `class`, `style` and `tabIndex` props. These receive their values from the properties side bar (in the **Common** section of the **Properties** and **Styling** tabs). -1. In *src/TextBox.tsx*, pass the properties from the runtime to the `TextInput` component: - - ```tsx - export function TextBox(props: TextBoxContainerProps): ReactElement { - const value = props.textAttribute.value ?? ""; - return ; - } - ``` - - You may notice that we do not [destructure](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) the props object into variables for our Container component. The reason is that `class` [is a reserved keyword](https://developer.mozilla.org/en-US/docs/Web/API/Element/className#notes) in JavaScript and cannot be used as a variable name. This is also why we use `className` in the JSX of our components. - -2. Until we update the type of our TextInputProps, Typescript will display errors in *TextBox.tsx*. In *src/components/TextInput.tsx*, add the missing properties to the interface and pass them to the `input` component: - - ```tsx - import { CSSProperties, ReactElement } from "react"; - - export interface TextInputProps { - value: string; - className?: string; - style?: CSSProperties; - tabIndex?: number; - } - - export function TextInput({ value, tabIndex, style, className }: TextInputProps): ReactElement { - return ; - } - ``` - - Explaining the code: - * The `style` property is an object containing CSS properties which can be used to quickly add styling to a component. It is available on all [HTML components offered by React](https://react.dev/reference/react-dom/components/common#applying-css-styles). - * The question marks in the props indicate that [a property is optional](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties). This is why the unchanged usage of `TextInput` in *src/TextBox.editorPreview.tsx* is not causing type errors. - * To ensure our input has basic input [styling from Bootstrap](https://getbootstrap.com/docs/5.3/forms/form-control/), we prepend the css class `form-control` to the `className` property. Similar to how you would add classes to an HTML `class` attribute. There is no need to include Bootstrap, as Mendix' Atlas UI is based on Bootstrap. +1. In _src/TextBox.tsx_, pass the properties from the runtime to the `TextInput` component: + + ```tsx + export function TextBox(props: TextBoxContainerProps): ReactElement { + const value = props.textAttribute.value ?? ""; + return ( + + ); + } + ``` + + You may notice that we do not [destructure](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) the props object into variables for our Container component. The reason is that `class` [is a reserved keyword](https://developer.mozilla.org/en-US/docs/Web/API/Element/className#notes) in JavaScript and cannot be used as a variable name. This is also why we use `className` in the JSX of our components. + +2. Until we update the type of our TextInputProps, Typescript will display errors in _TextBox.tsx_. In _src/components/TextInput.tsx_, add the missing properties to the interface and pass them to the `input` component: + + ```tsx + import { CSSProperties, ReactElement } from "react"; + + export interface TextInputProps { + value: string; + className?: string; + style?: CSSProperties; + tabIndex?: number; + } + + export function TextInput({ + value, + tabIndex, + style, + className, + }: TextInputProps): ReactElement { + return ( + + ); + } + ``` + + Explaining the code: + + - The `style` property is an object containing CSS properties which can be used to quickly add styling to a component. It is available on all [HTML components offered by React](https://react.dev/reference/react-dom/components/common#applying-css-styles). + - The question marks in the props indicate that [a property is optional](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties). This is why the unchanged usage of `TextInput` in _src/TextBox.editorPreview.tsx_ is not causing type errors. + - To ensure our input has basic input [styling from Bootstrap](https://getbootstrap.com/docs/5.3/forms/form-control/), we prepend the css class `form-control` to the `className` property. Similar to how you would add classes to an HTML `class` attribute. There is no need to include Bootstrap, as Mendix' Atlas UI is based on Bootstrap. 3. Refresh your Mendix app in the browser, the result should be a well-styled input widget. If the change does not appear immediately, open your browser's devtools and disable cache. This ensures you are loading your widget's latest assets. - {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/styledinputwidgets.png" alt="A live Mendix app with two text fields with a similar appearance." class="no-border" >}} + {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/styledinputwidgets.png" alt="A live Mendix app with two text fields with a similar appearance." class="no-border" >}} ### Labeling the Input{#label-input} @@ -270,30 +283,30 @@ Comparing our widget to the Mendix text input widget we are still missing a labe 1. In the widget definition file, add a new property group. The caption can be anything, but we will name it `"Label"`. To the property group, add a new `systemProperty` element with the key `"Label"`. - ```xml - - - - ``` + ```xml + + + + ``` -1. Open *src/TextBox.tsx* and remove the `style` and `className` props from `TextInput`. Now that the widget is a labeled input, it should no longer have the layout styling applied to it. In fact, the `pluggable-widget-tools` removed them from the type definition in *typings/TextBoxProps.d.ts*. +1. Open _src/TextBox.tsx_ and remove the `style` and `className` props from `TextInput`. Now that the widget is a labeled input, it should no longer have the layout styling applied to it. In fact, the `pluggable-widget-tools` removed them from the type definition in _typings/TextBoxProps.d.ts_. - ```tsx {hl_lines=[3]} - export function TextBox(props: TextBoxContainerProps): ReactElement { - const value = props.textAttribute.value ?? ""; - return ; - } - ``` + ```tsx {hl_lines=[3]} + export function TextBox(props: TextBoxContainerProps): ReactElement { + const value = props.textAttribute.value ?? ""; + return ; + } + ``` 1. **Synchronize** your project and **update** all widgets. Now open the widget **Properties** and open the **Label** tab. - This will show the **Show label** radio buttons. When **Show label** is set to true, it will automatically render the label for you in the page editor and the browser: + This will show the **Show label** radio buttons. When **Show label** is set to true, it will automatically render the label for you in the page editor and the browser: - {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/edittextboxtwo.png" alt="The widget properties opened at the Label tab. The show label setting is set to true, which reveals a label caption field with the value TextBox." class="no-border" >}} + {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/edittextboxtwo.png" alt="The widget properties opened at the Label tab. The show label setting is set to true, which reveals a label caption field with the value TextBox." class="no-border" >}} - {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/editdataviewone.png" alt="The page editor showing the caption TextBox in front of the pluggable widget" class="no-border" >}} + {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/editdataviewone.png" alt="The page editor showing the caption TextBox in front of the pluggable widget" class="no-border" >}} - {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/inputwidgetswithlabel.png" alt="A live Mendix app with two text fields with labels. The second text field has the label TextBox" class="no-border" >}} + {{< figure src="/attachments/howto/extensibility/pluggable-widgets/create-a-pluggable-widget-one/inputwidgetswithlabel.png" alt="A live Mendix app with two text fields with labels. The second text field has the label TextBox" class="no-border" >}} {{% alert color="info" %}}The labels will appear in front of, or above the inputs. This depends on the surrounding [data view's properties](/refguide/data-view/#orientation) (**form orientation** and **label width**) and the size of the screen.{{% /alert %}} @@ -301,60 +314,80 @@ Comparing our widget to the Mendix text input widget we are still missing a labe Our widget now looks like a Mendix widget, but does not behave like one yet. While it is able to display the value of the text attribute, it is not able to update it yet. In this section we will close that loop. -1. In *src/components/TextInput.tsx* add an `onChange` property to the `TextInputProps`. This will act as a callback, allowing our `TextInput` component to signal changes in the value. It does this by calling the function with the new value. This is the same mechanism by which the `TextInput` component receives updated values from the `input` component. - - ```tsx {hl_lines=[6,"15-17"]} - export interface TextInputProps { - value: string; - className?: string; - style?: CSSProperties; - tabIndex?: number; - onChange?: (value: string) => void; - } - - export function TextInput({ value, onChange, tabIndex, style, className }: TextInputProps): ReactElement { - return ( - { - onChange?.(event.target.value); - }} - className={"form-control " + className} - style={style} - tabIndex={tabIndex} - /> - ); - } - ``` - - Explaining the code: - - * Each time the input component has new data, it will trigger the `onChange` callback. In `TextInput` we tell the input component to execute the anonymous function written on lines 15-17. It receives an event object similar to the one provided by [event listeners](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent). From the event object we extract the value of the input element, and call the onChange callback with that value. - * The [optional chaining operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) `?.`, allows us to call a function that may be `undefined`. In that case the rest of the expression does not evaluate, avoiding a runtime error. It is equivalent to writing a nullish check: - - ```ts - if (onChange != null && onChange != undefined) { - onChange(event.target.value) - } - ``` - -2. Now that `TextInput` is able to provide updates, we need to make sure that those updates are forwarded to the Mendix client. To do this open *src/TextBox.tsx* and pass the `setValue` method of `props.textAttribute` to the `onChange` prop of `TextInput`. - - ```tsx {hl_lines=5} - export function TextBox(props: TextBoxContainerProps): ReactElement { - const value = props.textAttribute.value ?? ""; - return ; - } - ``` - - Explaining the code: - - * By defining a callback prop, a component advertises what data it is able to provide. Consumers of the callback get to decide how to respond to the availability of the new data. In this case we decided in `TextBox` that each time `TextInput` has new data, it is given to `props.textAttribute.setValue()`. +1. In _src/components/TextInput.tsx_ add an `onChange` property to the `TextInputProps`. This will act as a callback, allowing our `TextInput` component to signal changes in the value. It does this by calling the function with the new value. This is the same mechanism by which the `TextInput` component receives updated values from the `input` component. + + ```tsx {hl_lines=[6,"15-17"]} + export interface TextInputProps { + value: string; + className?: string; + style?: CSSProperties; + tabIndex?: number; + onChange?: (value: string) => void; + } + + export function TextInput({ + value, + onChange, + tabIndex, + style, + className, + }: TextInputProps): ReactElement { + return ( + { + onChange?.(event.target.value); + }} + className={"form-control " + className} + style={style} + tabIndex={tabIndex} + /> + ); + } + ``` + + Explaining the code: + + - Each time the input component has new data, it will trigger the `onChange` callback. In `TextInput` we tell the input component to execute the anonymous function written on lines 15-17. It receives an event object similar to the one provided by [event listeners](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent). From the event object we extract the value of the input element, and call the onChange callback with that value. + - The [optional chaining operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) `?.`, allows us to call a function that may be `undefined`. In that case the rest of the expression does not evaluate, avoiding a runtime error. It is equivalent to writing a nullish check: + + ```ts + if (onChange != null && onChange != undefined) { + onChange(event.target.value); + } + ``` + +2. Now that `TextInput` is able to provide updates, we need to make sure that those updates are forwarded to the Mendix client. To do this open _src/TextBox.tsx_ and pass the `setValue` method of `props.textAttribute` to the `onChange` prop of `TextInput`. + + ```tsx {hl_lines=5} + export function TextBox(props: TextBoxContainerProps): ReactElement { + const value = props.textAttribute.value ?? ""; + return ( + + ); + } + ``` + + Explaining the code: + + - By defining a callback prop, a component advertises what data it is able to provide. Consumers of the callback get to decide how to respond to the availability of the new data. In this case we decided in `TextBox` that each time `TextInput` has new data, it is given to `props.textAttribute.setValue()`. + +## Building & Releasing Your Widget + +Development build tested successfully. You can now create a release build to bundle with your module or publish the widget to the Marketplace. + +{{% alert color="info" %}}**Dev Build vs. Release Build**: Release builds are minified, optimized, and stripped of heavy console logging/source maps for optimal runtime performance in production. {{% /alert %}} + +1. Run `npm run prerelease` to scan for any linting errors. +1. Fix any errors that are found. +1. Run `npm run release` to create a release build. +1. Locate the generated `.mpk` file at `{widgetName}/dist/{version}/{widgetName}.mpk`. +1. Use this `.mpk` file for the Marketplace release or include it in your module for production. Congratulations, you have now made a fully functional input widget! @@ -362,7 +395,7 @@ Continue with the next tutorial to learn how to add validation feedback, custom ## Read More -* [Build a Pluggable Web Widget: Part 2 (Advanced)](/howto/extensibility/create-a-pluggable-widget-two/) -* [Pluggable Widgets API](/apidocs-mxsdk/apidocs/pluggable-widgets/) -* [Client APIs Available to Pluggable Widgets](/apidocs-mxsdk/apidocs/pluggable-widgets-client-apis/) -* [Pluggable Widget Property Types](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/) +- [Build a Pluggable Web Widget: Part 2 (Advanced)](/howto/extensibility/create-a-pluggable-widget-two/) +- [Pluggable Widgets API](/apidocs-mxsdk/apidocs/pluggable-widgets/) +- [Client APIs Available to Pluggable Widgets](/apidocs-mxsdk/apidocs/pluggable-widgets-client-apis/) +- [Pluggable Widget Property Types](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/) diff --git a/content/en/docs/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md b/content/en/docs/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md index dc369be8623..02f13ae3e4e 100644 --- a/content/en/docs/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md +++ b/content/en/docs/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md @@ -12,11 +12,11 @@ Pluggable web widgets are the new generation of custom-built widgets. These widg This how-to teaches you how to do the following: -* Generate a widget structure -* Create a basic text input widget -* Add a label +- Generate a widget structure +- Create a basic text input widget +- Add a label -**Are you in a hurry?** +**Are you in a hurry?** Clone this [code sample](https://github.com/mendix/text-box-sample) from GitHub with the basic and advanced features already implemented. @@ -24,9 +24,9 @@ Clone this [code sample](https://github.com/mendix/text-box-sample) from GitHub Before starting this how-to, make sure you have completed the following prerequisites: -* Install the LTS version of [Node.js](https://nodejs.org/en/download/). -* Install an integrated development environment (IDE) of your choice (Mendix recommends [Microsoft Visual Studio Code](https://code.visualstudio.com/)) -* Have a basic understanding of [TypeScript](https://www.typescriptlang.org/) +- Install the LTS version of [Node.js](https://nodejs.org/en/download/). +- Install an integrated development environment (IDE) of your choice (Mendix recommends [Microsoft Visual Studio Code](https://code.visualstudio.com/)) +- Have a basic understanding of [TypeScript](https://www.typescriptlang.org/) ## Creating a TextBox Input Widget @@ -36,29 +36,30 @@ The following steps teach you how to build a pluggable input widget, and show yo 1. Open Studio Pro and create a new test app by selecting **File > New App** from the top menu bar and then **Blank App**. 2. Create a test case for the new widget:
- 1. In the domain model of **MyFirstModule**, add a new entity.
- 2. Add a new attribute of type **String**.
- 3. Open **MyFirstModule’s** **Home** page.
- 4. There, add a **Data view** widget, double-click the widget, and give it a data source microflow by selecting **Data source** > **Type** > **Microflow**.
- 5. Next to the microflow field click the **Select** button, and click **New**.
- 6. Provide the name *DSS_CreateTestObject* to this new microflow.
- 7. Click the **Show** button. This will open the microflow editor. Then click the **OK** button to close the dialog box.
- 8. Add a new **Create object** action on your microflow. + + 1. In the domain model of **MyFirstModule**, add a new entity.
+ 2. Add a new attribute of type **String**.
+ 3. Open **MyFirstModule’s** **Home** page.
+ 4. There, add a **Data view** widget, double-click the widget, and give it a data source microflow by selecting **Data source** > **Type** > **Microflow**.
+ 5. Next to the microflow field click the **Select** button, and click **New**.
+ 6. Provide the name _DSS_CreateTestObject_ to this new microflow.
+ 7. Click the **Show** button. This will open the microflow editor. Then click the **OK** button to close the dialog box.
+ 8. Add a new **Create object** action on your microflow. 3. Open the new **Create object** action's properties by double-clicking it. For its **Entity**, click the **Select** button and choose the entity you created above. Then click **OK** to close the dialog box. 4. Right-click the **Create Entity** activity, then click **Set $NewEntity as Return Value**. 5. Go back to the home page, open the **Add Widget** menu, and then add a **TextBox** widget inside the data view. 6. Open the Textbox's properties and select the **Datasource Attribute (path)** string attribute you created above. Then click the **OK** button to close the dialog box. The end result should look like this: - {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/createtestobject.png" alt="The page editor in Studio Pro in structure mode. The page contains a Data View that has a Microflow as its datasource. It contains a Text Input widget referencing the text attribute created in this section." class="no-border" >}} + {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/createtestobject.png" alt="The page editor in Studio Pro in structure mode. The page contains a Data View that has a Microflow as its datasource. It contains a Text Input widget referencing the text attribute created in this section." class="no-border" >}} - {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/microflowcreateentity.png" alt="A Microflow with a single Create Entity Action. The Action creates an instance of the entity created in the steps above. The Microflow ends with returning the new entity." class="no-border" >}} + {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/microflowcreateentity.png" alt="A Microflow with a single Create Entity Action. The Action creates an instance of the entity created in the steps above. The Microflow ends with returning the new entity." class="no-border" >}} ### Scaffolding the Widget The Pluggable Widget Generator is the quickest way to start developing a widget. It creates a widget’s recommended folder structure and files. -Using a terminal or command line, navigate to your new Mendix app's folder, create a new folder named *myPluggableWidgets*, and start the generator using: +Using a terminal or command line, navigate to your new Mendix app's folder, create a new folder named _myPluggableWidgets_, and start the generator using: ```shell mkdir myPluggableWidgets @@ -68,23 +69,23 @@ npx @mendix/generator-widget@10.24-latest TextBox The generator will ask you a few questions during setup. Answer the questions by specifying the following information: -* Widget name: *{Your widget name}* -* Widget Description: *{Your widget description}* -* Organization Name: *{Your organization name}* -* Copyright: *{Your copyright date}* -* License: *{Your license}* -* Initial Version:*{Your initial version number}* -* Author: *{Your author name}* -* Mendix App path: **../../** -* Programming language: **TypeScript** -* Which type of components do you want to use? **Function Components** -* Widget type: **For web and hybrid mobile apps** -* Widget template: **Empty widget (recommended for more experienced developers)** -* Unit tests: **No** -* End-to-end tests: **No** +- Widget name: _{Your widget name}_ +- Widget Description: _{Your widget description}_ +- Organization Name: _{Your organization name}_ +- Copyright: _{Your copyright date}_ +- License: _{Your license}_ +- Initial Version:_{Your initial version number}_ +- Author: _{Your author name}_ +- Mendix App path: **../../** +- Programming language: **TypeScript** +- Which type of components do you want to use? **Function Components** +- Widget type: **For web and hybrid mobile apps** +- Widget template: **Empty widget (recommended for more experienced developers)** +- Unit tests: **No** +- End-to-end tests: **No** {{% alert color="warning" %}} -Currently, **Organization Name** cannot include a dash "-" character. Dashes in the organization name will result in an error when running your app, asking to check if the widgets "were generated with the latest version of the pluggable-widgets-tools and are ES6 modules." +Currently, **Organization Name** cannot include a dash "-" character. Dashes in the organization name will result in an error when running your app, asking to check if the widgets "were generated with the latest version of the pluggable-widgets-tools and are ES6 modules." To fix this in an existing widget, modify the `packagePath` property of its **package.json** file and rebuild the widget. {{% /alert %}} @@ -109,164 +110,176 @@ When the build script completes it will package your widget as a `.mpk` file and 1. Drag the Text Box widget to the Data View added in [Creating a Test Project](#creating-a-test-project). 1. Run your app locally and open it in the browser. The homepage should now display Hello World below the text widget: - {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/hello-world.png" alt="A live mendix app displaying a text field and below it the pluggable widget with the text Hello World." class="no-border" >}} + {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/hello-world.png" alt="A live mendix app displaying a text field and below it the pluggable widget with the text Hello World." class="no-border" >}} ### Adding the Attribute -Open the *(YourMendixApp)/myPluggableWidgets/textBox* folder in your IDE of choice. From now on, all file references will be relative to this path. To set up your new widget, first you must use an attribute of the context object and display that attribute in an input field: +Open the _(YourMendixApp)/myPluggableWidgets/textBox_ folder in your IDE of choice. From now on, all file references will be relative to this path. To set up your new widget, first you must use an attribute of the context object and display that attribute in an input field: -1. Remove the file *src/components/HelloWorldSample.tsx*. Errors in *TextBox.editorPreview.tsx* will be dealt with in step 6 below. +1. Remove the file _src/components/HelloWorldSample.tsx_. Errors in _TextBox.editorPreview.tsx_ will be dealt with in step 6 below. 2. The generator creates the widget definition file `src/TextBox.xml` with preset properties. Replace the `sampleText` property following this snippet: - ```xml - - - Text Box - Edit text input - - - - - Attribute (path) - - - - - - - - - ``` - - Explaining the code: - - * *TextBox.xml* is the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition) used in Studio Pro which reads the widget's capabilities - * The property `pluginWidget=true` will make the widget work with the new widget API - * The property `needsEntityContext=true` is set up to allow the attribute to be taken from context - * The property of the [type attribute](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/#attribute) only allows the selection of string attributes from the domain model + ```xml + + + Text Box + Edit text input + + + + + Attribute (path) + + + + + + + + + ``` + + Explaining the code: + + - _TextBox.xml_ is the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition) used in Studio Pro which reads the widget's capabilities + - The property `pluginWidget=true` will make the widget work with the new widget API + - The property `needsEntityContext=true` is set up to allow the attribute to be taken from context + - The property of the [type attribute](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/#attribute) only allows the selection of string attributes from the domain model 3. The generator includes scripts which assist with building and packaging your widget. Use `npm start` to run the watcher, it will generate updated Typescript types based on the widget definition file. - When done generating the types can be found in `typings/TextBoxProps.d.ts` + When done generating the types can be found in `typings/TextBoxProps.d.ts` - {{% alert color="info" %}}The console will display an error along the lines of `HelloWorldSample.tsx could not be found`. We will address this in the section [Labeling the Input](#label-input) of this how-to. It can be ignored for now.{{% /alert %}} + {{% alert color="info" %}}The console will display an error along the lines of `HelloWorldSample.tsx could not be found`. We will address this in the section [Labeling the Input](#label-input) of this how-to. It can be ignored for now.{{% /alert %}} 4. Create a new file, `src/components/TextInput.tsx`. This will be the display component. A display component is a regular React component and does not interact with Mendix APIs. It can be re-used in any React application. - Paste the following [React function component](https://react.dev/learn/your-first-component) into the newly create `TextInput.tsx` file. + Paste the following [React function component](https://react.dev/learn/your-first-component) into the newly create `TextInput.tsx` file. + + ```tsx + import { createElement, ReactElement } from "react"; - ```tsx - import { createElement, ReactElement } from "react"; + export interface TextInputProps { + value: string; + } - export interface TextInputProps { - value: string; - } + export function TextInput({ value }: TextInputProps): ReactElement { + return ; + } + ``` - export function TextInput({ value }: TextInputProps): ReactElement { - return ; - } - ``` - - In short, the component receives an input object, called props, containing a string property named `value`. In turn the component returns a [React input element](https://react.dev/reference/react-dom/components/input) with its value set to what the `TextInput` component received in `props.value`. While the syntax looks like HTML, [it actually is JavaScript](https://react.dev/learn/writing-markup-with-jsx). + In short, the component receives an input object, called props, containing a string property named `value`. In turn the component returns a [React input element](https://react.dev/reference/react-dom/components/input) with its value set to what the `TextInput` component received in `props.value`. While the syntax looks like HTML, [it actually is JavaScript](https://react.dev/learn/writing-markup-with-jsx). -5. The container component *src/TextBox.tsx* receives the properties in the runtime, and forwards the data to the display component. The container works like glue between the Mendix application and the display component. In *TextBox.tsx* update the component to look like this: +5. The container component _src/TextBox.tsx_ receives the properties in the runtime, and forwards the data to the display component. The container works like glue between the Mendix application and the display component. In _TextBox.tsx_ update the component to look like this: - ```tsx - import { createElement, ReactElement } from "react"; - import { TextBoxContainerProps } from "../typings/TextBoxProps"; - import { TextInput } from "./components/TextInput"; + ```tsx + import { createElement, ReactElement } from "react"; + import { TextBoxContainerProps } from "../typings/TextBoxProps"; + import { TextInput } from "./components/TextInput"; - import "./ui/TextBox.css"; + import "./ui/TextBox.css"; - export function TextBox(props: TextBoxContainerProps): ReactElement { - const value = props.textAttribute.value || ""; - return ; - } - ``` + export function TextBox(props: TextBoxContainerProps): ReactElement { + const value = props.textAttribute.value || ""; + return ; + } + ``` - Explaining the code: + Explaining the code: - * Line 2 imports the generated types for the widget. The types were generated when we ran `npm start` in the previous section and its properties are based on the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition). - * The `textAttribute` is a data access object provided by the Pluggable Widgets API. It not only provides the widget with the latest value of the attribute, but also offers methods for updating the value and performing validation. React will re-render the component when the props change. - * On line 8 we provide a fallback value for the textAttribute value. This is necessary, as its value may be `undefined` and our TextInput display component only accepts `string`. The OR operator offers [short-circuit evaluation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR#short-circuit_evaluation), which will return the value on the left if it evaluates to truthy. Otherwise it will return the value provided on the right side. + - Line 2 imports the generated types for the widget. The types were generated when we ran `npm start` in the previous section and its properties are based on the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition). + - The `textAttribute` is a data access object provided by the Pluggable Widgets API. It not only provides the widget with the latest value of the attribute, but also offers methods for updating the value and performing validation. React will re-render the component when the props change. + - On line 8 we provide a fallback value for the textAttribute value. This is necessary, as its value may be `undefined` and our TextInput display component only accepts `string`. The OR operator offers [short-circuit evaluation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR#short-circuit_evaluation), which will return the value on the left if it evaluates to truthy. Otherwise it will return the value provided on the right side. -6. Pluggable Widgets also have a Preview component, which is used in the design mode of the Studio Pro page editor. Update *src/TextBox.editorPreview.tsx* such that the deleted `HelloWorldSample` component is replaced by our `TextInput` component. This will resolve the errors thrown by `npm start`. +6. Pluggable Widgets also have a Preview component, which is used in the design mode of the Studio Pro page editor. Update _src/TextBox.editorPreview.tsx_ such that the deleted `HelloWorldSample` component is replaced by our `TextInput` component. This will resolve the errors thrown by `npm start`. - ```tsx - import { ReactElement, createElement } from "react"; - import { TextBoxPreviewProps } from "../typings/TextBoxProps"; - import { TextInput } from "./components/TextInput"; + ```tsx + import { ReactElement, createElement } from "react"; + import { TextBoxPreviewProps } from "../typings/TextBoxProps"; + import { TextInput } from "./components/TextInput"; - export function preview({ textAttribute }: TextBoxPreviewProps): ReactElement { - return ; - } - ``` + export function preview({ + textAttribute, + }: TextBoxPreviewProps): ReactElement { + return ; + } + ``` - Unlike the Container component, the Preview component receives mocked values for the widget attributes. In this case `textAttribute` always receives a string. Thanks to this it is not necessary to deal with a possible `undefined` value. + Unlike the Container component, the Preview component receives mocked values for the widget attributes. In this case `textAttribute` always receives a string. Thanks to this it is not necessary to deal with a possible `undefined` value. -7. Once the watcher is done building your widget, go to Studio Pro and **synchronize** your project with F4. Synchronizing from the file system is a required step after changing the widget definition file. For other changes this is not necessary. +7. Once the watcher is done building your widget, go to Studio Pro and **synchronize** your project with F4. Synchronizing from the file system is a required step after changing the widget definition file. For other changes this is not necessary. - The widget now displays a red border indicating that it needs to be updated. Open its context menu with a right click and select **Update all widgets**. + The widget now displays a red border indicating that it needs to be updated. Open its context menu with a right click and select **Update all widgets**. - {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/updateallwidgets.png" alt="A demonstration of opening the context menu of a widget in the page editor to reveal the Update all widgets command." class="no-border" >}} + {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/updateallwidgets.png" alt="A demonstration of opening the context menu of a widget in the page editor to reveal the Update all widgets command." class="no-border" >}} 8. Open the widget properties. In the **Data source** tab **select** the **text attribute** created in [section 3.1](#creating-a-test-project). 9. **Run** the app locally to see the results, the new widget is already functional. The first text box is a standard Text box widget and the second is your pluggable web widget. Select the first text box and enter some text, unfocus the text box and the pluggable widget will now display the same data. - {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/twotextwidgets.png" alt="A live Mendix App featuring two text fields displaying the identical text: test." class="no-border" >}} + {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/twotextwidgets.png" alt="A live Mendix App featuring two text fields displaying the identical text: test." class="no-border" >}} ### Adding Style The input works, but the styling could be improved. In the next code snippets, you will add the default styling to make your TextBox widget look like a Mendix widget. All pluggable widgets receive [standard properties](/apidocs-mxsdk/apidocs/pluggable-widgets-client-apis/#standard-properties). To allow users of your widget to style it like any other Mendix widget you will need to apply the `class`, `style` and `tabIndex` props. These receive their values from the properties side bar (in the **Common** section of the **Properties** and **Styling** tabs). -1. In *src/TextBox.tsx*, pass the properties from the runtime to the `TextInput` component: - - ```tsx - export function TextBox(props: TextBoxContainerProps): ReactElement { - const value = props.textAttribute.value || ""; - return ; - } - ``` - - You may notice that we do not [destructure](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) the props object into variables for our Container component. The reason is that `class` [is a reserved keyword](https://developer.mozilla.org/en-US/docs/Web/API/Element/className#notes) in JavaScript and cannot be used as a variable name. This is also why we use `className` in the JSX of our components. - -2. Until we update the type of our TextInputProps, Typescript will display errors in *TextBox.tsx*. In *src/components/TextInput.tsx*, add the missing properties to the interface and pass them to the `input` component: - - ```tsx - import { createElement, CSSProperties, ReactElement } from "react"; - - export interface TextInputProps { - value: string; - className?: string; - style?: CSSProperties; - tabIndex?: number; - } - - export function TextInput({ value, tabIndex, style, className }: TextInputProps): ReactElement { - return ; - } - ``` - - Explaining the code: - * The `style` property is an object containing CSS properties which can be used to quickly add styling to a component. It is available on all [HTML components offered by React](https://react.dev/reference/react-dom/components/common#applying-css-styles). - * The question marks in the props indicate that [a property is optional](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties). This is why the unchanged usage of `TextInput` in *src/TextBox.editorPreview.tsx* is not causing type errors. - * To ensure our input has basic input [styling from Bootstrap](https://getbootstrap.com/docs/5.3/forms/form-control/), we prepend the css class `form-control` to the `className` property. Similar to how you would add classes to an HTML `class` attribute. There is no need to include Bootstrap, as Mendix' Atlas UI is based on Bootstrap. +1. In _src/TextBox.tsx_, pass the properties from the runtime to the `TextInput` component: + + ```tsx + export function TextBox(props: TextBoxContainerProps): ReactElement { + const value = props.textAttribute.value || ""; + return ( + + ); + } + ``` + + You may notice that we do not [destructure](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) the props object into variables for our Container component. The reason is that `class` [is a reserved keyword](https://developer.mozilla.org/en-US/docs/Web/API/Element/className#notes) in JavaScript and cannot be used as a variable name. This is also why we use `className` in the JSX of our components. + +2. Until we update the type of our TextInputProps, Typescript will display errors in _TextBox.tsx_. In _src/components/TextInput.tsx_, add the missing properties to the interface and pass them to the `input` component: + + ```tsx + import { createElement, CSSProperties, ReactElement } from "react"; + + export interface TextInputProps { + value: string; + className?: string; + style?: CSSProperties; + tabIndex?: number; + } + + export function TextInput({ + value, + tabIndex, + style, + className, + }: TextInputProps): ReactElement { + return ( + + ); + } + ``` + + Explaining the code: + + - The `style` property is an object containing CSS properties which can be used to quickly add styling to a component. It is available on all [HTML components offered by React](https://react.dev/reference/react-dom/components/common#applying-css-styles). + - The question marks in the props indicate that [a property is optional](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties). This is why the unchanged usage of `TextInput` in _src/TextBox.editorPreview.tsx_ is not causing type errors. + - To ensure our input has basic input [styling from Bootstrap](https://getbootstrap.com/docs/5.3/forms/form-control/), we prepend the css class `form-control` to the `className` property. Similar to how you would add classes to an HTML `class` attribute. There is no need to include Bootstrap, as Mendix' Atlas UI is based on Bootstrap. 3. Refresh your Mendix app in the browser, the result should be a well-styled input widget. If the change does not appear immediately, open your browser's devtools and disable cache. This ensures you are loading your widget's latest assets. - {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/styledinputwidgets.png" alt="A live Mendix app with two text fields with a similar appearance." class="no-border" >}} + {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/styledinputwidgets.png" alt="A live Mendix app with two text fields with a similar appearance." class="no-border" >}} ### Labeling the Input{#label-input} @@ -274,27 +287,27 @@ Comparing our widget to the Mendix text input widget we are still missing a labe 1. In the widget definition file, add a new property group. The caption can be anything, but we will name it `"Label"`. To the property group, add a new `systemProperty` element with the key `"Label"`. - ```xml - - - - ``` + ```xml + + + + ``` -1. Open *src/TextBox.tsx* and remove the `style` and `className` props from `TextInput`. Now that the widget is a labeled input, it should no longer have the layout styling applied to it. In fact, the `pluggable-widget-tools` removed them from the type definition in *typings/TextBoxProps.d.ts*. +1. Open _src/TextBox.tsx_ and remove the `style` and `className` props from `TextInput`. Now that the widget is a labeled input, it should no longer have the layout styling applied to it. In fact, the `pluggable-widget-tools` removed them from the type definition in _typings/TextBoxProps.d.ts_. - ```tsx - return ; - ``` + ```tsx + return ; + ``` 1. **Synchronize** your project and **update** all widgets. Now open the widget **Properties** and open the **Label** tab. - This will show the **Show label** radio buttons. When **Show label** is set to true, it will automatically render the label for you in the page editor and the browser: + This will show the **Show label** radio buttons. When **Show label** is set to true, it will automatically render the label for you in the page editor and the browser: - {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/edittextboxtwo.png" alt="The widget properties opened at the Label tab. The show label setting is set to true, which reveals a label caption field with the value TextBox." class="no-border" >}} + {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/edittextboxtwo.png" alt="The widget properties opened at the Label tab. The show label setting is set to true, which reveals a label caption field with the value TextBox." class="no-border" >}} - {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/editdataviewone.png" alt="The page editor showing the caption TextBox in front of the pluggable widget" class="no-border" >}} + {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/editdataviewone.png" alt="The page editor showing the caption TextBox in front of the pluggable widget" class="no-border" >}} - {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/inputwidgetswithlabel.png" alt="A live Mendix app with two text fields with labels. The second text field has the label TextBox" class="no-border" >}} + {{< figure src="/attachments/howto10/extensibility/pluggable-widgets/create-a-pluggable-widget-one/inputwidgetswithlabel.png" alt="A live Mendix app with two text fields with labels. The second text field has the label TextBox" class="no-border" >}} {{% alert color="info" %}}The labels will appear in front of, or above the inputs. This depends on the surrounding [data view's properties](/refguide10/data-view/#orientation) (**form orientation** and **label width**) and the size of the screen.{{% /alert %}} @@ -302,60 +315,80 @@ Comparing our widget to the Mendix text input widget we are still missing a labe Our widget now looks like a Mendix widget, but does not behave like one yet. While it is able to display the value of the text attribute, it is not able to update it yet. In this section we will close that loop. -1. In *src/components/TextInput.tsx* add an `onChange` property to the `TextInputProps`. This will act as a callback, allowing our `TextInput` component to signal changes in the value. It does this by calling the function with the new value. This is the same mechanism by which the `TextInput` component receives updated values from the `input` component. - - ```tsx {hl_lines=[6,"15-17"]} - export interface TextInputProps { - value: string; - className?: string; - style?: CSSProperties; - tabIndex?: number; - onChange?: (value: string) => void; - } - - export function TextInput({ value, onChange, tabIndex, style, className }: TextInputProps): ReactElement { - return ( - { - onChange?.(event.target.value); - }} - className={"form-control " + className} - style={style} - tabIndex={tabIndex} - /> - ); - } - ``` - - Explaining the code: - - * Each time the input component has new data, it will trigger the `onChange` callback. In `TextInput` we tell the input component to execute the anonymous function written on lines 15-17. It receives an event object similar to the one provided by [event listeners](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent). From the event object we extract the value of the input element, and call the onChange callback with that value. - * The [optional chaining operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) `?.`, allows us to call a function that may be `undefined`. In that case the rest of the expression does not evaluate, avoiding a runtime error. It is equivalent to writing a nullish check: - - ```ts - if (onChange != null && onChange != undefined) { - onChange(event.target.value) - } - ``` - -2. Now that `TextInput` is able to provide updates, we need to make sure that those updates are forwarded to the Mendix client. To do this open *src/TextBox.tsx* and pass the `setValue` method of `props.textAttribute` to the `onChange` prop of `TextInput`. - - ```tsx {hl_lines=5} - export function TextBox(props: TextBoxContainerProps): ReactElement { - const value = props.textAttribute.value || ""; - return ; - } - ``` - - Explaining the code: - - * By defining a callback prop, a component advertises what data it is able to provide. Consumers of the callback get to decide how to respond to the availability of the new data. In this case we decided in `TextBox` that each time `TextInput` has new data, it is given to `props.textAttribute.setValue()`. +1. In _src/components/TextInput.tsx_ add an `onChange` property to the `TextInputProps`. This will act as a callback, allowing our `TextInput` component to signal changes in the value. It does this by calling the function with the new value. This is the same mechanism by which the `TextInput` component receives updated values from the `input` component. + + ```tsx {hl_lines=[6,"15-17"]} + export interface TextInputProps { + value: string; + className?: string; + style?: CSSProperties; + tabIndex?: number; + onChange?: (value: string) => void; + } + + export function TextInput({ + value, + onChange, + tabIndex, + style, + className, + }: TextInputProps): ReactElement { + return ( + { + onChange?.(event.target.value); + }} + className={"form-control " + className} + style={style} + tabIndex={tabIndex} + /> + ); + } + ``` + + Explaining the code: + + - Each time the input component has new data, it will trigger the `onChange` callback. In `TextInput` we tell the input component to execute the anonymous function written on lines 15-17. It receives an event object similar to the one provided by [event listeners](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent). From the event object we extract the value of the input element, and call the onChange callback with that value. + - The [optional chaining operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) `?.`, allows us to call a function that may be `undefined`. In that case the rest of the expression does not evaluate, avoiding a runtime error. It is equivalent to writing a nullish check: + + ```ts + if (onChange != null && onChange != undefined) { + onChange(event.target.value); + } + ``` + +2. Now that `TextInput` is able to provide updates, we need to make sure that those updates are forwarded to the Mendix client. To do this open _src/TextBox.tsx_ and pass the `setValue` method of `props.textAttribute` to the `onChange` prop of `TextInput`. + + ```tsx {hl_lines=5} + export function TextBox(props: TextBoxContainerProps): ReactElement { + const value = props.textAttribute.value || ""; + return ( + + ); + } + ``` + + Explaining the code: + + - By defining a callback prop, a component advertises what data it is able to provide. Consumers of the callback get to decide how to respond to the availability of the new data. In this case we decided in `TextBox` that each time `TextInput` has new data, it is given to `props.textAttribute.setValue()`. + +## Building & Releasing Your Widget + +Development build tested successfully. You can now create a release build to bundle with your module or publish the widget to the Marketplace. + +{{% alert color="info" %}}**Dev Build vs. Release Build**: Release builds are minified, optimized, and stripped of heavy console logging/source maps for optimal runtime performance in production. {{% /alert %}} + +1. Run `npm run prerelease` to scan for any linting errors. +1. Fix any errors that are found. +1. Run `npm run release` to create a release build. +1. Locate the generated `.mpk` file at `{widgetName}/dist/{version}/{widgetName}.mpk`. +1. Use this `.mpk` file for the Marketplace release or include it in your module for production. Congratulations, you have now made a fully functional input widget! @@ -363,7 +396,7 @@ Continue with the next tutorial to learn how to add validation feedback, custom ## Read More -* [Build a Pluggable Web Widget: Part 2 (Advanced)](/howto10/extensibility/create-a-pluggable-widget-two/) -* [Pluggable Widgets API](/apidocs-mxsdk/apidocs/pluggable-widgets/) -* [Client APIs Available to Pluggable Widgets](/apidocs-mxsdk/apidocs/pluggable-widgets-client-apis/) -* [Pluggable Widget Property Types](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/) +- [Build a Pluggable Web Widget: Part 2 (Advanced)](/howto10/extensibility/create-a-pluggable-widget-two/) +- [Pluggable Widgets API](/apidocs-mxsdk/apidocs/pluggable-widgets/) +- [Client APIs Available to Pluggable Widgets](/apidocs-mxsdk/apidocs/pluggable-widgets-client-apis/) +- [Pluggable Widget Property Types](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/) diff --git a/content/en/docs/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md b/content/en/docs/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md index 8ed5e2306cb..2a6a1dba117 100644 --- a/content/en/docs/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md +++ b/content/en/docs/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one.md @@ -12,11 +12,11 @@ Pluggable web widgets are the new generation of custom-built widgets. These widg This how-to teaches you how to do the following: -* Generate a widget structure -* Create a basic text input widget -* Add a label +- Generate a widget structure +- Create a basic text input widget +- Add a label -**Are you in a hurry?** +**Are you in a hurry?** Clone this [code sample](https://github.com/mendix/text-box-sample) from GitHub with the basic and advanced features already implemented. @@ -24,9 +24,9 @@ Clone this [code sample](https://github.com/mendix/text-box-sample) from GitHub Before starting this how-to, make sure you have completed the following prerequisites: -* Install the LTS version of [Node.js](https://nodejs.org/en/download/). -* Install an integrated development environment (IDE) of your choice (Mendix recommends [Microsoft Visual Studio Code](https://code.visualstudio.com/)) -* Have a basic understanding of [TypeScript](https://www.typescriptlang.org/) +- Install the LTS version of [Node.js](https://nodejs.org/en/download/). +- Install an integrated development environment (IDE) of your choice (Mendix recommends [Microsoft Visual Studio Code](https://code.visualstudio.com/)) +- Have a basic understanding of [TypeScript](https://www.typescriptlang.org/) ## Creating a TextBox Input Widget @@ -36,31 +36,31 @@ The following steps teach you how to build a pluggable input widget, and show yo 1. Open Mendix Studio Pro and create a new test app by selecting **File > New App** from the top menu bar and then **Blank App**. 2. Open the **Domain model** of **MyFirstModule**. - 1. Add a new **entity** with an attribute of type **String**. + 1. Add a new **entity** with an attribute of type **String**. 3. Create a page for testing using **MyFirstModule’s** **Home** page. - 1. Add a **Data view** widget, double-click the widget, and give it a data source microflow by selecting **Data source** > **Type** > **Microflow**. - 2. Next to the microflow field click the **Select** button, and click **New**. - 3. Provide the name *DSS_CreateTestObject* to this new microflow. - 4. Click the **Show** button. This will open the microflow editor. Then click the **OK** button to close the dialog box.
+ 1. Add a **Data view** widget, double-click the widget, and give it a data source microflow by selecting **Data source** > **Type** > **Microflow**. + 2. Next to the microflow field click the **Select** button, and click **New**. + 3. Provide the name _DSS_CreateTestObject_ to this new microflow. + 4. Click the **Show** button. This will open the microflow editor. Then click the **OK** button to close the dialog box.
4. Configure the Microflow to return an instance of your test entity. - 1. Add a new **Create object** action on your microflow. - 2. Open the new **Create object** action's properties by double-clicking it. For its **Entity**, click the **Select** button and choose the entity you created above. Then click **OK** to close the dialog box. - 3. Right-click the **Create Entity** activity, then click **Set $NewEntity as Return Value**. + 1. Add a new **Create object** action on your microflow. + 2. Open the new **Create object** action's properties by double-clicking it. For its **Entity**, click the **Select** button and choose the entity you created above. Then click **OK** to close the dialog box. + 3. Right-click the **Create Entity** activity, then click **Set $NewEntity as Return Value**. 5. Go back to the home page to finish the test page. - 1. Open the **Add Widget** menu, and then add a **TextBox** widget inside the data view. - 2. Open the Textbox's properties and select the **Datasource Attribute (path)** string attribute you created above. - 3. Click the **OK** button to close the dialog box. + 1. Open the **Add Widget** menu, and then add a **TextBox** widget inside the data view. + 2. Open the Textbox's properties and select the **Datasource Attribute (path)** string attribute you created above. + 3. Click the **OK** button to close the dialog box. 6. The end result should look like this: - {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/createtestobject.png" class="no-border" >}} + {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/createtestobject.png" class="no-border" >}} - {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/microflowcreateentity.png" class="no-border" >}} + {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/microflowcreateentity.png" class="no-border" >}} ### Widget Scaffolding The Pluggable Widget Generator is the quickest way to start developing a widget. It creates a widget’s recommended folder structure and files. -Using a terminal or command line, navigate to your new Mendix app's folder, create a new folder named *CustomWidgets*, and start the generator using: +Using a terminal or command line, navigate to your new Mendix app's folder, create a new folder named _CustomWidgets_, and start the generator using: ```powershell mkdir CustomWidgets @@ -70,23 +70,23 @@ npx @mendix/generator-widget@10.24-latest TextBox The generator will ask you a few questions during setup. Answer the questions by specifying the following information: -* Widget name: *{Your widget name}* -* Widget Description: *{Your widget description}* -* Organization Name: *{Your organization name}* -* Copyright: *{Your copyright date}* -* License: *{Your license}* -* Initial Version:*{Your initial version number}* -* Author: *{Your author name}* -* Mendix App path: *../../* -* Programming language: **TypeScript** -* Which type of components do you want to use? **Class Components** -* Widget type: **For web and hybrid mobile apps** -* Widget template: **Empty widget (recommended for more experienced developers)** -* Unit tests: **No** -* End-to-end tests: **No** +- Widget name: _{Your widget name}_ +- Widget Description: _{Your widget description}_ +- Organization Name: _{Your organization name}_ +- Copyright: _{Your copyright date}_ +- License: _{Your license}_ +- Initial Version:_{Your initial version number}_ +- Author: _{Your author name}_ +- Mendix App path: _../../_ +- Programming language: **TypeScript** +- Which type of components do you want to use? **Class Components** +- Widget type: **For web and hybrid mobile apps** +- Widget template: **Empty widget (recommended for more experienced developers)** +- Unit tests: **No** +- End-to-end tests: **No** {{% alert color="warning" %}} -Currently, **Organization Name** cannot include a dash "-" character. Dashes in the organization name will result in an error when running your app, asking to check if the widgets "were generated with the latest version of the pluggable-widgets-tools and are ES6 modules." +Currently, **Organization Name** cannot include a dash "-" character. Dashes in the organization name will result in an error when running your app, asking to check if the widgets "were generated with the latest version of the pluggable-widgets-tools and are ES6 modules." To fix this in an existing widget, modify the `packagePath` property of its **package.json** file and rebuild the widget. {{% /alert %}} @@ -97,131 +97,130 @@ Note that whenever it is required to reinstall NPM package dependencies inside t ### Adding the Attribute -Open the *(YourMendixApp)/CustomWidgets/TextBox* folder in your IDE of choice (any IDE is fine if it can execute commands). From now on, all file references will be relative to this path. To set up your new widget, first you must use an attribute of the context object and display that attribute in an input field: - -1. To prevent future errors, remove the file *src/components/HelloWorldSample.tsx*. Errors in *TextBox.editorPreview.tsx* will be dealt with in step 6 below. -2. In *src/TextBox.xml*, the generator creates a sample property `sampleText`. Remove this property and add the new property `Text attribute`: - - ```xml - - - Text Box - Edit text input - - - - - Attribute (path) - - - - - - - - - ``` - - Explaining the code: - - * *TextBox.xml* is the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition) used in Studio Pro which reads the widget's capabilities - * The property `pluginWidget=true` will make the widget work with the new widget API - * The property `needsEntityContext=true` is set up to allow the attribute to be taken from context - * The property of the [type attribute](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/#attribute) only allows the selection of string attributes from the domain model +Open the _(YourMendixApp)/CustomWidgets/TextBox_ folder in your IDE of choice (any IDE is fine if it can execute commands). From now on, all file references will be relative to this path. To set up your new widget, first you must use an attribute of the context object and display that attribute in an input field: + +1. To prevent future errors, remove the file _src/components/HelloWorldSample.tsx_. Errors in _TextBox.editorPreview.tsx_ will be dealt with in step 6 below. +2. In _src/TextBox.xml_, the generator creates a sample property `sampleText`. Remove this property and add the new property `Text attribute`: + + ```xml + + + Text Box + Edit text input + + + + + Attribute (path) + + + + + + + + + ``` + + Explaining the code: + + - _TextBox.xml_ is the [widget definition file](/apidocs-mxsdk/apidocs/pluggable-widgets/#widget-definition) used in Studio Pro which reads the widget's capabilities + - The property `pluginWidget=true` will make the widget work with the new widget API + - The property `needsEntityContext=true` is set up to allow the attribute to be taken from context + - The property of the [type attribute](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/#attribute) only allows the selection of string attributes from the domain model 3. The typescript typing based on the XML will be generated automatically. Start the development process with the following command: `npm start`. - This process will bundle the widget and generate the properties into *typings/TextBoxProperties.d.ts*. + This process will bundle the widget and generate the properties into _typings/TextBoxProperties.d.ts_. - {{% alert color="info" %}}The console will show the error below, as we did not implement our `TextInput` component. We will solve the error in the [Labeling the Input](#label-input) of this how-to.{{% /alert %}} + {{% alert color="info" %}}The console will show the error below, as we did not implement our `TextInput` component. We will solve the error in the [Labeling the Input](#label-input) of this how-to.{{% /alert %}} - ```shell - ERROR in ./src/TextBox.tsx - Module not found: Error: Can't resolve './components/HelloWorldSample' in 'C:\Users\john.doe\textboxtest-main\CustomWidgets\TextBox\src' - @ ./src/TextBox.tsx 14:0-28:2 - ``` + ```shell + ERROR in ./src/TextBox.tsx + Module not found: Error: Can't resolve './components/HelloWorldSample' in 'C:\Users\john.doe\textboxtest-main\CustomWidgets\TextBox\src' + @ ./src/TextBox.tsx 14:0-28:2 + ``` -4. Create a new file, *components/TextInput.tsx*. This will be the display component. A display component does not interact with APIs and can be re-used in any React application. Paste the following code into *TextInput.tsx*: +4. Create a new file, _components/TextInput.tsx_. This will be the display component. A display component does not interact with APIs and can be re-used in any React application. Paste the following code into _TextInput.tsx_: - ```tsx - import { Component, ReactNode, createElement } from "react"; - - export interface InputProps { - value: string; - } - - export class TextInput extends Component { + ```tsx + import { Component, ReactNode, createElement } from "react"; - render(): ReactNode { - return ; - } - } - ``` + export interface InputProps { + value: string; + } - Explaining the code: + export class TextInput extends Component { + render(): ReactNode { + return ; + } + } + ``` - * The interface defines the properties of the React components—the value is passed to the component and it will render an HTML input element with the given value - * The component is a class extending `Component` and should be exported to be used in other components - * The render method is the only required function in a component, and it will return the expected DOM for the browser (for more information, see React’s [component documentation](https://reactjs.org/docs/react-component.html)) -5. The container component *TextBox.tsx* receives the properties in the runtime, and forwards the data to the display component. The container works like glue between the Mendix application and the display component. In the *TextBox.tsx* overwrite the render function until they look like this: + Explaining the code: - ```tsx - import { Component, ReactNode, createElement } from "react"; - import { TextBoxContainerProps } from "../typings/TextBoxProps"; - import { TextInput } from "./components/TextInput"; + - The interface defines the properties of the React components—the value is passed to the component and it will render an HTML input element with the given value + - The component is a class extending `Component` and should be exported to be used in other components + - The render method is the only required function in a component, and it will return the expected DOM for the browser (for more information, see React’s [component documentation](https://reactjs.org/docs/react-component.html)) - import "./ui/TextBox.css"; +5. The container component _TextBox.tsx_ receives the properties in the runtime, and forwards the data to the display component. The container works like glue between the Mendix application and the display component. In the _TextBox.tsx_ overwrite the render function until they look like this: - export class TextBox extends Component { + ```tsx + import { Component, ReactNode, createElement } from "react"; + import { TextBoxContainerProps } from "../typings/TextBoxProps"; + import { TextInput } from "./components/TextInput"; - render(): ReactNode { - const value = this.props.textAttribute.value || ""; - return ; - } - } - ``` + import "./ui/TextBox.css"; - Be sure all the imports are included before moving on from this step. + export class TextBox extends Component { + render(): ReactNode { + const value = this.props.textAttribute.value || ""; + return ; + } + } + ``` - Explaining the code: + Be sure all the imports are included before moving on from this step. - * The `textAttribute` is an object that will automatically have the actual data stored in the attribute—when the data is changed, it will cause an update of the component, and the new data will be displayed in the input + Explaining the code: -6. Alter *Textbox.editorPreview.tsx* by adding the `TextInput` import to *Textbox.editorPreview.tsx*: + - The `textAttribute` is an object that will automatically have the actual data stored in the attribute—when the data is changed, it will cause an update of the component, and the new data will be displayed in the input - ```tsx - import { TextInput } from "./components/TextInput"; - ``` +6. Alter _Textbox.editorPreview.tsx_ by adding the `TextInput` import to _Textbox.editorPreview.tsx_: - Then, override the class lines in *Textbox.editorPreview.tsx* until they look like this: + ```tsx + import { TextInput } from "./components/TextInput"; + ``` - ```tsx - export class preview extends Component { + Then, override the class lines in _Textbox.editorPreview.tsx_ until they look like this: - render(): ReactNode { - return ; - } - } - ``` + ```tsx + export class preview extends Component { + render(): ReactNode { + return ; + } + } + ``` - Before moving on from this step, you should remove the import lines concerning the **Hello World** sample text from *TextBox.editorPreview.tsx* and *TextBox.tsx*, as these lines are no longer in use. + Before moving on from this step, you should remove the import lines concerning the **Hello World** sample text from _TextBox.editorPreview.tsx_ and _TextBox.tsx_, as these lines are no longer in use. 7. Add a test widget to the project home page:
- 1. To find your widget for the first time you need to refresh from the files system. Use F4 or select **App** > **Synchronize Project Directory** from the Studio Pro menu.
- 2. Navigate to **Home > Add widget** in the editor menu.
- 3. Select the newly-created **TextBox** widget at the bottom of the list.
- 4. Place the widget below the standard text widget.
- 5. Open the widget properties. In the **Data source** tab **select** the **Text attribute** from the attribute created in [Creating a Test App](#creating-a-test-project) above. - The end result will be similar to the screenshot below: + 1. To find your widget for the first time you need to refresh from the files system. Use F4 or select **App** > **Synchronize Project Directory** from the Studio Pro menu.
+ 2. Navigate to **Home > Add widget** in the editor menu.
+ 3. Select the newly-created **TextBox** widget at the bottom of the list.
+ 4. Place the widget below the standard text widget.
+ 5. Open the widget properties. In the **Data source** tab **select** the **Text attribute** from the attribute created in [Creating a Test App](#creating-a-test-project) above. - {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/updateallwidgets.png" class="no-border" >}} + The end result will be similar to the screenshot below: - {{% alert color="info" %}}The widgets in Studio Pro are not automatically updated. First, run the `npm start` command again. To refresh your widgets, press F4 or select **App** > **Synchronize App Directory** from the Studio Pro menu to reload the widgets from the file system. Finally, right-click the widget and select Update all widgets to update the newly-changed properties in the widget.{{% /alert %}} + {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/updateallwidgets.png" class="no-border" >}} -8. When running the app, the new widget is already functional. The first text box is a standard Text box widget and the second is your pluggable web widget. When data is changed in the first input and the cursor is moved to the next widget, the data of your widget is also updated: + {{% alert color="info" %}}The widgets in Studio Pro are not automatically updated. First, run the `npm start` command again. To refresh your widgets, press F4 or select **App** > **Synchronize App Directory** from the Studio Pro menu to reload the widgets from the file system. Finally, right-click the widget and select Update all widgets to update the newly-changed properties in the widget.{{% /alert %}} - {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/twotextwidgets.png" alt="two text widgets" class="no-border" >}} +8. When running the app, the new widget is already functional. The first text box is a standard Text box widget and the second is your pluggable web widget. When data is changed in the first input and the cursor is moved to the next widget, the data of your widget is also updated: + + {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/twotextwidgets.png" alt="two text widgets" class="no-border" >}} ### Adding Style @@ -229,168 +228,192 @@ The input works, but the styling could be improved. In the next code snippets, y {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/customwidgetedit.png" alt="custom widget" class="no-border" >}} -1. In *TextBox.tsx*, pass the properties from the runtime to the `TextInput` component: - - ```tsx - export class TextBox extends Component { - - render(): ReactNode { - const value = this.props.textAttribute.value || ""; - return ; - } - } - ``` - -2. In *components/TextInput.tsx*, add the attributes to the interface and render them in the input: - - ```tsx - import { CSSProperties, Component, ReactNode, createElement } from "react"; - import classNames from "classnames"; - - export interface InputProps { - value: string; - className?: string; - index?: number; - style?: CSSProperties; - tabIndex?: number; - } - export class TextInput extends Component { - - render(): ReactNode { - const className = classNames("form-control", this.props.className); - return ; - } - } - ``` - - Explaining the code: - * The style property is a React style object which can be passed to an HTML element directly - * `classNames` is an external utility function which dynamically creates and combines class names; it must be imported before it can be used (for the full API, see the property’s [documentation](https://github.com/JedWatson/classnames)) - * Each property with a question mark is optional +1. In _TextBox.tsx_, pass the properties from the runtime to the `TextInput` component: + + ```tsx + export class TextBox extends Component { + render(): ReactNode { + const value = this.props.textAttribute.value || ""; + return ( + + ); + } + } + ``` + +2. In _components/TextInput.tsx_, add the attributes to the interface and render them in the input: + + ```tsx + import { CSSProperties, Component, ReactNode, createElement } from "react"; + import classNames from "classnames"; + + export interface InputProps { + value: string; + className?: string; + index?: number; + style?: CSSProperties; + tabIndex?: number; + } + export class TextInput extends Component { + render(): ReactNode { + const className = classNames("form-control", this.props.className); + return ( + + ); + } + } + ``` + + Explaining the code: + + - The style property is a React style object which can be passed to an HTML element directly + - `classNames` is an external utility function which dynamically creates and combines class names; it must be imported before it can be used (for the full API, see the property’s [documentation](https://github.com/JedWatson/classnames)) + - Each property with a question mark is optional + 3. Your efforts will result in a well-styled input widget: - {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/styledinputwidgets.png" alt="styled widgets" class="no-border" >}} + {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/styledinputwidgets.png" alt="styled widgets" class="no-border" >}} ### Labeling the Input{#label-input} While the Mendix input elements come with labels, you will need to add one to TextBox manually. With the new API it is easy to [add a label](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/#label) to any widget. -1. In the *TextBox.xml* file, add an element `` with a child element `` above the existing `` element: +1. In the _TextBox.xml_ file, add an element `` with a child element `` above the existing `` element: - ```xml - - - - ``` + ```xml + + + + ``` - This will add the **Show label** radio buttons in the widget properties tab **Label** (after synchronizing the app directory and updating the widget). When **Show label** is set to true, it will automatically render the label for you in the page editor and the browser: + This will add the **Show label** radio buttons in the widget properties tab **Label** (after synchronizing the app directory and updating the widget). When **Show label** is set to true, it will automatically render the label for you in the page editor and the browser: - {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/edittextboxtwo.png" alt="edit text box two" class="no-border" >}} + {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/edittextboxtwo.png" alt="edit text box two" class="no-border" >}} -2. In *TextBox.tsx* remove the usages of `{this.props.style}` and `{this.props.class}`, because labeled widgets do not have them and the styling is applied to the surrounding FormGroup. +2. In _TextBox.tsx_ remove the usages of `{this.props.style}` and `{this.props.class}`, because labeled widgets do not have them and the styling is applied to the surrounding FormGroup. 3. Preview the label in the page editor: - {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/editdataviewone.png" alt="edit data view one" class="no-border" >}} + {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/editdataviewone.png" alt="edit data view one" class="no-border" >}} 4. This will result in a label above or next to the input depending on the available space, data view `Form orientation`, and the `Label width (weight)`: - {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/inputwidgetswithlabel.png" alt="input elements with label" class="no-border" >}} + {{< figure src="/attachments/howto9/extensibility/pluggable-widgets/create-a-pluggable-widget-one/inputwidgetswithlabel.png" alt="input elements with label" class="no-border" >}} ### Handling Updates -The value from the attribute can be displayed and updated using the other input, however you cannot change the value directly from within your widget. You can close the loop by following these steps. - -1. In *TextBox.tsx*, create a function that will update the attribute and pass it to the `TextInput` component: - - ```tsx - export class TextBox extends Component { - - private readonly onUpdateHandle = this.onUpdate.bind(this); - - render(): ReactNode { - const value = this.props.textAttribute.value || ""; - return ; - } - - private onUpdate(value: string): void { - this.props.textAttribute.setValue(value); - } - } - ``` - - Explaining the code: - - * JavaScript can pass functions from one object to another—this way, the Mendix API stays in the container `TextBox component` and provides a function to the display component to pass updates back to the attribute - * When a function is passed to another component, the function might have a scoping issue—this can be solved by binding the context `this` to the function before passing it to the display component (for more information, see this [freeCodeCamp blog post](https://medium.freecodecamp.org/this-is-why-we-need-to-bind-event-handlers-in-class-components-in-react-f7ea1a6f93eb)) - -2. In *components/TextInput.tsx*, handle the change events of the input and pass the new value to the `onUpdate` function of the container component: - - ```tsx - import { CSSProperties, ChangeEvent, Component, ReactNode, createElement } from "react"; - import classNames from "classnames"; - - export interface InputProps { - value: string; - className?: string; - index?: number; - style?: CSSProperties; - tabIndex?: number; - onUpdate?: (value: string) => void; - } - - export class TextInput extends Component { - - private readonly handleChange = this.onChange.bind(this); - - render(): ReactNode { - const className = classNames("form-control", this.props.className); - return ; - } - - private onChange(event: ChangeEvent): void { - if (this.props.onUpdate) { - this.props.onUpdate(event.target.value); - } - } - } - ``` - - Explaining the code: - - * The input's `value` is set by the `this.props.value`, and this property is not changed directly; the update function will use the `setValue` to trigger a re-render with the updated property - * There are two ways of handling input changes in React: [controlled components](https://reactjs.org/docs/forms.html#controlled-components) or [uncontrolled components](https://reactjs.org/docs/uncontrolled-components.html) - * The `onUpdate` function is optional and it should be checked for availability before executing it - * The custom widget TextBox will still not pass text to the Text box widget after this step—it will gain this functionality in [Build a Text Box Pluggable Widget: Part 2 (Advanced)](/howto9/extensibility/create-a-pluggable-widget-two/). +The value from the attribute can be displayed and updated using the other input, however you cannot change the value directly from within your widget. You can close the loop by following these steps. + +1. In _TextBox.tsx_, create a function that will update the attribute and pass it to the `TextInput` component: + + ```tsx + export class TextBox extends Component { + private readonly onUpdateHandle = this.onUpdate.bind(this); + + render(): ReactNode { + const value = this.props.textAttribute.value || ""; + return ( + + ); + } + + private onUpdate(value: string): void { + this.props.textAttribute.setValue(value); + } + } + ``` + + Explaining the code: + + - JavaScript can pass functions from one object to another—this way, the Mendix API stays in the container `TextBox component` and provides a function to the display component to pass updates back to the attribute + - When a function is passed to another component, the function might have a scoping issue—this can be solved by binding the context `this` to the function before passing it to the display component (for more information, see this [freeCodeCamp blog post](https://medium.freecodecamp.org/this-is-why-we-need-to-bind-event-handlers-in-class-components-in-react-f7ea1a6f93eb)) + +2. In _components/TextInput.tsx_, handle the change events of the input and pass the new value to the `onUpdate` function of the container component: + + ```tsx + import { + CSSProperties, + ChangeEvent, + Component, + ReactNode, + createElement, + } from "react"; + import classNames from "classnames"; + + export interface InputProps { + value: string; + className?: string; + index?: number; + style?: CSSProperties; + tabIndex?: number; + onUpdate?: (value: string) => void; + } + + export class TextInput extends Component { + private readonly handleChange = this.onChange.bind(this); + + render(): ReactNode { + const className = classNames("form-control", this.props.className); + return ( + + ); + } + + private onChange(event: ChangeEvent): void { + if (this.props.onUpdate) { + this.props.onUpdate(event.target.value); + } + } + } + ``` + + Explaining the code: + + - The input's `value` is set by the `this.props.value`, and this property is not changed directly; the update function will use the `setValue` to trigger a re-render with the updated property + - There are two ways of handling input changes in React: [controlled components](https://reactjs.org/docs/forms.html#controlled-components) or [uncontrolled components](https://reactjs.org/docs/uncontrolled-components.html) + - The `onUpdate` function is optional and it should be checked for availability before executing it + - The custom widget TextBox will still not pass text to the Text box widget after this step—it will gain this functionality in [Build a Text Box Pluggable Widget: Part 2 (Advanced)](/howto9/extensibility/create-a-pluggable-widget-two/). + +## Building & Releasing Your Widget + +Development build tested successfully. You can now create a release build to bundle with your module or publish the widget to the Marketplace. + +{{% alert color="info" %}}**Dev Build vs. Release Build**: Release builds are minified, optimized, and stripped of heavy console logging/source maps for optimal runtime performance in production. {{% /alert %}} + +1. Run `npm run prerelease` to scan for any linting errors. +1. Fix any errors that are found. +1. Run `npm run release` to create a release build. +1. Locate the generated `.mpk` file at `{widgetName}/dist/{version}/{widgetName}.mpk`. +1. Use this `.mpk` file for the Marketplace release or include it in your module for production. Congratulations, you have now made a fully functional input widget! -Continue with the next tutorial to learn how to add validation feedback, custom validations, and an on-change event activity. You will also learn how to handle a read-only state, improve web accessibility, and make a Mendix Studio Pro preview. +Continue with the next tutorial to learn how to add validation feedback, custom validations, and an on-change event activity. You will also learn how to handle a read-only state, improve web accessibility, and make a Studio Pro preview. ## Read More -* [Build a Pluggable Web Widget: Part 2 (Advanced)](/howto9/extensibility/create-a-pluggable-widget-two/) -* [Pluggable Widgets API](/apidocs-mxsdk/apidocs/pluggable-widgets/) -* [Client APIs Available to Pluggable Widgets](/apidocs-mxsdk/apidocs/pluggable-widgets-client-apis/) -* [Pluggable Widget Property Types](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/) +- [Build a Pluggable Web Widget: Part 2 (Advanced)](/howto9/extensibility/create-a-pluggable-widget-two/) +- [Pluggable Widgets API](/apidocs-mxsdk/apidocs/pluggable-widgets/) +- [Client APIs Available to Pluggable Widgets](/apidocs-mxsdk/apidocs/pluggable-widgets-client-apis/) +- [Pluggable Widget Property Types](/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/)