REACT

Golf Scorecard React App GIR

React Golf Scorecard application updated to show Greens in Regulation (GIR). For this we needed figure out how to trigger an on change event when the input value for GIR is updated dynamically when strokes and putts are entered. It is recommended that you read Part 1 which contains the information on how to install and build this application locally.

You can view a demo of the app here

demo with source code inspection using the Chrome developer tools
demo with source maps

Here are the steps for adding the greens in regulation functionality to the app src.

This commit contains all of the GIR changes that are covered in this post.

First we’re going to move the digitVal function into a module so it can be shared between components.

Create a new folder named modules with this common.js file.

modules/common.js
/**
 * Replaces non-digit element value with an empty string
 * @param {*} el
 * @returns
 */
export function digitVal(el) {
	if (el.value && isNaN(el.value)) {
		return el.value.replace(/[^\d]+/g,'');
	} else {
		return el.value;
	}
}
score.js

Update score.js to import and remove the previous location of the function from within the component.

Append the digitVal import as follows to the other imports at the top of the component file.

import { digitVal } from './modules/common';

Now we can remove the previous digitVal function located above the handleSubmit function.

remove digitVal from Score component - React golf scorecard

After the penaltyTotal state variable near the top of the Score component function.

const [penaltyTotal, setPenaltyTotal] = useState(0);

Add these state variables for girOut, girIn and girTotal with a default value of 0.

const [girOut, setGirOut] = useState(0);
const [girIn, setGirIn] = useState(0);
const [girTotal, setGirTotal] = useState(0);

This update will allow additional characters to be entered for fairway accuracy. At the beginning of the init() function, add T and S to the allowed array. T represents tee shots through the fairway, S represents shots short the fairway.

Replace

const allowed = ['L', 'F', 'R'];

With

const allowed = ['L', 'F', 'R', 'T', 'S']; // T:Through, S:Short

Within the if (props.action == 'update') conditional block, update the gir* state values with their property values.

After

setPenaltyTotal(props.content.penaltyTotal);

Add

setGirOut(props.content.girOut);
setGirIn(props.content.girIn);
setGirTotal(props.content.girTotal);

Within the handleSubmit function, update the score objects gir* values with their respective state values.

After

score['penaltyTotal'] = penaltyTotal;

Add

score['girOut'] = girOut;
score['girIn'] = girIn;
score['girTotal'] = girTotal;

Within the handleScoreChange function, add this conditional block for the gir* inputs.

Before

