gg logo

gg

Documentation

Changelog

Blog

Using lists

→ Click here to see the state of the app after completing this step.

→ Click here to see the changes we'll make in this step in GitHub.


When learning about JSX in gg we touched on expressions. Those allow us to use all the power of JavaScript to create out markup. In particular we can use array methods like map to create some markup for each item in the array.

However, this won't work when it comes to dynamic arrays, i.e. arrays that are stored in client-side state. In that case, we have to let gg know how it should handle each item in the array and what markup it should return for each item.

To finish of our calculator app we will add a list of previous calculation results below the calculator. By doing this we will learn how to handle dynamic lists in gg.

Note that the list API is still considered unstable at the moment. There are known bugs when using nested lists. Without nesting lists, the API should be pretty stable though.

Each dynamic list has to be a state variable. At the moment we cannot use selectors for that. So let's create a new state for the list of calculation results.

22 +23const previousResults = gg.state([]);+24 25const setCalculatorState = gg.setState(

In order for gg to be able to update the list in the DOM, it needs to somehow indentify the individual items of dynamic lists. Therefore it is required that the list items are objects that contain a key named id, which has to contain a unique value for each item in the array.

Right now our state variable has the type State<never[], {}>, because TypeScript infers the type never[] to an empty array. To solve it we have to explicitly type the initial value of our state.

22 -const previousResults = gg.state([]);+23type Result = { id: string; result: string };+24const initialValue: Result[] = [];+25const previousResults = gg.state(initialValue); 26

Apart from the id key, which again is required, we added a key named result which will store the actual calculation result.

Before we generate the dynamic list, let's quickly handle updating the state. We'll create a set-state-handler that also depends on the calculatorState. That way we can read the calculation result from there instead of duplicating the calculation logic from the other reducer function. We'll use a stringified timestamp as id.

166 +167const setPreviousResults = gg.setState(+168 previousResults,+169 (value, event, [calculatorStateValue]) => {+170 if (!(event.target instanceof HTMLButtonElement)) {+171 return value;+172 }+173 const buttonText = event.target.innerText;+174 if (buttonText === "C" && calculatorStateValue.result) {+175 value.unshift({+176 id: Date.now().toString(),+177 result: calculatorStateValue.result,+178 });+179 return value;+180 }+181 return value;+182 },+183 [calculatorState],+184);+185 186const buttonStyles = gg.stylesheet(`

When the user completely clears the calculator we also want to reset this state.

174 if (buttonText === "C" && calculatorStateValue.result) { 175 value.unshift({ 176 id: Date.now().toString(), 177 result: calculatorStateValue.result, 178 }); 179 return value; 180 }+181 if (buttonText === "AC") {+182 return [];+183 } 184 return value;

Now both set-state-handlers want to update their state when the C button is pressed. In order to still be able to read the result property from the calculatorState, we have to execute the handler for the previousResults state first.

232 return (- <button class={classes} onclick={setCalculatorState}>+233 <button class={classes} onclick={[setPreviousResults, setCalculatorState]}> 234 {args.children} 235 </button> 236 );

Last step before adding the dynamic list is adding some markup. By adding a selector to the previousResults state we can display a note if there are no results yet.

23type Result = { id: string; result: string }; 24const initialValue: Result[] = [];-const previousResults = gg.state(initialValue);+25const previousResults = gg.state(initialValue, {+26 hasResults: (value) => value.length > 0,+27}); 303 </div>+304 <h2>Previous results</h2>+305 <p hidden={previousResults.selectors.hasResults}>+306 No calculation has been finished yet.+307 </p> 308 </body>

In order to finally include the dynamic list into out markup, we use the gg.unstable_list method. Here we have to pass two arguments:

  1. The state variable on which the dynamic list is based.
  2. An object containing list-item-selectors as values, i.e. functions that will be called with a single item of the state-array and return a primitive value.

The gg.unstable_list method returns an object with a method named map (in accordance with the array method). As you'd expect, you can pass a callback to that method. It will be called with an object containing the selectors based of the functions specified in the argument of gg.unstable_list. The callback function should then return the markup for a single list item.

Let's see it in action!

307 </p>+308 <ul>+309 {gg+310 .unstable_list(previousResults, {+311 result: (value) => value.result,+312 })+313 .map((selectors) => (+314 <li>{selectors.result}</li>+315 ))}+316 </ul> 317 </body>

For the sake of demonstration, let's add another selector. We'll use the id (which is a stringified timestamp) to also display the time when the result was calculated.

309 {gg 310 .unstable_list(previousResults, { 311 result: (value) => value.result,+312 time: (value) => {+313 const date = new Date(parseInt(value.id));+314 return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;+315 }, 316 }) 317 .map((selectors) => (- <li>{selectors.result}</li>+318 <li>+319 {selectors.result} (calculated at <time>{selectors.time}</time>)+320 </li> 321 ))}

And that's it! Now you know how to create dynamic lists with gg.


To finish the tutorial, we'll do a final refactoring. Note that we don't actually need the previousResult key in the calculatorState anymore. We can just use the first item of the previousResults state. Let's first add the dependency.

29const setCalculatorState = gg.setState( 30 calculatorState,- (value, event) => {+31 (value, event, [previousResultsValue]) => { 32 if (!(event.target instanceof HTMLButtonElement)) { 164 return value; 165 },- [],+166 [previousResults], 167);

With that we can replace value.previousState with previousResultsValue[0].result. Note that we can also combine the cases for the C and the AC buttons, as they are completely equal now.

103 const parsedFirstNumber = parseFloat( 104 value.firstNumber === "ANS"- ? value.previousResult+105 ? previousResultsValue[0].result 106 : value.firstNumber, 107 ); 108 const parsedSecondNumber = parseFloat( 109 value.secondNumber === "ANS"- ? value.previousResult+110 ? previousResultsValue[0].result 111 : value.secondNumber, 137 }- case "C": {- if (value.result) {- value.previousResult = value.result;- }- value.firstNumber = "";- value.operator = "";- value.secondNumber = "";- value.result = "";- break;- }+138 case "C": 139 case "AC": { 140 value.firstNumber = ""; 141 value.operator = ""; 142 value.secondNumber = ""; 143 value.result = "";- value.previousResult = ""; 144 break; 145 } 146 case "ANS": {- if (value.result || !value.previousResult) {+147 if (value.result || !previousResultsValue[0].result) { 148 break; 149 }

And with that we don't need the previousResult key anymore.

4 { 5 firstNumber: "", 6 operator: "", 7 secondNumber: "", 8 result: "",- previousResult: "", 9 },

And that is it! We successfully created a basic gg application showcasing all methods needed in order to handle basic client-side-state and ended up with a absolutely mininal HTML page:

  • 7.280 characters
  • 2.9 KB gzipped
  • including all CSS and JS

We hope you enjoyed the tutorial. Now go out and build some amazing static websites!