jaetask / xstate-form

xstate form integration
MIT License
20 stars 0 forks source link

More similar shape to XState? #2

Open tomByrer opened 3 years ago

tomByrer commented 3 years ago

Instead of:

import { form, fields } from 'xstate-form';

const machine = form({
  id: 'myAwesomeForm',
  validate: (values, event, meta, name) => {
    const errors = {};
    if (values.username.match(/[0-9]+/g)) {
      errors.username = 'Username cannot include a number';
    }
    if (values.password.length <= 8) {
      errors.password = 'Password must be > 8 chars';
    }
    return errors;
  },
  fields: {
    username: fields.text('username'),
    password: fields.text('password'),
    // ...
    submitForm: fields.submit('submitForm'),
  },
  initialValues: {
    username: 'jaetask',
    password: 'ThisIsTheWay',
  },
});

Maybe this? More verbose, but reduces jumping around if you're adding 1 new field.

import { form, fields } from 'xstate-form';
const [ text, password, submit ] = fields

const machine = form({
  id: 'myAwesomeForm',
  fields: {
    username: text({
      label: 'Username',
      validate: valPassword,
      // all HTML attributes should be echoed; easier to learn & in case HTML is used vs JSX
      required: true,
      value: 'jaetask',
    }),
    password: password({
      label: 'Password',
      validate: valPassword,
      required: true,
      value: 'ThisIsTheWay',
    }),
    submitForm: submit({
      label: 'Submit Form',
      validate: valForm,
    }),
  },
  validations: {
    valUsername: (value)=>{
      if (value..match(/[0-9]+/g)) {
        return 'Username cannot include a number'
    }},
    valPassword: (value)=>{
      if (value..length <= 8) {
       return 'Password must be > 8 chars';
    }},
    valForm: (requiredValues)=>{
      requiredValues.map...//check all values exist for required
        return 'Please fill required fields'
    }},
  },
});

Not sure if the validation should be broken out like I have it or inlined.

jaetask commented 3 years ago

Hi Tom, That's a really interesting idea.

Are we saying that fields would be true actors and retain their own internal value? If the actors are spawned then we would have a reference to them and be able to forward messages.

In this validation method, how would you be able to validate a field based on the value/state of another field?

i.e.

if x is disabled then y should be a, b or c.

tomByrer commented 3 years ago

fields would be true actors and retain their own internal value

I don't know about all form fields, but for my Radio Button Group, each Radio Item is its own machine. I decided to do it that way since it is for testing, where the Item will show the correct answer, so I have 4 final-states (selected or not, valid or not). But right now, I don't use XState's 'actors' per se (I find them confusing right now for what I'm doing). When the master inputGroupMachine gets data, it then iterates over the options to spin up the machines for the Radio Items in a separate function. (still Alpha, may reorg as I build)

In regular HTML forms, disabled / hidden fields are still sent to the server, so that is used as a trick to send things like referral & other tracking codes.

Can you give specific use-cases when you have to "validate a field based on the value/state of another field" outside of when you Submit the form? I can not think of any, so I'd program any non-individual element checks as part of the 'form validation' function.

tomByrer commented 3 years ago

BTW, the reasoning I have each form element's data better segregated is for better JS-object composing like pedestrianStates here. Could make it easier for element re-use, & re-organization (eg drag & drop builder).

jaetask commented 3 years ago

I'm definitely open to this. It does provide a clean API, I'll need to think through the internals of how the messages would connect. I started out with a Formik style lib in mind, but formik does a lot of magic when connecting forms, so I tried to simplify away from that. The end goal is to achieve a clean API that allows users to do what they need, not to enforce them to work a specific way.

I agree with keeping things local, possibly even the fields current value, isPristine, isDIrty etc. All of which could make for a super reusable API. If each field is a standalone machine that can be run as services, then people could even roll their own form and just consume the field machines.

jaetask commented 3 years ago

I do have a concern over passing text strings into the core form library. In this example, we tie the text value of the label to the form item, what happens if the user changes language?

text({
      label: 'Username',
      validate: valPassword,
      // all HTML attributes should be echoed; easier to learn & in case HTML is used vs JSX
      required: true,
      value: 'jaetask',
    }),  

One part of me wants to move this to the UI libraries, which will sit on top of xstate-form. They can then choose how to internationalise.

Another part of me loves this idea and would imagine the API to include placeholder, alt texts etc. All of which could be stored in the fields meta data and made available to UI Builders, model-based testing and the likes.

And thinking about it further, there is no reason that those values have to be strings, they could also accept functions which accept the field and know what to translate. Again, letting the user chose the i18n library of choice and keeping things highly configurable. see article

In which case the API could look like this

text({
      label: (fieldName, field) => i18n.t(`${fieldName}Label`)
      value: 'jaetask',
}),  

or using a more xstate way, in the machineOptions object like this

text({
      label: 'getFieldLabel'
      value: 'jaetask',
}),  
// ...
{
    services: {
        'getFieldLabel': (fieldName, field) => i18n.t(`${fieldName}Label`)
    }
}
tomByrer commented 3 years ago

i18n is important! You could do it that way, but I try to avoid functions for performance reasons. A simple object can be used to hold translations:

// let's pretend these `let` statemnts are imports as...
let english = {
  username: {
    label: 'Username',
    error: 'Please enter a username without numbers.',
  },
}
let italian = {
  username: {
    label: 'Nome utente ',
    error: 'Inserisci un nome utente senza numeri.',
  },
}

let language = english

text({
      label: language.username.label,
      value: 'jaetask',
      error: language.username.error,
}),  

In action: https://jsbin.com/setijun/edit?js,console

tomByrer commented 3 years ago

If each field is a standalone machine that can be run as services

I'm not suggesting it is best to do it that way, just that I'm doing it this way because I understand it better. It might be more performant or better to reason to build 1 big machine with each field as a parallel state.

So there are 3 ways an XState form can run, 1 big parallel, several independent machines, & parent machine spawning child actors? (I need to figure this out for my next project, A/V player with XState.)

jaetask commented 3 years ago

I agree. it's not the only way, but it does have lots of advantages.

The downside is managing state, messaging & validation. I've been doing this today and it is harder, (just a steep learning curve) but I have a working example, will push it up tomorrow evening.

jaetask commented 3 years ago

Ooh, there's one more bonus. Each machine appears individually within xstate/inspector, which makes debugging so much easier and clearer