if (evt.target.name.startsWith('fairway_')) {

Insert

if (evt.target.name.startsWith('gir_')) {
	evt.target.value = digitVal(evt.target);
	const gir = document.querySelectorAll(`input[name^='gir_']`);
	const sumGir = sumScores(gir);

	setGirOut(sumGir.out);
	setGirIn(sumGir.in);
	setGirTotal(sumGir.in + sumGir.out);
}

For the final change to score.js, add these props to the <HoleScore function component include.

After

penaltyTotal={penaltyTotal}

Add

girOut={girOut}
girIn={girIn}
girTotal={girTotal}
hole-score.js

Update the hole-score.js component as follows.

Similar to what we did for the Score component, add the digitVal import to the top of the hole-score.js component file after the Button component import.

After

import Button from "./button";

Add

import { digitVal } from './modules/common';

Next, we’re going to refactor the existing input objects and add the gir input objects.

HoleScore component changes part 1 - React golf scorecard

As shown in the image above, move bunkerInput, fairwayInput, penaltyInput, puttsInput and strokesInput to constants named bunkerName, fairwayName, penaltyName, PuttsName and strokesName. Insert girName between the fairwayName and penaltyName constants.

const bunkerName = `bunker_${props.item.number}`;
const fairwayName = `fairway_${props.item.number}`;
const girName = `gir_${props.item.number}`;
const penaltyName = `penalty_${props.item.number}`;
const puttsName = `putts_${props.item.number}`;
const strokesName = `strokes_${props.item.number}`;

Create the following input element objects using query selectors. We’re setting these to null if the component’s input elements have not yet been rendered to the DOM.

const hole = document.querySelector(`.hole[data-hole="${props.item.number}"]`);
const girInput = (hole) ? hole.querySelector(`input[name="${girName}"]`) : null;
const puttsInput = (hole) ? hole.querySelector(`input[name="${puttsName}"]`) : null;
const strokesInput = (hole) ? hole.querySelector(`input[name="${strokesName}"]`) : null

Add these two functions before the if (props.index == 0 || props.index == 9) {...} conditional block. The scoreChange function serves as a proxy to the scoreChange function sent in props from the Score component. We need this to handle strokes or putts input changes and update the component gir objects as needed.

The checGir function uses the par, putts and strokes values for the hole to determine if the result is a green in regulation. When strokes minus putts is less than or equal to the par minus 2, then we have a green in regulation. For a green in regulation, the girInput value is set to 1 and the gir class is added to the div element wrapping the girInput. Since React overrides the input value setter, we’re calling the set function directly on the input as context. Then we create a new change event to dispatch for the input after its value is set.

Before

if (props.index == 0 || props.index == 9) {...}

Add

function checkGir(par, strokes, putts) {
	const diff = strokes - putts;
	const girDiff = par - 2;

	// needed to change girInput value and then dispatchEvent manually so onChange event is fired for girInput
	var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;

	if (diff <= girDiff ) {
		nativeInputValueSetter.call(girInput, 1);
		girInput.closest('div').classList.add('gir');
	} else {
		nativeInputValueSetter.call(girInput, '');
		girInput.closest('div').classList.remove('gir');
	}

	const evt = new Event('change', { bubbles: true});
	girInput.dispatchEvent(evt);
}

function scoreChange(evt) {
	if (evt.target.name.startsWith('strokes_')) {

		// check for putts
		if (puttsInput !== null && digitVal(puttsInput) > 0) {
			checkGir(hole.dataset.par, digitVal(evt.target), digitVal(puttsInput));
		}
	}
	if (evt.target.name.startsWith('putts_')) {
		checkGir(hole.dataset.par, digitVal(strokesInput), digitVal(evt.target));
	}

	props.scoreChange(evt);
}

In the HTML returned by the HoleScore component, add these elements as follows.

Within the if (props.index == 0 || props.index == 9) {...} conditional block…

After

<div className="label adv">Penalty</div>

Add

<div className="label adv">GIR</div>

Within the else if (props.index == 8) {...} conditional block…

After

<div className="label adv penalty">{props.penaltyOut}</div>

Add

<div className="label adv">{props.girOut}</div>

Replace the else if (props.index == 17) {...} conditional block as follows.

} else if (props.index == 17) {
	contentIn =
		<div className="in">
			<div>IN</div>
			<div className="yards">{props.yardsIn}</div>
			<div>{props.parIn}</div>
			<div className="label strokes">{props.strokesIn}</div>
			<div>Hcp.</div>
			<div className="label adv putts">{props.puttsIn}</div>
			<div className="label adv fairway">{props.fairwayIn}</div>
			<div className="label adv bunker">{props.bunkerIn}</div>
			<div className="label adv penalty">{props.penaltyIn}</div>
		</div>;
	contentTotal =
	<div>
			<div>&nbsp;</div>
			<div className="yards">{props.yards}</div>
			<div>{props.par}</div>
			<div className="label strokes">{props.strokesTotal}</div>
			<div>&nbsp;</div>
			<div className="label adv putts">{props.puttsTotal}</div>
			<div className="label adv fairway">{props.fairwayTotal}</div>
			<div className="label adv bunker">{props.bunkerTotal}</div>
			<div className="label adv penalty">{props.penaltyTotal}</div>
		</div>;
	sum = true;
}

With

} else if (props.index == 17) {
	contentIn =
		<div className="in">
			<div>IN</div>
			<div className="yards">{props.yardsIn}</div>
			<div>{props.parIn}</div>
			<div className="label strokes">{props.strokesIn}</div>
			<div>Hcp.</div>
			<div className="label adv putts">{props.puttsIn}</div>
			<div className="label adv fairway">{props.fairwayIn}</div>
			<div className="label adv bunker">{props.bunkerIn}</div>
			<div className="label adv penalty">{props.penaltyIn}</div>
			<div className="label adv">{props.girIn}</div>
		</div>;
	contentTotal =
	<div className="total">
			<div>&nbsp;</div>
			<div className="yards">{props.yards}</div>
			<div>{props.par}</div>
			<div className="label strokes">{props.strokesTotal}</div>
			<div>&nbsp;</div>
			<div className="label adv putts">{props.puttsTotal}</div>
			<div className="label adv fairway">{props.fairwayTotal}</div>
			<div className="label adv bunker">{props.bunkerTotal}</div>
			<div className="label adv penalty">{props.penaltyTotal}</div>
			<div className="label adv">{props.girTotal}</div>
		</div>;
	sum = true;
}

