Browse Source

Revamped CustomInput completely.

KrisVos130 7 years ago
parent
commit
25686d7700

+ 2 - 2
frontend/app/js/views/Auth/CustomErrors.jsx

@@ -26,10 +26,10 @@ export default class CustomErrors extends Component {
 		this.props.onRef(null);
 	}
 
-	clearErrors = () => {
+	clearErrors = (cb = () => {}) => {
 		this.setState({
 			errors: [],
-		});
+		}, cb);
 	};
 
 	addError = (error) => {

+ 110 - 91
frontend/app/js/views/Auth/CustomInput.jsx

@@ -2,121 +2,118 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import reactTriggerChange from "react-trigger-change";
 
+const isLength = (string, min, max) => {
+	return !(typeof string !== "string" || string.length < min || string.length > max);
+};
+
 const regex = {
 	azAZ09_: /^[A-Za-z0-9_]+$/,
 	azAZ09: /^[A-Za-z0-9]+$/,
 	az09_: /^[a-z0-9_]+$/,
 	az09: /^[a-z0-9]+$/,
-	emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
+	emailSimple: /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
 	password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]/,
 	ascii: /^[\x00-\x7F]+$/,
 };
 
-const isLength = (string, min, max) => {
-	return !(typeof string !== "string" || string.length < min || string.length > max);
-};
-
-// TODO add features where inputs need to be the same
-
-const validation = {
-	username: (value) => {
-		const errors = [];
-		if (!isLength(value, 2, 32)) errors.push("Username must be between 2 and 32 characters long.");
-		if (!regex.azAZ09_.test(value)) errors.push("Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.");
-		return errors;
+const dictionary = {
+	username: {
+		inputType: "text",
+		minLength: 2,
+		maxLength: 32,
+		regex: regex.azAZ09_,
+		errors: {
+			format: "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _."
+		},
 	},
-	email: (value) => {
-		const errors = [];
-		if (!isLength(value, 3, 254)) errors.push("Email must be between 3 and 254 characters long.");
-		if (value.indexOf("@") !== value.lastIndexOf("@") || !regex.emailSimple.test(value)) errors.push("Invalid email format.");
-		return errors;
+	email: {
+		inputType: "email",
+		minLength: 3,
+		maxLength: 254,
+		regex: regex.emailSimple,
+		errors: {
+			format: "Invalid email format. Email must contain one @ symbol.",
+		},
 	},
-	password: (value) => {
-		const errors = [];
-		if (!isLength(value, 6, 200)) errors.push("Password must be between 6 and 200 characters long.");
-		if (!regex.password.test(value)) errors.push("Invalid password format.");
-		return errors;
+	password: {
+		inputType: "password",
+		minLength: 6,
+		maxLength: 200,
+		regex: regex.password,
+		errors: {
+			format: "Invalid password format. Password must have at least 1 lowercase letter, 1 uppercase letter, 1 number and one special character ($@$!%*?&).",
+		},
 	},
-	uniqueCode: (value) => {
-		const errors = [];
-		if (!isLength(value, 8, 8)) errors.push("Code must be 8 characters long.");
-		if (!regex.azAZ09.test(value)) errors.push("Invalid code format.");
-		return errors;
+	uniqueCode: {
+		inputType: "text",
+		minLength: 8,
+		maxLength: 8,
+		regex: regex.azAZ09,
+		errors: {
+			length: "Code must be 8 characters long.",
+			format: "Invalid code format.",
+		}
 	},
 };
 
 export default class CustomInput extends Component {
 	static propTypes = {
 		type: PropTypes.string,
-		inputType: PropTypes.string,
 		name: PropTypes.string,
-		value: PropTypes.string,
 		label: PropTypes.string,
 		placeholder: PropTypes.string,
-		customInputEvents: PropTypes.object,
-		validationCallback: PropTypes.func,
 		onRef: PropTypes.func,
 	};
 
 	static defaultProps = {
-		type: "text",
-		inputType: "text",
+		type: "",
 		name: "",
-		value: "",
 		label: "",
 		placeholder: "",
-		customInputEvents: {},
-		validationCallback: () => {},
+		valid: false,
 		onRef: () => {},
 	};
 
-	static validationCallback = (ctx) => {
-		return (name, invalid) => {
-			const inputInvalid = ctx.state.inputInvalid;
-			inputInvalid[name] = invalid;
-			ctx.setState({ inputInvalid });
-		};
+	static initialize = (context) => {
+		context.input = {}; // eslint-disable-line no-param-reassign
 	};
 
-	static hasInvalidInput = (inputInvalid, properties) => {
+	static hasInvalidInput = (input, properties) => {
 		let invalid = false;
 		if (properties) {
 			properties.forEach((property) => {
-				if (inputInvalid[property]) invalid = true;
+				if (!input[property].isValid()) invalid = true;
 			});
 		} else {
-			Object.keys((key) => {
-				if (key) invalid = true;
+			Object.keys(input).forEach((key) => {
+				if (!input[key].isValid()) invalid = true;
 			});
 		}
 		return invalid;
 	};
 
+	static isTheSame = (input, properties) => {
+		let invalid = false;
+		let value = properties[Object.keys[0]].getValue();
+		Object.keys(input).forEach((key) => {
+			if (input[key].getValue() !== value) invalid = true;
+		});
+		return invalid;
+	};
+
 	constructor(props) {
 		super(props);
 
 		this.state = {
-			customInputEvents: props.customInputEvents,
-			errors: "",
-			value: props.value,
-			validateOnChange: false,
+			inputText: dictionary[props.type].inputText,
+			value: "",
+			original: "",
+			errors: [],
+			pristine: true,
+			disabled: false,
+			valid: false,
 		};
-
-		if (this.state.customInputEvents.onBlur) {
-			const oldOnBlur = this.state.customInputEvents.onBlur;
-			this.state.customInputEvents.onBlur = () => {
-				this.onBlurHandler();
-				oldOnBlur();
-			};
-		} else this.state.customInputEvents.onBlur = this.onBlurHandler;
-
-		if (this.state.customInputEvents.onChange) {
-			const oldOnChange = this.state.customInputEvents.onChange;
-			this.state.customInputEvents.onChange = (event) => {
-				this.onChangeHandler(event);
-				oldOnChange(event);
-			};
-		} else this.state.customInputEvents.onChange = this.onChangeHandler;
+		// More values/functions needs like isEmpty, isRequired
 	}
 
 	componentDidMount() {
@@ -126,25 +123,34 @@ export default class CustomInput extends Component {
 		this.props.onRef(null);
 	}
 
-	// Triggered when user stops focusing on the input element
-	onBlurHandler = () => {
-		this.validateInput();
+	onBlur = () => {
+		this.validate();
+	};
+
+	onFocus = () => {
+		this.setState({
+			pristine: false,
+		});
 	};
 
-	// Triggered when the input element's value changes
-	onChangeHandler = (event) => {
+	onChange = (event) => {
 		this.setState({
 			value: event.target.value,
-		}, () => {
-			if (this.state.validateOnChange === true) {
-				this.setState({
-					validateOnChange: false,
-				});
-				this.validateInput();
-			}
 		});
 	};
 
+	setValue = (value, original = false) => {
+		const state = {
+			value,
+		};
+		if (original) state.original = value;
+		this.setState(state);
+	};
+
+	getValue = () => {
+		return this.state.value;
+	};
+
 	listErrors = () => {
 		let errors = this.state.errors;
 		let key = 0;
@@ -161,17 +167,28 @@ export default class CustomInput extends Component {
 		} return "";
 	};
 
-	validateInput = () => {
-		const value = this.state.value;
-		const type = this.props.type;
-		const errors = (validation[type]) ? validation[type](value) : [];
-		this.setState({ errors });
-		this.props.validationCallback(this.props.name, errors.length > 0);
+	isValid = () => {
+		return this.state.valid;
+	};
+
+	isOriginal = () => {
+		return this.state.original === this.state.value;
 	};
 
-	triggerChangeEvent = (validateOnChange) => {
-		reactTriggerChange(this.inputElement);
-		this.setState({ validateOnChange });
+	isPristine = () => {
+		return this.state.pristine;
+	};
+
+	validate = (cb = () => {}) => {
+		const errors = [];
+		const info = dictionary[this.props.type];
+		const value = this.state.value;
+		if (!isLength(value, info.minLength, info.maxLength)) errors.push((info.errors.length) ? info.errors.length : `Value must be between ${ info.minLength } and ${ info.maxLength } characters long.`);
+		if (!info.regex.test(value)) errors.push(info.errors.format);
+		this.setState({
+			errors,
+			valid: errors.length === 0,
+		}, cb);
 	};
 
 	render() {
@@ -180,11 +197,13 @@ export default class CustomInput extends Component {
 				<span>{ this.props.label }</span>
 				<input
 					placeholder={ this.props.placeholder }
-					type={ this.props.inputType }
+					type={ this.state.inputType }
 					name={ this.props.name }
-					value={ this.props.value }
+					value={ this.state.value }
 					className={ (this.state.errors.length > 0) ? "has-validation-errors" : "" }
-					{ ...this.state.customInputEvents }
+					onBlur={ this.onBlur }
+					onFocus={ this.onFocus }
+					onChange={ this.onChange }
 					ref={ (input) => this.inputElement = input }
 				/>
 				{ this.listErrors() }

+ 12 - 28
frontend/app/js/views/Auth/ForgotPassword.jsx

@@ -29,21 +29,12 @@ export default class Settings extends Component {
 
 		this.state = {
 			step: 1,
-			email: "",
 			resetCode: "",
-			newPassword: "",
-			newPasswordAgain: "",
-			inputInvalid: {
-				email: true,
-				resetCode: true,
-				newPassword: true,
-				newPasswordAgain: true,
-			},
 		};
 	}
 
 	getActions = () => {
-		const emailInput = <CustomInput key="email" label="Email" placeholder="Email" inputType="email" type="email" name="email" value={ this.state.email } customInputEvents={ { onChange: event => this.updateField("email", event) } } validationCallback={ this.validationCallback } />;
+		const emailInput = <CustomInput key="email" type="email" name="email" label="Email" placeholder="Email" onRef={ ref => (this.input.email = ref) } />;
 		const requestResetCodeButton = (<button key="requestResetCode" onClick={ this.requestResetCode }>
 			Request reset code
 		</button>);
@@ -51,13 +42,13 @@ export default class Settings extends Component {
 			I already have a reset code
 		</button>);
 
-		const resetCodeInput = <CustomInput key="resetCode" label="Reset code" placeholder="Reset code" inputType="text" type="uniqueCode" name="resetCode" value={ this.state.resetCode } customInputEvents={ { onChange: event => this.updateField("resetCode", event) } } validationCallback={ this.validationCallback } />;
+		const resetCodeInput = <CustomInput key="resetCode" type="uniqueCode" name="resetCode" label="Reset code" placeholder="Reset code" onRef={ ref => (this.input.email = ref) } />;
 		const verifyResetCode = (<button key="verifyResetCode" onClick={ this.verifyResetCode }>
 			Verify reset code
 		</button>);
 
-		const newPasswordInput = <CustomInput key="newPassword" label="New password" placeholder="New password" inputType="password" type="password" name="newPassword" value={ this.state.newPassword } customInputEvents={ { onChange: event => this.updateField("newPassword", event) } } validationCallback={ this.validationCallback } />;
-		const newPasswordAgainInput = <CustomInput key="newPasswordAgain" label="New password again" placeholder="New password again" inputType="password" type="password" name="newPasswordAgain" value={ this.state.newPasswordAgain } customInputEvents={ { onChange: event => this.updateField("newPasswordAgain", event) } } validationCallback={ this.validationCallback } />;
+		const newPasswordInput = <CustomInput key="newPassword" type="password" name="newPassword" label="New password" placeholder="New password" onRef={ ref => (this.input.newPassword = ref) } />;
+		const newPasswordAgainInput = <CustomInput key="newPasswordAgain" type="password" name="newPasswordAgain" label="New password again" placeholder="New password again" onRef={ ref => (this.input.newPasswordAgain = ref) } />;
 		const changePassword = (<button key="changePassword" onClick={ this.changePassword }>
 			Change password
 		</button>);
@@ -69,15 +60,13 @@ export default class Settings extends Component {
 		} return [newPasswordInput, newPasswordAgainInput, changePassword];
 	};
 
-	validationCallback = CustomInput.validationCallback(this);
-
 	requestResetCode = () => {
-		if (CustomInput.hasInvalidInput(this.state.inputInvalid, ["email"])) {
+		if (CustomInput.hasInvalidInput(this.input, ["email"])) {
 			this.errors.clearAddError("Some fields are incorrect. Please fix them before continuing.");
 		} else {
 			this.errors.clearErrors();
 			io.getSocket(socket => {
-				socket.emit("users.requestPasswordReset", this.state.email, res => {
+				socket.emit("users.requestPasswordReset", this.input.email.getValue(), res => {
 					if (res.status === "success") {
 						alert("Success!");
 						this.setState({
@@ -92,16 +81,17 @@ export default class Settings extends Component {
 	};
 
 	verifyResetCode = () => {
-		if (CustomInput.hasInvalidInput(this.state.inputInvalid, ["resetCode"])) {
+		if (CustomInput.hasInvalidInput(this.input, ["resetCode"])) {
 			this.errors.clearAddError("Some fields are incorrect. Please fix them before continuing.");
 		} else {
 			this.errors.clearErrors();
 			io.getSocket(socket => {
-				socket.emit("users.verifyPasswordResetCode", this.state.resetCode, res => {
+				socket.emit("users.verifyPasswordResetCode", this.input.resetCode.getValue(), res => {
 					if (res.status === "success") {
 						alert("Success!");
 						this.setState({
 							step: 3,
+							resetCode: this.input.resetCode.getValue(),
 						});
 					} else {
 						this.errors.addError(res.message);
@@ -112,14 +102,14 @@ export default class Settings extends Component {
 	};
 
 	changePassword = () => {
-		if (CustomInput.hasInvalidInput(this.state.inputInvalid, ["newPassword", "newPasswordAgain"])) {
+		if (CustomInput.hasInvalidInput(this.input, ["newPassword", "newPasswordAgain"])) {
 			this.errors.clearAddError("Some fields are incorrect. Please fix them before continuing.");
-		} else if (this.state.newPassword !== this.state.newPasswordAgain) {
+		} else if (CustomInput.isTheSame(this.input, ["newPassword", "newPasswordAgain"])) {
 			this.errors.clearAddError("New password and new password again need to be the same.");
 		} else {
 			this.errors.clearErrors();
 			io.getSocket(socket => {
-				socket.emit("users.changePasswordWithResetCode", this.state.resetCode, this.state.newPassword, res => {
+				socket.emit("users.changePasswordWithResetCode", this.state.resetCode, this.input.newPassword.getValue(), res => {
 					if (res.status === "success") {
 						alert("Success!");
 						location.href = "/login";
@@ -137,12 +127,6 @@ export default class Settings extends Component {
 		});
 	};
 
-	updateField(field, event) {
-		this.setState({
-			[field]: event.target.value,
-		});
-	}
-
 	render() {
 		return (
 			<div>

+ 6 - 23
frontend/app/js/views/Auth/Login.jsx

@@ -10,31 +10,16 @@ export default class Login extends Component {
 	constructor() {
 		super();
 
-		this.state = {
-			password: "",
-			email: "",
-			inputInvalid: {
-				email: true,
-				password: true,
-			},
-		};
-
-		this.login = this.login.bind(this);
-	}
-
-	updateField(field, event) {
-		this.setState({
-			[field]: event.target.value,
-		});
+		CustomInput.initialize(this);
 	}
 
-	login() {
-		if (CustomInput.hasInvalidInput(this.state.inputInvalid)) {
+	login = () => {
+		if (CustomInput.hasInvalidInput(this.input)) {
 			this.errors.clearAddError("Some fields are incorrect. Please fix them before continuing.");
 		} else {
 			this.errors.clearErrors();
 			io.getSocket(socket => {
-				socket.emit("users.login", this.state.email, this.state.password, res => {
+				socket.emit("users.login", this.input.email.getValue(), this.input.password.getValue(), res => {
 					if (res.status === "success") {
 						const date = new Date();
 						date.setTime(new Date().getTime() + (2 * 365 * 24 * 60 * 60 * 1000));
@@ -55,14 +40,12 @@ export default class Login extends Component {
 		localStorage.setItem("github_redirect", window.location.pathname);
 	}
 
-	validationCallback = CustomInput.validationCallback(this);
-
 	render() {
 		return (
 			<div>
 				<CustomErrors onRef={ ref => (this.errors = ref) } />
-				<CustomInput label="Email" placeholder="Email" inputType="email" type="email" name="email" value={ this.state.email } customInputEvents={ { onChange: event => this.updateField("email", event) } } validationCallback={ this.validationCallback } />
-				<CustomInput label="Password" placeholder="Password" inputType="password" type="password" name="password" value={ this.state.password } customInputEvents={ { onChange: event => this.updateField("password", event) } } validationCallback={ this.validationCallback } />
+				<CustomInput type="email" name="email" label="Email" placeholder="Email" onRef={ ref => (this.input.email = ref) } />
+				<CustomInput type="password" name="password" label="Password" placeholder="Password" onRef={ ref => (this.input.password = ref) } />
 				<p>By logging in/registering you agree to our <a href="/terms">Terms of Service</a> and <a href="/privacy">Privacy Policy</a>.</p>
 				<button onClick={ this.login }>Login</button>
 				<a href={ `${ config.serverDomain }/auth/github/authorize` } onClick={ this.githubRedirect }>

+ 9 - 25
frontend/app/js/views/Auth/Register.jsx

@@ -10,19 +10,11 @@ export default class Register extends Component {
 	constructor() {
 		super();
 
+		CustomInput.initialize(this);
+
 		this.state = {
-			password: "",
-			username: "",
-			email: "",
 			recaptcha: "",
-			inputInvalid: {
-				email: true,
-				username: true,
-				password: true,
-			},
 		};
-
-		this.register = this.register.bind(this);
 	}
 
 	componentDidMount() {
@@ -31,19 +23,13 @@ export default class Register extends Component {
 		});
 	}
 
-	updateField(field, event) {
-		this.setState({
-			[field]: event.target.value,
-		});
-	}
-
-	register() {
-		if (CustomInput.hasInvalidInput(this.state.inputInvalid)) {
+	register = () => {
+		if (CustomInput.hasInvalidInput(this.input)) {
 			this.errors.clearAddError("Some fields are incorrect. Please fix them before continuing.");
 		} else {
 			this.errors.clearErrors();
 			io.getSocket(socket => {
-				socket.emit("users.register", this.state.username, this.state.email, this.state.password, grecaptcha.getResponse(this.state.recaptcha), res => {
+				socket.emit("users.register", this.input.username.getValue(), this.input.email.getValue(), this.input.password.getValue(), grecaptcha.getResponse(this.state.recaptcha), res => {
 					if (res.status === "success") {
 						if (res.SID) {
 							const date = new Date();
@@ -60,21 +46,19 @@ export default class Register extends Component {
 				});
 			});
 		}
-	}
+	};
 
 	githubRedirect() {
 		localStorage.setItem("github_redirect", window.location.pathname);
 	}
 
-	validationCallback = CustomInput.validationCallback(this);
-
 	render() {
 		return (
 			<div>
 				<CustomErrors onRef={ ref => (this.errors = ref) } />
-				<CustomInput label="Email" placeholder="Email" inputType="email" type="email" name="email" value={ this.state.email } customInputEvents={ { onChange: event => this.updateField("email", event) } } validationCallback={ this.validationCallback } />
-				<CustomInput label="Username" placeholder="Username" inputType="text" type="username" name="username" value={ this.state.username } customInputEvents={ { onChange: event => this.updateField("username", event) } } validationCallback={ this.validationCallback } />
-				<CustomInput label="Password" placeholder="Password" inputType="password" type="password" name="password" value={ this.state.password } customInputEvents={ { onChange: event => this.updateField("password", event) } } validationCallback={ this.validationCallback } />
+				<CustomInput type="email" name="email" label="Email" placeholder="Email" onRef={ ref => (this.input.email = ref) } />
+				<CustomInput type="username" name="username" label="Username" placeholder="Username" onRef={ ref => (this.input.username = ref) } />
+				<CustomInput type="password" name="password" label="Password" placeholder="Password" onRef={ ref => (this.input.password = ref) } />
 				<div id="recaptcha" />
 				<p>By logging in/registering you agree to our <a href="/terms">Terms of Service</a> and <a href="/privacy">Privacy Policy</a>.</p>
 				<button onClick={ this.register }>Register</button>

+ 51 - 79
frontend/app/js/views/Auth/Settings.jsx

@@ -1,4 +1,5 @@
 import React, { Component } from "react";
+import async from "async";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
@@ -27,42 +28,21 @@ export default class Settings extends Component {
 	constructor(props) {
 		super(props);
 
+		CustomInput.initialize(this);
+
 		this.state = {
-			email: "",
-			username: "",
-			currentPassword: "",
-			newPassword: "",
-			newPasswordAgain: "",
 			passwordLinked: false,
 			gitHubLinked: false,
-			inputInvalid: {
-				email: true,
-				username: true,
-				newPassword: true,
-			},
-			savedValue: {
-				email: "",
-				username: "",
-			},
 		};
 
-		this.customInputs = {};
-
 		io.getSocket(socket => {
 			socket.emit("users.findBySession", res => {
 				if (res.status === "success") {
+					this.input.email.setValue(res.data.email.address, true);
+					this.input.username.setValue(res.data.username, true);
 					this.setState({
-						email: res.data.email.address,
-						username: res.data.username,
 						passwordLinked: res.data.password,
 						gitHubLinked: res.data.github,
-						savedValue: {
-							email: res.data.email.address,
-							username: res.data.username,
-						},
-					}, () => {
-						this.customInputs.email.triggerChangeEvent(true);
-						this.customInputs.username.triggerChangeEvent(true);
 					});
 				} else {
 					this.errors.addError("You are currently not logged in.");
@@ -70,14 +50,7 @@ export default class Settings extends Component {
 			});
 
 			socket.on("event:user.username.changed", username => {
-				this.setState({
-					username,
-					savedValue: {
-						username,
-					},
-				}, () => {
-					this.customInputs.username.triggerChangeEvent(true);
-				});
+				this.input.username.setValue(username, true);
 			});
 
 			// TODO Email changed event?
@@ -108,62 +81,63 @@ export default class Settings extends Component {
 		});
 	}
 
-	updateField(field, event) {
-		this.setState({
-			[field]: event.target.value,
-		});
-	}
-
 	/* githubRedirect() {
 		localStorage.setItem("github_redirect", window.location.pathname);
 	} */
-	isTheSame = (type) => {
-		return this.state[type] === this.state.savedValue[type];
-	};
 
 	saveChanges = () => {
-		if (CustomInput.hasInvalidInput(this.state.inputInvalid, ["username", "email"])) {
-			this.errors.clearAddError("Some fields are incorrect. Please fix them before continuing.");
-		} else if (this.isTheSame("username") && this.isTheSame("email")) {
-			this.errors.clearAddError("Username or email hasn't changed.");
-		} else {
-			this.errors.clearErrors();
-			const email = this.state.email;
-			const username = this.state.username;
-			io.getSocket(socket => {
-				if (!this.isTheSame("email")) {
-					socket.emit("users.updateEmail", this.props.user.userId, email, res => {
-						if (res.status === "success") {
-							alert("Success!");
-						} else {
-							this.errors.addError(res.message);
-						}
-					});
-				}
+		async.waterfall([
+			(next) => {
+				if (this.input.username.isPristine()) this.input.username.validate(next);
+				else next();
+			},
+			(next) => {
+				if (this.input.email.isPristine()) this.input.email.validate(next);
+				else next();
+			},
+		], () => {
+			if (CustomInput.hasInvalidInput(this.input, ["username", "email"])) {
+				this.errors.clearAddError("Some fields are incorrect. Please fix them before continuing.");
+			} else if (this.input.username.isOriginal() && this.input.email.isOriginal()) {
+				this.errors.clearAddError("Username or email hasn't changed.");
+			} else {
+				this.errors.clearErrors();
+				const email = this.input.email.getValue();
+				const username = this.input.username.getValue();
+				io.getSocket(socket => {
+					if (!this.input.email.isOriginal()) {
+						socket.emit("users.updateEmail", this.props.user.userId, email, res => {
+							if (res.status === "success") {
+								alert("Success!");
+							} else {
+								this.errors.addError(res.message);
+							}
+						});
+					}
 
-				if (!this.isTheSame("username")) {
-					socket.emit("users.updateUsername", this.props.user.userId, username, res => {
-						if (res.status === "success") {
-							alert("Success!");
-						} else {
-							this.errors.addError(res.message);
-						}
-					});
-				}
-			});
-		}
+					if (!this.input.username.isOriginal()) {
+						socket.emit("users.updateUsername", this.props.user.userId, username, res => {
+							if (res.status === "success") {
+								alert("Success!");
+							} else {
+								this.errors.addError(res.message);
+							}
+						});
+					}
+				});
+			}
+		});
 	};
 
 	changePassword = () => {
-		if (CustomInput.hasInvalidInput(this.state.inputInvalid, ["newPassword"])) {
+		if (CustomInput.hasInvalidInput(this.input, ["newPassword"])) {
 			this.errors.clearAddError("Some fields are incorrect. Please fix them before continuing.");
 		} else if (!this.state.passwordLinked) {
 			this.errors.clearAddError("You don't have a password set.");
 		} else {
 			this.errors.clearErrors();
-			const newPassword = this.state.newPassword;
 			io.getSocket(socket => {
-				socket.emit("users.updatePassword", newPassword, res => {
+				socket.emit("users.updatePassword", this.input.newPassword.getValue(), res => {
 					if (res.status === "success") {
 						alert("Success!");
 					} else {
@@ -229,21 +203,19 @@ export default class Settings extends Component {
 		} return linkGitHub;
 	};
 
-	validationCallback = CustomInput.validationCallback(this);
-
 	render() {
 		return (
 			<div>
 				<CustomErrors onRef={ ref => (this.errors = ref) } />
 				<div>
 					<h2>General</h2>
-					<CustomInput label="Email" placeholder="Email" inputType="email" type="email" name="email" value={ this.state.email } customInputEvents={ { onChange: event => this.updateField("email", event) } } validationCallback={ this.validationCallback } onRef={ ref => (this.customInputs.email = ref) } />
-					<CustomInput label="Username" placeholder="Username" inputType="text" type="username" name="username" value={ this.state.username } customInputEvents={ { onChange: event => this.updateField("username", event) } } validationCallback={ this.validationCallback } onRef={ ref => (this.customInputs.username = ref) } />
+					<CustomInput type="email" name="email" label="Email" placeholder="Email" onRef={ ref => (this.input.email = ref) } />
+					<CustomInput type="username" name="username" label="Username" placeholder="Username" onRef={ ref => (this.input.username = ref) } />
 					<button onClick={ this.saveChanges }>Save changes</button>
 				</div>
 				<div>
 					<h2>Security</h2>
-					<CustomInput label="New password" placeholder="New password" inputType="password" type="password" name="newPassword" value={ this.state.newPassword } customInputEvents={ { onChange: event => this.updateField("newPassword", event) } } validationCallback={ this.validationCallback } />
+					<CustomInput type="password" name="newPassword" label="New password" placeholder="New password" onRef={ ref => (this.input.newPassword = ref) } />
 					<button onClick={ this.changePassword }>Change password</button>
 					{ this.linkButtons() }
 					<button onClick={ this.logOutEverywhere }>Log out everywhere</button>

+ 0 - 1
frontend/package-lock.json

@@ -269,7 +269,6 @@
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
       "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==",
-      "dev": true,
       "requires": {
         "lodash": "4.17.4"
       }

+ 1 - 0
frontend/package.json

@@ -50,6 +50,7 @@
     "webpack-dev-server": "^2.2.1"
   },
   "dependencies": {
+    "async": "^2.5.0",
     "babel-polyfill": "^6.23.0",
     "dom-serializer": "^0.1.0",
     "domelementtype": "^1.3.0",