Development

PCF: Adding external handlers to controls

After I published the initial version of my PCF Control Toolkit I was asked how I was able to use external JS files possible in one of the controls. In this post, I decided to provide follow along steps to enable the same possibility in controls you develop and demonstrate the approach on the example control.

In order to configure the control, I added 2 additional inputs to pass the reference to the JavaScript file and the name of the function to be executed:

<property name="WebResource" display-name-key="JS File" description-key="JS File that contains the code you want to run" of-type="SingleLine.Text" usage="input" required="true" />
<property name="MethodName" display-name-key="Method Name" description-key="Name of the function you want to run on click of the button" of-type="SingleLine.Text" usage="input" required="true" />

In order to load the content from external file, I add the following code to init method of the control:

const scriptUrl = context.parameters.WebResource.raw!;

let scriptNode = document.createElement("script");
scriptNode.setAttribute("type", "text/javascript");
scriptNode.setAttribute("src", scriptUrl);
scriptNode.onload = () => {
	//Add code that will be executed once the external code is loaded
};
scriptNode.onerror = (error) => {
	//Add code that will handle an error
};

document.head.appendChild(scriptNode);

Once this step is completed and code becomes available for executing, the next step to consider is how the external function can be dynamically called from the code of the PCF control. In this case I use the “new Function” to instantiate the function and call it afterwards. Here is an example of the code that can be used:

const callBackName = context.parameters.MethodName.raw!;

const dynamicFunctionCall = new Function(callBackName + "()");

try {
	dynamicFunctionCall();
} catch (error) {
	console.error(error);
}

As an example, I decided to create a PCF control that will show the button instead of the field and allows custom JavaScript to be called when the button is clicked. Here is the code of the PCF control:

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { CustomButton } from './CustomButton';

export class PCFCustomHandler implements ComponentFramework.StandardControl<IInputs, IOutputs> {

	private container: HTMLDivElement;

	constructor() {

	}

	public init(context: ComponentFramework.Context<IInputs>,
		notifyOutputChanged: () => void,
		state: ComponentFramework.Dictionary,
		container: HTMLDivElement) {
		this.container = container;

		const scriptUrl = context.parameters.WebResource.raw!;
		const callBackName = context.parameters.MethodName.raw!;

		let scriptNode = document.createElement("script");
		scriptNode.setAttribute("type", "text/javascript");
		scriptNode.setAttribute("src", scriptUrl);
		scriptNode.onload = () => {
			//Add code that will be executed once the external code is loaded
			const btnControl = React.createElement(CustomButton, {
				label: context.parameters.ButtonLabel.raw!,
				clickHandler: () => {
					const dynamicFunctionCall = new Function(callBackName + "()");
	
					try {
						dynamicFunctionCall();
					} catch (error) {
						console.error(error);
					}
				}
			});

			ReactDOM.render(btnControl, this.container);
		};
		scriptNode.onerror = (error) => {
			container.innerText = "Can't load referenced script";
		};

		document.head.appendChild(scriptNode);
	}

	public updateView(context: ComponentFramework.Context<IInputs>): void {
	}

	public getOutputs(): IOutputs {
		return {};
	}

	public destroy(): void {
		ReactDOM.unmountComponentAtNode(this.container);
	}
}

Here is the code of the React component:

import * as React from 'react';
import { PrimaryButton } from 'office-ui-fabric-react/lib/components/Button';
import { Stack } from "office-ui-fabric-react/lib/components/Stack";

export interface ICustomButtonProps{
	label: string;
	clickHandler: () => void;
}

export class CustomButton extends React.Component<ICustomButtonProps, {}> {
	render(){
		return (<>
			<Stack>
				<PrimaryButton 
					text={this.props.label}
					onClick={this.props.clickHandler}
				/>
			</Stack>
			</>);
	}	
}

The code called on click is really simple:

function onCustomButtonClick(){
	alert("Called from custom JS");
}

Once PCF control is built and deployed to CDS here is how it can be configured:

Any text field can be used as a placeholder for the control, enter the reference pointing to the JS File to the “JS File” input, name of the function to be called on click to “Method Name” and the label to be shown on the button to “Button Label” input.

Here is the demonstration on how it works:

Source code of this PCF is available by the following URL – https://github.com/AndrewButenko/CustomButtonControl

8 Comments

  1. This is great!

    Is it possible to get the execution context sent to the JS file to use the Client API?

    Example:
    The PCF component creates a new record and relates it to the record on the form.

    Currently, there is no way for the PCF component to update the lookup field on the form so the user can see the new record without refreshing (and also for other business rules, etc. to take effect).

    Is there some way to send the execution context from the PCF control to the JS file, and then use the JS file to perform Client API actions fromContext.getAttribute(arg).setValue(arg)?

    1. Hello,
      There is no good and fully supported way of doing it. What you can alternatively do is to add your lookup bound property to your PCF control and even it’s not supported in the tooling you can change it – https://www.youtube.com/watch?v=NEQtbfQrAMU and set the bound property to the value you want – the trick here – add this field to the form and hide it and set it from the code of the PCF control.
      Feel free to update the thread if you have issues with it.
      Thanks,
      Andrew

      1. Thanks, Andrew. I will look into that.

        I did also experiment with the JS file for the past hour or so.

        It does appear that you can get the Xrm.Page still from the JS file, even when it’s not loaded on the form or through an event handler on the form.

        Here is an example of the JS file. Everything else on the PCF control is exactly like in your example.

        function onCustomButtonClick(){
        let nameValue = Xrm.Page.getAttribute(“new_name”).getValue();
        console.log(nameValue)

        }

        In this example, I’m just retrieving the name field of an entity but I could just as easily substitute it with a lookup field and also setting the value, I think.

        But using Xrm.Page probably isn’t a good solution, as that feature is deprecated I understand. Hopefully Microsoft will either release support for binding Lookups to PCF components or a new object that can provide similar functionality as Xrm.Page.

        1. Yeah, Xrm.Page is deprecated and eventually goes away. On the other hand I know that lookup attributes will be added to the supported list soon and my hacky way is not that hacky. I tried it and it worked with no issues in my environment. So you can play around it as well.

      2. I created a new project today to test out adding Lookup.Simple per the video.

        When I checked the ManifestSchema.json file, Lookup.Simple was already there as well as the following:
        Object
        File
        Lookup.Simple
        Lookup.Customer
        Lookup.Owner
        Lookup.PartyList
        Lookup.Regarding

        Unfortunately, when I attempted to alter the ControlManifest.Input.xml file so that the property would be of-type Lookup.Simple, the build still failed stating the “Lookup data types are not supported.”

        Perhaps they have changed the way that the build validates the schema manifest in the latest release. I’m on version 1.4.4+g0574e87

        1. It seems you may need to also update featureflags.json to enable Lookup.Simple.

          node_modules\pcf-scripts\featureflags.json

          set “pcfAllowLookup” value to “On”

          This allowed the build to work when the bound property was set to “Lookup.Simple”

        2. Thanks for heads up. Did not know that update is available. I updated to the latest version and I was able to reproduce the behavior that you described. In order to resolve your issue open node_modules\pcf-scripts\featureflags.json file and look for pcfAllowLookup setting – change it to “on” and save it. After this build should work with no issues.

          Andrew

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.