Can Autocomplete component have different value and option types? - autocomplete

As per docs, Autocomplete component has no distinction between option and actual value.
I have a list of options as objects with ids. When I select an option I want to get its id as a value, not the object itself. Also, when I set the value of Autocomplete I want to pass in id only.
Is it possible?
<Autocomplete
options={[{id: 1, label: 'foo'}, {id: 2, label: 'bar'}]}
value={1}
onChange={(_, value) => { /* value should be number (id) */ }}
/>
Update: option label should remain configurable

Ciao, unfortunately value on onChange event returns one of the options selected. So is not possible to return only one attribute of the element.
The only thing you can do is take the value.id:
<Autocomplete
options={[
{ id: 1, label: "foo" },
{ id: 2, label: "bar" }
]}
getOptionLabel={(option) => option.label} // this to show label on Autocomplete
getOptionSelected={(option, value) => option.id === value.id} // this to compare option on id value
onChange={(event, value) => console.log(value.id)} // here access to id property of value object
...
/>
Here a codesandbox example.

Related

React Material Data Grid: Sorting a currency field

I am passing in rows of financial data to a React MUI Data Grid component, however, as a currency symbol exists within the cell data the default sorting function is not working. Is there anyway I can ignore the currency symbol when applying sort?
[
{id: 0, cpa: '£20', cpl: '£0', revshare: '0%', startDate: '02/03/2022'}
{id: 1, cpa: '£10', cpl: '£0', revshare: '0%', startDate: '02/02/2022'}
]
<DataGrid
disableSelectionOnClick
headerHeight={65}
rowHeight={70}
autoHeight
className={classes.root}
rows={displayRowData(rowsData)}
columns={columns}
/>
You can add a custom sortComparator in the columns of your <DataGrid/>.
Assuming your currency symbol is always the first character, your cpa column definition would look like this:
const columns = [
...
{
field: "cpa",
headerName: "cpa",
sortComparator: (a: string, b: string) =>
Number(a.slice(1)) - Number(b.slice(1)),
},
...
];
For more information regarding sorting I would recommend checking out the official documentation.

Why do my dynamically generated forms not update correctly when I delete one of them using Vue?

I want to populate a form with multiple text input fields. However, when I delete one of these input fields, the UI doesn't update correctly according to the data present.
Condition.vue
<template v-slot:custom-form>
<el-row v-for="(condition, index) of customFormData" type="flex" :key="`${randomSalt}${index}`" class="condition row-bg" :justify="isElse(condition) ? 'start' : 'space-around'">
<el-col class="operator" :class="[ index === 0 ? 'operator-if' : '', isElse(condition) ? 'operator-else' : '']" :span=".5">
{{condition.type.toLowerCase() === 'else' ? 'ELSE' : index > 0 ? 'ELSE IF' : 'IF'}}
</el-col>
<el-col ref="index" v-if="condition.type.toLowerCase() !== 'else'" :span="6">
<editable-value name="inputAction" dataType="property" v-model="condition.inputAction" :schema="condition.schema"></editable-value>
</el-col>
<el-col v-if="condition.type.toLowerCase() !== 'else'" class="operator" :span=".5">
=
</el-col>
<el-col v-if="condition.type.toLowerCase() !== 'else'" :span="6">
<editable-value name="inputActionValue" dataType="value" v-model="condition.inputActionValue" :schema="condition.schema"></editable-value>
</el-col>
<el-col v-if="condition.type.toLowerCase() !== 'else'" class="operator" :span=".5">
THEN
</el-col>
<el-col :span="6">
<editable-value name="ouputAction" dataType="output" v-model="condition.ouputAction" :schema="condition.schema"></editable-value>
</el-col>
<el-col :span=".5">
<el-button icon="el-icon-delete" type="text" plain class="trans scale delete-condition el-col-offset-1 ev-controls" #click="e => { deleteCondition(index) }"></el-button>
</el-col>
</el-row>
</template>
export default {
components: { EditableValue },
data () {
return {
customFormData: [
{
type: 'if',
schema: Schema
}
],
salt: 1043423
}
},
computed: {
randomSalt () {
return this.salt + this.getRnd()
}
},
methods: {
deleteCondition (index) {
this.customFormData.splice(index, 1)
},
getRnd () {
return Math.floor((Math.random() * 1000000 / (Math.random() - 1)) + 1)
},
chainCondition (type) {
this.customFormData.push({ ...this.customFormData[this.customFormData.length], type, schema: this.schema })
}
}
}
Basically, each Editable-Values component represents an input field, customFormData is an array listing each row of input fields belonging to the main form. When deleting a row, we simply splice that row from the array and vice versa for addition.
Main Problem
Everything else works fine except when I delete a row that is neither the first nor last in the array. When such a deletion happens, the data (code) in the app is correct and represents the deletion.
The UI however, seems as if the row below the intended one was deleted. Using pseudocode, the behavior is as follows:
UI
<row 1 data="one"/>
<row 2 data="two" />
<row 3 data="three"/>
Code
data: {
1: 'one',
2: 'two',
3: 'three'
}
// If we delete row 2, the UI becomes as follows:
UI
<row 1 data="one"/>
<row 2 data="two" />
Code
data: {
1: 'one',
3: 'three'
}
What can I do to make sure that each row represents its data correctly.?
I tried forcing a rerender by changing the row key, however, this just ends up displaying empty input fields. I have also tried using an object instead of an array to display the rows, but the results are much worse, data doesn't seem so reactive with objects.
What can I do to fix this?
Your problem most probably comes from using v-for index as a key. key must be unique and stable - index satisfies only 1st condition (when the 2nd row is deleted, 3rd row now gets index 2 instead of 3 and Vue rendering mechanism makes false assumptions about what needs to be re-rendered/updated in the DOM)
Workaround with random key doesn't work either. computed caches the value and changes only when some of the reactive dependencies are changed (this.salt in this case) so you are actually using same key for all the rows. Changing it into method will lead to actually re-rendering everything all the time - this is not something you want either
Only way to solve it correctly is to assign unique ID to each row - it may very well be an index but it must be stored with each row so it does not change every time some row is deleted...

