I am getting my head around Vee-Validate next (v4) and how I might incorporate it in a Vue 3 project without loosing Vue's reactivity (i.e. not relying on the values simply being passed to the Form submit event).
By way of example, if I were making a hypothetical component which has autocomplete functionality, and sent a get request to the server once 3 letters had been typed, but for the input itself to be valid it required 8 letters, how would I get the value associated with the input?
using plain Vue, with pseudo-code something like:
defineComponent({
setup () {
const myVal = ref('')
const options = ref([])
watchEffect(() => if (myVal.value.length > 3) {
axios.get(...).then(serverVals => options.value = serverVals))
})
return { myVal, options }
how would I achieve this with vee-validate 4.x?
defineComponent({
setup () {
const schema = yup.object({ myVal: yup.string().required().min(8) })
// ???? what now to watch myVal
please note this is not about autocomplete - a range slider where I wanted a server call when the value was greater than 10 but a validation message if greater than 90 would also suffice as an example.
You could employ useField here to get a reactive value that's automatically watched.
const { value: myVal, errorMessage } = useField('myVal', undefined, {
initialValue: ''
});
const options = ref([])
watchEffect(() => if (myVal.value.length > 3) {
axios.get(...).then(serverVals => options.value = serverVals))
})
return { myVal, options }
Documentation has an example of using useField:
https://vee-validate.logaretm.com/v4/guide/composition-api#usefield()
Note that you don't have to use useForm, if you are using <Form> component and passing schema to it then that should work just fine.
Related
I'm having some trouble migrating one thing from the old addon-knobs to the new controls. Let me explain, maybe it's not such difficult task but I'm blocked at the moment.
I'm using StencilJS to generate Web Components and I have a custom select component that accepts a options prop, this is an array of objects (the options of the select)
So, the story for this component in the previous version of Storybook looks something like this:
export const SelectWithArray = () => {
const selectElement = document.createElement('my-select');
selectElement.name = name;
selectElement.options = object('Options', options);
selectElement.disabled = boolean('Disabled', false);
selectElement.label = text('Label', 'Label');
return selectElement;
};
This works fine, the select component receives the options property correctly as an array of objects.
Now, migrating this to the new Storybook version without addon-knobs, the story is looking like this:
const TemplateWithArray: Story<ISelect> = (args) => {
return `
<my-select
label="${args.label}"
disabled="${args.disabled}"
options="${args.options}"
>
</my-select>
`;
};
export const SelectWithArray: Story<ISelect> = TemplateWithArray.bind({});
SelectWithArray.argTypes = {
options: {
name: 'Options',
control: { type: 'object' },
}
}
SelectWithArray.args = {
options: [
{ text: 'Option 1', value: 1 },
]
}
And with this new method, the component is not able to receive the property as expected.
I believe the problem is that now, the arguments is being set directly on the HTML (which would only be accepting strings) and before it was being set on the JS part, so you could set attributes other than strings.
Is there a way to achieve this? without having to send the arguments as a string.
Thanks a lot!!
One way I've discovered so far is to bind the object after the canvas has loaded via the .play function;
codeFullArgs.play = async () => {
const component = document.getElementsByTagName('your-components-tag')[0];
component.jobData = FullArgs.args.jobData;
}
Most of my existing codebase uses a 'id' only in few places 'data-testId' attribute present.
tried this code
import { configure } from '#testing-library/cypress';
configure({ testIdAttribute: ['data-testId','id'] });
But, still its not working.
Is there any way to use 'id' value in any of the testing-library functions.
My HTML code is something like:
<div class="some random class name" id="userprofile-open" role="button">SB</div>
I want click that element with this code:
cy.findByTestId("userprofile-open", { timeout: 120000 }).click();
I don't think you can configure testing-library with an array of ids, ref API configuration,
import { configure } from '#testing-library/cypress'
configure({ testIdAttribute: 'id' })
But even this fails. Instead you have to use the Cypress command to change the attribute name (only one name is allowed).
cy.configureCypressTestingLibrary({ testIdAttribute: 'id' })
To use either/or attribute name you can change the attribute name on the fly, wrapping it in a custom command (based on Custom Queries)
Cypress.Commands.add('findByTestIdOrId', (idToFind) => {
let result;
const { queryHelpers } = require('#testing-library/dom');
let queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'data-testId');
result = queryAllByTestId(Cypress.$('body')[0], idToFind)
if (result.length) return result;
queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'id');
result = queryAllByTestId(Cypress.$('body')[0], idToFind);
if (result.length) return result;
throw `Unable to find an element by: [data-test-id="${idToFind}"] or [id="${idToFind}"]`
})
cy.findByTestIdOrId('my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
Note this custom command works only for synchronous DOM.
If you need to have Cypress retry and search for either/or attribute, don't use testing-library in the custom command.
Instead use Cypress .should() to enable retry
Cypress.Commands.add('findByTestIdOrId', (selector, idToFind) => {
cy.get(selector)
.should('satisfy', $els => {
const attrs = [...$els].reduce((acc, el) => {
const id = el.id || el.getAttribute('data-test-id') // either/or attribute
if (id) {
acc.push(id)
}
return acc
}, [])
return attrs.some(attr => attr === idToFind); // retries when false
})
.first(); // may be more than one
})
cy.findByTestIdOrId('div', 'my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
The usual cypress way - which has an inherent check on the element visibility and existence as well as included retries for a period of time is using cy.get()
If you want to select element using property like data-id you need this sintax: cy.get('[propertyName="propertyValue"]')
If you want select an element by CSS selector you just pass CSS selector like this:
cy.get('#id')
Using Redux-form, I'm running into issues with validation.
I would like to access the store for referencing a piece of state in validation.
I have standard redux form export:
function mapStateToProps({address_object}) {
return{
address_object: address_object}
}
export default reduxForm({
form: 'wizard',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
validate,
})(
connect(mapStateToProps,mapDispatchToProps)(WizardFormFirstPage)
);
and my validate function:
const validate = (values, props) => {
console.log("DEBUG :",props)
... if / else etc
which lets me gets at the props of the form
however I would like to run validation against something that is stored in state. something like:
if (<something in state> === values.<thing>) {
errors.field = "broken"}
console logging the props that validation receives, I can't get it to see anything in state. I can see the address_object in the WizardFormFirstPage component obviously (so actions and reducers are working fine)
Do I need to declare the validate function inside the component (that's connected with mapStateToProps) in order to access this.props.whatever ?? If thats the case, does it work if I call it from the export reduxForm()??
redux noob - apologies if dumb question
Your almost there.
When you connect your form add the state you need for validation in mapStateToProps.
The state will then be available in the validate props:
reduxForm({
form: 'wizard',
validate => (values, { stateForValidation }) => {
if(values.myField === stateForValidation)) {
errors.myField = 'invalid'
}
}
})(connect(
({ stateForValidation }) => ({ stateForValidation })
)(form))
If anyone cares I ended up handling validation at the Field level with
<Field
validate={this.validateAddress} ...>
then the component method:
validateAddress = () => {
errors = {}
// validation stuff
errors._error = "string to display" //use this instead of errors.fieldname = ""
return errors
}
which gave me access to props. However #pariesz answer above answers this question much better.
Is it anyway possible to submit also the empty values on a form? If not how do you properly initialize the form from state?
If it is not possible do I really need to do aField: this.props.data.aField || '' for every field I want to initialize? This seems like a lot of typing and repeating especially on forms which have FormSections and nesting.
If it would be possible I could just do something in the lines of this.
handleInit() {
const { patient, initialize } = this.props;
initialize({
patient.aField,
// Other fields
});
}
Not sure if this is applicable to your scenario, but you can specify initial form values in the mapStateToProps phase:
const mapStateToProps = state => {
return {
initialValues: { ...state.patient } // Use this property to set your initial data
};
}
This is also explained here: http://redux-form.com/6.6.1/examples/initializeFromState/
redux-form version: v6.2.0
I have a credit card info form. (number, expiry, cvc). I don't have enough space for an error on each field, so I want just 1 form-level error field instead of many field-level input field errors.
This would be easy enough to do with form-level validation (adding an _error: firstFieldLevelError) but I don't want to show it unless that field has been touched (and of course touched is only available to the field).
So, I resorted to grabbing it manually out of the state:
const mapStateToProps = (state, props) => {
const formState = state.form[props.form];
if (formState) {
const {syncErrors} = formState;
if (syncErrors) {
const firstErrorField = Object.keys(syncErrors)[0];
const {touched} = formState.fields[firstErrorField] || {};
if (touched) {
return {
syncFormError: syncErrors[firstErrorField]
}
}
}
}
return {};
};
Pretty darn verbose for what I imagine to be something pretty common. Is there a better way?