alpinejs / alpine

A rugged, minimal framework for composing JavaScript behavior in your markup.
https://alpinejs.dev
MIT License
28.46k stars 1.24k forks source link

Magics: Introduce new `$mixin` magic helper #4200

Closed ryangjchandler closed 6 months ago

ryangjchandler commented 6 months ago

This pull request introduces a new $mixin magic helper that aims to make components more easily composable.

At the minute, if you want to use multiple data providers for a component, you have to do something like this:

<div x-data="{
    ...foo(),
    ...bar(),
}">

This doesn't feel very nice – plus it has some downsides such as init() and destroy() functions being overwritten by the final item in the spread.

The goals of the $mixin helper are:

  1. Make the API feel a little nicer to write.
  2. Handle the init() and destroy() functions correctly.

The above code would become something like this:

<div x-data="$mixin(foo, bar)">
</div>

If both foo() and bar() have an init(), $mixin() is smart enough to call both of those functions in the correct order (whichever order they're being mixed in). The same goes for destroy() methods.

Of course, conflicting properties in each function are still an issue but that's not really avoidable without some weird object nesting stuff and I'd say that's not very intuitive.

Either way, I think this is a nice API for core and something I'd find myself using a lot, especially given the nicer syntax!

Let me know what you think 🤞

ekwoka commented 6 months ago

This will break getters and setters.

To preserve them you would need more like

components.reduce((acc, obj) => {
        Object.defineProperties(acc, Object.getOwnPropertyDescriptors(obj));
        return acc;
      }, {})

instead of the spread operator.

Spreading will simply take the value out of a getter right then, and setters would be lost entirely.

I use the above (as a similar style of mixin) in production.

Also, fyi, if you define it as an Alpine.data instead of a Alpine.magic it can be then only used in the x-data directive, which is pretty cool.

calebporzio commented 6 months ago

Nice work, and good name for it. Can you give me some real-world examples to chew on before adding it? I'm leaning towards this being a good merge though. Also thanks for the notes @ekwoka

ryangjchandler commented 6 months ago

Yep, you're right @ekwoka – completely forgot about the get and set thing with ...!

Will get that sorted and tested.

ryangjchandler commented 6 months ago

Changes made r.e. get and set. Tests are all passing locally – can't see any failures in the Action output either, so maybe worker is timing out or it's flaky?

joshhanley commented 6 months ago

@ryangjchandler I've triggered tests to re-run

calebporzio commented 6 months ago

Does anyone have any compelling real-world examples of this they'd like to share? or links to discussions or other community evidence around this?

cipriano200 commented 6 months ago

Few weeks ago i was doing an implementation for a wordpress premium form builder, had to create 3 custom form fields each of them are dependent on each other.

The issue was i didn't had control over form element(tag) so i couldn't do: <form x-data="component">

Mixing would solve this issue by mixing the 3 custom fields components

ekwoka commented 6 months ago

@calebporzio

Broadly, what this solves is just wanting to have the same root used for two (or more) components.

This can be implemented with 2 elements with each of the components (increases div soup), or with multiple x-data on the single element (can provide questionable issues of the cascade in the event of overlapping keys).

Those would be the two current ways to implement it without reaching for this kind of utility.

It's overall simple to implement if a dev needs to, though the case of lifecycle methods and getters/setters may be missed or cause some headache.

So it is another abstraction that does not NEED to be part of core, like some other functionality improvements might need to.

I've only used it a very small number of times, and use it less and less with newer stuff (leaning towards custom directives for more and more stuff), but it is something that many would likely want to do and not fully understand how to.