MongoDB field only accepts 3 special values

slider_value: {
type: Number,
required: false,
},
This is the Mongoose schema for one of the fields in my MongoDB model.
It may only accept the integer values of 1, 4, and 10.
How can this validator be specified in the schema?
If you only need to store either one of these three values, storing them as a string, and validating using the enum key would be reasonable. For example that could look like this:
{
slider_value: {
type: String,
enum: ["1", "4", "10"],
},
}
Alternatively, if it is a requirement to store them in form of an int, you could use a custom validator to check a value before it's saved. That would look like this:
{
slider_value: {
type: Number,
validate: {
validator: value => value === 1 || value === 4 || value === 10,
message: props => `${props.value} is invalid for slider_value`,
},
},
}
For more details on custom validators and validation in mongoose in generell, here are the mongoose validation docs.

Password Validation on Dynamically Generated Form

So I'm just learning Angular and I have basic routing setup and a partial for setting up a basic page with a form (theoretically any form), and based on the controller I load it loads that form from a fields array in the controller.
One of those forms is a registration form where I want to verify that the passwords match.
So in the partial I have (a mode complicated version of) this
<input ng-repeat="field in fields" ng-model="field.model" mustEqual="fields.mustEqual">
I found a directive which does password comparison:
taskDivApp.directive('mustEqual', function() {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, elem, attrs, ngModel)
{
if(!ngModel) return; // do nothing if no ng-model
// watch own value and re-validate on change
scope.$watch(attrs.ngModel, function()
{
validate();
});
// observe the other value and re-validate on change
attrs.$observe('equals', function (val)
{
validate();
});
var validate = function()
{
// values
var val1 = ngModel.$viewValue;
var val2 = attrs.mustEqual;
// set validity
ngModel.$setValidity('equals', val1 === val2);
};
}
}
});
So basically all I want to do is be able to load a literal string into ng-model from the controller so that I can pair up the model and the must equal, so the data for the form would look like:
$scope.fields = [
{
'type':"Email",
'placeholder':"Email Address",
'req': true,
'focus':true
},
{
'type':"Password",
'placeholder':"Password",
'model': "password",
'mustEqual': "passwordConf"
},
{
'type':"Password",
'placeholder':"Comfirm Password",
'model':"passwordConf",
'mustEqual': "password"
}
];
However what happens now is that the main password field gets bound to the "model" field of its index in the array and similar for passwordConf (ie inital values literally "password" and "passwordConf")
The fact that this isn't easy makes me think I have the wrong mindset - is this not a good way to do forms, should I just have them hard coded?
If this is okay then any ideas on how I could accomplish it would be appreciated!

