Skip to content

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

Merged
Madoshakalaka merged 2 commits intoyewstack:masterfrom
Casheeew:allow-option-string-conversions
Mar 3, 2026
Merged

feat: allow &str and String conversion for Option<Html>#4020
Madoshakalaka merged 2 commits intoyewstack:masterfrom
Casheeew:allow-option-string-conversions

Conversation

@Casheeew
Copy link
Copy Markdown
Contributor

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
Copy Markdown

github-actions Bot commented Feb 28, 2026

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

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

(expires Tue, 10 Mar 2026 13:43:50 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 28, 2026

Benchmark - core

Yew Master

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

Pull Request

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.404 ns      │ 3.656 ns      │ 2.407 ns      │ 2.434 ns      │ 100     │ 1000000000

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 28, 2026

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.472 93.472 0 0.000%
communication_grandchild_with_grandparent 105.248 105.248 0 0.000%
communication_grandparent_to_grandchild 101.609 101.609 0 0.000%
communication_parent_to_child 90.899 90.899 0 0.000%
contexts 105.151 105.151 0 0.000%
counter 86.282 86.282 0 0.000%
counter_functional 88.276 88.276 0 0.000%
dyn_create_destroy_apps 90.320 90.320 0 0.000%
file_upload 99.346 99.346 0 0.000%
function_delayed_input 94.374 94.374 0 0.000%
function_memory_game 172.943 172.943 0 0.000%
function_router 406.410 406.410 0 0.000%
function_todomvc 164.160 164.160 0 0.000%
futures 235.159 235.159 0 0.000%
game_of_life 104.718 104.718 0 0.000%
immutable 255.879 255.879 0 0.000%
inner_html 80.803 80.803 0 0.000%
js_callback 109.379 109.379 0 0.000%
keyed_list 179.728 179.728 0 0.000%
mount_point 84.146 84.146 0 0.000%
nested_list 113.059 113.059 0 0.000%
node_refs 91.526 91.526 0 0.000%
password_strength 1729.146 1729.146 0 0.000%
portals 93.034 93.034 0 0.000%
router 376.976 376.976 0 0.000%
suspense 113.464 113.464 0 0.000%
timer 88.634 88.634 0 0.000%
timer_functional 98.877 98.877 0 0.000%
todomvc 142.088 142.088 0 0.000%
two_apps 86.146 86.146 0 0.000%
web_worker_fib 136.239 136.239 0 0.000%
web_worker_prime 187.470 187.470 0 0.000%
webgl 83.224 83.224 0 0.000%

✅ None of the examples has changed their size significantly.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 28, 2026

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 310.900 313.155 311.301 0.679
Hello World 10 486.922 509.145 493.183 7.241
Function Router 10 31942.208 32590.999 32197.611 217.603
Concurrent Task 10 1006.098 1007.881 1006.941 0.699
Many Providers 10 1092.713 1112.462 1101.355 6.444

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 310.821 312.998 311.483 0.798
Hello World 10 468.487 492.773 476.435 7.803
Function Router 10 31465.050 31956.732 31749.681 154.181
Concurrent Task 10 1006.600 1007.853 1007.181 0.445
Many Providers 10 1111.247 1144.830 1121.760 12.503

@Madoshakalaka
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor 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

@Casheeew Casheeew force-pushed the allow-option-string-conversions branch from d247e3b to 318392c Compare March 2, 2026 16:12
@Casheeew
Copy link
Copy Markdown
Contributor Author

Casheeew commented Mar 2, 2026

@Madoshakalaka I dont have the 1.84 Rust can you run the trybuild command for me pwetty pwease

@Madoshakalaka
Copy link
Copy Markdown
Member

I don't see any anomaly. will merge when CI passes.

Copy link
Copy Markdown
Member

@Madoshakalaka Madoshakalaka left a comment

Choose a reason for hiding this comment

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

solid

@Madoshakalaka Madoshakalaka merged commit 815dff6 into yewstack:master Mar 3, 2026
25 checks passed
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow assigning &str and String to Option<Html>

2 participants