For last update to hole-score.js, Replace the component’s return (...) block as follows.

return (
	<div className={`hole${sum = sum ? ' sum' : ''}${labels = labels ? ' labels' : ''}`}>
		{contentLabels}
		<div>
			<div>{props.item.number}</div>
			<div className="yards">{props.item.yards}</div>
			<div>{props.item.par}</div>
			<div><input name={strokesInput} type="tel" required="" aria-required="true" onChange={props.scoreChange} maxLength="2" defaultValue={props.item.strokes} readOnly={props.locked} /></div>
			<div>{props.item.handicap}</div>
			<div className="adv"><input name={puttsInput} type="tel" required="" aria-required="false" onChange={props.scoreChange} maxLength="1" defaultValue={props.item.putts} readOnly={props.locked} /></div>
			<div className="adv"><input name={fairwayInput} type="text" required="" aria-required="false" maxLength="1" defaultValue={props.item.fairway} readOnly={props.locked} /></div>
			<div className="adv"><input name={bunkerInput} type="tel" required="" aria-required="false" onChange={props.scoreChange} maxLength="1" defaultValue={props.item.bunker} readOnly={props.locked} /></div>
			<div className="adv"><input name={penaltyInput} type="tel" required="" aria-required="false" onChange={props.scoreChange} maxLength="1" defaultValue={props.item.penalty} readOnly={props.locked} /></div>
		</div>
		{contentOut}
		{contentIn}
		{contentTotal}
	</div>
);

With

return (
	<div className={`hole${sum = sum ? ' sum' : ''}${labels = labels ? ' labels' : ''}`}
		data-hole={props.item.number}
		data-par={props.item.par}>
		{contentLabels}
		<div>
			<div>{props.item.number}</div>
			<div className="yards">{props.item.yards}</div>
			<div>{props.item.par}</div>
			<div><input name={strokesName} type="tel" required="" aria-required="true" onChange={scoreChange} maxLength="2" defaultValue={props.item.strokes} readOnly={props.locked} /></div>
			<div>{props.item.handicap}</div>
			<div className="adv"><input name={puttsName} type="tel" required="" aria-required="false" onChange={scoreChange} maxLength="1" defaultValue={props.item.putts} readOnly={props.locked} /></div>
			<div className="adv"><input name={fairwayName} type="text" required="" aria-required="false" maxLength="1" defaultValue={props.item.fairway} readOnly={props.locked} /></div>
			<div className="adv"><input name={bunkerName} type="tel" required="" aria-required="false" onChange={props.scoreChange} maxLength="1" defaultValue={props.item.bunker} readOnly={props.locked} /></div>
			<div className="adv"><input name={penaltyName} type="tel" required="" aria-required="false" onChange={props.scoreChange} maxLength="1" defaultValue={props.item.penalty} readOnly={props.locked} /></div>
			<div className="adv"><input name={girName} type="tel" required="" aria-required="false" onChange={props.scoreChange} maxLength="1" defaultValue={props.item.gir} /></div>
		</div>
		{contentOut}
		{contentIn}
		{contentTotal}
	</div>
);
tees.scss

Our final update is for the CSS to show a checkmark html entity when the gir class is present as follows, tees.scss.

Within .edit .scorecard .hole {...} selectors,

.edit {
	...

	.scorecard {
		...

		.hole {
			...
		}
	}
}

Add

&.sum~.hole {
	margin-top: 1.5rem;
}

.gir {
	&:before {
		content: '\02713';
	}
}

input[name^='gir_'] {
	display: none;
}

That’s it! Now our scorecard displays a Greens In Regulation checkmark when applicable with respective totals.

screen recording showing greens in regulation check
greens in regulation check

Source Code
comments powered by Disqus