Overhaul the autocomplete component, for sake of new custorder
turns out we had some issues with our understanding of how that all was supposed to work. this seems to be much cleaner and even semi-documented :)
This commit is contained in:
parent
232a02b944
commit
52fbe73893
|
@ -4,16 +4,54 @@ const TailboneAutocomplete = {
|
||||||
template: '#tailbone-autocomplete-template',
|
template: '#tailbone-autocomplete-template',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
|
||||||
|
// this is the "input" field name essentially. primarily is
|
||||||
|
// useful for "traditional" tailbone forms; it normally is not
|
||||||
|
// used otherwise. it is passed as-is to the buefy
|
||||||
|
// autocomplete component `name` prop
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
|
// the url from which search results are to be obtained. the
|
||||||
|
// url should expect a GET request with a query string with a
|
||||||
|
// single `term` parameter, and return results as a JSON array
|
||||||
|
// containing objects with `value` and `label` properties.
|
||||||
serviceUrl: String,
|
serviceUrl: String,
|
||||||
|
|
||||||
|
// callers do not specify this directly but rather by way of
|
||||||
|
// the `v-model` directive. this component will emit `input`
|
||||||
|
// events when the value changes
|
||||||
value: String,
|
value: String,
|
||||||
|
|
||||||
|
// callers may set an initial label if needed. this is useful
|
||||||
|
// in cases where the autocomplete needs to "already have a
|
||||||
|
// value" on page load. for instance when a user fills out
|
||||||
|
// the autocomplete field, but leaves other required fields
|
||||||
|
// blank and submits the form; page will re-load showing
|
||||||
|
// errors but the autocomplete field should remain "set" -
|
||||||
|
// normally it is only given a "value" (e.g. uuid) but this
|
||||||
|
// allows for the "label" to display correctly as well
|
||||||
initialLabel: String,
|
initialLabel: String,
|
||||||
assignedValue: String,
|
|
||||||
|
// TODO: i am not sure this is needed? but current logic does
|
||||||
|
// handle it specially, so am leaving for now. if this prop
|
||||||
|
// is set by the caller, then the `assignedLabel` will *always*
|
||||||
|
// be shown for the button (when "selection" has been made)
|
||||||
assignedLabel: String,
|
assignedLabel: String,
|
||||||
|
|
||||||
|
// simple placeholder text for the input box
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
|
|
||||||
|
// TODO: pretty sure this can be ignored..?
|
||||||
|
// (should deprecate / remove if so)
|
||||||
|
assignedValue: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
||||||
|
// we want to track the "currently selected option" - which
|
||||||
|
// should normally be `null` to begin with, unless we were
|
||||||
|
// given a value, in which case we use `initialLabel` to
|
||||||
|
// complete the option
|
||||||
let selected = null
|
let selected = null
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
selected = {
|
selected = {
|
||||||
|
@ -21,89 +59,55 @@ const TailboneAutocomplete = {
|
||||||
label: this.initialLabel,
|
label: this.initialLabel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
selected: selected,
|
|
||||||
isFetching: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
return {
|
||||||
value(to, from) {
|
|
||||||
if (from && !to) {
|
// this contains the search results; its contents may
|
||||||
this.clearSelection(false)
|
// change over time as new searches happen. the
|
||||||
}
|
// "currently selected option" should be one of these,
|
||||||
},
|
// unless it is null
|
||||||
|
data: [],
|
||||||
|
|
||||||
|
// this tracks our "currently selected option" - per above
|
||||||
|
selected: selected,
|
||||||
|
|
||||||
|
// since we are wrapping a component which also makes use
|
||||||
|
// of the "value" paradigm, we must separate the concerns.
|
||||||
|
// so we use our own `value` prop to interact with the
|
||||||
|
// caller, but then we use this `buefyValue` data point to
|
||||||
|
// communicate with the buefy autocomplete component.
|
||||||
|
// note that `this.value` will always be either a uuid or
|
||||||
|
// null, whereas `this.buefyValue` may be raw text as
|
||||||
|
// entered by the user.
|
||||||
|
buefyValue: this.value,
|
||||||
|
|
||||||
|
// // TODO: we are "setting" this at the appropriate time,
|
||||||
|
// // but not clear if that actually affects anything.
|
||||||
|
// // should we just remove it?
|
||||||
|
// isFetching: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
clearSelection(focus) {
|
// fetch new search results from the server. this is invoked
|
||||||
if (focus === undefined) {
|
// via the `@typing` event from buefy autocomplete component.
|
||||||
focus = true
|
// the doc at https://buefy.org/documentation/autocomplete
|
||||||
}
|
// mentions `debounce` as being optional. at one point i
|
||||||
this.selected = null
|
// thought it would fix a performance bug; not sure `debounce`
|
||||||
this.value = null
|
// helped but figured might as well leave it
|
||||||
if (focus) {
|
|
||||||
this.$nextTick(function() {
|
|
||||||
this.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: should emit event for caller logic (can they cancel?)
|
|
||||||
// $('#' + oid + '-textbox').trigger('autocompletevaluecleared');
|
|
||||||
},
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.$refs.autocomplete.focus()
|
|
||||||
},
|
|
||||||
|
|
||||||
getDisplayText() {
|
|
||||||
if (this.assignedLabel) {
|
|
||||||
return this.assignedLabel
|
|
||||||
}
|
|
||||||
if (this.selected) {
|
|
||||||
return this.selected.display || this.selected.label
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: should we allow custom callback? or is event enough?
|
|
||||||
// function (oid) {
|
|
||||||
// $('#' + oid + '-textbox').on('autocompletevaluecleared', function() {
|
|
||||||
// ${cleared_callback}();
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
selectionMade(option) {
|
|
||||||
this.selected = option
|
|
||||||
|
|
||||||
// TODO: should emit event for caller logic (can they cancel?)
|
|
||||||
// $('#' + oid + '-textbox').trigger('autocompletevalueselected',
|
|
||||||
// [ui.item.value, ui.item.label]);
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: should we allow custom callback? or is event enough?
|
|
||||||
// function (oid) {
|
|
||||||
// $('#' + oid + '-textbox').on('autocompletevalueselected', function(event, uuid, label) {
|
|
||||||
// ${selected_callback}(uuid, label);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
itemSelected(value) {
|
|
||||||
if (this.selected || !value) {
|
|
||||||
this.$emit('input', value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: buefy example uses `debounce()` here and perhaps we should too?
|
|
||||||
// https://buefy.org/documentation/autocomplete
|
|
||||||
getAsyncData: debounce(function (entry) {
|
getAsyncData: debounce(function (entry) {
|
||||||
|
|
||||||
|
// since the `@typing` event from buefy component does not
|
||||||
|
// "self-regulate" in any way, we a) use `debounce` above,
|
||||||
|
// but also b) skip the search unless we have at least 3
|
||||||
|
// characters of input from user
|
||||||
if (entry.length < 3) {
|
if (entry.length < 3) {
|
||||||
this.data = []
|
this.data = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isFetching = true
|
|
||||||
|
// and perform the search
|
||||||
this.$http.get(this.serviceUrl + '?term=' + encodeURIComponent(entry))
|
this.$http.get(this.serviceUrl + '?term=' + encodeURIComponent(entry))
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
this.data = data
|
this.data = data
|
||||||
|
@ -112,10 +116,81 @@ const TailboneAutocomplete = {
|
||||||
this.data = []
|
this.data = []
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
.finally(() => {
|
|
||||||
this.isFetching = false
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// this method is invoked via the `@select` event of the buefy
|
||||||
|
// autocomplete component. the `option` received will either
|
||||||
|
// be `null` or else a simple object with (at least) `value`
|
||||||
|
// and `label` properties
|
||||||
|
selectionMade(option) {
|
||||||
|
|
||||||
|
// we want to keep track of the "currently selected
|
||||||
|
// option" so we can display its label etc. also this
|
||||||
|
// helps control the visibility of the autocomplete input
|
||||||
|
// field vs. the button which indicates the field has a
|
||||||
|
// value
|
||||||
|
this.selected = option
|
||||||
|
|
||||||
|
// reset the internal value for buefy autocomplete
|
||||||
|
// component. note that this value will normally hold
|
||||||
|
// either the raw text entered by the user, or a uuid. we
|
||||||
|
// will not be needing either of those b/c they are not
|
||||||
|
// visible to user once selection is made, and if the
|
||||||
|
// selection is cleared we want user to start over anyway
|
||||||
|
this.buefyValue = null
|
||||||
|
|
||||||
|
// here is where we alert callers to the new value
|
||||||
|
this.$emit('input', option ? option.value : null)
|
||||||
|
},
|
||||||
|
|
||||||
|
// clear the field of any value, i.e. set the "currently
|
||||||
|
// selected option" to null. this is invoked when you click
|
||||||
|
// the button, which is visible while the field has a value.
|
||||||
|
// but callers can invoke it directly as well.
|
||||||
|
clearSelection(focus) {
|
||||||
|
|
||||||
|
// clear selection for the buefy autocomplete component
|
||||||
|
this.$refs.autocomplete.setSelected(null)
|
||||||
|
|
||||||
|
// maybe set focus to our (autocomplete) component
|
||||||
|
if (focus) {
|
||||||
|
this.$nextTick(function() {
|
||||||
|
this.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// set focus to this component, which will just set focus to
|
||||||
|
// the buefy autocomplete component
|
||||||
|
focus() {
|
||||||
|
this.$refs.autocomplete.focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
// this determines the "display text" for the button, which is
|
||||||
|
// shown when a selection has been made (or rather, when the
|
||||||
|
// field actually has a value)
|
||||||
|
getDisplayText() {
|
||||||
|
|
||||||
|
// always use the "assigned" label if we have one
|
||||||
|
// TODO: where is this used? what is the use case?
|
||||||
|
if (this.assignedLabel) {
|
||||||
|
return this.assignedLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a "currently selected option" then use its
|
||||||
|
// label. all search results / options have a `label`
|
||||||
|
// property as that is shown directly in the autocomplete
|
||||||
|
// dropdown. but if the option also has a `display`
|
||||||
|
// property then that is what we will show in the button.
|
||||||
|
// this way search results can show one thing in the
|
||||||
|
// search dropdown, and another in the button.
|
||||||
|
if (this.selected) {
|
||||||
|
return this.selected.display || this.selected.label
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have nothing to go on here..
|
||||||
|
return ""
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,12 +65,11 @@
|
||||||
<b-autocomplete ref="autocomplete"
|
<b-autocomplete ref="autocomplete"
|
||||||
:name="name"
|
:name="name"
|
||||||
v-show="!assignedValue && !selected"
|
v-show="!assignedValue && !selected"
|
||||||
v-model="value"
|
v-model="buefyValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:data="data"
|
:data="data"
|
||||||
@typing="getAsyncData"
|
@typing="getAsyncData"
|
||||||
@select="selectionMade"
|
@select="selectionMade"
|
||||||
@input="itemSelected"
|
|
||||||
keep-first>
|
keep-first>
|
||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
{{ props.option.label }}
|
{{ props.option.label }}
|
||||||
|
@ -79,7 +78,7 @@
|
||||||
|
|
||||||
<b-button v-if="assignedValue || selected"
|
<b-button v-if="assignedValue || selected"
|
||||||
style="width: 100%; justify-content: left;"
|
style="width: 100%; justify-content: left;"
|
||||||
@click="clearSelection()">
|
@click="clearSelection(true)">
|
||||||
{{ getDisplayText() }} (click to change)
|
{{ getDisplayText() }} (click to change)
|
||||||
</b-button>
|
</b-button>
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,7 @@
|
||||||
<div :style="{'flex-grow': contactNotes.length ? 0 : 1}">
|
<div :style="{'flex-grow': contactNotes.length ? 0 : 1}">
|
||||||
|
|
||||||
<b-field label="Customer" grouped>
|
<b-field label="Customer" grouped>
|
||||||
<b-field style="margin-left: 1rem;""
|
<b-field style="margin-left: 1rem;"
|
||||||
:expanded="!contactUUID">
|
:expanded="!contactUUID">
|
||||||
<tailbone-autocomplete ref="contactAutocomplete"
|
<tailbone-autocomplete ref="contactAutocomplete"
|
||||||
v-model="contactUUID"
|
v-model="contactUUID"
|
||||||
|
@ -927,12 +927,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
||||||
contactIsKnown: function(val) {
|
contactIsKnown: function(val) {
|
||||||
// if user has already specified a proper contact, then
|
|
||||||
// clicks the "contact is unknown" button, then we want
|
// if user has already specified a proper contact,
|
||||||
// to *clear out* the existing contact
|
// i.e. `contactUUID` is not null, *and* user has
|
||||||
if (!val && this.contactUUID) {
|
// clicked the "contact is not yet in the system"
|
||||||
this.contactChanged(null)
|
// button, i.e. `val` is false, then we want to *clear
|
||||||
|
// out* the existing contact selection. this is
|
||||||
|
// primarily to avoid any ambiguity.
|
||||||
|
if (this.contactUUID && !val) {
|
||||||
|
this.$refs.contactAutocomplete.clearSelection()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1053,6 +1058,7 @@
|
||||||
% else:
|
% else:
|
||||||
that.contactUUID = response.data.person_uuid
|
that.contactUUID = response.data.person_uuid
|
||||||
% endif
|
% endif
|
||||||
|
that.contactDisplay = response.data.contact_display
|
||||||
that.orderPhoneNumber = response.data.phone_number
|
that.orderPhoneNumber = response.data.phone_number
|
||||||
that.orderEmailAddress = response.data.email_address
|
that.orderEmailAddress = response.data.email_address
|
||||||
that.addOtherPhoneNumber = response.data.add_phone_number
|
that.addOtherPhoneNumber = response.data.add_phone_number
|
||||||
|
|
Loading…
Reference in a new issue