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
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.
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.
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> </div>
<div className="yards">{props.yards}</div>
<div>{props.par}</div>
<div className="label strokes">{props.strokesTotal}</div>
<div> </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> </div>
<div className="yards">{props.yards}</div>
<div>{props.par}</div>
<div className="label strokes">{props.strokesTotal}</div>
<div> </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.