I am building a new/edit item form using Vuetify(v2.6.3). My form has multiple combobox's that pull their items from a backend API as objects. I would like to be able to add new items, which the combobox is able to do. However, when I add a new item it's added as a string. Is there a way to add that new item as an object instead?
I only need the new object to have a name key in order to send it to the API to be created
Desired New item from v-combobox:
{
name: 'New Item',
}
Simplified list of items returned from API and available as combobox choices:
[
{
id: 1,
name: 'Item 1',
createdAt: '2020-01-01',
updatedAt: '2020-01-01'
},
{
id: 2,
name: 'Item 2',
createdAt: '2020-01-01',
updatedAt: '2020-01-01'
},
{
id: 3,
name: 'Item 3',
createdAt: '2020-01-01',
updatedAt: '2020-01-01'
}
]
Here is a simplified version of my Form:
<template>
<v-card>
<v-card-title>
<span class="text-h5">Edit Item</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col>
<v-combobox
v-model="item"
:items="items"
item-text="name"
item-value="id"
label="ComboBox"
return-object
></v-combobox>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card>
</template>
<script>
export default {
name: 'TestForm',
data() {
return {
item: null,
items: [
{
id: 1,
name: 'Item 1',
createdAt: '2020-01-01',
updatedAt: '2020-01-01',
},
{
id: 2,
name: 'Item 2',
createdAt: '2020-01-01',
updatedAt: '2020-01-01',
},
{
id: 3,
name: 'Item 3',
createdAt: '2020-01-01',
updatedAt: '2020-01-01',
},
],
}
},
}
</script>
In my method which sends the form to the API, I could check the value of each combobox and convert to an object if needed, but I am wondering if it's possible to handle it in the combobox component.
VComboBox doesn't seem to support that, but you could workaround it by using the component's change-event to add a new object with the entry if needed:
Add a change-event handler (e.g., named onChange) on the v-combobox.
In onChange(), lookup the entry in items by name (i.e., corresponding to the VComboBox's item-text prop).
If found, set item to the existing item.
Otherwise, create a new object with that entry, and set item to the newly created object. Note the new object's id (i.e., corresponding to VComboBox's item-value prop) must be unique for VComboBox to create and track it.
<v-combobox
v-model="item"
#change="onChange" 1️⃣
/>
let nextId = 1
export default {
⋮
methods: {
addItem(name) {
const newItem = {
id: nextId++,
name,
}
this.items.push(newItem)
return newItem
},
1️⃣
onChange(entry) {
if (typeof entry === 'string' && entry.trim()) {
2️⃣
const item = this.items.find(item => item.name === entry)
if (item) {
3️⃣
this.item = item
} else {
4️⃣
this.item = this.addItem(entry)
}
}
},
},
}
demo
I found the accepted answer useful but here is a similar solution that allows for a multiple selection combo box using chips. I used this to implement a way for a user to select multiple "Tags":
Ex. Stackblitz runnable example
<v-combobox
v-model="selectedItems"
:items="items"
chips
clearable
multiple
#change="OnChange"
item-text="TagName"
item-value="TagId"
label="Categories"
solo
prepend-icon="mdi-tag-multiple"
return-object>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
color="primary"
close
#click="select"
#click:close="RemoveTag(item)"
>
<span>{{ item.TagName }}</span>
</v-chip>
</template>
</v-combobox>
Here is the code that handles the Add/Removal of these tags:
private OnChange(tags: any) {
let tagsArray = tags as [];
tagsArray.forEach(function(tag: any) {
// New tags are added as type "string" so we need to convert this to a Tag
// Iterate through the selected Tags and for each "string" found convert it to a Tag
if (typeof tag === 'string' && tag.trim()) {
const item = this.selectedItems.find(item => item === tag);
if (item) {
let newTag = new Tag(0, tag);
// Remove the string based tag
const index = this.selectedItems.indexOf(tag, 0);
if (index > -1) {
this.selectedItems.splice(index, 1);
}
// Add a new tag object instead
this.selectedItems.push(newTag);
return;
}
}
}.bind(this))
}
private RemoveTag(tagToRemove: Tag) {
var indexOfItemToRemove = this.selectedItems.findIndex(x => x.TagName == tagToRemove.TagName);
this.selectedItems.splice(indexOfItemToRemove, 1);
}
It does not appear that a ComboBox can directly return an object when adding a new item. However it looks like you can leverage the input event of the component to run a method that converts to an object.
Here is how I call the method on the component's input event:
<v-combobox
v-model="item"
:items="items"
item-text="name"
item-value="id"
label="ComboBox"
return-object
#input="makeObject"
></v-combobox>
#input="makeObject" listens for the component's input event and calls the makeObject method and passes the currently selected item
Then I was able to write a method which checks if the combobox value is a string. If so then it converts it to an object
<script>
export default {
name: "TestForm",
data() {
return {
item: null
// Remaining code omitted for readability
}
}
methods: {
makeObject(val) {
if (typeof val === "string") {
this.item = {
name: val,
};
}
},
},
};
</script>
Here is my full component:
<template>
<v-card>
<v-card-title>
<span class="text-h5">Edit Item</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col>
<v-combobox
v-model="item"
:items="items"
item-text="name"
item-value="id"
label="ComboBox"
return-object
#input="makeObject"
></v-combobox>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-text>
{{ item }}
</v-card-text>
</v-card>
</template>
<script>
export default {
name: "TestForm",
data() {
return {
item: null,
items: [
{
id: 1,
name: "Item 1",
createdAt: "2020-01-01",
updatedAt: "2020-01-01",
},
{
id: 2,
name: "Item 2",
createdAt: "2020-01-01",
updatedAt: "2020-01-01",
},
{
id: 3,
name: "Item 3",
createdAt: "2020-01-01",
updatedAt: "2020-01-01",
},
],
};
},
methods: {
makeObject(val) {
if (typeof val === "string") {
this.item = {
name: val,
};
}
},
},
};
</script>
sandbox link
Related
This seems like the simplest of requests but I can't seem to retrieve a set of rows from a Tabulator object.
Here's the code which instantiates the Tabulator object.........
function TabulatorInvitees(divId, companyName, userEmail) {
try {
var table = new Tabulator(divId, {
columns: [
{
title: "<div style='width:20%; float:left; text-align:left; color:blue; font-size:14px;'>Vendor Invitees</div>",
columns: [
{ title: "Id", field: "Id", visible: false },
{ title: "Added", field: "Added", visible: false },
{ title: "Changed", field: "Changed", visible: false },
{ title: "MarkedForExclusion", field: "MarkedForExclusion", visible: false },
{ title: "Email Address", field: "Email", widthGrow: 1, responsive: 0, hozAlign: "center", editor: "input", visible: true },
{ title: "First Name", field: "FirstName", widthGrow: 0.5, responsive: 1, hozAlign: "center", editor: "input", visible: true },
{ title: "Last Name", field: "LastName", widthGrow: 0.5, responsive: 1, hozAlign: "center", editor: "input", visible: true }
]
},
{
title: tabulatorAddUser(companyName),
field: "ManageRows",
widthGrow: 0.25,
responsive: 2,
hozAlign: "center",
formatter: "tickCross",
headerClick: function (e, row) {
row.getTable().addRow({ Id: 0, Added: true }, false);
},
cellClick: function (e, cell) {
tabulatorFreezeUnfreezeDelete(cell.getRow());
}
},
],
data: [],
height: "100%",
layout: "fitColumns", // required when using 'widthGrow'
placeholder: tabulatorPlaceholder(companyName), //display message to user on empty table
reactiveData: true, //enable reactive data
responsiveLayout: "collapse",
rowContextMenu: tabulatorContextMenu(),
});
table.on("rowTapHold", function (e, row) {
// from Tabulator documentation: "The rowTapHold event is triggered when a user taps on a row on a touch display and holds their finger down for over 1 second."
//e - the tap event object
//row - row component
tabulatorContextMenu();
});
table.on("tableBuilt", function () {
if (companyName.length > 0) {
table.setData(getDataSync({ caseSelector: "VendorMgmt_EmployeeList", companyCode: companyName, userEmail: userEmail }));
}
else {
table.setData([]);
}
});
}
catch (error) {
console.log(error);
}
}
The setData() function makes a call to a database function which returns three rows, similar to the following:
The following JQuery function is called when a radio button is clicked....
$(".vendorStatus").click(function (e) {
const status = e.target.value;
const tbls = Tabulator.findTable("#divVendorEmployees");
const tbl = tbls[0];
const tblRows = tbl.getRows();
console.log("tbls.length", tbls.length);
console.log("tblRows", tblRows);
});
The browser console indicates a table has been found (tbls.length = 1) but the tblRows array is empty:
I see the three rows in my Tabulator but I am not able to recall them programmatically. It seems like a simple problem which should have a simple answer.
I am using the most recent version of Tabulator (v5.4).
Any assistance is greatly appreciated.
After much searching, I finally came to the realization the DOM element associated with the Tabulator instance must be managed when attempting to refresh or replace data. I've implemented a method which allows me to delete and rebuild the DOM element each time I need to save data to my database and subsequently refresh my Tabulator instance.
Here's the code...
function refreshTabulatorObject(obj) {
let parentId = obj.parentId;
let childId = obj.childId;
//Empty and remove the current version of the [Tabulator] object.
const tables = Tabulator.findTable(childId);
if (tables.length > 0) {
var table = Tabulator.findTable(childId)[0];
table.setData([]);
}
//Remove the existing <div> from the DOM.
$(childId).remove();
//Re-create the <div> element for the [Tabulator] object and append it to the DOM.
var parentDiv = document.getElementById(parentId);
parentDiv.innerHTML = "";
var childDiv = document.createElement("div");
childDiv.setAttribute("id", childId);
parentDiv.appendChild(childDiv);
//Re-create the [Tabulator] object.
TabulatorInvitees("#" + childId, obj.companyName);
}
I'm sure those of you with a more intimate knowledge of Tabulator would likely suggest a more elegant method, however, this one appears to work and I've already spent far more time on this issue that I had anticipated. Unfortunately, elegance is not a luxury I can afford at this point.
I hope this solution might be of help to some other struggling soul.
I use Algolia autocomplete in "standalone" mode, meaning without the commercial Algolia service to return the search results; I have my own server respond with an array of search results.
How do I highlight matches in my returned items/strings (search results)?
The backend must return results that are already highlighted using a tag such as: <mark>this-is-to-be-highlighted</mark>. Here's an example result array for an search for "pie":
const items = [
{ some_attribute: 'Apple <mark>pie</mark>' },
{ some_attribute: 'American <mark>pie</mark>' },
{ some_attribute: 'Chocolate <mark>pie</mark>' }
]
The complete javascript code would then be something like this:
import { autocomplete } from "#algolia/autocomplete-js"
autocomplete({
container: '#search_input',
// ...
getSources({ query }) {
// This would be an example response from your server
const items = [
{ some_attribute: 'Apple <mark>pie</mark>' },
{ some_attribute: 'American <mark>pie</mark>' },
{ some_attribute: 'Chocolate <mark>pie</mark>' }
]
return [
{
sourceId: 'pies',
getItems({ query }) {
const HIGHLIGHT_PRE_TAG = '__aa-highlight__'
const HIGHLIGHT_POST_TAG = '__/aa-highlight__'
return items.map((item) => ({
item,
_highlightResult: {
some_attribute: {
value: item.some_attribute
.replace(/<mark>/g, HIGHLIGHT_PRE_TAG)
.replace(/<\/mark>/g, HIGHLIGHT_POST_TAG)
}
}
}))
},
templates: {
// ...
item({ item, components, html }) {
return html`<div className="aa-ItemWrapper">
<div className="aa-ItemContent">
<div className="aa-ItemContentBody">
<div className="aa-ItemContentTitle">
${components.Highlight({ hit: item, attribute: 'some_attribute' })}
</div>
</div>
</div>
</div>`
},
// ...
}
},
// ...
]
},
// ...
})
Getting error when accesing the data by using sort method
Error is
No index exists for this sort
Create index code is
db.createIndex({
index: { fields: ["timestamp", "upVote"] },
}).then(() => {
this.intitialDatafromDB(db);
});
find function is
db.find({
selector: { upVote: { $gt: 5 } },
sort: [{ upVote: "desc" }],
fields: ["news"],
}).then((result: any) => {
this.data = result.docs;
}).catch((err: any) => {
console.log(err);
});
The reason your query is failing is that the order of index fields matters.
From the pouchdb documentation Indexing on more than one field
One thing to note is that the order of these fields matters when
creating your index
By specifying the index as so
fields: ['timestamp','upVote']
The index looks like this
timestamp
upVote
1590399369500
3
1590399369600
4
1590399369700
1
1590399369700
2
1590399369700
3
1590399369800
1
Note the timestamp 1590399369700, and how the secondary field upVote sorts.
If your index fields were ordered like so
fields: ['upVote','timestamp']
Given the theoretical data above, the index would look like this
upVote
timestamp
1
1590399369700
1
1590399369800
2
1590399369700
3
1590399369500
3
1590399369700
4
1590399369600
and your query would return the results you expect, as is demonstrated in the snippet below. I recommend reading over Map/reduce queries; grasping the concepts presented in that documentation will provide a deeper understanding of why this is so.
const g_result = 'result';
const getEl = id => document.getElementById(id);
let db;
async function view() {
const view = getEl(g_result);
const result = await db.find({
selector: {
upVote: {
$gt: 5
},
},
sort: [{
'upVote': 'desc'
}],
fields: ['news','upVote']
}, );
view.innerText = JSON.stringify(result, undefined, 3);
}
// canned test documents
function getDocsToInstall() {
return [{
timestamp: 1590399369508,
upVote: 3,
news: "new item 1"
},
{
timestamp: 1590399248600,
upVote: 4,
news: "new item 2"
},
{
timestamp: 1590399248600,
upVote: 5,
news: "new item 3"
},
{
timestamp: 1590399248700,
upVote: 6,
news: "new item 4"
},
{
timestamp: 1590399248900,
upVote: 7,
news: "new item 5"
},
{
timestamp: 1590399249000,
upVote: 8,
news: "new item 6"
},
]
}
// init example db instance
async function initDb() {
db = new PouchDB('test', {
adapter: 'memory'
});
await db.bulkDocs(getDocsToInstall());
await db.createIndex({
index: {
fields: ['upVote', 'timestamp']
}
});
};
(async() => {
await initDb();
await view();
})();
https: //stackoverflow.com/questions/69122670/no-index-exists-for-this-sort-couchdb#
<script src="https://github.com/pouchdb/pouchdb/releases/download/7.1.1/pouchdb-7.1.1.min.js"></script>
<script src="https://github.com/pouchdb/pouchdb/releases/download/7.1.1/pouchdb.memory.min.js"></script>
<script src="https://github.com/pouchdb/pouchdb/releases/download/7.1.1/pouchdb.find.min.js"></script>
<pre id='view'></pre>
<div style='margin-top:2em'></div>
<pre id='result'>
</pre>
This question has been asked several times in various forms over the years in the Tabulator GitHub repository. Here are a few instances:
https://github.com/olifolkerd/tabulator/issues/527, https://github.com/olifolkerd/tabulator/issues/1759
I'm looking for an example of how to achieve this using a dropdown menu of some form --- ideally as described in #1759 (dropdown with checkboxes) but another solution that would work for us is a "select" editor that adds/removes CSVs in the header filter when a value is selected/deselected (extending on the example provided in #527).
Hopefully someone with experience working with custom header filters / editors in tabulator can provide an example of a multi-select header filter dropdown, but if not, then I will post a JSFiddle link myself once I've got something that works.
Checkout: https://github.com/olifolkerd/tabulator/issues/527#issuecomment-850900451
Simple Answer by AkshayaBrianTauro
{
field: "book_name",
title: "Book Name",
headerFilterPlaceholder: " ",
headerFilter: 'select',
headerFilterFunc:"in",
headerFilterParams: {values:true, sortValuesList:"asc", multiselect:true}
},
Here is an example of a custom header filter for tabulator of 'select multiple' type. It can be converted to a dropdown style if desired using external sources such as Chosen or multiselect.js
(I recommend running the below Code Snippet in Full Page view).
const speciesTypes = ['Human', 'Android', 'Betazoid', 'Klingon', 'Ferengi', 'Tamarian'];
function multiSelectHeaderFilter(cell) {
var values = speciesTypes;
const filterFunc = (rowData) => {
return values.includes(rowData['species']);
}
const getSelectedValues = (multiSelect) => {
var result = [];
var options = multiSelect && multiSelect.options;
var opt;
for (var i = 0, iLen = options.length; i < iLen; i++) {
opt = options[i];
if (opt.selected) {
result.push(opt.value || opt.text);
}
}
return result;
}
const onChange = () => {
var editor = document.getElementById('speciesSelector');
values = getSelectedValues(editor);
console.log("values: " + values);
cell.getColumn().getTable().removeFilter(filterFunc);
cell.getColumn().getTable().addFilter(filterFunc);
}
var select = document.createElement("select");
select.multiple = "multiple";
select.id = 'speciesSelector';
select.class = "chosen-select";
select.style = 'width: 100%';
speciesTypes.forEach(species => {
select.innerHTML += "<option id='" + species + "' value='" + species + "' selected='selected'>" + species + "</option>";
});
cell.getColumn().getTable().addFilter(filterFunc);
select.addEventListener('change', onChange);
return select;
}
var table = new Tabulator("#tabulator", {
layout: "fitColumns",
data: [{
name: 'Geordi La Forge',
species: 'Human'
}, {
name: 'Dathon',
species: 'Tamarian'
}, {
name: 'Jean-Luc Picard',
species: 'Human'
}, {
name: 'Worf, son of Mogh',
species: 'Klingon'
}, {
name: 'Tasha Yarr',
species: 'Human'
}, {
name: 'Data',
species: 'Android'
}, {
name: 'Wesley Crusher',
species: 'Human'
}, {
name: 'Jalad',
species: 'Tamarian'
}, {
name: 'Lwaxana Troi',
species: 'Betazoid'
}, {
name: 'Temba',
species: 'Tamarian'
}, {
name: 'T\'Kuvma',
species: 'Klingon'
}, {
name: 'Lore',
species: 'Android'
}, {
name: 'Noonian Soongh',
species: 'Human'
}, {
name: 'Darmok',
species: 'Tamarian'
}, {
name: 'Reittan Grax',
species: 'Betazoid'
}, {
name: 'Quark',
species: 'Ferengi'
}],
headerSort: true,
columns: [{
title: 'Name',
field: 'name',
sorter: 'string'
}, {
title: 'Species',
field: 'species',
sorter: 'string',
headerFilter: multiSelectHeaderFilter,
headerFilterLiveFilter: false
}, ],
});
<html>
<head>
<link href="https://unpkg.com/tabulator-tables#4.5.3/dist/css/tabulator.min.css" rel="stylesheet">
<script type="text/javascript" src="https://unpkg.com/tabulator-tables#4.5.3/dist/js/tabulator.min.js"></script>
</head>
<body>
<div id="tabulator"></div>
</body>
</html>
JSFiddle: https://jsfiddle.net/jjech/3th28pv0/
See my answer to Tabulator Multiple Filter in the same Column (show dropbox)
Extend as you see fit...
I dont think <select> support checkboxes as <option>'s, but it would be trivial to replace the <select> with a different style of "pulldown" that does.
Hello and Happy new Year guys,
Again I ask about v-treeview search. When I do my filter, the behavior do not satisfy me.
I updated my version of vuetify to 1.4.0. And I'm using vue 2.5.15
https://codepen.io/anon/pen/PXeMmy?&editors=101
HTML
<div id="app">
<v-container grid-list-md>
<v-layout wrap>
<v-flex xs6>
<!-- Search Field -->
<v-text-field label="search" v-model="search" box>
</v-text-field>
<!-- Treeview -->
<v-treeview :items="filteredTree"
v-model="selected"
active-class="grey lighten-4 indigo--text"
item-key="name"
selected-color="blue"
selectable
hoverable>
</v-treeview>
</v-flex>
<v-flex xs6>
<v-chip v-for="(s , i) in selected" :key="i">
{{s}}
</v-chip>
</v-flex>
</v-layout>
</v-container>
</div>
JS :
new Vue({
el: '#app',
data(){
return{
search: '',
tree: [
{
id: 1,
name: 'Applications',
children: [
{ id: 2, name: 'Calendar' },
{ id: 3, name: 'Chrome' },
{ id: 4, name: 'Webstorm' }
]
},
{
id: 5,
name: 'Languages',
children: [
{ id: 6, name: 'English' },
{ id: 7, name: 'French' },
{ id: 8, name: 'Spannish' }
]
}
],
selected: []
}
},
computed:{
filteredTree: {
get: function() {
let regexp = new RegExp(this.search, "i")
return this.filterTree(this.tree, regexp) || []
},
},
},
methods: {
filterTree: function(tree, filter) {
if (!Array.isArray(tree)) return null
return JSON.parse(JSON.stringify(tree)).filter(function matchName(o) {
let temp;
if (o.name.match(filter)) {
return true;
}
if (!Array.isArray(o.children)) {
return false;
}
temp = o.children.filter(matchName);
if (temp.length) {
o.children = temp;
return true;
}
});
}
}
})
In this exemple when I search "Calen", only "Application -> Calendar" is visible. Until now, it's what I want.
But when I select Calendar, "Application" is also selected; and when I clear the filter, all the children of "Application" are selected too. And I'd like to select "Calendar" and when I clear I don't want its siblings to be selected.
Thank you for reading