I was trying to play off of the example code shown here in order to have an overlay trigger that displays a popover upon invalid username field regex for my form component. I'm getting the classic Cannot update during an existing state transition (such as within "render"). Render methods should be a pure function of props and state. error upon trying to execute this code:
var UserField = React.createClass({
getInitialState: function() {
return {
value: ''
};
},
getValue: function() {
return this.refs.input.getValue();
},
validationState: function() {
let valueCode = this.state.value;
if (valueCode.length > 0) {
switch (valueCode.match(/^[A-Za-z0-9_-]+$/)) {
case null:
this.refs.userHint.show();
return 'warning';
break;
default:
this.refs.userHint.hide();
return '';
break;
}
}
},
handleChange: function() {
this.setState({
value: this.refs.input.getValue()
});
},
render: function() {
return (
<OverlayTrigger
ref="userHint"
trigger="manual"
placement="right"
overlay={<Popover title='Invalid Username Format'>
<strong>Warning!</strong> Valid user credentials only contain alphanumeric characters, as well as heifens and underscores.
</Popover>
}
>
<Input
type = 'text'
value = {this.state.value}
label = 'Username'
bsStyle = {this.validationState()}
ref = 'input'
groupClassName = 'input-group'
className = 'form-control'
onChange = {this.handleChange}
/>
</OverlayTrigger>
);
}
});
Any help on this is very much appreciated, as always. Thank you.
If I was developing this I would do it differently, with the Input managing it's own validation and exposing whether it's valid or not through its state (accessible through refs).
Without changing your approach, this should work better because it won't trigger changes in state of the parent component until the overlay has been shown or hidden:
validationState: function() {
let valueCode = this.state.value;
if (valueCode.length > 0) {
switch (valueCode.match(/^[A-Za-z0-9_-]+$/)) {
case null:
return 'warning';
break;
default:
return '';
break;
}
}
},
handleChange: function() {
let valueCode = this.refs.input.getValue();
if (valueCode.length > 0) {
switch (valueCode.match(/^[A-Za-z0-9_-]+$/)) {
case null:
this.refs.userHint.show();
break;
default:
this.refs.userHint.hide();
break;
}
}
this.setState({
value: valueCode
});
},
Related
I want to create a dropdown (or mat-select) to use as a sorting mechanism instead of the Angular Material Sort Header. So, if I for example click on the 'username' inside the dropdown, I want the table to sort by the username (instead of clicking on the header).
How can I do it? Any documentation online on how to achieve this?
Thank you for any help.
As required, I attach some code:
ngOnInit(): void {
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(""),
map((value) => this._filter(value))
);
}
ngAfterViewInit() {
this.providersAdmin.sort = this.sort;
}
getAllAdmins() {
this.isLoading = true;
this.homeService.getAllAdmins().subscribe(
(response) => {
this.admins = response;
this.providersAdmin = new MatTableDataSource(this.admins);
this.isLoading = false;
},
(error) => {}
);
}
sortTableBy(event: any) {
const sortState: Sort = {
active: "username",
direction: "desc",
};
this.sort.active = sortState.active;
this.sort.direction = sortState.direction;
this.sort.sortChange.emit(sortState);
console.log(event);
}
The sortTableBy method is the one I found on here but nothing happens.
I added matSort on the mat-table and I added mat-sort-header on the header cell.
EDIT:
Hi, I managed to fix the problem by writing the following:
sortTableBy(event: any) {
const sortState: Sort = {
active: "username",
direction: "desc",
};
this.sort.active = sortState.active;
this.sort.direction = sortState.direction;
this.sort.sortChange.emit(sortState);
this.providersAdmin.sort = this.sort;
}
There is an example for you:
Exmaple
Your sort function has a wrong implementation, this is work for me:
sortData(fieldName: string) {
if (!fieldName) {
return;
}
const sortState: MatSortable = {
id: fieldName,
start: 'desc',
disableClear: true
};
this.sort.sort(sortState);
}
I am going to set up an example which you can adapt easily:
compare(a: number | string, b: number | string, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
sortData() {
let isAsc = this.sort.direction != "" ?
event.direction == SortDirection.asc :
true;
let data = this.dataSource.data.slice();
data.sort((a, b) => {
switch (this.myChosenSort) {
case 'healthCareCenterName':
return this.compare(a.healthCareCenterName, b.healthCareCenterName, isAsc);
case 'address':
return this.compare(a.address, b.address, isAsc);
case 'contact':
return this.compare(a.contact, b.contact, isAsc);
default:
return 0;
}
});
this.dataSource = new MatTableDataSource<ServiceProviderTable>(data);
}
To change the sort.direction you need to play around a little bit with the code, maybe directly from the dropdown and hardcoding the isAsc when calling the compare method, depending on the value of the this.myChosenSort.
I'm newer to React and could really use a hand understanding how to create a new invoice within my project.
The Issue:
Currently, I can create a new Invoice no problem as shown in the images below. I changed the inputs to some test data to help illustrate the issue I'm having.
Here's the overhead view showing the total number of invoices within the stack so far.
The problem occurs when I go to create a second new invoice. It keeps all of the old data from my first one that I modified, even though I can click them and modify them independently from one another. The weird part is... only some of the values are staying the same while others can become independent from one another...
This is directly after creating a second invoice:
I changed the second invoice to all new data:
And this is the result within invoice 1:
And now when I create a 3rd new invoice:
This tells me that they're connected somehow.. A direct link to my project is here: https://github.com/Brent-W-Anderson/invoice-pdf/tree/invoices
Otherwise, I think the problem is how I'm creating a new invoice or how I'm modifying the data within it. Please look at line 113 where I modify the invoice or line 94 where I am creating a new one. I need all the help I can get, thank you!
https://github.com/Brent-W-Anderson/invoice-pdf/blob/invoices/src/components/app.js
import React from 'react';
import Moment from 'moment';
//components
import LoginSignUp from './login-signup/login-signup';
import Navigation from './navigation/navigation';
import Pages from './pages/pages';
//data
import UsersJSON from '../data/users.json'; // some test data for now. going to connect a database later.
import AppJSON from '../data/app.json';
//styling
import 'fontsource-roboto';
import '../styles/app.css';
export default class App extends React.Component {
state = {
loggedIn: false, // set to true to bypass logging in.
transitionOut: false,
activeUser: "", // can put whatever name you want here if loggedIn is set to true.
activePage: "invoices",
invoiceMode: "view", // dont change this unless you want to start with a specific manageable invoice.
userData: {}, // set to the specific array index from the users if looking for some sample data.
users: UsersJSON,
appData: AppJSON
};
setActiveModeView = (clicked) => { // view all of the invoices
this.setState({
invoiceMode: "view"
});
}
setActiveModeEdit = () => { // view a specific manageable/ editable invoice
this.setState({
invoiceMode: "edit"
});
}
login = (userData) => { // login and store the users data for component use.
let user = this;
let username = userData.personalInfo.name;
this.setState({
userData: userData,
transitionOut: false
});
setTimeout(function() { // let the app animate out before logging in.
user.setState({
loggedIn: true,
activeUser: username
});
}, 1000);
};
logout = () => { // logout and reset the users data.
let user = this;
this.setState({
transitionOut: true
});
setTimeout(function() { // let the app animate out before logging out.
user.setState({
loggedIn: false,
userData: {},
activePage: "invoices",
invoiceMode: "view",
activeUser: ""
});
}, 1500);
}
setActivePage = (page) => { // changing tabs
let pageName = page.target.innerHTML.toLowerCase().replace(/\s/g, '');
let app = this;
if(pageName !== "invoices") { // change view mode back to defaults if not within invoices.
setTimeout(function() {
app.setActiveModeView();
}, 500);
}else {
app.setActiveModeView("invoices");
};
this.setState({
activePage: pageName
});
};
createInvoice = idx => {
console.log(UsersJSON[0].invoices[0]);
this.setState(prevState => ({
userData: {
...prevState.userData,
invoices: [
...prevState.userData.invoices,
{
...UsersJSON[0].invoices[0],
invoiceID: idx + 1,
date: Moment(new Date()).format("YYYY-MM-DD")
}
]
}
}));
};
modifyInvoice = (userData, invoiceIdx, clientIdx, otherInputSelected, otherData) => (inputSelected) => { // editing specific invoice data and storing it back in state
const app = this;
let targetID, newVal;
if(inputSelected !== undefined) {
targetID = inputSelected.target.id;
newVal = inputSelected.target.value;
}else {
switch(otherInputSelected) {
case "billToEmail":
targetID = otherInputSelected;
newVal = otherData;
break;
case "fromEmail":
targetID = otherInputSelected;
newVal = otherData;
break;
default:
console.warn("no other input selected to save to app state.");
};
}
let newUserData = userData;
function overwriteState() {
app.setState({
userData: newUserData
});
}
switch(targetID) { // which input would you like to modify?
case "invoiceName":
newUserData.invoices[invoiceIdx].invoiceName = newVal;
overwriteState();
break;
// BILL TO
case "billToName":
newUserData.invoices[invoiceIdx].toName = newVal;
overwriteState();
break;
case "billToEmail":
newUserData.invoices[invoiceIdx].toEmail = newVal;
overwriteState();
break;
case "billToStreet":
newUserData.invoices[invoiceIdx].toAddress.street = newVal;
overwriteState();
break;
case "billToCityState":
newUserData.invoices[invoiceIdx].toAddress.cityState = newVal;
overwriteState();
break;
case "billToZip":
newUserData.invoices[invoiceIdx].toAddress.zip = newVal;
overwriteState();
break;
case "billToPhone":
newUserData.invoices[invoiceIdx].toPhone = newVal;
overwriteState();
break;
// FROM
case "fromName":
newUserData.invoices[invoiceIdx].fromName = newVal;
overwriteState();
break;
case "fromEmail":
newUserData.invoices[invoiceIdx].fromEmail = newVal;
overwriteState();
break;
case "fromStreet":
newUserData.invoices[invoiceIdx].fromAddress.street = newVal;
overwriteState();
break;
case "fromCityState":
newUserData.invoices[invoiceIdx].fromAddress.cityState = newVal;
overwriteState();
break;
case "fromZip":
newUserData.invoices[invoiceIdx].fromAddress.zip = newVal;
overwriteState();
break;
case "fromPhone":
newUserData.invoices[invoiceIdx].fromPhone = newVal;
overwriteState();
break;
// DETAILS
case "date":
newUserData.invoices[invoiceIdx].date = newVal;
overwriteState();
break;
case "description":
newUserData.invoices[invoiceIdx].items.description = newVal;
overwriteState();
break;
case "rate":
newUserData.invoices[invoiceIdx].items.rate = newVal;
overwriteState();
break;
case "qty":
newUserData.invoices[invoiceIdx].items.qty = newVal;
overwriteState();
break;
case "additionalDetails":
newUserData.invoices[invoiceIdx].items.additionalDetails = newVal;
overwriteState();
break;
default:
console.warn("something went wrong... selected target input:");
console.warn(targetID);
}
};
deleteInvoice = (invoice, idx) => { // deletes an invoice
let newUserData = this.state.userData;
newUserData.invoices.splice(idx, 1);
for(var x = 0; x < newUserData.invoices.length; x++) {
newUserData.invoices[x].invoiceID = (x + 1).toString();
}
this.setState({
userData: newUserData
});
}
render() {
let app = this.state;
if(app.loggedIn) { // if logged in
return (
<div className="app">
<Navigation
activeUser={app.activeUser}
setActivePage={this.setActivePage}
activePage={app.activePage}
appData={app.appData}
logout={this.logout}
/>
<Pages
setActiveModeView={this.setActiveModeView}
setActiveModeEdit={this.setActiveModeEdit}
invoiceMode={app.invoiceMode}
activePage={app.activePage}
appData={app.appData}
transitionOut={app.transitionOut}
userData={app.userData}
createInvoice={this.createInvoice}
modifyInvoice={this.modifyInvoice}
deleteInvoice={this.deleteInvoice}
/>
</div>
);
}else { // if not logged in
return (
<div className="app">
<LoginSignUp
login={this.login}
users={app.users}
/>
</div>
);
}
}
}
I believe one option would be to change:
case "billToStreet":
newUserData.invoices[invoiceIdx].toAddress.street = newVal;
overwriteState();
break;
to:
case "billToStreet":
newUserData.invoices[invoiceIdx].toAddress = { ...newUserData.invoices[invoiceIdx].toAddress, street: newVal };
overwriteState();
break;
and do the same for the other address fields.
I'm not sure why but I suspect that all of your toAddress entries are referencing the same object.
On initial render, the input components acts as normal. However after it is unfocused and refocused again, it will only accept one character. This also happens if I reset the form.
I created a FormValidationStateManager class to reuse Joi validation logic that is needed.
Form.jsx
class Form extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.validation = new FormValidationStateManager();
}
handleSubmit(e) {
e.preventDefault();
var result = this.validation.validateAll();
this.setState({
// need to do this, trigger render after validations
// we could eventually encapsulate this logic into FormValidationSM.
submit: Date.now()
}, function() {
if (result.valid) {
this.props.onSubmit(result.data);
}
}.bind(this));
}
resetForm(e) {
e.preventDefault();
this.validation.clearForm();
this.setState({
// need to do this, trigger render after validations,
// we could eventually encapsulate this logic into FormValidationSM.
submit: Date.now()
});
}
render() {
// TODO implement logic for rendering name from API call
let name = "John Doe";
return (
<form class='info-form' onSubmit={this.handleSubmit} autocomplete='off'>
<div class='info-block-section-title'>
<h2>Welcome {name}</h2>
<h4>Some nicely worded few sentences that let's the user know they just need to fill out a few bits of information to start the process of getting their student loans paid down!</h4>
</div>
<br />
<div class='info-block-section-body'>
<InfoFieldset validation={this.validation} />
</div>
<div class='info-form-button'>
<FormButton label="Continue" />
<FormButton type="button" onClick={this.resetForm.bind(this)} label="Reset" />
</div>
</form>
)
}
}
export default Form;
Fieldset
class Fieldset extends Component {
constructor(props) {
super(props);
this.state = {
secondaryEmailVisible: false
};
this.handleToggle = this.handleToggle.bind(this);
this.validation = this.props.validation || new FormValidationStateManager();
this.validation.addRules(this.validatorRules.bind(this));
}
validatorRules() {
var _rules = {
// TODO validate format of date, address and zip
date_of_birth: Joi.date().label("Date of Birth").required(),
address_street: Joi.string().label("Street Address").required(),
address_zip: Joi.string().label("Zip Code").required(),
password_confirmation: Joi.any()
.valid(Joi.ref('password'))
.required()
.label('Password Confirmation')
.messages({
"any.only": "{{#label}} must match password"
}).strip(),
password: Joi.string()
.pattern(/\d/, 'digit')
.pattern(/^\S*$/, 'spaces')
.pattern(/^(?!.*?(.)\1{2})/, 'duplicates')
.pattern(/[a-zA-z]/, 'alpha')
.required()
.min(CONSTANTS.PASSWORD_MIN)
.label('Password')
.messages({
"string.min": PASSWORD_HINT,
"string.pattern.name": PASSWORD_HINT
})
}
if (this.state.secondaryEmailVisible) {
_rules['secondary_email'] = Joi.string()
.label("Secondary Email")
.email({ tlds: false })
.required();
}
return _rules;
}
handleToggle() {
this.setState(state => ({
secondaryEmailVisible: !state.secondaryEmailVisible
}));
}
render() {
return(
<div class='info-fieldset'>
{/* for skipping chrome browser autocomplete feature
<input type='text' style={{ display: 'none '}} />
<input type='password' style={{ display: 'none '}} />
*/ }
<strong>Primary email for login: work#example.com</strong>
<FormElementToggle
field="secondary"
label={"Add another email for notifications?"}
onChange={this.handleToggle} />
<div class='info-form-element'>
<DisplayToggle remove={true} show={this.state.secondaryEmailVisible}>
<FormElementInput
field='secondary_email'
label="Secondary Email"
validation={this.validation}
placeholder='john#example.com' />
</DisplayToggle>
<FormElementInput
field='password'
type='password'
label={(
<div>
Password
<Tooltip
placement='top'
trigger={['hover']}
overlay={PASSWORD_HINT}>
<span><i className='fa fa-info-circle'></i></span>
</Tooltip>
</div>
)}
placeholder='********'
validation={this.validation} />
<FormElementInput
key={1}
field='password_confirmation'
type='password'
label="Password Confirmation"
placeholder='********'
validation={this.validation} />
<FormElementInput
field='date_of_birth'
label="Date of Birth"
validation={this.validation}
placeholder='MM/DD/YYYY' />
<FormElementInput
field='address_street'
label="Street Address"
validation={this.validation}
placeholder='123 Example St' />
<FormElementInput
field="address_zip"
label="Zip Code"
validation={this.validation}
placeholder='01234' />
</div>
</div>
)
}
}
export default Fieldset;
FormElementInput.jsx
class FormElementInput extends Component {
constructor(props) {
super(props);
this.input = React.createRef();
this.cachedErrors = {};
this.cachedValue = null;
this.state = { focused: false };
this.validation = this.props.validation || new FormValidationStateManager();
}
componentDidUpdate(prevProps, prevState) {
}
shouldComponentUpdate(nextProps, nextState) {
var filter = function(val, key, obj) {
if (_.isFunction(val)) {
return true;
}
// Certain props like "label" can take a React element, but those
// are created dynamically on the fly and they have an random internal UUID,
// which trips the deep-equality comparision with a false-positive.
// This is wasted cycles for rendering.
if (React.isValidElement(val)) {
return true;
}
// validation is a complex obj that we shouldn't be caring about
if (_.contains(['validation'], key)) {
return true;
}
};
// if the errors after a validation got updated, we should re-render.
// We have to compare in this manner because the validation object is not shallow-copied
// via props since it is managed at a higher-component level, so we need to
// do a local cache comparision with our local copy of errors.
if (!_.isEqual(this.cachedErrors, this.getErrors())) {
return true;
}
// if the errors after a validation got updated, we should re-render.
// We have to compare in this manner because the validation object is not shallow-copied
// via props since it is managed at a higher-component level, so we need to
// do a local cache comparision with our local copy of errors.
if (this.cachedValue !== this.getDisplayValue()) {
return true;
}
return !_.isEqual(_.omit(nextProps, filter), _.omit(this.props, filter)) || !_.isEqual(this.state, nextState);
}
setValue(value) {
console.error('deprecated - can not set value directly on FormElementInput anymore');
throw 'deprecated - can not set value directly on FormElementInput anymore';
}
isDollarType() {
return this.props.type === 'dollar';
}
isRateType() {
return this.props.type === 'rate';
}
getType() {
if (this.isDollarType() || this.isRateType()) {
return 'text';
} else {
return this.props.type;
}
}
getPrefix() {
if (this.isDollarType()) {
return _.compact([this.props.prefix, '$']).join(' ');
} else {
return this.props.prefix;
}
}
getPostfix() {
if (this.isRateType()) {
return _.compact([this.props.postfix, '%']).join(' ');
} else {
return this.props.postfix;
}
}
getDisplayValue() {
var value = this.props.value || this.validation.getFormValue(this.props.field);
// while DOM input is focused, just return the same value being typed
if (this.state.focused) {
return value;
}
if (this.isDollarType()) {
if (_.isUndefined(value)) {
return value;
}
// keep in sync with the validation check in getOnChange()
// even if this is different - it wont break - cause accounting.js handles gracefully
else if (_.isNumber(value) || !value.match(/[a-z]/i)) {
return accounting.formatMoney(value, '', 2);
} else {
return value;
}
} else if (this.isRateType()){
if (_.isUndefined(value)) {
return '';
}
return accounting.formatNumber(value, 3)
} else {
return value;
}
}
getErrors() {
return this.props.errors || this.validation.getValidationMessages(this.props.field);
}
onBlur(event) {
this.validation.validate(this.props.field);
this.setState({ focused: false });
}
onFocus(event) {
this.setState({ focused: true });
}
onChange(event) {
if (this.isDollarType()) {
// only accept if the input at least resembles a currency
// accounting.parse already strips alot of characters and does this silently
// we want to be a little less strict than accounting.parse since we need to allow more
// different types of inputs +-,. and also let accounting.parse do its job
// very basic check here for a-z characters
if (!event.target.value.match(/[a-z]/i)) {
var parsedValue = accounting.parse(event.target.value);
event._parsedValue = parsedValue;
}
}
let _value = event._parsedValue || event.target.value;
console.log(this.input)
this.validation.setFormValue(this.props.field, _value);
}
_eventHandlers(field) {
return {
onChange: this.onChange.bind(this),
onBlur: this.onBlur.bind(this),
onFocus: this.onFocus.bind(this)
};
}
_mergeEventHandlers(p1, p2) {
var events = [
'onChange',
'onBlur',
'onFocus'
];
var r1 = {};
events.map(function(e) {
r1[e] = FuncUtils.mergeFunctions(p1[e], p2[e]);
}.bind(this));
return r1;
}
render() {
let _errors = this.cachedErrors = this.getErrors();
let _value = this.cachedValue = this.getDisplayValue();
let attrs = {
id: this.props.htmlId || ('field_' + this.props.field),
className: classnames({
'error': _errors && _errors.length
}),
type: this.getType(),
placeholder: this.props.placeholder,
autoComplete: "false",
disabled: this.props.disabled,
readOnly: this.props.disabled,
value: _value,
ref: this.input,
};
attrs = _.extend(attrs, this._mergeEventHandlers(this.props, this._eventHandlers()));
const prefix = this.getPrefix();
const postfix = this.getPostfix();
return (
<FormElementWrapper
type="input"
field={this.props.field}
errors={_errors}
label={this.props.label}>
<div className="form-element-input">
<DisplayToggle remove={true} hide={!prefix}>
<span className="prefix">{prefix}</span>
</DisplayToggle>
<div className={classnames({
"has-prefix": !!prefix,
"has-postfix": !!postfix
})}>
<input {...attrs} />
</div>
<DisplayToggle remove={true} hide={!postfix}>
<span className="postfix">{postfix}</span>
</DisplayToggle>
</div>
<div className="form-error">
{_errors.map((msg, idx) => {
return (<FormError key={idx}>{msg}</FormError>)
})}
</div>
</FormElementWrapper>
)
}
};
FormElementInput.PropTypes = {
field: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
value: PropTypes.any,
label: PropTypes.any,
errors: PropTypes.array,
type: PropTypes.string,
placeholder: PropTypes.string,
onChange: PropTypes.func,
onFocus: PropTypes.func,
onKeyUp: PropTypes.func,
tooltip: PropTypes.string,
prefix: PropTypes.string,
postfix: PropTypes.string,
disabled: PropTypes.bool,
disableTrimming: PropTypes.bool
}
FormElementInput.defaultProps = {
type: 'text',
placeholder: 'Enter Value',
tooltip: undefined,
prefix: undefined,
postfix: undefined,
disabled: false,
disableTrimming: false
}
export default FormElementInput;
FormValidationStateManager.js
class FormValidationStateManager {
// default state, validation ref obj
constructor(defaultState) {
this.rules = [];
this.state = defaultState || {};
this.errors = {};
this.dirty = false;
}
getFormValue(key) {
return this.state[key];
}
setFormValue(key, value) {
this.dirty = true;
this.state[key] = value;
}
setFormState(newState) {
this.state = _.extend({}, newState);
}
addRules(rules) {
this.rules.push(rules);
}
isFormDirty(){
return this.dirty;
}
/**
* Clear all previous validations
*
* #return {void}
*/
clearValidations() {
this.dirty = false;
this.errors = {};
}
clearForm() {
this.state = _.mapObject(this.state, function(v, k) {
return '';
});
this.clearValidations();
}
/**
* Check current validity for a specified key or entire form.
*
* #param {?String} key to check validity (entire form if undefined).
* #return {Boolean}.
*/
isValid(key) {
return _.isEmpty(this.getValidationMessages(key));
}
// call within submit
validateAll() {
return this.validate();
}
/**
* Method to validate single form key or entire form against the component data.
*
* #param {String|Function} key to validate, or error-first containing the validation errors if any.
* #param {?Function} error-first callback containing the validation errors if any.
*/
validate(key, callback) {
if (_.isFunction(key)) {
callback = key;
key = undefined;
}
var schema = this._buildRules();
var data = this.state;
var result = this._joiValidate(schema, data, key);
if (key) {
this.errors[key] = result.errors[key];
} else {
this.errors = result.errors;
}
return {
valid: this.isValid(key),
errors: this.errors,
data: result.data
};
}
_buildRules() {
var allRules = this.rules.map(function(r) {
// Rules can be functions and be dynamically evalulated on-the-fly,
// to allow for more complex validation rules.
if (_.isFunction(r)) {
return r();
} else {
return r;
}
});
// collapse all the rules into one single object that will
// be evaluated against `this.state`
return Joi.object(_.extend({}, ...allRules));
}
/**
* Get current validation messages for a specified key or entire form.
*
* #param {?String} key to get messages, or entire form if key is undefined.
* #return {Array}
*/
getValidationMessages(key) {
let errors = this.errors || {};
if (_.isEmpty(errors)) {
return [];
} else {
if (key === undefined) {
return _.flatten(_.keys(errors).map(error => {
return errors[error] || []
}));
} else {
return errors[key] ? errors[key].map(he.decode) : [];
}
}
}
_joiValidate(joiSchema, data, key) {
joiSchema = joiSchema || Joi.object();
data = data || {};
const joiOptions = {
// when true, stops validation on the first error, otherwise returns all the errors found. Defaults to true.
abortEarly: false,
// when true, allows object to contain unknown keys which are ignored. Defaults to false.
allowUnknown: true,
// remove unknown elements from objects and arrays. Defaults to false
stripUnknown: true,
errors: {
escapeHtml: true,
// overrides the way values are wrapped (e.g. [] around arrays, "" around labels).
// Each key can be set to a string with one (same character before and after the value)
// or two characters (first character before and second character after), or false to disable wrapping:
label: false,
wrap: {
label: false,
array: false
}
},
messages: {
"string.empty": "{{#label}} is required",
"any.empty": "{{#label}} is required",
"any.required": "{{#label}} is required"
}
};
const result = joiSchema.validate(data, joiOptions);
let errors = this._formatErrors(result);
if (key) {
errors = _.pick(errors, key);
}
return {
errors: errors,
data: result.value
};
}
_formatErrors(joiResult) {
if (joiResult.error) {
return _.reduce(joiResult.error.details, (memo, detail) => {
// According to docs:
// - "detail.path": ordered array where each element is the accessor to the value where the error happened.
// - "detail.context.key": key of the value that erred, equivalent to the last element of details.path.
// Which is why we get the last element in "path"
var key = _.last(detail.path);
if (!Array.isArray(memo[key])) {
memo[key] = [];
}
if (!_.contains(memo[key], detail.message)) {
memo[key].push(detail.message);
}
return memo;
}, {});
} else {
return {};
}
}
}
export default FormValidationStateManager;
I think I got it. I changed value: _value to defaultValue: _value for the attributes in FormElementInput.jsx
saw it here
Anyone know why this fixed it under the hood?
I am using vueJs computed to create options for my component like this:
computed:{
fileOptions() {
let fileOptions = [
{
event:'event',
name:'Abcd',
disabled://based upon some condition,
display://based upon some condition
},
{
event://based upon some condition,
name:'Open Presentation',
disabled://based upon some condition,
display://based upon some condition
},
]
}
}
The event, disabled and display property are based upon multiple conditions.
One way of doing this is by using ternary operator
disabled:this.state.libraryActive=='presentations'?false:true
However, it is easy for one condition but for multiple conditions it becomes difficult.
Any suggestions?
The best practice is to use another computed property:
computed:{
fileOptions() {
let fileOptions = [
{
event:'event',
name:'Abcd',
disabled: this.isAbcdDisabled, // based upon some condition,
display: this.isAbcdVisible // based upon some condition,
},
{
event: this.getEventName, // based upon some condition,
name:'Open Presentation',
disabled: this.getDisabled(this.getEventName), // based upon some condition,
display: this.getVisible(this.getEventName) //based upon some condition
},
]
},
isAbcdDisabled ()
{
return this.state.libraryActive === 'presentations' && !this.admin ? false : true
},
isAbcdVisible ()
{
return true;
},
getEventName ()
{
return this.canEdit ? 'edit' : 'add';
}
},
methods:
{
getDisabled (eventName)
{
switch(eventName)
{
case 'edit': return false;
case 'add': return true;
default: return false;
}
},
getVisible (eventName)
{
switch(eventName)
{
case 'edit': return true;
case 'add': return true;
default: return false;
}
},
}
You don't need to use ternary operator, you could make it much easier:
disabled: this.state.libraryActive !== 'presentations'
It will return false if this.state.libraryActive is equal to 'presentations' and true otherwise.
I have the weirdest bug I have ever encountered. I am using Axios and Vee-Validate in my Vue project and from my api I get an error. So withing axios I have a catch.
example:
this.$http.post('v1/auth/register', {
first_name: this.first_name,
last_name: this.last_name,
email: this.email,
phone: this.phone,
password:this.password
}).then((response) => {
this.registration_card = 2;
}).catch((error) => {
if(error.data.error.message === "email_already_exists") {
let input = this.$refs['email'].$children[0];
input.errors.add({ field: 'email', msg: 'email already is use'});
this.loading = false;
console.log(input.errors);
console.log(this.loading);
}
});
Now here comes the weird part. With this code:
let input = this.$refs['email'].$children[0];
input.errors.add({ field: 'email', msg: 'email already is use'});
this.loading = false;
the input.errors is still empty and error wil not be displayed. BUT when i do this:
let input = this.$refs['email'].$children[0];
input.errors.add({ field: 'email', msg: 'email already is use'});
// this.loading = false;
So this.loading will NOT get set, then the error will get set and displayed in my view.
But I want this.loading still be false because I want my loading icon not be displayed. Anyone have a explanation about this.
EDIT: More code
methods: {
register: function () {
let anyError = false;
this.$validate(this, ['first_name', 'last_name', 'phone', 'email', 'password'], function (value, last_item) {
this.loading = true;
if (value === false) {
anyError = true;
}
if (anyError || !last_item) {
return;
}
this.$http.post('v1/auth/register', {
first_name: this.first_name,
last_name: this.last_name,
email: this.email,
phone: this.phone,
password: this.password
}).then((response) => {
this.registration_card = 2;
}).catch((error) => {
if (error.data.error.message === "email_already_exists") {
let input = this.$refs['email'].$children[0];
input.errors.add({field: 'email', msg: 'email already is use'});
this.loadingTest = false;
console.log(input.errors);
console.log(this.loadingTest);
}
});
}.bind(this));
},
}
this.$validate does this:
export default function(scope, arrayOfValues, callback) {
let total = arrayOfValues.length - 1;
let last_item = false;
arrayOfValues.forEach(function(value, index) {
let input = scope.$refs[value].$children[0];
input.$validator.validate().then(value => callback(value, total === index, index));
});
}
I do this because i have custom input components
EDIT: this is where i am using loading:
<j-button label="Register" :loading="loading" #click.native="register"/>
And button coomponent is:
<template>
<button type="button">
<span v-if="!loading">{{label}}</span>
<loading v-if="loading"/>
</button>
</template>
<script>
import loading from 'vue-loading-spinner/src/components/Circle'
export default {
name: 'j-button',
props: [
'label',
'loading'
],
components: {
loading
}
}
</script>
EDIT: Even more code!!!!!
My j-input component
<template>
<div>
<label v-bind:class="{ 'active': (newValue.length > 0)}">{{label}}</label>
<input v-bind:class="{ 'error': (errors.has(name))}" type="text" :name="name" v-validate="rules" :placeholder="label" v-model="newValue" v-on:input="updateValue()" ref="input">
<span v-if="errors.has(name)">{{errors.first(name)}}</span>
</div>
</template>
<script>
export default {
name: 'j-text',
inject: ['$validator'],
props: [
'label',
'name',
'rules',
'value',
],
data() {
return {
newValue: ''
}
},
created() {
this.newValue = this.value;
this.updateValue();
},
methods: {
updateValue: function () {
this.$emit('input', this.newValue);
},
}
}
</script>
So i have found the issue and it is still very strange why. I will make another question for this. Its about my j-button component:
<template>
<button type="button">
<span v-if="!loading">{{label}}</span>
<loading v-if="loading"/>
</button>
</template>
<script>
import loading from 'vue-loading-spinner/src/components/Circle'
export default {
name: 'jellow-button',
props: [
'label',
'loading'
],
components: {
loading
}
}
</script>
To fix this weird issue I had to change this:
<loading v-if="loading"/>
To:
<loading v-show="loading"/>
If I changed this, then the error will be loaded and the button loading icon will be turned off doing this in my catch:
}).catch(error => {
if(error.data.error.message === "email_already_exists") {
let input = this.$refs['email'].$children[0];
input.errors.add({field: 'email', msg: 'email already in use'});
this.loading = false;
}
});
But again. If I do the v-if instead of the v-show in my button then the error will not be showing. Very strange. I will create another question and I hope I get a answer on that.
This is very simple. Only reference change refreshes Vue view.
When you do this:
new Vue({
data: ['property'],
method: {
change() {
this.property = "yes"; // will get refreshed
}
}
});
The view gets refreshed (changes are displayed). But when you change the object's field reference (not the object itself) or when you call a method on it, it won't get refreshed.
change() {
this.property.field = "yes"; // won't get refreshed
this.property.mtehod("yes"); // won't get refreshed
}
Only some certain methods (like array.push()) are tweaked by Vue.js to recognize that those methods get view refreshed. If you want to make it work you need to call this.$forceUpdate() or use Vue.set() to change vales.
So when you add your errors, the view won't get refreshed, only when you change your data property (loading) the view recognize that data value changed and refreshed your view.
Please read Reactivity in Depth, especially chapter "How Changes Are Tracked". Please see which ways of setting data are reactive and which aren't.