vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.88k stars 546 forks source link

@click.outside to handle clicks outside an element #167

Open jakedohm opened 4 years ago

jakedohm commented 4 years ago

Proposal

I would love to be able to write either @click.outside to handle any click outside of an element. This is a common use-case for things like modals or dropdowns that should close when you click away from them. The demand for this has already been proved by v-click-outside which has 60,000 weekly downloads on NPM.

This seems like a trivial thing, but I think it would improve the DX of working with Vue.

Naming

I think @click.outide is the most intuitive API. Alpine.js went with @click.away but when I asked the author why, he said he wasn't sure and that "outside might honestly be better" (source)

API

I think a simple API like @click.outside="handleOuterClick" makes sense. I'm not sure if there's enough value in adding some of the options that v-click-outside has to warrant the larger API surface area, and kinda awkward API. For more advanced/complex use-cases people can always

Prior Art

If this gets support, core team members are okay with it, then I'm happy to take a crack at an implementation and PR it!

bangjelkoski commented 4 years ago

How would you then register a normal @click event on the element that has this event handler?

Example

<button @click="onClick" @click.away="onClickAway" />
jakedohm commented 4 years ago

Hmm, great question. You should be able to register both, just like that, depending on if Vue's internals will allow that to happen. Because: the @click.outside doesn't actually register a click handler on the element, it registers one on the window/document, so there's not a conflict there.

bangjelkoski commented 4 years ago

I like the idea and I have used the package you mentioned myself in numerous occasions, but I think this would be better if it was included as a directive and not a modifier to the @click event.

Lets see what others say about this. Thanks for the rfc, I think it will spike up some good discussion :)

aztalbot commented 4 years ago

I like the idea. But, I'm also unsure about it because v-click-outside is just one additional character than @click.outside and actually reflects better what is happening behind the scenes (it's not attaching an event listener to the element).

On the other hand, I find @click.outside more intuitive since I'd already be thinking about events (not directives) when thinking about adding this logic to an element. I also find in my own habits I am more likely to write this logic myself than bring in a third-party library for a simple directive (maybe just me, not sure if anyone else is similar). It would be nice to have something (directive or modifier) built in for this, since it is so common. That way it can be covered in documentation, so users don't feel the need to find a third-party library or write the logic themselves.

I'd also add, having this functionality built in probably makes sense with the new built-in <Teleport> component. They sort of go hand-in-hand:

    <teleport to="#endofbody" v-if="showModal">
      <div id="content" v-click-outside="showModal = false">
        <p>
          this will be moved to #endofbody.<br />
          Pretend that it's a modal
        </p>
        <Child />
      </div>
    </teleport>
jakedohm commented 4 years ago

@aztalbot good points 😄. I agree that I'd often write this logic myself vs pulling in a third-party library. I actually did it this morning in a Vue 3 (Beta) project. For me, it's definitely less about the amount of characters, or even library weight (since vue-click-outside is 1kb) and more about developer experience. It's a proven common use-case that IMO makes sense to solve out of the box.

@bangjelkoski I don't really love using a directive for this, because this use-case is so close to being the same as handling a normal click event. We've already got a way to manage events, and we've already got modifiers. In my mind this is a valid use-case of an event+modifier.

jods4 commented 4 years ago

