Customizing Search
If optionally you wish to add the ability for users to free text search your list of items (and your API supports such functionality), you can add just a couple of plugins to do so.
The first thing you need to add is a SearchBar plugin, which will look something like this:
SearchBar Plugin installation¶
A SearchBar at its core is a DataViewer, so it's installed using that plugin type, but specifically tuned to be used as a search plugin on GroupScreen's
molten.addPlugin( 'DataViewer', {
id : 'molten.dataViewer.search.PetSearchBar',
type : 'molten.Search',
name : 'Search Bar',
icon : 'fa fa-search fa-fw',
// this is what tells the GroupScreen where to put this plugin
location : [ 'search' ],
// this search bar can only be used against group screens of petApi.pet's
matches : {
type : 'GroupDataViewer',
objectType : 'petApi.pet'
},
component : SearchBar,
} )
SearchBar Component¶
For the ease of the pet shop demo, we opted to use the SearchBar from molten, and simply wrap it to adjust some of its props
import React from 'react'
import ResponsiveSearchBar from '../../../src/screens/group/search/SearchBar'
export default function SearchBar( props ) {
const { objectType, path } = props
const client = 'molten.group.SearchBar'
// changing the use prevents Imagine API suggestions from being rendered, which would break
const suggestionsMatchContext = { use : 'petApi.suggestions', client, objectType, path }
return <ResponsiveSearchBar {...props} suggestionsMatchContext={suggestionsMatchContext} />
}
Suggestions Plugin Installation¶
SearchSuggesters are a specialized plugin that allows you to tack additional suggestions onto any search bar (not just the pet shop one!)
molten.addPlugin( 'SearchSuggester', {
id : 'petApi.Suggestions',
name : 'API Suggestions',
// NOTE: this use matches the use from the SearchBar component suggestionsMatchContext
matches : { use : 'petApi.suggestions', client : 'molten.group.SearchBar' },
handles : () => true,
// could be useful for storing recent searches
onSearchChange : ( { search } ) => {},
suggestions : Suggestions
} )
Suggestions Component¶
This component as a note is very similar to the ApiSuggestions in the molten library, but with some changes to make it work with the pet shop example.
Get Suggestions Function¶
import { GlobalState } from '@leverege/ui-redux'
import { Attributes } from '@leverege/ui-attributes'
import Path from '@leverege/path'
import { Config } from '@leverege/plugin'
import Model from '../../../src/screens/group/search/filter/GroupedTextFilterModel'
const getSuggestions = async ( actions, filter, search, objectType, api ) => {
const f = filter
const res = await GlobalState.dispatch( actions.search( {
filter : f ? { ...f, value : Model.toImagineValue( search, filter, objectType ) } : null,
limit : 100
}, { queryName : 'searchSuggestions' } ) )
const items = {}
const toCheck = Model.getLastToken( search ).toLowerCase()
if ( f?.fields && f.fields.length > 0 ) {
f.fields.forEach( ( field ) => {
let accessor = () => null
if ( typeof field === 'string' ) {
accessor = Path( field )
} else if ( typeof field === 'object' && field?.field ) {
accessor = Path( field.field )
}
res?.items.forEach( ( i ) => {
const val = accessor.get( i )
if ( typeof val === 'string' && val.toLowerCase().includes( toCheck ) ) {
items[val] = true
}
} )
} )
} else {
const attrs = Attributes.getAttributesFor( objectType )
attrs.forEach( ( attr ) => {
if ( attr.valueType !== 'string' ) {
return
}
res?.items.forEach( ( i ) => {
const val = attr.get( i )
if ( typeof val === 'string' && val.toLowerCase().includes( toCheck ) ) {
items[val] = true
}
} )
} )
}
return Object.keys( items )
}
const updateSuggestions = ( opts ) => {
const { actions, filterModel, search, objectType } = opts
const filter = Model.toImagineFilter( filterModel, objectType )
if ( !search || Model.getLastToken( search ).length < Config.get( 'ApiSuggestions', 'minSuggestionLength', 3 ) ) {
opts.setItems( [] )
return Promise.resolve( [] )
}
// do this so we can get at the raw api object and not touch api-redux
return new Promise( ( resolve ) => {
process.nextTick(
GlobalState.dispatch,
async ( dispatch, getState, { api } ) => {
const items = await getSuggestions( actions, filter, search, objectType, api )
opts.setItems( items )
resolve( items )
}
)
} )
}
export {
updateSuggestions,
getSuggestions
}
Suggestion Component¶
import React from 'react'
import Suggestion from '../../../src/screens/group/search/suggestions/Suggestion'
// an individual suggestion
export default function PetSuggestion( props ) {
const { item, search } = props
const match = item?.toLowerCase().lastIndexOf( search?.toLowerCase() )
if (
!item ||
match < 0 ||
item?.toLowerCase() === search?.toLowerCase() ||
search?.toLowerCase().endsWith( item?.toLowerCase() )
) {
return null
}
const contents = (
<>
{item.slice( 0, match )}
<b>{item.slice( match, match + search.length )}</b>
{item.slice( match + search.length )}
</>
)
return (
<Suggestion {...props} newSearch={item} contents={contents} />
)
}
Suggestions Component¶
import React from 'react'
import { Config } from '@leverege/plugin'
import Suggestion from './Suggestion'
import Suggestions from '../../../src/screens/group/search/suggestions/Suggestions'
import { updateSuggestions } from './Util'
const searchLongEnough = ( search ) => {
return search?.length >= Config.get( 'ApiSuggestions', 'minSuggestionLength', 3 )
}
// a list of suggestions
export default function PetSuggestions( props ) {
return (
<Suggestions
{...props}
updateSuggestions={updateSuggestions}
searchLongEnough={searchLongEnough}
suggestionClass={Suggestion}
minSuggestionLength={Config.get( 'ApiSuggestions', 'minSuggestionLength', 3 )}
searchDebounceMs={Config.get( 'ApiSuggestions', 'searchDebounceMs', 100 )} />
)
}