I am creating a custom code control using the react framework. It is a text input that should return a text as output after the user entered text in the control.
Now I have the following manifest (shorted):
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<property name="Text"
display-name-key="Text"
description-key="Text of the text input"
of-type="SingleLine.Text"
usage="output"
required="true" />
<resources>
...
</resources>
</control>
</manifest>
Then I have the following index.ts:
imports ...
export class CustomTextInput implements ComponentFramework.ReactControl<IInputs, IOutputs> {
private notifyOutputChanged: () => void;
private _text: string | undefined;
private _props: ITextInputProps;
constructor() { }
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
void {
this.notifyOutputChanged = notifyOutputChanged;
this._props = {
// ... some properties
default: context.parameters.Default.raw!,
maxLength: context.parameters.MaxLength.raw!,
onTextChange: this.onTextChange,
}
console.log("init calls this.onTextChange: " + JSON.stringify(this._props.default))
this.onTextChange(this._props.default);
}
public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement {
console.log("update view");
if (context.updatedProperties.indexOf('Default') > -1 || this._props.default !== context.parameters.Default.raw!) {
this._props.default = context.parameters.Default.raw!;
}
if (context.updatedProperties.indexOf('MaxLength') > -1 || this._props.maxLength !== context.parameters.MaxLength.raw!) {
const newMaxLength = this._props.maxLength = context.parameters.MaxLength.raw!;
if (this._text && this._text.length > newMaxLength) {
this._text = this._text.substring(0, newMaxLength) || "";
}
}
return React.createElement(
TextInput, this._props
);
}
public getOutputs(): IOutputs {
console.log("getOutputs, this._text: " + JSON.stringify(this._text));
return { Text: this._text } as IOutputs;
}
onTextChange = (newValue: string | undefined): void => {
console.log("onTextChange");
console.log("newValue: " + JSON.stringify(newValue));
if (newValue && newValue.trim().length !== 0) {
this._text = newValue;
} else {
this._text = undefined;
}
console.log(this._text);
this.notifyOutputChanged();
};
My React Component file TextInput.tsx looks like this:
imports ...
export interface ITextInputProps {
default?: string;
maxLength?: number;
onTextChange: (newText: string) => void
}
export interface ITextInputState {
text: string;
}
export class TextInput extends React.Component<ITextInputProps, ITextInputState> {
constructor(props: ITextInputProps) {
super(props);
this.state = {
text: props.default || ""
};
}
public render(): React.ReactNode {
return (
<ThemeProvider theme={this.props.theme}>
<TextInputContainer>
<LabelsContainer>
<Label>{this.props.label}</Label>
{this.props.hasClearButton && <ClearLabel onClick={this.handleOnClear}>Clear</ClearLabel>}
</LabelsContainer>
<Input placeholder={this.props.hintText} value={this.state.text.trim() !== "" ? this.state.text : ""} spellCheck={this.props.shouldSpellCheck}
onChange={this.handleInputChange} type={this.props.format === "Text" ? "text" : "number"}
maxLength={this.props.maxLength} />
</TextInputContainer>
</ThemeProvider>
)
}
componentDidUpdate(prevProps: ITextInputProps) {
if (prevProps.maxLength !== this.props.maxLength || this.props.maxLength && this.props.maxLength < this.state.text.length) {
const { maxLength, onTextChange } = this.props;
let { text } = this.state;
if (maxLength === undefined || maxLength === null) {
text = "";
} else if (maxLength && text.length > maxLength) {
text = text.substring(0, maxLength);
}
this.setState({ text }, () => onTextChange(text));
}
}
handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
console.log("handleInputChange: newValue: " + JSON.stringify(newValue));
this.setState({
text: newValue
});
this.props.onTextChange(newValue);
}
handleOnClear = () => {
console.log("handleOnClear")
console.log("this.state before: "+ JSON.stringify(this.state));
this.setState({
text: ""
}, () => console.log("this.state after: " + JSON.stringify(this.state)));
this.props.onTextChange("");
}
}
When I add the code control to an app and I add a normal powerapps textlabel it looks like this:
With TextLabel.Text being: "CustomTextInput1.Text: " & CustomTextInput1.Text
Now when I type text in the control it updates the label as well. Only if I try to delete all characters, it will stick with the old value unless I save and refresh the page.
Same thing when I hit the clear button: It deletes the content in the control, but doesn't update the value of CustomTextInput1.Text in the TextLabel. It looks like this (note the logs on the right).
Does anyone know why it would not update the TextProperty although getOutputs is called with the undefined value?
Also note that in the test harness the output value also shows the value of undefined after activating the clear or deleting the letters. And if I delete the letters one by one in the power app it updates the label each time except for the last letter (control input is empty but CustomTextInput1.Text shows the last letter).
I was able to solve my problem by returning null instead of undefined in handleInputChange. I think this causes the output property to be read as or converted to "Blank()" in Power Apps.
So my code now looks like this:
index.ts
onTextChange = (newValue: string | undefined): void => {
// ...
if (newValue && newValue.trim().length !== 0) {
this._text = newValue;
} else {
this._text = null;
}
// ...
this.notifyOutputChanged();
};
TextInput.tsx (didn't really change anything except I am using the handleInputChange method now)
<ClearLabel onClick={() => isEditMode ? this.handleInputChange("") : {}}
className={displayMode?.toLowerCase()}>Clear</ClearLabel>
Interestingly enough it seems I get the following errors in the testharness (but the code runs as it should).
But in my app I don't get these errors so maybe that's something about updating values in the test harness.
Hello @DianaBirkelbach ! Thank you for replying!
Sorry if my question was confusing.
What does the control do?
The control is a custom text input. The app user clicks in it, types something and can provide that way a string input (not via a property in the property panel, but by using the control).
That input value should be reusable in the app. So I need to get its value as output from the control.
The property I am using for this is called "Text".
So CustomTextInput1.Text should return the value that was provided by the user in the control.
What does work?
Now when I type for example "Apple" and I delete letters one by one (or a chunk of them) the output value (CustomTextInput1.Text) is visibly updated. So Text is updated on text changes (when they are not empty string).
What is the problem?
BUT, when I remove all letters,(or use the "onClear" function) that is, when the control input is completely empty (empty string), it does NOT update the value in CustomTextInput1.Text to be Blank().
I am using state.text for containing and displaying the user input and it does work as expected (state.text is "" when deleting all the letters, also the control shows that it is empty). BUT the output value of the property seems to not be taken correctly when it's an empty string (undefined) value.
The thing is, I do call the props.onTextChange() with the value of "" (empty string). it propagates through to the getOutputs method and actually does return "undefined" as output value (as the logs show). But still the CustomTextInput1.Text variable does not show the updated value.
TDLR;
getOutputs returns {Text: undefined} but the corresponding output variable CustomTextInput1.Text in the App still contains the last value that is not undefined unless I save and refresh the page.
Hope this helps to get a better understanding. I have the feeling state.text is not the problem, because getOutputs does return undefined.
Also notice that in the logs updateView is called, it shows state.text before the update and state.text after the update and its value shows empty string.
Hi @Anonymous ,
It's a little hard to follow... not sure if I get it right, since I don't really understand what the PCF should do.
But usually for this kind of issues it's important to understand how the updateView works.
When a value changes inside the PCF, and you call the notifyOutputChanges(), the getOutputs gets called by the platform. Then the Platform Runtime evaluates the value, and then calls the updateView again, providing the new value. So after that the updateView will be called, and your TextInput component will rerender.
I guess the problems are cased by the TextInput component using an internal state for the "text", which doesn't get updated after the updateView.
Have a look to this PCF-Tutorial from the docs, and the explanation about "Controlled React component": https://learn.microsoft.com/en-us/power-apps/developer/component-framework/tutorial-create-model-driven-field-component?tabs=before&WT.mc_id=BA-MVP-5004107#controlled-react-component
Hope it helps!
WarrenBelz
87
Most Valuable Professional
mmbr1606
71
Super User 2025 Season 1
Michael E. Gernaey
65
Super User 2025 Season 1