Skip to content

Commit d1d036a

Browse files
committed
customization: support custom lodash template to render result row
this is the second (after CSS), very powerful customization feature: it allows a user of the element to define a <template row> tag inside the custom element, and inside of that they can create HTML markup as well as access the data of the row (the Feature) through the lodash template language. Example: <ge-autocomplete …> <template row> <div>${feature.properties.gid}: ${feature.properties.name}</div> </template> </ge-autocomplete>
1 parent 4df54bf commit d1d036a

File tree

5 files changed

+142
-16
lines changed

5 files changed

+142
-16
lines changed

package-lock.json

Lines changed: 67 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"@geocodeearth/core-js": "^0.0.7",
1313
"downshift": "6.1.3",
1414
"lodash.debounce": "^4.0.8",
15+
"lodash.escape": "^4.0.1",
16+
"lodash.template": "^4.5.0",
17+
"lodash.unescape": "^4.0.1",
1518
"react": "^17.0.2",
1619
"react-dom": "^17.0.2"
1720
},

src/autocomplete/autocomplete.js

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import debounce from 'lodash.debounce'
55
import css from './autocomplete.css'
66
import strings from '../strings'
77
import { LocationMarker, Loading } from '../icons'
8+
import escape from '../escape'
89

910
const emptyResults = {
1011
text: '',
@@ -22,7 +23,8 @@ export default ({
2223
onSelect: userOnSelectItem,
2324
onChange: userOnChange,
2425
onError: userOnError,
25-
environment = window
26+
environment = window,
27+
rowTemplate
2628
}) => {
2729
const [results, setResults] = useState(emptyResults)
2830
const [isLoading, setIsLoading] = useState(false)
@@ -150,20 +152,33 @@ export default ({
150152

151153
<ol {...getMenuProps()} className={showResults ? 'results' : 'results-empty'}>
152154
{showResults &&
153-
results.features.map((item, index) => (
154-
<li
155-
className={
156-
highlightedIndex === index
157-
? 'result-item result-item-active'
158-
: 'result-item'
159-
}
160-
key={item.properties.id}
161-
{...getItemProps({ item, index })}
162-
>
163-
<LocationMarker className='result-item-icon' />
164-
{itemToString(item)}
165-
</li>
166-
))}
155+
results.features.map((item, index) => {
156+
// render row with custom template, if available
157+
// the feature itself is recursively escaped as we can’t guarantee safe data from the API
158+
if (typeof rowTemplate === 'function') {
159+
return <li
160+
key={item.properties.id}
161+
{...getItemProps({ item, index })}
162+
dangerouslySetInnerHTML={{ __html: rowTemplate(escape({
163+
...item,
164+
active: highlightedIndex === index
165+
})) }}
166+
/>
167+
} else {
168+
return <li
169+
className={
170+
highlightedIndex === index
171+
? 'result-item result-item-active'
172+
: 'result-item'
173+
}
174+
key={item.properties.id}
175+
{...getItemProps({ item, index })}
176+
>
177+
<LocationMarker className='result-item-icon' />
178+
{itemToString(item)}
179+
</li>
180+
}
181+
})}
167182

168183
<div className='attribution'>
169184
©&nbsp;<a href="https://geocode.earth">Geocode Earth</a>,&nbsp;

src/escape.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import _escape from 'lodash.escape'
2+
3+
// escape takes any value (primarily objects) and recursively escapes the values
4+
const escape = (v) => {
5+
if (typeof v === 'string') return _escape(v)
6+
if (typeof v === 'number' || typeof v === 'boolean') return v
7+
if (Array.isArray(v)) return v.map(l => escape(l))
8+
9+
return Object.keys(v).reduce(
10+
(attrs, key) => ({
11+
...attrs,
12+
[key]: escape(v[key]),
13+
}),
14+
{}
15+
)
16+
}
17+
18+
export default escape

src/index.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React, { useMemo } from 'react'
22
import ReactDOM from 'react-dom'
33
import Autocomplete from './autocomplete'
44
import compact from './compact'
5+
import _template from 'lodash.template'
6+
import _unescape from 'lodash.unescape'
57

68
const customElementName = 'ge-autocomplete'
79

@@ -131,6 +133,7 @@ class GEAutocomplete extends HTMLElement {
131133

132134
connectedCallback () {
133135
this.importStyles()
136+
this.importRowTemplate()
134137
this.render()
135138
}
136139

@@ -146,9 +149,29 @@ class GEAutocomplete extends HTMLElement {
146149
this.shadowRoot.appendChild(styles)
147150
}
148151

152+
// importRowTemplate looks for a <template row> tag inside the custom element
153+
// and stores its contents as a lodash template, which is then passed on to
154+
// the autocomplete component
155+
importRowTemplate() {
156+
const tmpl = this.querySelector('template[row]')
157+
if (tmpl === null) return
158+
159+
this.rowTemplate = _template(
160+
_unescape(tmpl.innerHTML.trim()), // unescape is important for `<%` etc. lodash tags
161+
{ variable: 'feature' } // namespace the passed in Feature as `feature` so missing keys don’t throw
162+
)
163+
164+
// contrary to the way custom styles are handled above we remove the <template> when we’re done
165+
// so it doesn’t hang around in the host document (not the Shadow DOM)
166+
tmpl.remove()
167+
}
168+
149169
render () {
150170
ReactDOM.render(
151-
<WebComponent {...this.props} host={this} />,
171+
<WebComponent
172+
{...this.props}
173+
host={this}
174+
rowTemplate={this.rowTemplate} />,
152175
this.shadowRoot
153176
)
154177
}

0 commit comments

Comments
 (0)