Problem
-
In yesterday’s recipe we saw how to use template strings to generate an HTML list based on an array of objects:
let persons = [ { firstName: 'John', lastName: 'Doe', address: { city: 'London' } }, { firstName: 'Jane', lastName: 'Doe', address: { city: 'Birmingham' } }, { firstName: 'Jane', lastName: 'Smith', address: { city: 'Birmingham' } }, { firstName: 'Dave', lastName: 'Smith', address: { city: 'London' } }, { firstName: 'Jane', lastName: 'Carpenter', address: { city: 'Birmingham' } } ] const htmlListItem = object => `\n <li>${object.firstName + object.lastName}</li>` const htmlList = array => `<ul>${array.map(htmlListItem).join('')}\n</ul>` console.log(htmlList(persons))
-
However our solution had one limitation: it only works for those objects that have
firstName
andlastName
properties but it doesn’t work for other objects. For example the following code …let cities = [ { name: 'Ney York' }, { name: 'Miami' }, { name: 'San Francisco' }, { name: 'Chicago' }, { name: 'Seattle' } ] const htmlListItem = object => `\n <li>${object.firstName + object.lastName}</li>` const htmlList = array => `<ul>${array.map(htmlListItem).join('')}\n</ul>` console.log(htmlList(cities))
… generates this HTML code:
<ul> <li>NaN</li> <li>NaN</li> <li>NaN</li> <li>NaN</li> <li>NaN</li> </ul>
- So what we really want is a generic function
listItem()
that works with every type of object. Fortunately this is relatively easy as we see in today’s recipe.
Ingredients
- the
htmlList()
function from yesterday’s recipe - the
htmlListItem()
function from yesterday’s recipe - closures
- partial application
Directions
-
Given: the two functions
htmlList()
andhtmlListItem()
from yesterday’s recipe:const htmlListItem = object => ` <li>${object.firstName + object.lastName}</li>` const htmlList = array => `<ul>${array.map(htmlListItem).join('')} </ul>`
The idea now is that we change
htmlList()
andhtmlListItem()
to accept a function as parameter which determines the value (based on the particular object) that should be used for the list item (we call this function value function). BothhtmlList()
andhtmlListItem()
will then return functions and close in the value function. -
Change
htmlListItem()
to accept a function (the value function) and return another function (this is where the closure comes in) …const htmlListItem = fn => object => ` <li>${object.firstName + object.lastName}</li>` const htmlList = array => `<ul>${array.map(htmlListItem).join('')} </ul>`
-
… and inside the returned function call the value function with the particular object as argument.
const htmlListItem = fn => object => ` <li>${fn(object)}</li>` const htmlList = array => `<ul>${array.map(htmlListItem).join('')} </ul>`
-
Change
htmlList()
as well so that it accepts a value function and returns another function (again, this creates a closure) …const htmlListItem = fn => object => ` <li>${fn(object)}</li>` const htmlList = fn => array => `<ul>${array.map(htmlListItem).join('')} </ul>`
-
… and inside the returned function call
htmlListItem()
with the value function as argument.const htmlListItem = fn => object => ` <li>${fn(object)}</li>` const htmlList = fn => array => `<ul>${array.map(htmlListItem(fn)).join('')} </ul>`
-
Voilá, now you can use the
htmlList()
to produce the HTML list code for any type of objects by simply passing a value function that determines the value used for the label of the list item:For example this code here …
console.log(htmlList(object => object.name)(cities))
… produces this HTML code …
<ul> <li>Ney York</li> <li>Miami</li> <li>San Francisco</li> <li>Chicago</li> <li>Seattle</li> </ul>
… while this code here …
console.log(htmlList(object => object.firstName + ' ' + object.lastName)(persons))
… produces this HTML code:
<ul> <li>John Doe</li> <li>Jane Doe</li> <li>Jane Smith</li> <li>Dave Smith</li> <li>Jane Carpenter</li> </ul>
Notes
-
You can also use partial application to create partial applied versions of
htmlList()
, i.e., functions where the value function is closed in.const htmlListForCities = htmlList(object => object.name) const htmlListForPersons = htmlList(object => object.firstName + ' ' + object.lastName) console.log(htmlListForCities(cities)) console.log(htmlListForPersons(persons))
Alternative recipes
- Use a template engine like EJS or Jade.
-
Another alternative is to use yesterday’s recipe and introduce a common method in all objects, e.g.,
toString()
orgetListLabel()
.However this approach has two disadvantages: first you cannot use object literal notation for creating the objects without defining the method over and over again in every object. That means you would need to use some sort of inheritance, which is not bad, but it adds complexity to your object model.
The second disadvantage is that the logic of how an object is displayed in the generated list is tightly coupled to the object. If you want to change the value, you always need to change the object model. With the technique shown in today’s recipe we decoupled the logic from the object model (inside the value function).
Finally, an advantage of today’s recipe is that you can easily create different value functions for one and the same object model. For example, you could create two value functions, one just displaying the first name, one just displaying the last name of a person:
const htmlListFirstName = htmlList(object => object.firstName) const htmlListLastName = htmlList(object => object.lastName)
With yesterday’s recipe this is not that easy.