Skip to content

feat: allow &str and String conversion for Option<Html>#4020

Open
Casheeew wants to merge 1 commit intoyewstack:masterfrom
Casheeew:allow-option-string-conversions
Open

feat: allow &str and String conversion for Option<Html>#4020
Casheeew wants to merge 1 commit intoyewstack:masterfrom
Casheeew:allow-option-string-conversions

Conversation

@Casheeew
Copy link

Description

Add the following cases to impl_into_prop_value_via_display!:

- TOption<VNode>
- &TOption<VNode>
- Option<T> → Option<VNode>

Add the following cases to impl_into_prop_value_via_attr_value!:

- TOption<VNode>
- Option<T> → Option<VNode> 

This allows something like this to compile:

#[test]
    fn test_option_html_prop_compiles() {
        use crate::prelude::*;

        #[derive(PartialEq, Properties)]
        pub struct Props {
            pub title: Option<Html>,
        }

        #[component]
        fn Foo(props: &Props) -> Html {
            match &props.title {
                Some(title) => html! { <h1>{ title.clone() }</h1> },
                None => html! {},
            }
        }

        let _ = html! { <Foo title="Title" /> };

        let _ = html! { <Foo title={String::from("Title")} /> };

        let _ = html! { <Foo title={Some("Title")} /> };

        let _ = html! { <Foo title={Option::<Html>::None} /> };
    }

This is different from something like

// #[prop_or_default]
// title: Html
html! { <h1>{ props.title.clone() }</h1> }

because based on my understanding, #[prop_or_default] with no parameters passed still create h1 in the DOM but empty, whereas with Option<Html>

// <h1> is absent from the DOM entirely when title is None
if let Some(title) = &props.title {
    html! { <h1>{ title.clone() }</h1> }
}

Resolves #3449

Checklist

  • [V] I have reviewed my own code
  • [V] I have added tests

@github-actions
Copy link

Visit the preview URL for this PR (updated for commit d247e3b):

https://yew-rs-api--pr4020-allow-option-string-sjgkwf5h.web.app

(expires Sat, 07 Mar 2026 08:08:58 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link

Benchmark - core

Yew Master

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.361 ns      │ 2.477 ns      │ 2.366 ns      │ 2.37 ns       │ 100     │ 1000000000

Pull Request

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.403 ns      │ 3.504 ns      │ 2.406 ns      │ 2.426 ns      │ 100     │ 1000000000

@github-actions
Copy link

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)
async_clock 100.197 100.197 0 0.000%
boids 168.080 168.080 0 0.000%
communication_child_to_parent 93.469 93.469 0 0.000%
communication_grandchild_with_grandparent 105.250 105.250 0 0.000%
communication_grandparent_to_grandchild 101.606 101.606 0 0.000%
communication_parent_to_child 90.900 90.900 0 0.000%
contexts 105.149 105.149 0 0.000%
counter 86.282 86.282 0 0.000%
counter_functional 88.272 88.272 0 0.000%
dyn_create_destroy_apps 90.321 90.321 0 0.000%
file_upload 99.346 99.346 0 0.000%
function_delayed_input 94.375 94.375 0 0.000%
function_memory_game 172.939 172.939 0 0.000%
function_router 405.547 405.547 0 0.000%
function_todomvc 164.148 164.148 0 0.000%
futures 235.159 235.159 0 0.000%
game_of_life 104.718 104.718 0 0.000%
immutable 255.868 255.868 0 0.000%
inner_html 80.803 80.803 0 0.000%
js_callback 109.374 109.374 0 0.000%
keyed_list 179.724 179.724 0 0.000%
mount_point 84.146 84.146 0 0.000%
nested_list 113.058 113.058 0 0.000%
node_refs 91.525 91.525 0 0.000%
password_strength 1728.822 1728.822 0 0.000%
portals 93.035 93.035 0 0.000%
router 376.134 376.134 0 0.000%
suspense 113.452 113.452 0 0.000%
timer 88.634 88.634 0 0.000%
timer_functional 98.875 98.875 0 0.000%
todomvc 142.088 142.088 0 0.000%
two_apps 86.146 86.146 0 0.000%
web_worker_fib 136.224 136.224 0 0.000%
web_worker_prime 187.438 187.438 0 0.000%
webgl 83.223 83.223 0 0.000%

✅ None of the examples has changed their size significantly.

@github-actions
Copy link

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 291.084 294.084 291.761 1.120
Hello World 10 490.269 505.427 494.261 4.322
Function Router 10 34491.622 43902.068 37331.174 2772.894
Concurrent Task 10 1006.477 1007.544 1007.053 0.305
Many Providers 10 1105.195 1184.281 1124.204 25.077

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 291.038 291.596 291.246 0.177
Hello World 10 470.972 476.570 472.932 1.696
Function Router 10 34497.172 44041.129 37782.287 3099.088
Concurrent Task 10 1005.801 1007.543 1007.022 0.489
Many Providers 10 1054.074 1104.996 1066.372 15.566

@Madoshakalaka
Copy link
Member

thanks!

can you fix the macro output comparison tests?
pass TRYBUILD=overwrite, git diff to make sure the compiler output changes are only cosmetic

@Madoshakalaka Madoshakalaka added A-yew Area: The main yew crate breaking change labels Feb 28, 2026
@Madoshakalaka
Copy link
Member

Madoshakalaka commented Feb 28, 2026

This is a breaking change because bare Nones won't compile any more for Option<Html> properties

#[derive(PartialEq, Properties)]
pub struct Props {
    pub title: Option<Html>,
}

// bare None without type annotation
let _ = html! { <Foo title={None} /> };

On master: html! { <Foo title={None} /> } compiles fine, None is inferred as Option<VNode> via the identity impl.
With the PR: it fails with

E0283: type annotations needed

because None is now ambiguous across dozens of Option<X> types that all implement IntoPropValue<Option<VNode>>
To make it compile, one will have pass Option::<Html>::None instead

For reference, we already have broken Option<String> and Option<AttrValue> props where bare Nones already don't work, see #3747

This PR will make it "uniformly broken" for all three.

html! { <Foo title={Option::<Html>::None} /> } imo is a horrible pattern nobody wants to write. I will pause merging this and think of a fix first. I have some ideas.

@ctron do you have any opinions?

@Casheeew
Copy link
Author

Casheeew commented Feb 28, 2026

The easiest way is to use the #[prop_or_default] directive for
pub title: Option.

This way, if you just leave out the prop:

<Foo />, you get None for title and you can do stuff with the value.

Otherwise, since you're not designating #[prop_or_default], there is an argument that you should be passing everything explicitly anyway, so Option::<Html>::None is not that bad, but ymmw

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

Labels

A-yew Area: The main yew crate blocked breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow assigning &str and String to Option<Html>

2 participants