wearebraid / vue-formulate

⚡️ The easiest way to build forms with Vue.
https://vueformulate.com
MIT License
2.25k stars 245 forks source link

Proposal: a class map for easy class manipulation. #66

Closed justin-schroeder closed 4 years ago

justin-schroeder commented 4 years ago

Describe the new feature you'd like Proposed solution for allowing classes to be overriden on any sub-element of vue-formulate:

The default would be something like:

Vue.use(VueFormulate, {
  classmap: context => ({
    "outer": `formulate-input`,
    "outer-wrapper": `formulate-input-wrapper`,
    "element-wrapper": `formulate-input-element formulate-input-element--${context.type}`,
    "element": '',
    "help": "formulate-input-help",
    "errors": "formulate-input-errors",
    "error": "formulate-input-error"
  })
}

The idea is that classMap function would be defined globally for all inputs, and then each sub-type could override it, and then as granular as individual input fields could also override each:

<FormulateInput
  type="text"
  classmap-element="form-input block w-full sm:text-sm sm:leading-5"
/>

The idea is that the above solution would allow a developer to setup their own fields using a class-based ui framework and handle 90% of a project's form styles globally for all FormulateInput elements, and then on a case by case basis easily override the styles on a per-element basis, and lastly scoped slots as a nuclear option.

I'll reiterate the goal of Vue Formulate is to make high quality form building fast and easy, so I we want to provide that 90% solution as painlessly as possible. 90% of inputs shouldn't require any additional configuration, scoped slot usage, etc.

What percentage of vue-formulate users would benefit? Probably about 20%


I'm looking for feedback from users of frameworks like Tailwind, Tachyons, or Bootstrap.

gilesbutler commented 4 years ago

I think the classMap function sounds like an interesting idea. Just to clarify your example though would it be something like this...

<FormulateInput
  type="text"
  classmap-element-wrapper="mt-1 relative rounded-md shadow-sm"
  classmap-element="form-input block w-full sm:text-sm sm:leading-5"
/>

classmap- just name spaces the input you want to apply classes to?

I think you're right, that should help for 90% of cases and then people can use scoped slots if they want to go deeper.

justin-schroeder commented 4 years ago

Correct, the classmap- would just be the prop namespace for consistency. Think it's too verbose? We could drop the map class-element-wrapper="form-input" if that seems better.

gilesbutler commented 4 years ago

Dropping the map may help beginners a bit more. It's a bit more succinct I guess.

Garito commented 4 years ago

There is no need for this way to work I work with OpenApi schemas so let me explain how I'm doing it that way: In the schema you could add field's classes by using x-yourlabel-class and the rest of the layout you could define it by putting the layout as a slot and move the fields to their positions with vue-portal (that vue v3.0 will have as a build in component) So from the vue-formulate perspective, I will include a prop (use-portals) to render the forminput with a portal and then the default slot for the form will be the layout you what to use

bbugh commented 4 years ago

This will probably be great for utility-based libraries like Tailwind, but I am not so sure that this is going to be enough for Bootstrap, which expects a certain markup along with the styles.

I'm really stoked on the library, but currently finding it somewhat limiting because the markup is fixed. For example, something like an error message currently is forced as a ul, but our current validation errors use sentences.

Thing must be at least 30 characters long, start with the word 'fruit', and contain at least 1 emoji.

Have you considered an alternative in renderless components that can be used to compose other components? (I thought that's what this library was and was thrilled.)

For example, you might have a FormulateErrors that doesn't draw anything, but handles the error logic, and returns a slot with the errors.

<formulate-errors v-slot="{ dirty, errors, label }">
  <div v-if="dirty && errors.length">
    {{ label }} {{ errors.join(", ") }}
  </div>
</formulate-errors>

Then FormulateInput would be composed of these renderless components, and people can use it if it works for them. For other cases that won't work for, they can build their own SomeInput that is composed of the renderless components.

Garito commented 4 years ago

Giving that portals will be a built in feature for vue, the approach could be: This is the bootstrap input widget:

<div class="form-group">
  <label for="exampleInputEmail1">Email address</label>
  <input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
  <small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>

Using portals, the label, input and messages/errors should be wrapped by a portal and the widget should be written like:

<div class="form-group">
  <label for="exampleInputEmail1"><PortalTarget name="label"></PortalTarget></label>
  <PortalTarget name="input"></PortalTarget>
  <small id="emailHelp" class="form-text text-muted"><PortalTarget name="msg"></PortalTarget></small>
</div>

The only part that will need some attributes will be the input

Would be more simple and better for this library if we could focus on the specifics of managing forms and not how they should look (seems that is the whole point of vue-portal: separation between the layout and the functionality)

bbugh commented 4 years ago

@Garito I hear that you think portals are a solution, but it's not. It seems like you are confused about the use case for Teleport (which is what portals are called now). The Teleport component is when multiple different locations throughout the app need to write to a single location for things like modals, toasts, alerts, banners, etc.

Teleport targets are globally accessible and must be uniquely named. If multiple teleport targets exist with the same selector, Vue 3 will pick whichever is rendered first in the DOM. In your example, there's also no way to know which FormulateInput would be writing to which portal.

<!-- somewhere in the app -->
<div class="teleport-target"> <!-- This will get teleport content --> </div>
<div class="teleport-target"> <!-- This will not, and will be empty --> </div>

<!-- somewhere else -->
<Teleport to=".teleport-target">
  stuff to teleport
</Teleport>

Even if that would work, using teleports as you suggested means that any part of the app could write to the target, which is not ideal for encapsulation. My NavBar doesn't need to write to the input box of a random form.

I asked about renderless components because it's an existing, established pattern in Vue 2 to solve issues like this.

Garito commented 4 years ago

Perhaps your are the confused one but, ok, I will mind my business

Garito commented 4 years ago

According with Linus Borg's portal-vue: "A feature-rich Portal Plugin for Vuejs, for rendering DOM outside of a component, anywhere our app or the entire document."

Bye

bbugh commented 4 years ago

For example, FormulateErrors can be extended to simply add a render function and removing the template:

  render() {
    return this.$scopedSlots.default({
      visibleErrors,
      type
    })
  },

And used to compose other components - maybe this is the default for the Input errors slot, instead of the rendering being done in the FormulateErrors

  <formulate-errors v-slot="{ visibleErrors, type }">
    <ul
      v-if="visibleErrors.length"
      :class="`formulate-${type}-errors`"
    >
      <li
        v-for="error in visibleErrors"
        :key="error"
        :class="`formulate-${type}-error`"
        v-text="error"
      />
    </ul>
  </formulate-errors>

And we can use it in our own code to compose our own form input classes.

<!-- OurCustomFormInput.vue -->

  <formulate-errors v-slot="{ visibleErrors, type }">
    <div class="errors" v-if="visibleErrors.length"> 
      {{ sentencify(visibleErrors) }}
    </div>
  </formulate-errors>

The way FormulateInput works is still very markup dependent, even using slots. But the form system itself is really useful and the most important part. Offering a useful default would be nice but really we're after the form handling!

dosstx commented 4 years ago

Thanks for making this plugin, Very nice. Unfortunately, I can't easily add Bootstrap classes to it.

n10000k commented 4 years ago

Simple solution, use tailwind ;)

n10000k commented 4 years ago

I'm waiting on this been talking about vue-formulate for a long time, just want to throw my own classes in it and dev quicker and not have any bs.

andrew-boyd commented 4 years ago

@narwy Vue Formulate does not want to take strong stances on layout (https://vueformulate.com/guide/#what-it-isn-t). The base theme is currently provided mainly for the sake of having good-looking examples.

Whether your CSS approach of choice is to roll your own styles, use a full UI framework such as Bootstrap, or leverage a utility-based class system such as Tailwind we’re seeking to make the library work for you.

Rather than “just use Tailwind” we’d prefer to solve the hard problem of creating an API that allows Tailwind users to get just as much value out of Vue Formulate as any other CSS aficionado.

We’re not there yet though, so as a Tailwind user your input would be invaluable to us (we are not users of Tailwind ourselves). We need feedback on any proposed class-map system to ensure it’ll be robust enough to handle whatever CSS system is currently in vogue.

n10000k commented 4 years ago

@andrew-boyd I'd just create the correct props/api "config" as such to handle for passing classes. This day and age people make classes regardless, seen as tailwindcss is a utility driven css framework which is classes only this is a good thing. Other frameworks like BS4, Bulma etc as long as you can pass a class to the style a component you want i think that would cover it all.

Something to take into account in tailwind we have pseudo class variants an example would be a group so covering top level is a must also - ref: https://tailwindcss.com/docs/pseudo-class-variants/#app

I think covering each element is a must. I'll be honest when It comes to forms I'd rather have a package like formulate that just is generic where I have any input in my app.. I'd have some form of config that I can change in one place instead of on each element, and maybe if i pass a custom style on that component, it will override the config? I don't know spit-balling ideas here where I could see it benefiting.

This is the only thing that's been putting me off using formulate more is the styling, i want to have ease when it comes to throwing something together vs manually doing everything, especially when an app maybe form input heavy.

Don't know if this helps or if I'm rambling on, but please add a way to style items in a easier way/format vs each element manually styling.

andrew-boyd commented 4 years ago

Thanks for the input. The proposed system (and the one that’s also in the works) allows for setting custom classes on each level of hierarchy that Vue Formulate outputs. You’d be able to set both global “these classes should be on every Vue Formulate element” defaults as well as per-input overrides when you need exceptions.

If you have a form-heavy project then even with class maps it may be worth it to create your own abstraction components that output specific input lock-ups that are common in your project’s UI — but that is something we would leave up to the individual project author to complete.

Keep an eye on this issue and we’d love to get your reaction to class maps when they ship. Current target is for release in version 2.4.0.

hmaesta commented 4 years ago

Super excited about this 🙌

I up vote for class- instead of classmap- on FormulateInput.

First time using Tailwind (coming from Bulma) and Vue Formulate was my first "can't do" so far, as detailed here.

Simple solution, use tailwind

@narwy Are you able to use Tailwind on Vue Formulate with inline classes? What is your workaround? (withou @apply etc)

n10000k commented 4 years ago

@hmaesta No work around for me, have to use @apply and define utility classes under that. I actually can't wait for this though, will reduce my apply definitions which is a good thing <3

justin-schroeder commented 4 years ago

Hey @narwy @hmaesta and @gilesbutler — this feature is almost finished now. I've got an early preview of the docs here:

https://vueformulatecom-git-feature-classmaps.braid.now.sh/guide/theming/#customizing-classes

I think this should handle almost every use case, and like @narwy mentioned you can just configure your tailwind classes once at the global level and be done — then when you need a little more customization on a particular input extend those globals. Would love feedback though!

justin-schroeder commented 4 years ago

also @bbugh somehow I missed your helpful conversation on here.

I've considered including renderless too. It would be pretty easy to implement, but I'm slightly concerned about it undermining the main the goals of this project: to make form authoring really easy, fast, and high quality (best practices, accessibility out of the box, etc) so markup is actually a pretty important part of what we're trying to do here — and getting people to conform to a system that allows those features to be improved over time is really valuable for the community at large. For example, if we get some good accessibility feedback an update could help hundreds of sites improve accessibility at the same time.

That said, I understand that occasionally people need to deviate from that or their use case really doesn't need those features. I was hoping slots, and slot components would suffice.

hmaesta commented 4 years ago

That's really nice, @justin-schroeder 🤩

I read the doc and everything looks great. But since you are already working on this... shouldn't Vue Formulate support pseudo-classes on input elements?

For example:

Thinking about Tailwind specific, if @tailwind/ui plugin is present it shouldn't be any problem, since .form-input class handles these pseudo-classes –– but with pure Tailwind user won't be able to add styling to :hover, :active :focus and :checked.

Since this is a project that handles forms, at least :focus and :checked are mandatory for accessibility proposes (for example, how would I know if a checkbox is selected without :checked?)

I have some fears about performance, but I think it should be supported.

Tailwind UI is a paid plugin for Tailwind CSS. Right now everyone is using (including me) because it's early stage (and free), but it's not so clear if this plugin will continue free after the public release. What I understand so far is that they will charge for the "example codes", but since the plugin doesn't have a public repository I feel that it maybe won't be free.

Anyway, Tailwind is just an argument. I really think pseudo-class support would be great for advanced CSS styling.


Just to illustrate, this is my Tailwind code so far – without using Tailwind UI:

ScreenFlow

.formulate-input {
    .formulate-input-label {
        @apply block text-sm leading-5 text-gray-700;
    }

    .formulate-input-element {
        @apply mt-1 relative rounded-md shadow-sm;

        &.formulate-input-element--text {
            input {
                @apply block w-full px-3 py-2 border-gray-100 border-solid border rounded-md transition duration-100 ease-in-out;

                &:hover {
                    @apply border-gray-200;
                }

                &:focus {
                    @apply border-blue-400 outline-none shadow-outline;
                }
            }
        }
    }
}

Ps: On the sentence "Simply target the class key you’d like..." the "class key" link is broken. You pointed to #class-map and it should be #class-keys 🙂

hmaesta commented 4 years ago

Also thinking about status classes... I am not happy about adding more complexity for this, but I believe it's best to bring all cases to discussion. 🤐

justin-schroeder commented 4 years ago

@hmaesta So if you want to conditionally add classes you could always use functions and then change things based on the context, but are you sure that you need separate class keys for each of these? I'm not much of a tailwind user, only dabbled, but since psuedo's are just classes in tailwind would it work something like this?

Standard HTML:

<input class="bg-gray-200 hover:bg-white hover:border-gray-300 focus:outline-none focus:bg-white focus:shadow-outline focus:border-gray-300">

Vue Formulate:

<FormulateInput
  input-class="bg-gray-200 hover:bg-white hover:border-gray-300 focus:outline-none focus:bg-white focus:shadow-outline focus:border-gray-300"
/>

Am I way off on that @hmaesta

hmaesta commented 4 years ago

Oh god 🙉 Today I was so immersed with the @apply function that I totally forgot about Tailwind inline support for pseudo-classes.

For that scenario you are totally right. No need for specific "pseudo-keys" on Vue Formulate. But I don't know if competitors (Bootstrap?) have support for this.

bbugh commented 4 years ago

@hmaesta you're correct, bootstrap doesn't have hover psuedo-classes like Tailwind. Generally, the class map idea seems like it will work great with Tailwind and not so well for non-utility frameworks. To get a global styling for Bootstrap, we'd likely have to shadow/extend the built-in formulate classes with custom CSS and/or create a custom wrapper component for FormulateInput.

justin-schroeder commented 4 years ago

Yeah, with non-utilities we'll never have enough options. There are so many combinations of things out there. The current approach gives people an escape hatch to implement their own functions to apply as needed, for example to add input-error-class on the <input> only when there are errors:

Vue.use(VueFormulate, {
  classes: {
    input: (context, baseClasses) => {
      if (context.visibleValidationErrors.length) {
        baseClasses.push('input-error-class')
      }
      return baseClasses
    }
  }
})
justin-schroeder commented 4 years ago

@hmaesta and @bbugh do you guys think we need to add more class keys even with functional class assignment? Trying to figure out what the right balance is (appreciate all the feedback 🙏)

hmaesta commented 4 years ago

In my mind I can imagine 4 levels for this:

To be honest I think we should go at least with Level 2 – simply because we should be responsible for UX. Here's an example:

Artboard

We can't deny what input is way easier to recognise that has wrong data.

Looking both inputs side by side it's clear that the second one is better. For the average user, on a form full of filled inputs, this makes a lot of difference. It's 100% don't make me think.

I know we can offer a nice sample code of "how to do it yourself", but we have to be honest with ourselves: on a cheap project with a near deadline, no developer will care about this kind of detail. So, again, we have to be responsible for UX and the "average user". It's not a huge change for us, but can benefit a lot of people.

I don't feel like I am in a position of telling what should be done – since I can't even help with coding and I am not paying for this great work – but every one of these states significant improves user experience. So, if you guys have the time and it's possible to go with Level 4 without perfomance issues, I would go for it – but if you are looking for the best balance, I think it is the Level 2.

Why don't we start with Level 2 and them, based on community feedback (aka issues "how to do this?"), we add support for other levels? 🙂

justin-schroeder commented 4 years ago

@hmaesta strong argument, and I cant disagree that we should give a better UX to more end users by embracing sensible defaults. Great feedback. I'll try and implement 1-3. I'm concerned that Level 4 is a “bridge too far” since we'd need to tracking cursors/touch start etc. However your Level 4 would make for a compelling little plugin if someone wants to write one.

justin-schroeder commented 4 years ago

Phew — ok everyone. This is finally merged and live in 2.4. You can read more in the docs here:

https://vueformulate.com/guide/theming/customizing-classes/

hmaesta commented 4 years ago

That's the fastest open source project team on GitHub 😆 Already using with Nuxt without problems 🙌 🚀

For the error classes, that's how I am doing:

let error_class = '';

Vue.use(VueFormulate, {
  classes: {
    input: (context) => {

      if (context.hasErrors) {
        error_class = 'border-red-400 hover:border-red-400';
      } else {
        error_class = '';
      }

      switch (context.classification) {
        case "select":
          return 'form-select ' + error_class;
        default:
          return 'form-input ' + error_class;
      }
    },

I am a designer... Not sure if that's the best approach – if you have suggestions, let me know.

Thank you for the fast implementation 👍

mwojtul commented 4 years ago

Props on the 2.4 release! We're in the process of adding VueFormulate to our codebase and the newly introduced classes functionality has saved us from needing to create custom inputs that override the defaults, which had been necessary due to our Boostrap css setup.

justin-schroeder commented 4 years ago

That’s awesome @mwojtul! So glad that saved some pain!

@hmaesta Thanks for the kind words! I think the way you're doing it looks great. Also, we did implement your suggestion for states like hasErrors, isValid etc so if you want you can do:

Vue.use(VueFormulate, {
  classes: {
    input: (context) => {
      switch (context.classification) {
        case "select":
          return 'form-select ' + error_class;
        default:
          return 'form-input ' + error_class;
      }
    },
    inputHasErrors: 'border-red-400 hover:border-red-400'
  }

These are "state keys" and are always additive (they wont override). They're your idea :) and totally a helpful shortcut!

https://vueformulate.com/guide/theming/customizing-classes/#state-keys

gilesbutler commented 4 years ago

Congrats on the 2.4 release @justin-schroeder and the rest of the Braid team. Awesome work and amazing results!

hmaesta commented 4 years ago

we did implement your suggestion for states

Oh, I don't why I thought that just isValid was implemented. This is perfect! 🤩

Thanks, guys.