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

[Feature]: Rethink Type Safety and Flexibility of the view! Macro for Conditional Logic #3378

Open
wiseaidev opened this issue Dec 17, 2024 · 5 comments

Comments

@wiseaidev
Copy link

wiseaidev commented Dec 17, 2024

Hiya!

I've been using Leptos for a while now (ever since building this crate), and I've noticed that working with the view! macro can be quite cumbersome when adding conditional logic (if-else) inside the macro. Let me explain with some examples:

The Problem

To add if-else logic in view!, it always has to be wrapped in a closure ({move ||}). Here's a complex example:

<div class="wallet-info">
    {move ||
        if connected.get() {
            Some(view!{
                {move ||
                    if let Some(key) = phantom_wallet_adapter.get().public_key() {
                        view!{
                            <p>"Connected Wallet: " {move || phantom_wallet_adapter.get().name()} </p>
                            <p>"Connected Public Key: " {move || key.to_string() } </p>
                            <div class="forms">
                                <div class="send-sol-form">
                                    <h2 class="form-title">{ "Transfer SOL" }</h2>
                                    <form on:submit={transfer_sol_phantom}>
                                        <div class="form-group">
                                            <label for="destination-address">
                                                { "Destination Address" }
                                            </label>
                                            <input
                                                id="destination-address"
                                                type="text"
                                                class="form-control"
                                                node_ref={input_dest_ref}
                                                required=true
                                                value=dest
                                            />
                                        </div>
                                        <div class="form-group">
                                            <label for="sol-amount">
                                                { "SOL Amount (in lamports)" }
                                            </label>
                                            <input
                                                id="sol-amount"
                                                type="number"
                                                class="form-control"
                                                node_ref={input_amount_ref}
                                                required=true
                                                value=amount
                                            />
                                        </div>
                                        <button type="submit" class="submit-button">{ "Send" }</button>
                                    </form>
                                </div>
                                <div class="sign-form">
                                    <h2 class="form-title">{ "Sign Message" }</h2>
                                    <form on:submit={sign_msg_phantom}>
                                        <div class="form-group">
                                            <label for="message">
                                                { "Message" }
                                            </label>
                                            <input
                                                id="Message"
                                                type="text"
                                                class="form-control"
                                                node_ref={input_msg_ref}
                                                required=true
                                                value=msg
                                            />
                                        </div>
                                        <button type="submit" class="submit-button">{ "Sign" }</button>
                                    </form>
                                </div>
                            </div>
                            {move ||
                                if confirmed.get() {
                                    Some(view!{
                                        <div class="transaction-info">
                                            <p>{ "Transaction Successful!" }</p>
                                            <a
                                                href={format!("https://solscan.io/tx/{}", sig.get())}
                                                target="_blank"
                                                rel="noopener noreferrer"
                                                class="view-transaction-button"
                                            >
                                                { "View Transaction" }
                                            </a>
                                        </div>
                                    })
                                } else {
                                    None
                                }
                            }
                        }
                    } else if let Some(key) = solflare_wallet_adapter.get().public_key() {
                        view!{
                            <p>"Connected Wallet: " {move || solflare_wallet_adapter.get().name()} </p>
                            <p>"Connected Public Key: " {move || key.to_string() } </p>
                        }
                    } else {
                        view!{
                            <p>"Connected but no wallet info available"</p>
                            <p>{}</p>
                        }
                    }
                }
            })
        } else {
            None
        }
    }
</div>

This setup becomes messy and hard to read or maintain when handling complex structures.

  1. Closure Wrapping: Every conditional requires {move || }.
  2. Tag Type Consistency: Both branches of the if-else must use the same HTML tag type, making it impossible to render different tags (e.g., <input> in one branch and <textarea> in the other).
  3. Attribute Type Consistency: Even attributes within the same tag must have the same type in both branches. For example:
if something {
    view! {
        <input class="str type" />
    }
} else {
    view! {
        <input class={"string type".to_string()} />
    }
}

The above fails because the class attribute types differ (&str vs. String).

Real-World Frustration
When I was building a crate to simplify input components for WASM frameworks, I found these limitations made it frustrating to handle dynamic, conditional logic while keeping the code clean and maintainable.

Suggestion

To improve usability and maintainability of the view! macro:

  1. Remove Tag-Type Restrictions: Allow different HTML tag types in each branch of the if-else if optional, when wrapped with Some().
    if something {
        view! {
            Some(<input />)
        }
    } else {
        view! {
            Some(<textarea />
        }
    }
  2. Relax Attribute Type Matching: Automatically coerce attribute types to avoid unnecessary type mismatches.
  3. Simplify Closures: Minimize or remove the need for {move ||} closures to simplify writing dynamic logic.

This would make it far easier to write concise and readable logic inside view!, especially for more complex use cases.

Thanks for considering this! 😊

Best!

@gbj
Copy link
Collaborator

gbj commented Dec 17, 2024

Are you interested in working on a PR?

@wiseaidev
Copy link
Author

Are you interested in working on a PR?

Absolutely! I'd love to contribute to this framework and help make it easier for everyone to build UIs in Leptos and beyond. However, I'll need some guidance on where to start implementing this feature since there are too many crates in this repository. But, I will have a look and study this repo crates over the weekend.

Looking forward to your guidance!

@gbj
Copy link
Collaborator

gbj commented Dec 17, 2024

I'll need some guidance on where to start implementing this feature since there are too many crates in this repository

The view macro is in leptos_macro

@geovie
Copy link
Contributor

geovie commented Dec 18, 2024

I agree that it's a bit cumbersome if you have lots of conditionals in the view macro.

Here are some tips from my (short) experience working with the leptos view macro:

if connected.get() {
  Some(view!{ ... })
} else {
  None
}

could be condensed a bit by using bool then:

connected.get().then(|| view!{ ... })

and for

if let Some(key) = phantom_wallet_adapter.get().public_key() {
  view!{ ... }.into_any()
} else if let Some(key) = solflare_wallet_adapter.get().public_key() {
  view!{ ... }.into_any()
} else {
  view!{ ... }.into_any()
}

the either macro could be used, which takes care of wrapping each arm in an Either* variant

either!(
    (Some(1), Some(2)),
        (Some(a), Some(b)) => view! {},
        (Some(a), None) => view! {},
        (None, Some(b)) => view! {},
        _ => view! {},
)

@wiseaidev
Copy link
Author

Thanks for sharing these tips, @geovie! However, this doesn't fully address the main issues outlined in the issue. I believe there's an opportunity to make it easier to build logic within the view macro while still maintaining type soundness and other benefits.

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

No branches or pull requests

3 participants