grapp-dev / nui-components.nvim

A feature-rich and highly customizable library for creating user interfaces in Neovim.
https://nui-components.grapp.dev
MIT License
303 stars 6 forks source link

Feature: add `signal:map()` method #19

Closed b0o closed 5 months ago

b0o commented 5 months ago

I often find myself wanting to use several different signal values in a single prop. I'm currently doing something like this:

local n = require 'nui-components'

local renderer = n.create_renderer {
  width = math.min(vim.o.columns, 64),
  height = math.min(vim.o.lines, 20),
}

local signal = n.create_signal {
  foo = false,
  bar = false,
  qux = false,
}

renderer:render(
  --
  n.rows(
    n.paragraph {
      is_focusable = false,

      -- This is the important part:
      lines = signal.foo:map(function()
        local val = signal:get_value()
        return {
          n.line(n.text('Foo: ', 'Keyword'), tostring(val.foo)),
          n.line(n.text('Bar: ', 'Keyword'), tostring(val.bar)),
          n.line(n.text('Qux: ', 'Keyword'), tostring(val.qux)),
        }
      end),

    },
    n.gap(1),
    n.columns(
      { flex = 0 },
      n.button {
        label = ' Toggle Foo ',
        autofocus = true,
        on_press = function()
          signal.foo = not signal.foo:get_value()
        end,
      },
      n.gap(1),
      n.button {
        label = ' Toggle Bar ',
        on_press = function()
          signal.bar = not signal.bar:get_value()
        end,
      },
      n.gap(1),
      n.button {
        label = ' Toggle Qux ',
        on_press = function()
          signal.qux = not signal.qux:get_value()
        end,
      }
    )
  )
)

In the above example, although I'm mapping over the foo signal value, the function seems to be called when any of the signal's values change. In fact, it seems we can map over any signal field, even if it doesn't exist, like signal.baz:map(fn).

My request: It would be nice to be able to map directly over the signal, signal:map(fn). The function fn should be called with the full value of the signal as the argument whenever any of the signal’s values change:

n.paragraph {
  is_focusable = false,
  lines = signal:map(function(val)
    return {
      n.line(n.text('Foo: ', 'Keyword'), tostring(val.foo)),
      n.line(n.text('Bar: ', 'Keyword'), tostring(val.bar)),
      n.line(n.text('Qux: ', 'Keyword'), tostring(val.qux)),
    }
  end),
},
mobily commented 5 months ago

Hello @b0o! For this specific case, I recommend using the combine_latest operator (I fixed it in #24). This will allow you to combine signal values from signal.foo, signal.bar, and signal.qux. Here's an example code snippet:

n.paragraph({
  is_focusable = false,
  -- This is the important part:
  lines = signal.foo:combine_latest(signal.bar, signal.qux, function(foo, bar, qux)
    return {
      n.line(n.text("Foo: ", "Keyword"), tostring(foo)),
      n.line(n.text("Bar: ", "Keyword"), tostring(bar)),
      n.line(n.text("Qux: ", "Keyword"), tostring(qux)),
    }
  end),
}),

I recommend using combine_latest instead of mapping over the signal because mapping may cause unnecessary rerenders. Feel free to let me know if you have any other questions.

b0o commented 5 months ago

Oh that is really cool, thank you! I think some more examples in the Signal docs would be really helpful in understanding the use-cases of the different methods, like this.