The idea sounds nice but I'm not sure it's worth it. Disclaimer: I do use (my own) v-click-outside as a fundamental block for controls in my application.

  1. v-click-outside isn't bad. Not every useful directive should have special support in Core, what makes this one worthy of a special treatment?

  2. Semantically it's not correct and it's gonna require twists in Vue compiler. @click adds listeners to the click event of its target element. Modifiers are applied to the handler function. Check the codegen here This is not where @click.away needs to go. The directive is actually attaching/removing a click handler on the document on mount / unmount.

  3. This might be best served by user-land libraries because there are several useful variations that make its behavior not-so-well-defined for something in core. For example: it's common to combine the clicking outside with a focus outside. E.g. if you write the popup of a dropdown. When the user tabs away you want to dismiss it and it's nice if your v-click-outside is able to handle both. Other example: sometimes I have two distinct elements that are the "click outside" target and my v-click-outside supports that. This happens if you can't/don't want to put your popup in the same container as the element targets it (e.g. an input control). Other considerations: is this a click or mousedown? Do you take into account touch, maybe pointerdown? Is it a misnomer then? How far back do you want to support browsers? Or is this modifier gonna apply to other events as well? (That last bit might make it more interesting for core, although I'm not sure if there are many use cases.)

aztalbot commented 4 years ago

@jods4 you're last comment is interesting. Do you think it might make more sense to provide a basic building block then, like a v-outside directive that attaches an event listener to document and only fires if it occurred outside the element? That would be fairly simple to support, and the user would then be able to choose whether to ignore if event came from a second element, or to filter out certain events. It could also be something like v-outside:click and v-outside:focus, but then that's going beyond a basic building block.

jods4 commented 4 years ago

@aztalbot That's an interesting primitive to have! Although to be honest I can't come up with much uses besides focusin and pointerdown that I use to build v-click-outside. To be worth it, we'd need more use cases, otherwise building v-click-outside directly is enough. Maybe someone else is more creative?

In the realm of "interesting ideas": Vue compiler is extensible you can create plugins. I don't know if it's flexible enough that creating a userland .outside modifier would be possible.

Unrelated spam: extending modifiers opens up stuff like a .delegate modifier, something that I would find interesting in core.

michaeldrotar commented 4 years ago

I was also thinking what about a @focus.outside.. and for that matter, would @mouseover.outside or @keydown.outside make sense... are there any events that don't make sense?

For focus/blur specifically, there's the caveat that focusin and focusout bubble while focus and blur do not.. so would @focus.outside have to be smart enough to be a @focusin.outside or would it be an obscure user trap that does nothing and causes frustration?

I think this RFC brings up interesting things to solve:

I agree that .outside is semantically incorrect and I think the caveats make it potentially confusing and inconsistent with how people would normally use @focus and @blur.

That said, there's an obvious need to be able to register events outside the DOM of your component in order to even begin to solve these types of issues.

I could see it being a core feature since it's something that every component of a certain type of component would need to care about. Like every dialog/modal/tooltip/dropdown component should care about this.. and if my dialog component is using one lib and dropdown another and tooltip makes up its own solution then that bloats my final app with 3 solutions for the same thing.

There may also be missed performance opportunities... is it better to register a single event handler on the body and iterate through which callbacks it needs to fire or better to register one per callback? I believe jquery still does the former so I assume that's still the better option.


Perhaps a low-level core solution is to simply allow adding event handlers to the body (I don't believe I've seen anything for this as of yet)... and the "filtering" of whether they're inside or outside of your component is up to the implementation so userland could build on top of it to provide inside/outside functionality or likely other useful things as well (game controls come to mind).

I'm also wondering if we'd need to add event handlers to things other than body, but I'm not having a use case come to mind for it.

ByScripts commented 4 years ago

I'm divided.

Like many others, I'm using v-click-outside. And a @click.outside seem interesting.

But my main concern is that the modifiers are supposed to alter the event that is attached to the element.

@click.prevent means "apply the prevent modifier to the onclick listener" @click.foobar means "apply the foobar modifier to the onclick listener"

But @click.outside would mean "attach the click listener to the root document"

9mm commented 4 years ago

FWIW I'd love to see someones implementation of vue-clickaway (maybe @jods4 ?). I'm using vue 3 RC and that no longer appears to work. I have spent a bit of time digging into how to get it working but to no avail.

jods4 commented 4 years ago

@9mm are you in discord? Nag me over there and I'll share what I have with you when I'm at work.

marcotas commented 2 years ago

1 and half years later and this is still open? IMHO this modifier should definitely exist. Using Alpinejs for this is super handy and I really miss this in Vue. I don't like to install small packages like that in real applications even though they do a really good job on this, but I don't want to have one more tiny package to audit against security rules in a company for open source projects and dependencies, especially in a growing hostile environment for npm packages. I can't see a package with Vue that doesn't use such a feature. This is simply needed for dropdown elements and floating menus which basically EVERY application has.

I'd love to contribute, but unfortunately, I have no idea where to start and don't have enough time for this right now. But I'd like to leave my feedback on this issue.

By the way, thank you for the awesome framework. 👍🏻 🚀

jods4 commented 2 years ago

I don't like installing too many packages either, but it's not a great argument. It is not good for Vue to merge every single small package out there into core. For example everything in Vueuse is pretty small and useful to some people, yet I think we would agree Vueuse should remain its own package.

v-click-outside can be so small that if you don't want to take a dep. on a package, you could just write the code directly in your app. Thorsten has a basic click outside directive in less than 20 lines of JS here: https://jsfiddle.net/Linusborg/yzm8t8jq/

I raised the concern before that the semantics of a useful click-outside might be not so well defined for core inclusion. To contrast with that 20 lines example, the v-click-outside npm implementation is 130+ lines and has code to handle touch events and iframes... https://github.com/ndelvalle/v-click-outside/blob/master/src/v-click-outside.js

For this to make it into core Vue, I think the first step is to discuss the specifications of what we want .outside to be, exactly.