-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3683 from marmelab/use-choices
[RFR] Introduce useChoices & useSuggestions hook
- Loading branch information
Showing
24 changed files
with
1,236 additions
and
1,020 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import React from 'react'; | ||
import expect from 'expect'; | ||
import { render, cleanup } from '@testing-library/react'; | ||
import useChoices from './useChoices'; | ||
import { renderWithRedux } from '../util'; | ||
import { TestTranslationProvider } from '../i18n'; | ||
|
||
describe('useChoices hook', () => { | ||
afterEach(cleanup); | ||
const defaultProps = { | ||
choice: { id: 42, name: 'test' }, | ||
optionValue: 'id', | ||
optionText: 'name', | ||
translateChoice: true, | ||
}; | ||
|
||
const Component = ({ | ||
choice, | ||
optionText, | ||
optionValue, | ||
translateChoice, | ||
}) => { | ||
const { getChoiceText, getChoiceValue } = useChoices({ | ||
optionText, | ||
optionValue, | ||
translateChoice, | ||
}); | ||
|
||
return ( | ||
<div data-value={getChoiceValue(choice)}> | ||
{getChoiceText(choice)} | ||
</div> | ||
); | ||
}; | ||
|
||
it('should use optionValue as value identifier', () => { | ||
const { getByText } = render(<Component {...defaultProps} />); | ||
expect(getByText('test').getAttribute('data-value')).toEqual('42'); | ||
}); | ||
|
||
it('should use optionText with a string value as text identifier', () => { | ||
const { queryAllByText } = render(<Component {...defaultProps} />); | ||
expect(queryAllByText('test')).toHaveLength(1); | ||
}); | ||
|
||
it('should use optionText with a function value as text identifier', () => { | ||
const { queryAllByText } = render( | ||
<Component | ||
{...defaultProps} | ||
optionText={choice => choice.foobar} | ||
choice={{ id: 42, foobar: 'test' }} | ||
/> | ||
); | ||
expect(queryAllByText('test')).toHaveLength(1); | ||
}); | ||
|
||
it('should use optionText with an element value as text identifier', () => { | ||
const Foobar = ({ record }: { record?: any }) => ( | ||
<span>{record.foobar}</span> | ||
); | ||
const { queryAllByText } = render( | ||
<Component | ||
{...defaultProps} | ||
optionText={<Foobar />} | ||
choice={{ id: 42, foobar: 'test' }} | ||
/> | ||
); | ||
expect(queryAllByText('test')).toHaveLength(1); | ||
}); | ||
|
||
it('should translate the choice by default', () => { | ||
const { queryAllByText } = renderWithRedux( | ||
<TestTranslationProvider translate={x => `**${x}**`}> | ||
<Component {...defaultProps} /> | ||
</TestTranslationProvider> | ||
); | ||
expect(queryAllByText('test')).toHaveLength(0); | ||
expect(queryAllByText('**test**')).toHaveLength(1); | ||
}); | ||
|
||
it('should not translate the choice if translateChoice is false', () => { | ||
const { queryAllByText } = renderWithRedux( | ||
<TestTranslationProvider translate={x => `**${x}**`}> | ||
<Component {...defaultProps} translateChoice={false} /> | ||
</TestTranslationProvider> | ||
); | ||
expect(queryAllByText('test')).toHaveLength(1); | ||
expect(queryAllByText('**test**')).toHaveLength(0); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { ReactElement, isValidElement, cloneElement, useCallback } from 'react'; | ||
import get from 'lodash/get'; | ||
|
||
import { useTranslate } from '../i18n'; | ||
import { Record } from '../types'; | ||
|
||
export type OptionTextElement = ReactElement<{ | ||
record: Record; | ||
}>; | ||
export type OptionText = (choice: object) => string | OptionTextElement; | ||
|
||
export interface ChoicesProps { | ||
choices: object[]; | ||
optionValue?: string; | ||
optionText?: OptionTextElement | OptionText | string; | ||
translateChoice?: boolean; | ||
} | ||
|
||
export interface UseChoicesOptions { | ||
optionValue?: string; | ||
optionText?: OptionTextElement | OptionText | string; | ||
translateChoice?: boolean; | ||
} | ||
|
||
/* | ||
* Returns helper functions for choices handling. | ||
* | ||
* @param optionText Either a string defining the property to use to get the choice text, a function or a React element | ||
* @param optionValue The property to use to get the choice value | ||
* @param translateChoice A boolean indicating whether to option text should be translated | ||
* | ||
* @returns An object with helper functions: | ||
* - getChoiceText: Returns the choice text or a React element | ||
* - getChoiceValue: Returns the choice value | ||
*/ | ||
const useChoices = ({ | ||
optionText = 'name', | ||
optionValue = 'id', | ||
translateChoice = true, | ||
}: UseChoicesOptions) => { | ||
const translate = useTranslate(); | ||
|
||
const getChoiceText = useCallback( | ||
choice => { | ||
if (isValidElement<{ record: any }>(optionText)) { | ||
return cloneElement<{ record: any }>(optionText, { | ||
record: choice, | ||
}); | ||
} | ||
const choiceName = | ||
typeof optionText === 'function' | ||
? optionText(choice) | ||
: get(choice, optionText); | ||
|
||
return translateChoice | ||
? translate(choiceName, { _: choiceName }) | ||
: choiceName; | ||
}, | ||
[optionText, translate, translateChoice] | ||
); | ||
|
||
const getChoiceValue = useCallback(choice => get(choice, optionValue), [ | ||
optionValue, | ||
]); | ||
|
||
return { | ||
getChoiceText, | ||
getChoiceValue, | ||
}; | ||
}; | ||
|
||
export default useChoices; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import expect from 'expect'; | ||
import { getSuggestionsFactory as getSuggestions } from './useSuggestions'; | ||
|
||
describe('getSuggestions', () => { | ||
const choices = [ | ||
{ id: 1, value: 'one' }, | ||
{ id: 2, value: 'two' }, | ||
{ id: 3, value: 'three' }, | ||
]; | ||
|
||
const defaultOptions = { | ||
choices, | ||
allowEmpty: false, | ||
emptyText: '', | ||
emptyValue: null, | ||
getChoiceText: ({ value }) => value, | ||
getChoiceValue: ({ id }) => id, | ||
limitChoicesToValue: false, | ||
matchSuggestion: undefined, | ||
optionText: 'value', | ||
optionValue: 'id', | ||
selectedItem: undefined, | ||
}; | ||
|
||
it('should return all suggestions when filtered by empty string', () => { | ||
expect(getSuggestions(defaultOptions)('')).toEqual(choices); | ||
}); | ||
|
||
it('should filter choices according to the filter argument', () => { | ||
expect(getSuggestions(defaultOptions)('o')).toEqual([ | ||
{ id: 1, value: 'one' }, | ||
{ id: 2, value: 'two' }, | ||
]); | ||
}); | ||
|
||
it('should filter choices according to the filter argument when it contains RegExp reserved characters', () => { | ||
expect( | ||
getSuggestions({ | ||
...defaultOptions, | ||
choices: [ | ||
{ id: 1, value: '**one' }, | ||
{ id: 2, value: 'two' }, | ||
{ id: 3, value: 'three' }, | ||
], | ||
})('**o') | ||
).toEqual([{ id: 1, value: '**one' }]); | ||
}); | ||
|
||
it('should filter choices according to the currently selected values if selectedItem is an array', () => { | ||
expect( | ||
getSuggestions({ | ||
...defaultOptions, | ||
selectedItem: [choices[0]], | ||
})('') | ||
).toEqual(choices.slice(1)); | ||
}); | ||
|
||
it('should not filter choices according to the currently selected value if selectedItem is not an array and limitChoicesToValue is false', () => { | ||
expect( | ||
getSuggestions({ | ||
...defaultOptions, | ||
limitChoicesToValue: false, | ||
selectedItem: choices[0], | ||
})('one') | ||
).toEqual(choices); | ||
}); | ||
|
||
it('should filter choices according to the currently selected value if selectedItem is not an array and limitChoicesToValue is true', () => { | ||
expect( | ||
getSuggestions({ | ||
...defaultOptions, | ||
limitChoicesToValue: true, | ||
selectedItem: choices[0], | ||
})('one') | ||
).toEqual([choices[0]]); | ||
}); | ||
it('should add emptySuggestion if allowEmpty is true', () => { | ||
expect( | ||
getSuggestions({ | ||
...defaultOptions, | ||
allowEmpty: true, | ||
})('') | ||
).toEqual([ | ||
{ id: 1, value: 'one' }, | ||
{ id: 2, value: 'two' }, | ||
{ id: 3, value: 'three' }, | ||
{ id: null, value: '' }, | ||
]); | ||
}); | ||
|
||
it('should limit the number of choices', () => { | ||
expect( | ||
getSuggestions({ | ||
...defaultOptions, | ||
suggestionLimit: 2, | ||
})('') | ||
).toEqual([{ id: 1, value: 'one' }, { id: 2, value: 'two' }]); | ||
|
||
expect( | ||
getSuggestions({ | ||
...defaultOptions, | ||
suggestionLimit: 2, | ||
allowEmpty: true, | ||
})('') | ||
).toEqual([ | ||
{ id: 1, value: 'one' }, | ||
{ id: 2, value: 'two' }, | ||
{ id: null, value: '' }, | ||
]); | ||
}); | ||
}); |
Oops, something went wrong.