liveview-native / liveview-client-swiftui

MIT License
353 stars 31 forks source link

Add `binding` macro to Elixir library #244

Closed carson-katri closed 1 year ago

carson-katri commented 1 year ago

Currently, there is no macro available in the Elixir library that allows you to create a live binding. However, they are necessary for features of certain Views like TextEditor and List.

A `bindings` macro was proposed in #164. This could be brought in as-is. ```ex defmacro bindings(names_and_defaults) do quote bind_quoted: [names_and_defaults: names_and_defaults] do on_mount {__MODULE__, :_set_binding_defaults} def on_mount(:_set_binding_defaults, _params, _session, socket) do { :cont, socket |> assign(unquote(names_and_defaults)) |> push_event("_live_bindings", Map.new(unquote(names_and_defaults))) } end Enum.each(names_and_defaults, fn {name, _default} -> def unquote(:"get_#{name}")(socket) do socket.assigns[unquote(name)] end def unquote(:"set_#{name}")(socket, value) do socket |> assign(unquote(name), value) # todo: it would be nice if there were a hook that let us coalesce multiple changes in a single callback into one event |> push_event("_live_bindings", %{unquote(name) => value}) end def handle_event(unquote(to_string(name)), %{"value" => value}, socket) do {:noreply, assign(socket, unquote(name), value)} end end) end end ```

Alternatively, one could be designed that allows for more detailed information to be specified per binding:

defmodule MyAppWeb.TestLive do
  use MyAppWeb, :live_view
  use LiveViewNative.LiveView

  # Each binding is it's own call with a type and default
  native_binding :my_bool, :boolean, default: false
  native_binding :counter, :integer, default: 5
  # Maybe allow custom types to be used here?
  native_binding :sort_order, LiveViewNativeSwiftUi.Types.SortOrder, default: %LiveViewNativeSwiftUi.Types.SortOrder{ id: "id", order: true }

  def handle_event("toggle", _, socket) do
    # Setting a binding with an atom gives more flexibility than a single function per binding.
    # A getter function is probably unnecessary since they can be accessed via `socket.assigns`
    {:noreply, assign_native_bindings(socket, :my_bool, !socket.assigns.my_bool)}
    # This would also allow multiple bindings to be set at once.
    {:noreply, assign_native_bindings(socket, my_bool: !socket.assigns.my_bool, counter: 0)}
  end
end

The name native_binding is used in the example to make it clear that this is a feature of LVN, not LiveView in general.

fyi @shadowfacts @supernintendo

shadowfacts commented 1 year ago

I'm currently working on a new macro implementation that takes the value type explicitly, since more complex types are needed for MultiDatePicker

carson-katri commented 1 year ago

This could also provide options for a Binding animation:

native_binding :my_bool, :boolean, default: false, animation: :default
shadowfacts commented 1 year ago

Hmm, binding animations are interesting. I think they should be specified somehow where the binding is used, rather than where it's declared? The declaration is really more like @State, and so using it in a template would be analogous to passing $myStateProp.animation(.default) into another view.

And also keeping the animation option at the use-site would let rest of the backend bindings support be platform-agnostic.