angularstrap typeahead with json object array is not working

I am using angularstrap typeahead directive. Its working fine with single object json values but its not working when replacing the json with my json object array.
Demo Json:
typeahead= ["Alabama","Alaska","Arizona","Arkansas","California","Colorado","Connecticut","Delaware","Florida","Georgia"];
<input type="text" ng-model="typeaheadValue" bs-typeahead="typeahead">
The above code is working fine.
My JSON object array:
typeahead = [
{id: 1, name: 'name1', email: 'email1#domain.com'},
{id: 2, name: 'name2', email: 'email2#domain.com'},
{id: 3, name: 'name3', email: 'email3#domain.com'}
];
$scope.typeaheadFn = function(query) {
return $.map($scope.typeahead, function(contacts) {
return contacts;
});
}
<input type="text" ng-model="typeaheadValue" bs-typeahead="typeaheadFn">
Please give me some solution for this.
You want to map your items to a list of strings, I believe.
Try:
$scope.typeaheadFn = function(query) {
return $.map($scope.typeahead, function(contact) {
return contact.name;
});
}
(I should add that I am currently stumped by something similar)
If you have, for example:
items = [
{id: 1, name: 'name1', email: 'email1#domain.com'},
{id: 2, name: 'name2', email: 'email2#domain.com'},
{id: 3, name: 'name3', email: 'email3#domain.com'}
];
You will need:
<input type="text" bs-typeahead ng-model="selectedItem" ng-options="item.name for item in items|orederBy:'name'|filter:{name:$viewValue}:optionalCompareFn"></input>
If you exclude filter from ng-options matching will be done on every property of item object, so if you want it to be done on one property add filter:{propName:$viewValue}. Also, if you exclude optionalCompareFn, default comparison from angular will be applied, but you can add your custom one (on your $scope), with signature (actual is property value of the item, stated in filter, not the whole object).
optionalCompareFn(expected,actual){ return /compare and return true or false/}
Attempt 1
I finally got this semi-working after a huge amount of frustration.
An easy way to get your desired text appearing is for each item to have a toString method.
You might have something like
typeaheadData = [
{id: 1, text: "abc", toString: function() { return "abc"; }},
{id: 2, text: "def", toString: function() { return "def"; }}
]
Then you will see the correct text in the options that popup, but the matching won't yet work properly (the items shown by the widget won't match the text the user enters in the box).
To get this working I used the new filter option that's been added in the current git version of angular-strap. Note that it's not even in the pre-built dist/angular-strap.js file in the repository, you will need to rebuild this file yourself to get the new feature. (As of commit ce4bb9de6e53cda77529bec24b76441aeaebcae6).
If your bs-typeahead widget looks like this:
<input bs-typeahead ng-options="item for item in items" filter="myFilter" ng-model="myModel" />
Then the filter myFilter is called whenever the user enters a key. It's called with two arguments, the first being the entire list you passed to the typeahead, and the second being the text entered. You can then loop over the list and return the items you want, probably by checking whether the text matches one or more of the properties of an item. So you might define the filter like this:
var myApp = angular.module('myApp', ['mgcrea.ngStrap'])
.filter('myFilter', function() {
return function(items, text) {
var a = [];
angular.forEach(items, function(item) {
// Match an item if the entered text matches its `text` property.
if (item.label.indexOf(text) >= 0) {
a.push(item);
}
});
return a;
};
});
Unfortunately this still isn't quite right; if you select an item by clicking on it, then the text parameter will be the actual object from the items list, not the text.
Attempt 2
I still found this too annoying so I made a fork of angular-strap (https://github.com/amagee/angular-strap) which lets you do this:
typeaheadData = [
{id: 1, text: "abc"},
{id: 2, text: "def"}
]
//...
$scope.myFormatter = function(id) {
if (id == null) { return; }
typeaheadData.forEach(function(d) {
if (d.id === id) {
return d.text;
}
});
};
<input bs-typeahead ng-options="item for item in items" ng-model="myModel"
key-field="id" text-field="text" formatter="myFormatter" />
With no need to fuss around with toString methods or filters. The text-field is what the user sees, and the key-field is what is written to the model.
(Nope! Still doesn't work if you update the model without going through the view).
The formatter option is needed so when you set the model value to just the key field, the widget can figure out the right text to display.