Usage
The HTML and JavaScript tabs build the identical setup shown above, declaratively via
attributes or imperatively via the JS API. allowCustomValue lets a value with
no matching item still be selected, multiSelectTagging renders each selection
as a removable button and disableExpandOnClick keeps this feeling like a
search field rather than a dropdown, the list only opens once there's something to search
for. The .NET tab shows why a tag someone types never becomes visible to anyone else.
<div id="comboBoxTags"
ui-type="ComboBox"
ui-observing="true"
ui-placeholder="Search or add a tag"
ui-allow-input="true"
ui-load-on-demand="true"
ui-multi-select="true"
ui-multi-select-tagging="true"
ui-allow-custom-value="true"
ui-disable-expand-on-click="true"
ui-ajax='{"load":{"url":"DataServer"}}'
ui-events='{
"onPreLoadItemList": "$bindary.controller.comboBoxTags.preLoadItemList",
"onItemSelect": "$bindary.controller.comboBoxTags.itemSelect"
}'>
<div ui-template="Default" style="display:none">{text}</div>
<div ui-template="NoResult" style="display:none"><span>No matching tags, press Enter to add this one</span></div>
</div>
The functions referenced above by name live on this demo's Bindary controller:
$bindary.controller.comboBoxTags =
{
preLoadItemList: (comboBox, args) =>
{
comboBox.ajax.load.customParameters = window.JSON.stringify(
window.createDataServerMsg('Components.ComboBoxTagController.LoadTagData')
);
},
// Fires for every selection, including a brand-new custom value the
// person just typed. A custom tag's id and text are identical (see
// addCustomValue in ComboBox.js), an existing tag's id never matches
// its display text, that's how we tell them apart here.
itemSelect: async (comboBox, args) =>
{
const item = args.item;
if (item.id !== item.text)
return; // existing tag, nothing to add
await $bindary.dataSendAsync({ text: item.text }, 'Components.ComboBoxTagController.AddSessionTag');
}
};
const comboBoxTags = $UI.createComponent(componyx.UI.ComboBox, {
id: 'comboBoxTags',
observing: true,
placeholder: 'Search or add a tag',
allowInput: true,
loadOnDemand: true,
multiSelect: true,
multiSelectTagging: true,
allowCustomValue: true,
disableExpandOnClick: true,
containerElement: document.body
});
comboBoxTags.ajax.load.url = 'DataServer';
comboBoxTags.events.onPreLoadItemList.add(function ()
{
comboBoxTags.ajax.load.customParameters = window.JSON.stringify(
window.createDataServerMsg('Components.ComboBoxTagController.LoadTagData')
);
});
comboBoxTags.events.onItemSelect.add(async function (comboBox, args)
{
const item = args.item;
if (item.attributes && item.attributes.existing)
return; // came from the server, nothing to add
await $bindary.dataSendAsync(
{ text: item.text },
'Components.ComboBoxTagController.AddSessionTag');
});
comboBoxTags.setNoResultTemplate('<span>No matching tags, press Enter to add this one</span>');
comboBoxTags.show();
public static class ComboBoxTagController
{
private const string SessionKey = "Tags";
private static readonly List<string> BuiltInTags = new List<string>
{
"JavaScript", "TypeScript", "CSS", "HTML", "Accessibility",
"Performance", "Data Binding", "Validation", "Responsive Design", "Dark Mode",
"Animation", "Testing", "Documentation", "API", "Component",
"Theming", "Localization", "Forms", "Grids", "Routing"
};
public static async Task LoadTagData(Client client, DataRequest request)
{
TagRequest tagRequest = JsonConvert.DeserializeObject<TagRequest>(request.Message);
ItemList<ComboBoxItem> itemList = new ItemList<ComboBoxItem>();
ComboBoxAjaxResult result = new ComboBoxAjaxResult(itemList);
// Combine the shared built-in tags with tags added during this session.
// This provides the autocomplete suggestions as the person types,
// without exposing session-specific tags to other visitors.
List<string> sessionTags = GetSessionTags(client);
IEnumerable<string> allTags = BuiltInTags.Concat(sessionTags);
if (!string.IsNullOrEmpty(tagRequest.Text))
allTags = allTags.Where(t =>
t.IndexOf(tagRequest.Text, StringComparison.OrdinalIgnoreCase) > -1);
foreach (var tag in allTags.Take(10))
{
var attributes = new Dictionary<string, object>
{
{ "existing", true }
};
itemList.Add(new ComboBoxItem
{
Id = tag,
Value = tag,
Text = tag,
Attributes = new Attributes(attributes)
});
}
await Server.Send(client, result.Serialize(), request.XhrRequestId);
}
public static async Task AddSessionTag(Client client, DataRequest request)
{
TagRequest addRequest = JsonConvert.DeserializeObject<TagRequest>(request.Message);
string text = addRequest.Text?.Trim();
if (string.IsNullOrEmpty(text))
{
await Server.Send(
client,
JsonConvert.SerializeObject(new { success = false }),
request.XhrRequestId);
return;
}
List<string> sessionTags = GetSessionTags(client);
if (!sessionTags.Contains(text, StringComparer.OrdinalIgnoreCase))
sessionTags.Add(text);
SetSessionTags(client, sessionTags);
await Server.Send(
client,
JsonConvert.SerializeObject(new { success = true }),
request.XhrRequestId);
}
private static List<string> GetSessionTags(Client client)
{
string json = client.Session.GetString(SessionKey);
return string.IsNullOrEmpty(json)
? new List<string>()
: JsonConvert.DeserializeObject<List<string>>(json);
}
private static void SetSessionTags(Client client, List<string> tags)
{
client.Session.SetString(
SessionKey,
JsonConvert.SerializeObject(tags));
}
}
public class TagRequest
{
public string Text { get; set; }
}