Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to specify input vs state #5

Closed
alexcjohnson opened this issue Jan 22, 2021 · 8 comments
Closed

How to specify input vs state #5

alexcjohnson opened this issue Jan 22, 2021 · 8 comments

Comments

@alexcjohnson
Copy link
Collaborator

Right now the docs use dx.arg for all inputs/state, defaulting to input but adaptable with kind="state". To create an update button you need to add kind="state" to all the existing inputs and add another like:
n_clicks=dx.arg(html.Button("Update").props["n_clicks"]) (I've omitted the implied kind="input")

(Related point: this is inside the inputs=dict(...) - that might be a little funny since it includes both input and state items, would it be better to call this args?)

The other options that have been floated previously are

  1. dx.input / dx.state - this is slightly more concise but not in the fully-immediate case (which we expect will be the most common case) and requires users to learn about the input/state distinction up front. So overall I probably agree about using dx.arg instead.
  2. a separate argument manual=True that turns all the arguments into State and creates a new button that isn't an argument to the function but is the only triggering Input. For the specific case of delaying all the inputs this is really nice as it's very concise (no need to change every arg) and doesn't require adding an arg to your function that you aren't going to use. But it's very restricted, as you can't mix and match state and input, you can't control what the button looks like.

So kind="input"|"state" does seem like the simplest way to get the flexibility we need. But can we make it easier to change between immediate and on-submit modes? If we implement Trigger inputs in base Dash plotly/dash#1054, maybe we could make a dx.trigger("Update") that creates a button with the given text, and when you add that to your args list it flips the default kind for other args from "input" to "state" (so underneath, the default would be "auto" or something).

We could also give the base Trigger a hidden=True option to omit it from the function args, and perhaps let that be the default if your definition only has a single dx.trigger but turn it off if there are multiple triggers. Or perhaps name your trigger something like _ in the inputs dict and it'll be omitted from the function args?

Too magical? Just trying to find more concise syntax for the common cases.

@jonmmease
Copy link
Contributor

Thanks for the ideas. Let me think on them some more.

@jonmmease
Copy link
Contributor

I kind of like the idea of supporting a triggers list. These would be arguments that wouldn't be passed to the function, but would be used to trigger an update of the function. When there are triggers, the default kind could become state (but you could still set it explicitly to override the default).

dx.callback(
    inputs=...
    triggers=[html.Button().props["nclicks"], Input(...)]
)

The downside is that you can't control where the trigger element is positioned in the layout this way.

We could also add trigger as an arg kind. And switch the default kind of everything else to state when a kind="trigger" arg is present.

dx.callback(
    inputs=[dx.arg(...), dx.arg(...), dx.arg(..., kind="trigger")]
)

This doesn't feel too magical to me. Downside is that there wouldn't be a way to use an external Input dependency object as the trigger.

But if both were supported, then I think you'd have full flexibility.

dx.callback(
    inputs=[dx.arg(...), dx.arg(..., kind="trigger"), dx.arg(...)],
    triggers=[Input(...)]
)

Neither of these changes would require any changes to Dash.

@jonmmease
Copy link
Contributor

With the incompatibilities discussed in #6 (comment), I am wondering if it would be better to not reuse the inputs/output/state callback arguments for the Dash Express features.

Like you suggested, we could use args for the input/state things. We would also need something other than output for the output things. (.e.g. args_out or out_args).

args could contain dx.arg(...) values (with kind of input/state/trigger) or Input(...)/State(...) dependency objects. And args_out could contains dx.arg() or Output(...) dependency objects.

All positional arguments and the contents of inputs/output/state keyword arguments would be processed exactly as they are now.

@jonmmease
Copy link
Contributor

How about this.

  1. The express features are only available when using the args input to dx.callback. So an express user would not use the input or state arguments, and they would remain backward compatible.
  2. The default value of kind in dx.arg is "auto", but other options are "input", "state", "output", and "trigger".
  3. We add a Trigger dependency object (mirroring Input and State) that is only supported by the args keyword argument.
  4. If there are any kind="trigger" arguments specified (or any Trigger dependency objects provided), then the kind="auto" value is treated as kind="state". Otherwise, it is treated as kind="input". The user can override either way by providing their own value to kind.
  5. Under the hood dx.arg(..., kind="trigger") and Trigger(...) values are treated as Input dependencies for the purpose of triggering the callback, but their values are not passed to the wrapped callback function.

cc @alexcjohnson @nicolaskruchten @chriddyp

@alexcjohnson
Copy link
Collaborator Author

I like it! Just two questions:

  1. Why would Trigger only be supported inside args? Seems like it would work fine in the dash 1.x syntax too.
  2. If there are multiple triggers, their values DO need to be passed to the function ("which button did you press?"), unless for that use case we want to require people to look in callback_context - that's why I liked the hidden=True idea.

@jonmmease
Copy link
Contributor

  1. I guess for the 1.x syntax we could add a triggers kwarg that would take a list of Trigger dependency objects and then just not pass them through to the wrapped function. Is that the kind of thing you had in mind?

  2. My thinking is that triggers should never be passed to the function. A trigger passed to the function would be identical to an input wouldn't it? Or are you thinking you'd want to set kind="trigger" in order to switch the default of the other args to "state", but still input the trigger component's value? That feels like a bit too many layers to me personally, but it wouldn't be a hard implementation.

Either way, I think we would still want the trigger prop names would still show up in callback context when they aren't passed to the callback function.

@alexcjohnson
Copy link
Collaborator Author

In plotly/dash#1054 I was thinking Trigger objects could be mixed in with Input objects, but instead of the prop value a Trigger would pass a boolean into the function, True if it's among the triggers of the callback.

@jonmmease
Copy link
Contributor

Ok, I see. Yeah, converting a triggered prop to a boolean would be interesting. Wonder if it would make sense for there to be a virtual property for components named triggered so that you could combine it with the tuple/dict grouping syntax.

button2 = html.Button(...)

@dx.callback(app,
    args=dict(
        button1=dx.arg(html.Button(), props=("triggered", "n_clicks"))
        button2=(Input("button2", "triggered"), Input("button2", "n_clicks"))
    )
)
def callback(button1, button2):
    button1_triggered, button1_nclicks = button1
    button2_triggered, button2_nclicks = button2
    ...

The rule would be that you can only use "triggered" in a tuple/dict grouping that includes at least one real component property. The triggered value would be true if there was a change to any other property in the grouping, and false otherwise. Also, it would only make sense for kind="input", because it would never be true for kind="state".

If there's only one other prop in the grouping, then you know exactly what component/prop triggered the update. But if there is more than one there would still be ambiguity that you'd need to sort out with callback_context if you needed to know exactly which prop was responsible.

FWIW, this would be compatible with the proposed Dash Express model, so it could be added after the MVP.

Doesn't really address the specifying state vs. input dimension though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants