Skip to content

Commit

Permalink
pkp/pkp-lib#10624 Add slots for rendering FieldBaseAutosuggest options
Browse files Browse the repository at this point in the history
  • Loading branch information
blesildaramirez committed Nov 22, 2024
1 parent a67fd26 commit 3039639
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 62 deletions.
9 changes: 9 additions & 0 deletions src/components/Form/fields/FieldBaseAutosuggest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,14 @@ The following autosuggest fields are available to be used.
- `FieldControlledVocab` can be used to enter metadata such as keywords and subjects.
- `FieldSelectSubmissions` can be used to find and select submissions.

## Customizing options

The component allows for flexible customization of its options through named slots. This lets you control how suggestions are displayed and interact within the options. See [Custom Options](?path=/story/forms-fieldbaseautosuggest--custom-options) for sample implemenation.

### Named Slots

- `input-slot`: Customizes the input area of the combobox.
- `option`: Defines how each suggestion is rendered in the dropdown.

<Primary />
<Controls />
86 changes: 86 additions & 0 deletions src/components/Form/fields/FieldBaseAutosuggest.stories.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {http, HttpResponse} from 'msw';

import FieldBaseAutosuggest from './FieldBaseAutosuggest.vue';
import Icon from '@/components/Icon/Icon.vue';
import FieldBaseMock from '../mocks/field-base';
import FieldBaseAutosuggestMock from '../mocks/field-autosuggest';
import UsernamesMock from '@/mocks/usernames.json';
import InstitutionsMock from '@/mocks/institutions.json';

export default {
title: 'Forms/FieldBaseAutosuggest',
Expand Down Expand Up @@ -97,3 +99,87 @@ export const Inline = {
isLabelInline: true,
},
};

const RORExample = {
extends: FieldBaseAutosuggest,
methods: {
setSuggestions(items) {
// Escape the search phrase for regex
// See: https://stackoverflow.com/a/3561711/1723499
const regex = new RegExp(
this.inputValue.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
'gi',
);

const suggestions = items
.filter((u) => u.fullName.match(regex))
.filter((u) => !this.currentValue.includes(u.id))
.map((u) => {
return {
value: u.id,
label: u.fullName,
hasSlot: u.ror,
};
});

this.suggestions = [...suggestions];
},
},
};

export const CustomOptions = {
render: (args) => ({
components: {RORExample, Icon},
setup() {
function change(name, prop, newValue, localeKey) {
if (localeKey) {
args[prop][localeKey] = newValue;
} else {
args[prop] = newValue;
}
}

return {args, change};
},
template: `
<RORExample v-bind="args" @change="change">
<template #input-slot>
<Icon icon="ROR" class="ms-2 h-auto w-6" :inline="true" />
</template>
<template #option="{ suggestion }">
{{ suggestion.label }}
<Icon v-if="suggestion.hasSlot" icon="ROR" class="ms-2 h-auto w-6" :inline="true" />
<a v-if="suggestion.hasSlot" href="#" target="_blank" class="ms-auto flex">
<span class="sr-only">Open link in new tab</span>
<Icon icon="NewTab" class="text-primary h-5 w-5" :inline="true" />
</a>
</template>
</RORExample>
`,
}),
args: {
...FieldBaseMock,
...FieldBaseAutosuggestMock,
apiUrl: 'https://mock/index.php/publicknowledge/api/v1/users',
label: 'Select Users',
selected: [],
isMultiple: false,
},
parameters: {
msw: {
handlers: [
http.get(
'https://mock/index.php/publicknowledge/api/v1/users',
async (req, res, ctx) => {
return HttpResponse.json(InstitutionsMock);
},
),
],
},
docs: {
story: {
height: '300px',
},
},
},
};
86 changes: 25 additions & 61 deletions src/components/Form/fields/FieldBaseAutosuggest.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
class="pkpAutosuggest__selection"
>
{{ item.label }}
<slot v-if="item.hasSlot" name="input-slot" />
<button
v-if="!isDisabled"
class="pkpAutosuggest__deselect text-negative hover:text-on-dark"
Expand All @@ -102,59 +103,20 @@
</span>
</button>
</PkpBadge>
<Combobox
<FieldComboBox
v-if="!isDisabled"
:id="autosuggestId"
:key="autosuggestId"
:model-value="null"
class="pkpAutosuggest__autosuggester"
as="div"
@update:modelValue="selectSuggestion"
ref="cb"
v-model:inputValue="inputValue"
v-model:isFocused="isFocused"
:input-props="inputProps"
:suggestions="suggestions"
@update:model-value="selectSuggestion"
>
<ComboboxInput
ref="autosuggestInput"
class="pkpAutosuggest__input"
v-bind="inputProps"
@change="inputValue = $event.target.value.trim()"
@focus="() => (isFocused = true)"
@blur="() => (isFocused = false)"
/>
<ComboboxOptions
v-if="suggestions.length || (allowCustom && inputValue?.length)"
class="autosuggest__results-container autosuggest__results"
>
<ComboboxOption
v-if="
allowCustom &&
inputValue?.length &&
!suggestions.includes(inputValue)
"
v-slot="{active}"
as="template"
>
<li
class="autosuggest__results-item"
:class="active && 'autosuggest__results-item--highlighted'"
>
{{ inputValue }}
</li>
</ComboboxOption>
<ComboboxOption
v-for="suggestion in suggestions"
:key="suggestion.value"
v-slot="{active}"
:value="suggestion"
as="template"
>
<li
class="autosuggest__results-item"
:class="active && 'autosuggest__results-item--highlighted'"
>
{{ suggestion.label }}
</li>
</ComboboxOption>
</ComboboxOptions>
</Combobox>
<template v-if="$slots.option" #option="{suggestion}">
<slot name="option" :suggestion="suggestion"></slot>
</template>
</FieldComboBox>
<span class="pkpAutosuggest__endslot">
<slot name="end"></slot>
</span>
Expand All @@ -176,6 +138,7 @@

