This post is part of a series on an ongoing project. The user can upload a file in one of the file input controls, but there are two more file input controls that need to work. Today’s entry is going to focus on reusing the existing code in the other two components.
InputFileController
To share the code between all three components, I’ll create a new class, InputFileController. Then I’ll move all my functions for onFileAdded, onUploadSucceeded, and onUploadFailed will move into the new class. But first, let’s take a look at where the controllers are created.
App.js
I create them in the App() constructor. Each one takes a reference to the App class, and the name of the corresponding InputFileState field.
class App extends React.Component {
constructor(props) {
super(props);
this.projectSheetController = new InputFileController(this, "projectSheet");
this.employeeCostController = new InputFileController(this, "employeeCost");
this.timeReportController = new InputFileController(this, "timeReport");
this.state = {
projectSheet: new InputFileState(),
employeeCost: new InputFileState(),
timeReport: new InputFileState(),
}
}
This also simplifies the App’s render() function. Instead of having to pass two functions to the <InputFile> component, I pass a single reference to the controller object.
render() {
const projectSheetState = this.state.projectSheet.copy();
const employeeCostState = this.state.employeeCost.copy();
const timeReportState = this.state.timeReport.copy();
return (
<div className="App">
<Column>
<Row>
<Card width="15em" title="Project Sheet">
<InputFile
state={projectSheetState}
controller={this.projectSheetController}
/>
</Card>
<Card width="15em" title="Employee Cost">
<EncryptedInputFile
state={employeeCostState}
controller={this.employeeCostController}
/>
</Card>
<Card width="15em" title="Time Report">
<InputFile
state={timeReportState}
controller={this.timeReportController}
/>
</Card>
Pretty simple!
InputFile.js
Back in InputFile.js is where I define the InputFileController class. The constructor simply stores the reference to the App and the name of the state field.
export class InputFileController {
constructor(app, stateFieldName) {
this.app = app;
this.stateFieldName = stateFieldName;
}
The next few functions are where it gets a bit interesting. The event functions (onFileAdded, onCleared, etc) change the InputFile component’s state by calling the setState() function and then take some other action – such as alerting the user to an error, uploading a file, etc.
onCleared = () => {
this.setState(this.getState().setHasNoFile());
};
onFileAdded = (file) => {
this.setState(this.getState().setFileUploading(file));
this.uploadFile(file);
};
onUploadSuccess = (localFileName, serverFileName) => {
this.setState(this.getState().setFileUploaded(serverFileName));
console.log("Uploaded local file: " + localFileName);
};
onUploadError = () => {
this.setState(this.getState().setError());
alert('Unable to upload file, please see console for more information.');
};
getState = () => {
return this.app.state[this.stateFieldName];
};
setState = (nextState) => {
this.app.setState(function() {
const returnObj = {};
returnObj[this.stateFieldName] = nextState;
return returnObj;
}.bind(this));
};
The controller passed a brand new state object to the setState() function – as returned by the state object’s setXXX() functions. Diving into setState() we’ll see some common React boilerplate state code. I’ve been able to keep these functions nice and short, with clear explicit names.
Let’s test it out!


EncryptedInputFile.js
Now the middle component actually needs a bit more functionality. It needs to accept a password that will be used to decrypt the employee cost file – so it has an extra text box to accept the password.
This is going to be a new component called an EncryptedInputFile that extends the basic InputFile component. It will likewise define an EncryptedInputFileState and EncryptedInputFileController that extend their respective InputFile state and controller classes.
Let’s start with the state class.
EncryptedInputFileState
export class EncryptedInputFileState extends InputFileState {
#password = "";
constructor() {
super();
this.#password = "";
}
getPassword() {
return this.#password;
}
setPassword(value) {
const nextState = this.copy();
nextState.#password = value;
return nextState;
}
setHasNoFile() {
const nextState = super.setHasNoFile();
nextState.#password = "";
return nextState;
}
copyValuesFrom(other) {
super.copyValuesFrom(other);
this.#password = other.#password;
}
copy() {
const copy = new EncryptedInputFileState();
copy.copyValuesFrom(this);
return copy;
}
}
Pretty straight forward, here. The class adds a single field, password, provides getters & setters, and overrides the setHasNoFile() to reset the password field in the event that the user clears the component.
EncryptedInputFileController
export class EncryptedInputFileController extends InputFileController {
onPasswordChange = (e) => {
this.setState(this.getState().setPassword(e.target.value));
};
}
Again, pretty simple. It listens for change events in the password field and updates the component state accordingly.
EncryptedInputFile
Finally to the component itself. This will look pretty similar to the InputFile component with a few alterations.
export class EncryptedInputFile extends InputFile {
renderOthers = () => {
const controller = this.props.controller;
const passwordValue = this.props.state.getPassword();
return (
<form onSubmit={this.onSubmit} className="Form">
<div className="FormDiv">
Password:
</div>
<div className="FormDiv">
<input type="password" size="12" value={passwordValue} onChange={controller.onPasswordChange}/>
</div>
</form>
);
}
}
It overrides the renderOthers() function from InputFile to add the password field.
App.js
At last it’s time to use the new EncryptedInputFile component. Back in the App.js file, let’s change the state and controller for the employee cost component.
class App extends React.Component {
constructor(props) {
super(props);
this.projectSheetController = new InputFileController(this, "projectSheet");
this.employeeCostController = new EncryptedInputFileController(this, "employeeCost");
this.timeReportController = new InputFileController(this, "timeReport");
this.state = {
projectSheet: new InputFileState(),
employeeCost: new EncryptedInputFileState(),
timeReport: new InputFileState(),
}
}
Wrap Up!
And there it is! A single controller object to share the state manipulation for three input file components, as well as a simple derived class to add a password field. This project is starting to come together, and next I’ll turn to integrating the Java code from the old Java Swing app that I’m replacing.