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

Dynamic Inputs #233

Merged
merged 6 commits into from
Jul 9, 2024
Merged

Dynamic Inputs #233

merged 6 commits into from
Jul 9, 2024

Conversation

maaslalani
Copy link
Contributor

@maaslalani maaslalani commented May 10, 2024

Dynamic Forms

Country / State form with dynamic inputs running.

huh? forms can now react to changes in other parts of the form. Replace
properties such as Options, Title, Description with their dynamic
counterparts: OptionsFunc, TitleFunc, and DescriptionFunc to recompute
properties values on changes when watched variables change.

Let’s build a simple state / province picker.

var country string
var state string

The country select will be static, we’ll use this value to recompute the
options and title for the next input.

huh.NewSelect[string]().
    Options(huh.NewOptions("United States", "Canada", "Mexico")...).
    Value(&country).
    Title("Country").

Define your Select with TitleFunc and OptionsFunc and bind them to the
&country value from the previous field. Whenever the user chooses a different
country, the TitleFunc and OptionsFunc will be recomputed.

Important

We have to pass &country as the binding to recompute the function only when
country changes, otherwise we will hit the API too often.

huh.NewSelect[string]().
    Value(&state).
    Height(8).
    TitleFunc(func() string {
        switch country {
        case "United States":
            return "State"
        case "Canada":
            return "Province"
        default:
            return "Territory"
        }
    }, &country).
    OptionsFunc(func() []huh.Option[string] {
        opts := fetchStatesForCountry(country)
        return huh.NewOptions(opts...)
    }, &country),

Lastly, run the form with these inputs.

err := form.Run()
if err != nil {
    log.Fatal(err)
}

form.go Outdated Show resolved Hide resolved
go.mod Outdated
@@ -1,21 +1,24 @@
module github.com/charmbracelet/huh

go 1.18
go 1.20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any particular reason to bump this up to Go 1.20?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, downgrading...

@ChrisRx
Copy link

ChrisRx commented May 14, 2024

I first wanted to say, thank you for this work and this amazing library!

I was trying this out with a huh.Option[int] and was unable to get it to work, so I looked at the example which was working and I think I had finally tracked down what was going on for me:

huh/group.go

Lines 247 to 257 in 133f205

switch msg := msg.(type) {
case spinner.TickMsg,
updateTitleMsg,
updateDescriptionMsg,
updateSuggestionsMsg,
updateOptionsMsg[string],
updatePlaceholderMsg:
m, cmd := g.fields[i].Update(msg)
g.fields[i] = m.(Field)
cmds = append(cmds, cmd)
}

It looks like the type switch only has updateOptionsMsg[string] in the case so other generic types do not match. I added updateOptionsMsg[int] just to see if it made it work and it seemed to yield the intended behavior. I hacked together something locally with an interface on updateOptionsMsg so that it could be more generally selected in that type switch, but unsure of the direction you all wanted to go. Happy to submit code or anything like that, if wanted, but also just wanted to make sure I pointed it out if that wasn't the intention to only support the string type of dynamic options.

Thanks again!

@maaslalani
Copy link
Contributor Author

@ChrisRx You're absolutely correct! This will definitely be fixed before merging / releasing. Thank you so much, really appreciate the review ❤️

@shaunco
Copy link

shaunco commented Jul 5, 2024

Really excited to see this make it into huh! Do we know when this PR will be merged?

@OliveiraCleidson
Copy link

Really excited to see this make it into huh! Do we know when this PR will be merged?

I also want to know, I made the local clone and I'm going to test it on an internal CLI haha

var (
styles = s.activeStyles()
c = styles.SelectSelector.String()
sb strings.Builder
)

if s.options.loading && time.Since(s.options.loadingStart) > spinnerShowThreshold {
s.spinner.Style = s.activeStyles().MultiSelectSelector.UnsetString()
sb.WriteString(s.spinner.View() + " Loading...")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @maaslalani, I noticed that the ‘Loading’ message is currently hardcoded. It might be beneficial to allow this message to be defined dynamically, perhaps with a method like ‘LoadingMessage()’. While this isn’t critical for functionality, it could be quite useful for customization and internationalization.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @OliveiraCleidson, thanks for pointing this out. We can fix this in a future release! Great call.

Adds `Func`-able properties to all fields to allow dynamic updates
based on updates in other parts of the group.

Note that bindings must be specified. Bindings allow the Function to
know when it should update as the functoin re-evaluates only when the
value of the binding changes.
@maaslalani maaslalani merged commit 1f1f7a2 into main Jul 9, 2024
40 checks passed
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

Successfully merging this pull request may close these issues.

6 participants