alpinejs / alpine

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

Adding support for custom directives / extensions #57

Closed ryangjchandler closed 4 years ago

ryangjchandler commented 4 years ago

I'm starting this issue for some conversations relating to custom directives / functionality for Alpine.

The related thread on Twitter can be found here: https://twitter.com/assertchris/status/1214953080137158660?s=21

ryangjchandler commented 4 years ago

cc/ @calebporzio

ryangjchandler commented 4 years ago

So here are some of initial thoughts:

import Alpine from 'alpinejs'

If we were to add an Alpine.directive() function, would this need to be done before calling Alpine.start() or will it work either way due to the MutationObserver? I imagine a Vue like syntax where you have to call Vue.use() before initialising a new Vue instance.

import Alpine from 'alpinejs'
import AlpinePlugin from 'alpine-plugin'

Alpine.use(AlpinePlugin);

Alpine.start()
kevinruscoe commented 4 years ago

It'll be even better if the "12 directives" were plugins, and we could only import the ones we need

SimoTod commented 4 years ago

@kevinruscoe Not sure about making everything a plugin. It just makes everything longer to set up. The library is quite light and someone could just want to import it using the CDN link.

@ryangjchandler About having a separate start function, alpine waits for the dom ready before initialising its components. Depending on the implementation maybe we won't need it but either ways look good to me. I would also love to see a Alpine.define('name', ...); to create a custom directive manually.

ryangjchandler commented 4 years ago

@kevinruscoe Not sure about making everything a plugin. It just makes everything longer to set up. The library is quite light and someone could just want to import it using the CDN link.

@ryangjchandler About having a separate start function, alpine waits for the dom ready before initialising its components. Depending on the implementation maybe we won't need it but either ways look good to me. I would also love to see a Alpine.define('name', ...); to create a custom directive manually.

Yeah, alongside the Alpine.use() syntax, there would be an underlying Alpine.directive() method that would let you extend without needing to write an entire plugin. The .use() method would just be a nice way of letting devs share their custom directives.

ryangjchandler commented 4 years ago

So, after @calebporzio implementing a Plugin API for Livewire, has anyone got ideas for an Alpine Plugin API?

caneco commented 4 years ago

I would like to make at least two plugins for Alpine…

  1. x-mask for input masking – great for phone numbers
  2. x-validate for input validation – Laravel style

@calebporzio would you think this will be achievable just by adding the plugin system? I mean, in both cases it would be like pairing x-validate|mask with x-model

ryangjchandler commented 4 years ago

I would like to make at least two plugins for Alpine…

  1. x-mask for input masking – great for phone numbers
  2. x-validate for input validation – Laravel style

@calebporzio would you think this will be achievable just by adding the plugin system? I mean, in both cases it would be like pairing x-validate|mask with x-model

I'd like to implement an x-filter directive that acts like x-text but supports Vue like filters. The x-mask one is a big one too!

caneco commented 4 years ago

@ryangjchandler x-filter would be a great addition

Already can see myself make some use for money formatting:

<span x-filter="money">10000.99<span>

€ 10.000,99

// yes, in euros 😁

ryangjchandler commented 4 years ago

@ryangjchandler x-filter would be a great addition

Already can see myself make some use for money formatting:

<span x-filter="money">10000.99<span>

€ 10.000,99

// yes, in euros 😁

Yeah, this would be super nice. Not sure if filters are something that @calebporzio wants to include in core...

caneco commented 4 years ago

Maybe in order to make the micro-framework, "micro" … start to thinking the needs of adding up stuff to the core directly, or injecting via a plugin.

SimoTod commented 4 years ago

@ryangjchandler @caneco As a workaround, it's worth noting that Alpine can access any functions defined in the global scope as well as the component scope.

It doesn't have the pipe syntax but we can achieve the same result of x-filter with something like

<div x-data="{foo : 1000}">
   <input x-model="foo" />
   <div x-text="money(foo)" />
</div>

<script>
  var money = function(value) {
    var locale = window.navigator.userLanguage || window.navigator.language;
    return Intl.NumberFormat(locale, {
      style: 'currency',
      currency: 'EUR',
      minimumFractionDigits: 2
    }).format(value);
  }
</script>

Alpine will pick the function from the window context and the variable from the local scope.

ryangjchandler commented 4 years ago

@ryangjchandler @caneco As a workaround, it's worth noting that Alpine can access any functions defined in the global scope as well as the component scope.

It doesn't have the pipe syntax but we can achieve the same result of x-filter with something like

<div x-data="{foo : 1000}">
   <input x-model="foo" />
   <div x-text="money(foo)" />
</div>

<script>
  var money = function(value) {
    var locale = window.navigator.userLanguage || window.navigator.language;
    return Intl.NumberFormat(locale, {
      style: 'currency',
      currency: 'EUR',
      minimumFractionDigits: 2
    }).format(value);
  }
</script>

Alpine will pick the function from the window context and the variable from the local scope.

This is exactly how I imagine something like x-filter working, each filter has its own function, so you can simply just pipe the return value into each function, that's my idea for something like that anyway.

ryangjchandler commented 4 years ago

@ryangjchandler @caneco As a workaround, it's worth noting that Alpine can access any functions defined in the global scope as well as the component scope. It doesn't have the pipe syntax but we can achieve the same result of x-filter with something like

<div x-data="{foo : 1000}">
   <input x-model="foo" />
   <div x-text="money(foo)" />
</div>

<script>
  var money = function(value) {
    var locale = window.navigator.userLanguage || window.navigator.language;
    return Intl.NumberFormat(locale, {
      style: 'currency',
      currency: 'EUR',
      minimumFractionDigits: 2
    }).format(value);
  }
</script>

Alpine will pick the function from the window context and the variable from the local scope.

This is exactly how I imagine something like x-filter working, each filter has its own function, so you can simply just pipe the return value into each function, that's my idea for something like that anyway.

As an extension to this, the implementation would be fairly straight forward. Split the string by the ' | ' delimiter, eval the first item in the array (the reactive variable) and simply run a map through each filter function and update the text of the element.

ryangjchandler commented 4 years ago

So, thought I'd put some initial thoughts here for a plugin api implementation: (@calebporzio )

  1. Currently, Alpine automatically starts after the DOM is ready. Should this still be the same when implementing a Plugin API, or should Alpine.start() be called manually, similar to how Vue needs to be instantiated. This would make registering custom directives much more straight forward, as Alpine would be aware of any custom directives before discovering and initialising components.

  2. What should the callback for a custom directive be passed? Each of the core directives have been moved into their own respective files, with their handlers exported but there isn't much consistency across the parameters that each directive handler has. I think this should be standardised before implementing a plugin API, so that there is some sort of structure between core and custom directives.

  3. The custom directive handlers are being registered outside of core. When the handle is called, is the raw expression passed to the handler, if this is the approach taken, the eval functions would need to be exposed to the public API, so that the expression can be evaluated in the context of the component. })

  4. Sharing custom directives via packages would probably need some sort of Alpine.use() syntax too, there would need to be some sort of standard for this alongside point 2 ^^^ as well.

There's some more stuff that I might add to this as it pops into my mind too...

SimoTod commented 4 years ago

Adding some thoughts:

1) Alpine doesn't need to know about custom directives before it executes them. Since they'll be defined in page before Alpine starts, I don't think we need to call Alpine.start() manually.

2) Directives could be stored in a key-value map into the Alpine class, the class could expose 2 functions to read and write from that map by key.

3) Key would be the name of the directive without the x- prefix.

4) Value would be a callable function

5) Utils.js would change to allow detection of any x-([a-zA-Z]+) attribute

6) In the component, the last branch of the switch statement would check if a custom directive exists. If it does, it would call the function setting this as execution context.

7) The signature should be flexible enough and support all the available parameters. I was thinking something like (el, expression, value, modifiers, initialUpdate, extraVars)

This is a proof of concept showing how it could look like (The implementation of the single directive is up to the dev, of course, but they will have access to all the Alpine tools): https://codepen.io/SimoTod/pen/WNbmKbV

ryangjchandler commented 4 years ago

Adding some thoughts:

  1. Alpine doesn't need to know about custom directives before it executes them. Since they'll be defined in page before Alpine starts, I don't think we need to call Alpine.start() manually.
  2. Directives could be stored in a key-value map into the Alpine class, the class could expose 2 functions to read and write from that map by key.
  3. Key would be the name of the directive without the x- prefix.
  4. Value would be a callable function
  5. Utils.js would change to allow detection of any x-([a-zA-Z]+) attribute
  6. In the component, the last branch of the switch statement would check if a custom directive exists. If it does, it would call the function setting this as execution context.
  7. The signature should be flexible enough and support all the available parameters. I was thinking something like (el, expression, value, modifiers, initialUpdate, extraVars)

This is a proof of concept showing how it could look like (The implementation of the single directive is up to the dev, of course, but they will have access to all the Alpine tools): https://codepen.io/SimoTod/pen/WNbmKbV

Beat me to the mark man! 100% on all of the points above.

ryangjchandler commented 4 years ago

Also, to follow up, why not pass an object as the argument to the function, then the directive callback can just destructure it to receive the variables that it needs. I think I'd prefer this approach.

caneco commented 4 years ago

Looking good @SimoTod ! 🙌

ryangjchandler commented 4 years ago

This has been put on the roadmap for v3, but I imagine the implementation details are going to be quite different from our work here so I'll close this issue.

devcircus commented 4 years ago

Didn't see this thread first, and had started playing with the idea.

I like the idea of normalizing the current directives into classes or functions in a way that could be called generically. It would be a total rewrite of the current directive system though.

Look forward to seeing how this progresses.