<script>
import FieldBase from './FieldBase.vue';
import FieldComboBox from './FieldComboBox.vue';
import PkpBadge from '@/components/Badge/Badge.vue';
import FormFieldLabel from '@/components/Form/FormFieldLabel.vue';
import Tooltip from '@/components/Tooltip/Tooltip.vue';
Expand All @@ -184,29 +147,20 @@ import FieldError from '@/components/Form/FieldError.vue';
import MultilingualProgress from '@/components/MultilingualProgress/MultilingualProgress.vue';
import Icon from '@/components/Icon/Icon.vue';
import {
Combobox,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue';
import ajaxError from '@/mixins/ajaxError';
import debounce from 'debounce';
export default {
name: 'FieldBaseAutosuggest',
components: {
PkpBadge,
Combobox,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
FormFieldLabel,
Tooltip,
HelpButton,
FieldError,
Icon,
MultilingualProgress,
FieldComboBox,
},
extends: FieldBase,
mixins: [ajaxError],
Expand Down Expand Up @@ -256,6 +210,11 @@ export default {
type: String,
required: true,
},
/** If the combobox should allow multiple options to be selected */
isMultiple: {
type: Boolean,
default: () => true,
},
},
data() {
return {
Expand Down Expand Up @@ -357,7 +316,7 @@ export default {
return;
}
this.$refs.autosuggestInput.$el.focus();
this.$refs.cb.$refs.autosuggestInput.$el.focus();
},
/**
Expand Down Expand Up @@ -421,6 +380,11 @@ export default {
* Emit events to change the selected items and the field's value
*/
setSelected(selected) {
if (selected?.length > 1 && !this.isMultiple) {
// override selected value if only one option can be selected
selected = [selected[1]];
}
this.$emit('change', this.name, 'selected', selected, this.localeKey);
this.$emit(
'change',
Expand Down
86 changes: 86 additions & 0 deletions src/components/Form/fields/FieldComboBox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<Combobox
:id="id"
:key="id"
:model-value="null"
class="pkpAutosuggest__autosuggester"
as="div"
>
<ComboboxInput
ref="autosuggestInput"
class="pkpAutosuggest__input"
v-bind="inputProps"
@change="(event) => handleChange(event, emit)"
@focus="() => handleFocus(emit)"
@blur="() => handleBlur(emit)"
/>
<ComboboxOptions
v-if="suggestions.length || (allowCustom && localInputValue?.length)"
class="autosuggest__results-container autosuggest__results"
>
<ComboboxOption
v-if="
allowCustom &&
localInputValue?.length &&
!suggestions.includes(localInputValue)
"
v-slot="{active}"
as="template"
>
<li
class="autosuggest__results-item"
:class="active && 'autosuggest__results-item--highlighted'"
>
{{ localInputValue }}
</li>
</ComboboxOption>
<ComboboxOption
v-for="suggestion in suggestions"
:key="suggestion.value"
v-slot="{active}"
:value="suggestion"
as="template"
>
<li
class="autosuggest__results-item flex items-center"
:class="active && 'autosuggest__results-item--highlighted'"
>
<slot v-if="slots['option']" name="option" :suggestion="suggestion" />
<span v-else>{{ suggestion.label }}</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</Combobox>
</template>
<script setup>
import {useSlots} from 'vue';
import {
Combobox,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue';
import {useAutosuggest} from '@/composables/useAutosuggest';
const slots = useSlots();
defineProps({
id: {
type: String,
required: true,
},
inputProps: {
type: Object,
required: true,
},
suggestions: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:inputValue', 'update:isFocused']);
const {allowCustom, localInputValue, handleChange, handleFocus, handleBlur} =
useAutosuggest();
</script>
5 changes: 4 additions & 1 deletion src/components/Form/fields/FieldControlledVocab.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
<script>
import {provide} from 'vue';
import FieldBaseAutosuggest from './FieldBaseAutosuggest.vue';
import debounce from 'debounce';
export default {
name: 'FieldControlledVocab',
extends: FieldBaseAutosuggest,
setup() {
provide('allowCustom', true);
},
data() {
return {
allSuggestions: [],
suggestionsLoaded: false,
suggestionsLoading: false,
allowCustom: true,
};
},
methods: {
Expand Down
Loading

0 comments on commit 3039639

Please sign in to comment.