Repository: yewstack/yew Branch: master Commit: 195730db49c2 Files: 1780 Total size: 5.2 MB Directory structure: gitextract_gxaqlxdv/ ├── .cargo/ │ └── config.toml ├── .firebaserc ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── documentation.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── auto-approve-maintainer-pr.yml │ ├── benchmark-core.yml │ ├── benchmark-ssr.yml │ ├── benchmark.yml │ ├── build-api-docs.yml │ ├── build-website.yml │ ├── clippy.yml │ ├── fmt.yml │ ├── inspect-next-changelogs.yml │ ├── main-checks.yml │ ├── post-benchmark-core.yml │ ├── post-benchmark-ssr.yml │ ├── post-benchmark.yml │ ├── post-size-cmp.yml │ ├── publish-api-docs.yml │ ├── publish-examples.yml │ ├── publish-website.yml │ ├── publish.yml │ ├── size-cmp.yml │ └── test-website.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile.toml ├── README.md ├── SECURITY.md ├── _typos.toml ├── api-docs/ │ ├── .gitignore │ ├── before-content.html │ └── styles.css ├── ci/ │ ├── collect_sizes.py │ ├── install-wasm-bindgen-cli.sh │ ├── make_benchmark_ssr_cmt.py │ ├── make_example_size_cmt.py │ └── write-min-size-flags.sh ├── examples/ │ ├── .cargo/ │ │ ├── config.toml │ │ ├── dummy-min-size-config.toml │ │ └── min-size-config.toml │ ├── .gitignore │ ├── README.md │ ├── async_clock/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── main.rs │ │ └── services.rs │ ├── boids/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── boid.rs │ │ ├── main.rs │ │ ├── math.rs │ │ ├── settings.rs │ │ ├── simulation.rs │ │ └── slider.rs │ ├── communication_child_to_parent/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── child.rs │ │ ├── main.rs │ │ └── parent.rs │ ├── communication_grandchild_with_grandparent/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── child.rs │ │ ├── grandparent.rs │ │ ├── main.rs │ │ └── parent.rs │ ├── communication_grandparent_to_grandchild/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── child.rs │ │ ├── grandparent.rs │ │ ├── main.rs │ │ └── parent.rs │ ├── communication_parent_to_child/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── child.rs │ │ ├── main.rs │ │ └── parent.rs │ ├── contexts/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ ├── main.rs │ │ ├── msg_ctx.rs │ │ ├── producer.rs │ │ ├── struct_component_producer.rs │ │ ├── struct_component_subscriber.rs │ │ └── subscriber.rs │ ├── counter/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ └── main.rs │ ├── counter_functional/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ └── main.rs │ ├── dyn_create_destroy_apps/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── counter.rs │ │ └── main.rs │ ├── file_upload/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── src/ │ │ │ └── main.rs │ │ └── styles.css │ ├── function_delayed_input/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ └── main.rs │ ├── function_memory_game/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── scss/ │ │ │ ├── chess_board.scss │ │ │ ├── chess_board_card.scss │ │ │ ├── game_progress.scss │ │ │ ├── game_status_board.scss │ │ │ ├── index.scss │ │ │ ├── score_board.scss │ │ │ └── score_board_best_score.scss │ │ └── src/ │ │ ├── components/ │ │ │ ├── app.rs │ │ │ ├── chessboard.rs │ │ │ ├── chessboard_card.rs │ │ │ ├── game_status_board.rs │ │ │ ├── score_board.rs │ │ │ ├── score_board_best_score.rs │ │ │ ├── score_board_logo.rs │ │ │ └── score_board_progress.rs │ │ ├── components.rs │ │ ├── constant.rs │ │ ├── helper.rs │ │ ├── main.rs │ │ └── state.rs │ ├── function_router/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── data/ │ │ │ ├── keywords.txt │ │ │ ├── syllables.txt │ │ │ └── yew.txt │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── app.rs │ │ ├── bin/ │ │ │ └── function_router.rs │ │ ├── components/ │ │ │ ├── author_card.rs │ │ │ ├── mod.rs │ │ │ ├── nav.rs │ │ │ ├── pagination.rs │ │ │ ├── post_card.rs │ │ │ └── progress_delay.rs │ │ ├── content.rs │ │ ├── generator.rs │ │ ├── imagegen.rs │ │ ├── lib.rs │ │ └── pages/ │ │ ├── author.rs │ │ ├── author_list.rs │ │ ├── home.rs │ │ ├── mod.rs │ │ ├── page_not_found.rs │ │ ├── post.rs │ │ └── post_list.rs │ ├── function_todomvc/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ ├── components/ │ │ │ ├── entry.rs │ │ │ ├── filter.rs │ │ │ ├── header_input.rs │ │ │ └── info_footer.rs │ │ ├── components.rs │ │ ├── hooks/ │ │ │ └── use_bool_toggle.rs │ │ ├── hooks.rs │ │ ├── main.rs │ │ └── state.rs │ ├── futures/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ ├── main.rs │ │ └── markdown.rs │ ├── game_of_life/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── src/ │ │ │ ├── conway.rs │ │ │ └── main.rs │ │ └── styles.css │ ├── immutable/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── array.rs │ │ ├── main.rs │ │ ├── map.rs │ │ └── string.rs │ ├── inner_html/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ ├── document.html │ │ └── main.rs │ ├── js_callback/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── js/ │ │ │ ├── imp.js │ │ │ └── unimp.js │ │ ├── src/ │ │ │ ├── bindings.rs │ │ │ └── main.rs │ │ └── trunk_post_build.rs │ ├── keyed_list/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── src/ │ │ │ ├── main.rs │ │ │ ├── person.rs │ │ │ └── random.rs │ │ └── styles.css │ ├── mount_point/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ └── main.rs │ ├── nested_list/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── src/ │ │ │ ├── app.rs │ │ │ ├── header.rs │ │ │ ├── item.rs │ │ │ ├── list.rs │ │ │ └── main.rs │ │ └── styles.scss │ ├── node_refs/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── src/ │ │ │ ├── input.rs │ │ │ └── main.rs │ │ └── styles.css │ ├── password_strength/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── app.rs │ │ ├── main.rs │ │ ├── password.rs │ │ └── text_input.rs │ ├── portals/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ └── main.rs │ ├── router/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── data/ │ │ │ ├── keywords.txt │ │ │ ├── syllables.txt │ │ │ └── yew.txt │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── components/ │ │ │ ├── author_card.rs │ │ │ ├── mod.rs │ │ │ ├── pagination.rs │ │ │ ├── post_card.rs │ │ │ └── progress_delay.rs │ │ ├── content.rs │ │ ├── generator.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ └── pages/ │ │ ├── author.rs │ │ ├── author_list.rs │ │ ├── home.rs │ │ ├── mod.rs │ │ ├── page_not_found.rs │ │ ├── post.rs │ │ └── post_list.rs │ ├── simple_ssr/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── index.html │ │ ├── src/ │ │ │ ├── bin/ │ │ │ │ ├── simple_ssr_hydrate.rs │ │ │ │ └── simple_ssr_server.rs │ │ │ └── lib.rs │ │ └── tests/ │ │ └── e2e.rs │ ├── ssr_router/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── index.html │ │ ├── src/ │ │ │ ├── bin/ │ │ │ │ ├── ssr_router_hydrate.rs │ │ │ │ └── ssr_router_server.rs │ │ │ └── lib.rs │ │ └── tests/ │ │ └── e2e.rs │ ├── suspense/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ ├── main.rs │ │ ├── struct_consumer.rs │ │ └── use_sleep.rs │ ├── timer/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ └── main.rs │ ├── timer_functional/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ ├── index.scss │ │ └── src/ │ │ └── main.rs │ ├── todomvc/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ ├── main.rs │ │ └── state.rs │ ├── two_apps/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ └── main.rs │ ├── wasi_ssr_module/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ ├── main.rs │ │ └── router.rs │ ├── web_worker_fib/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ ├── agent.rs │ │ ├── bin/ │ │ │ ├── app.rs │ │ │ └── worker.rs │ │ └── lib.rs │ ├── web_worker_prime/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Trunk.toml │ │ ├── index.html │ │ └── src/ │ │ ├── agent.rs │ │ ├── bin/ │ │ │ ├── app.rs │ │ │ └── worker.rs │ │ └── lib.rs │ └── webgl/ │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── index.html │ └── src/ │ ├── basic.frag │ ├── basic.vert │ └── main.rs ├── firebase.json ├── packages/ │ ├── yew/ │ │ ├── Cargo.toml │ │ ├── Makefile.toml │ │ ├── src/ │ │ │ ├── app_handle.rs │ │ │ ├── callback.rs │ │ │ ├── context.rs │ │ │ ├── dom_bundle/ │ │ │ │ ├── bcomp.rs │ │ │ │ ├── blist.rs │ │ │ │ ├── bnode.rs │ │ │ │ ├── bportal.rs │ │ │ │ ├── braw.rs │ │ │ │ ├── bsuspense.rs │ │ │ │ ├── btag/ │ │ │ │ │ ├── attributes.rs │ │ │ │ │ ├── listeners.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── btext.rs │ │ │ │ ├── fragment.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── position.rs │ │ │ │ ├── subtree_root.rs │ │ │ │ ├── traits.rs │ │ │ │ └── utils.rs │ │ │ ├── functional/ │ │ │ │ ├── hooks/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── use_callback.rs │ │ │ │ │ ├── use_context.rs │ │ │ │ │ ├── use_effect.rs │ │ │ │ │ ├── use_force_update.rs │ │ │ │ │ ├── use_memo.rs │ │ │ │ │ ├── use_prepared_state/ │ │ │ │ │ │ ├── feat_hydration.rs │ │ │ │ │ │ ├── feat_hydration_ssr.rs │ │ │ │ │ │ ├── feat_none.rs │ │ │ │ │ │ ├── feat_ssr.rs │ │ │ │ │ │ └── mod.rs │ │ │ │ │ ├── use_reducer.rs │ │ │ │ │ ├── use_ref.rs │ │ │ │ │ ├── use_state.rs │ │ │ │ │ └── use_transitive_state/ │ │ │ │ │ ├── feat_hydration.rs │ │ │ │ │ ├── feat_hydration_ssr.rs │ │ │ │ │ ├── feat_none.rs │ │ │ │ │ ├── feat_ssr.rs │ │ │ │ │ └── mod.rs │ │ │ │ └── mod.rs │ │ │ ├── html/ │ │ │ │ ├── classes.rs │ │ │ │ ├── component/ │ │ │ │ │ ├── children.rs │ │ │ │ │ ├── lifecycle.rs │ │ │ │ │ ├── marker.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── properties.rs │ │ │ │ │ └── scope.rs │ │ │ │ ├── conversion/ │ │ │ │ │ ├── into_prop_value.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── error.rs │ │ │ │ ├── listener/ │ │ │ │ │ ├── events.rs │ │ │ │ │ └── mod.rs │ │ │ │ └── mod.rs │ │ │ ├── lib.rs │ │ │ ├── platform.rs │ │ │ ├── renderer.rs │ │ │ ├── scheduler.rs │ │ │ ├── sealed.rs │ │ │ ├── server_renderer.rs │ │ │ ├── suspense/ │ │ │ │ ├── component.rs │ │ │ │ ├── hooks.rs │ │ │ │ ├── mod.rs │ │ │ │ └── suspension.rs │ │ │ ├── tests/ │ │ │ │ ├── layout_tests.rs │ │ │ │ └── mod.rs │ │ │ ├── utils/ │ │ │ │ └── mod.rs │ │ │ └── virtual_dom/ │ │ │ ├── key.rs │ │ │ ├── listeners.rs │ │ │ ├── mod.rs │ │ │ ├── vcomp.rs │ │ │ ├── vlist.rs │ │ │ ├── vnode.rs │ │ │ ├── vportal.rs │ │ │ ├── vraw.rs │ │ │ ├── vsuspense.rs │ │ │ ├── vtag.rs │ │ │ └── vtext.rs │ │ └── tests/ │ │ ├── common/ │ │ │ └── mod.rs │ │ ├── hydration.rs │ │ ├── layout.rs │ │ ├── mod.rs │ │ ├── raw_html.rs │ │ ├── suspense.rs │ │ ├── use_callback.rs │ │ ├── use_context.rs │ │ ├── use_effect.rs │ │ ├── use_memo.rs │ │ ├── use_prepared_state.rs │ │ ├── use_reducer.rs │ │ ├── use_ref.rs │ │ ├── use_state.rs │ │ └── use_transitive_state.rs │ ├── yew-agent/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ ├── codec.rs │ │ ├── lib.rs │ │ ├── oneshot/ │ │ │ ├── bridge.rs │ │ │ ├── hooks.rs │ │ │ ├── mod.rs │ │ │ ├── provider.rs │ │ │ ├── registrar.rs │ │ │ ├── spawner.rs │ │ │ ├── traits.rs │ │ │ └── worker.rs │ │ ├── reach.rs │ │ ├── reactor/ │ │ │ ├── bridge.rs │ │ │ ├── hooks.rs │ │ │ ├── messages.rs │ │ │ ├── mod.rs │ │ │ ├── provider.rs │ │ │ ├── registrar.rs │ │ │ ├── scope.rs │ │ │ ├── spawner.rs │ │ │ ├── traits.rs │ │ │ └── worker.rs │ │ ├── scope_ext.rs │ │ ├── traits.rs │ │ ├── utils.rs │ │ └── worker/ │ │ ├── bridge.rs │ │ ├── handler_id.rs │ │ ├── hooks.rs │ │ ├── lifecycle.rs │ │ ├── messages.rs │ │ ├── mod.rs │ │ ├── native_worker.rs │ │ ├── provider.rs │ │ ├── registrar.rs │ │ ├── scope.rs │ │ ├── spawner.rs │ │ └── traits.rs │ ├── yew-agent-macro/ │ │ ├── Cargo.toml │ │ ├── release.toml │ │ └── src/ │ │ ├── agent_fn.rs │ │ ├── lib.rs │ │ ├── oneshot.rs │ │ └── reactor.rs │ ├── yew-macro/ │ │ ├── Cargo.toml │ │ ├── Makefile.toml │ │ ├── release.toml │ │ ├── src/ │ │ │ ├── classes/ │ │ │ │ └── mod.rs │ │ │ ├── derive_props/ │ │ │ │ ├── builder.rs │ │ │ │ ├── field.rs │ │ │ │ ├── generics.rs │ │ │ │ ├── mod.rs │ │ │ │ └── wrapper.rs │ │ │ ├── function_component.rs │ │ │ ├── hook/ │ │ │ │ ├── body.rs │ │ │ │ ├── lifetime.rs │ │ │ │ ├── mod.rs │ │ │ │ └── signature.rs │ │ │ ├── html_tree/ │ │ │ │ ├── html_block.rs │ │ │ │ ├── html_component.rs │ │ │ │ ├── html_dashed_name.rs │ │ │ │ ├── html_element.rs │ │ │ │ ├── html_for.rs │ │ │ │ ├── html_if.rs │ │ │ │ ├── html_iterable.rs │ │ │ │ ├── html_list.rs │ │ │ │ ├── html_node.rs │ │ │ │ ├── lint/ │ │ │ │ │ └── mod.rs │ │ │ │ ├── mod.rs │ │ │ │ └── tag.rs │ │ │ ├── lib.rs │ │ │ ├── props/ │ │ │ │ ├── component.rs │ │ │ │ ├── element.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── prop.rs │ │ │ │ └── prop_macro.rs │ │ │ ├── stringify.rs │ │ │ ├── use_prepared_state.rs │ │ │ └── use_transitive_state.rs │ │ └── tests/ │ │ ├── classes_macro/ │ │ │ ├── classes-fail.rs │ │ │ ├── classes-fail.stderr │ │ │ └── classes-pass.rs │ │ ├── classes_macro_test.rs │ │ ├── derive_props/ │ │ │ ├── fail.rs │ │ │ ├── fail.stderr │ │ │ └── pass.rs │ │ ├── derive_props_test.rs │ │ ├── function_attr_test.rs │ │ ├── function_component_attr/ │ │ │ ├── applied-to-non-fn-fail.rs │ │ │ ├── applied-to-non-fn-fail.stderr │ │ │ ├── async-fail.rs │ │ │ ├── async-fail.stderr │ │ │ ├── bad-name-fail.rs │ │ │ ├── bad-name-fail.stderr │ │ │ ├── bad-props-param-fail.rs │ │ │ ├── bad-props-param-fail.stderr │ │ │ ├── bad-return-type-fail.rs │ │ │ ├── bad-return-type-fail.stderr │ │ │ ├── const-fail.rs │ │ │ ├── const-fail.stderr │ │ │ ├── extern-fail.rs │ │ │ ├── extern-fail.stderr │ │ │ ├── generic-lifetime-fail.rs │ │ │ ├── generic-lifetime-fail.stderr │ │ │ ├── generic-pass.rs │ │ │ ├── generic-props-fail.rs │ │ │ ├── generic-props-fail.stderr │ │ │ ├── hook_location-fail.rs │ │ │ ├── hook_location-fail.stderr │ │ │ ├── hook_location-pass.rs │ │ │ ├── lifetime-props-param-fail.rs │ │ │ ├── lifetime-props-param-fail.stderr │ │ │ ├── multiple-param-fail.rs │ │ │ ├── multiple-param-fail.stderr │ │ │ ├── mut-ref-props-param-fail.rs │ │ │ ├── mut-ref-props-param-fail.stderr │ │ │ ├── no-name-default-pass.rs │ │ │ ├── with-defaulted-type-param-pass.rs │ │ │ ├── with-props-pass.rs │ │ │ ├── with-receiver-fail.rs │ │ │ ├── with-receiver-fail.stderr │ │ │ └── without-props-pass.rs │ │ ├── hook_attr/ │ │ │ ├── hook-call-generics-pass.rs │ │ │ ├── hook-const-generic-pass.rs │ │ │ ├── hook-dynamic-dispatch-pass.rs │ │ │ ├── hook-impl-trait-pass.rs │ │ │ ├── hook-lifetime-pass.rs │ │ │ ├── hook-must-use-fail.rs │ │ │ ├── hook-must-use-fail.stderr │ │ │ ├── hook-must-use-pass.rs │ │ │ ├── hook-return-impl-trait-pass.rs │ │ │ ├── hook-return-ref-pass.rs │ │ │ ├── hook-trait-item-pass.rs │ │ │ ├── hook_location-fail.rs │ │ │ ├── hook_location-fail.stderr │ │ │ ├── hook_location-pass.rs │ │ │ ├── hook_macro-fail.rs │ │ │ ├── hook_macro-fail.stderr │ │ │ └── hook_macro-pass.rs │ │ ├── hook_attr_test.rs │ │ ├── hook_macro/ │ │ │ ├── use_prepared_state-fail.rs │ │ │ ├── use_prepared_state-fail.stderr │ │ │ ├── use_transitive_state-fail.rs │ │ │ └── use_transitive_state-fail.stderr │ │ ├── hook_macro_test.rs │ │ ├── html_lints/ │ │ │ ├── fail.rs │ │ │ └── fail.stderr │ │ ├── html_lints_test.rs │ │ ├── html_macro/ │ │ │ ├── as-return-value-pass.rs │ │ │ ├── block-fail.rs │ │ │ ├── block-fail.stderr │ │ │ ├── block-pass.rs │ │ │ ├── component-any-children-pass.rs │ │ │ ├── component-fail.rs │ │ │ ├── component-fail.stderr │ │ │ ├── component-pass.rs │ │ │ ├── component-unimplemented-fail.rs │ │ │ ├── component-unimplemented-fail.stderr │ │ │ ├── dyn-element-pass.rs │ │ │ ├── element-fail.rs │ │ │ ├── element-fail.stderr │ │ │ ├── for-fail.rs │ │ │ ├── for-fail.stderr │ │ │ ├── for-pass.rs │ │ │ ├── generic-component-fail.rs │ │ │ ├── generic-component-fail.stderr │ │ │ ├── generic-component-pass.rs │ │ │ ├── html-component-fail.stderr │ │ │ ├── html-element-pass.rs │ │ │ ├── html-if-fail.rs │ │ │ ├── html-if-fail.stderr │ │ │ ├── html-if-pass.rs │ │ │ ├── html-node-pass.rs │ │ │ ├── iterable-fail.rs │ │ │ ├── iterable-fail.stderr │ │ │ ├── iterable-pass.rs │ │ │ ├── list-fail.rs │ │ │ ├── list-fail.stderr │ │ │ ├── list-pass.rs │ │ │ ├── missing-props-diagnostics-fail.rs │ │ │ ├── missing-props-diagnostics-fail.stderr │ │ │ ├── node-fail.rs │ │ │ ├── node-fail.stderr │ │ │ ├── node-pass.rs │ │ │ └── svg-pass.rs │ │ ├── html_macro_test.rs │ │ ├── props_macro/ │ │ │ ├── props-fail.rs │ │ │ ├── props-fail.stderr │ │ │ ├── props-pass.rs │ │ │ ├── resolve-prop-fail.rs │ │ │ ├── resolve-prop-fail.stderr │ │ │ └── resolve-prop-pass.rs │ │ └── props_macro_test.rs │ ├── yew-router/ │ │ ├── Cargo.toml │ │ ├── Makefile.toml │ │ ├── README.md │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── link.rs │ │ │ │ ├── mod.rs │ │ │ │ └── redirect.rs │ │ │ ├── hooks.rs │ │ │ ├── lib.rs │ │ │ ├── macro_helpers.rs │ │ │ ├── navigator.rs │ │ │ ├── routable.rs │ │ │ ├── router.rs │ │ │ ├── scope_ext.rs │ │ │ ├── switch.rs │ │ │ └── utils.rs │ │ └── tests/ │ │ ├── basename.rs │ │ ├── browser_router.rs │ │ ├── hash_router.rs │ │ ├── link.rs │ │ ├── router_unit_tests.rs │ │ ├── url_encoded_routes.rs │ │ └── utils.rs │ └── yew-router-macro/ │ ├── Cargo.toml │ ├── Makefile.toml │ ├── release.toml │ ├── src/ │ │ ├── lib.rs │ │ └── routable_derive.rs │ └── tests/ │ ├── routable_derive/ │ │ ├── bad-ats-fail.rs │ │ ├── bad-ats-fail.stderr │ │ ├── invalid-not-found-fail.rs │ │ ├── invalid-not-found-fail.stderr │ │ ├── relative-path-fail.rs │ │ ├── relative-path-fail.stderr │ │ ├── route-with-hash-fail.rs │ │ ├── route-with-hash-fail.stderr │ │ ├── struct-fail.rs │ │ ├── struct-fail.stderr │ │ ├── unnamed-fields-fail.rs │ │ ├── unnamed-fields-fail.stderr │ │ └── valid-pass.rs │ └── routable_derive_test.rs ├── release.toml ├── rustfmt.toml ├── tools/ │ ├── benchmark-core/ │ │ ├── Cargo.toml │ │ └── benches/ │ │ └── vnode.rs │ ├── benchmark-hooks/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── Makefile.toml │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ └── src/ │ │ └── lib.rs │ ├── benchmark-ssr/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── benchmark-struct/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── Makefile.toml │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ └── src/ │ │ └── lib.rs │ ├── build-examples/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── bin/ │ │ │ └── update-wasm-opt.rs │ │ ├── lib.rs │ │ └── main.rs │ ├── changelog/ │ │ ├── Cargo.toml │ │ ├── Makefile.toml │ │ ├── src/ │ │ │ ├── cli.rs │ │ │ ├── create_log_line.rs │ │ │ ├── create_log_lines.rs │ │ │ ├── get_latest_version.rs │ │ │ ├── github_fetch.rs │ │ │ ├── github_issue_labels_fetcher.rs │ │ │ ├── github_user_fetcher.rs │ │ │ ├── lib.rs │ │ │ ├── log_line.rs │ │ │ ├── main.rs │ │ │ ├── mod.rs │ │ │ ├── new_version_level.rs │ │ │ ├── stdout_tag_description_changelog.rs │ │ │ ├── write_changelog_file.rs │ │ │ ├── write_log_lines.rs │ │ │ ├── write_version_changelog.rs │ │ │ └── yew_package.rs │ │ └── tests/ │ │ ├── generate_yew_changelog_file.rs │ │ ├── test_base.md │ │ └── test_expected.md │ ├── collect-release-info/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── process-benchmark-results/ │ │ ├── Cargo.toml │ │ ├── Makefile.toml │ │ ├── README.md │ │ └── src/ │ │ └── main.rs │ ├── ssr-e2e/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── ssr-e2e-harness/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ └── website-test/ │ ├── Cargo.toml │ ├── Makefile.toml │ ├── build.rs │ └── src/ │ └── lib.rs └── website/ ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── babel.config.js ├── blog/ │ ├── 2022-01-20-hello-yew.md │ ├── 2022-11-24-release-0-20.md │ ├── 2023-09-23-release-0-21.md │ ├── 2024-10-14-release-0-22.md │ ├── 2025-11-29-release-0-22.md │ └── authors.yml ├── check-translations.js ├── community/ │ ├── awesome.md │ └── external-libs.mdx ├── docs/ │ ├── advanced-topics/ │ │ ├── children.mdx │ │ ├── how-it-works.mdx │ │ ├── immutable.mdx │ │ ├── optimizations.mdx │ │ ├── portals.mdx │ │ ├── server-side-rendering.mdx │ │ └── struct-components/ │ │ ├── callbacks.mdx │ │ ├── hoc.mdx │ │ ├── introduction.mdx │ │ ├── lifecycle.mdx │ │ ├── properties.mdx │ │ ├── refs.mdx │ │ └── scope.mdx │ ├── concepts/ │ │ ├── agents.mdx │ │ ├── basic-web-technologies/ │ │ │ ├── css.mdx │ │ │ ├── html.mdx │ │ │ ├── js.mdx │ │ │ ├── wasm-bindgen.mdx │ │ │ └── web-sys.mdx │ │ ├── contexts.mdx │ │ ├── function-components/ │ │ │ ├── callbacks.mdx │ │ │ ├── children.mdx │ │ │ ├── communication.mdx │ │ │ ├── generics.mdx │ │ │ ├── hooks/ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ └── introduction.mdx │ │ │ ├── introduction.mdx │ │ │ ├── node-refs.mdx │ │ │ ├── properties.mdx │ │ │ ├── pure-components.mdx │ │ │ └── state.mdx │ │ ├── html/ │ │ │ ├── classes.mdx │ │ │ ├── components.mdx │ │ │ ├── conditional-rendering.mdx │ │ │ ├── elements.mdx │ │ │ ├── events.mdx │ │ │ ├── fragments.mdx │ │ │ ├── introduction.mdx │ │ │ ├── lists.mdx │ │ │ └── literals-and-expressions.mdx │ │ ├── router.mdx │ │ └── suspense.mdx │ ├── getting-started/ │ │ ├── build-a-sample-app.mdx │ │ ├── editor-setup.mdx │ │ ├── examples.mdx │ │ └── introduction.mdx │ ├── migration-guides/ │ │ ├── yew/ │ │ │ ├── from-0_19_0-to-0_20_0.mdx │ │ │ ├── from-0_20_0-to-0_21_0.mdx │ │ │ ├── from-0_21_0-to-0_22_0.mdx │ │ │ └── from-0_22_0-to-0_23_0.mdx │ │ ├── yew-agent/ │ │ │ ├── from-0_0_0-to-0_1_0.mdx │ │ │ ├── from-0_1_0-to-0_2_0.mdx │ │ │ ├── from-0_3_0-to-0_4_0.mdx │ │ │ └── from-0_4_0-to-0_5_0.mdx │ │ └── yew-router/ │ │ ├── from-0_15_0-to-0_16_0.mdx │ │ ├── from-0_16_0-to-0_17_0.mdx │ │ └── from-0_19_0-to-0_20_0.mdx │ ├── more/ │ │ ├── css.mdx │ │ ├── debugging.mdx │ │ ├── deployment.mdx │ │ ├── roadmap.mdx │ │ └── testing.mdx │ └── tutorial/ │ └── index.mdx ├── docusaurus.config.js ├── i18n/ │ ├── ja/ │ │ ├── code.json │ │ ├── docusaurus-plugin-content-blog/ │ │ │ └── options.json │ │ ├── docusaurus-plugin-content-docs/ │ │ │ ├── current/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── immutable.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ ├── portals.mdx │ │ │ │ │ ├── server-side-rendering.mdx │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── hoc.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── refs.mdx │ │ │ │ │ └── scope.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── basic-web-technologies/ │ │ │ │ │ │ ├── css.mdx │ │ │ │ │ │ ├── html.mdx │ │ │ │ │ │ ├── js.mdx │ │ │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ │ │ └── web-sys.mdx │ │ │ │ │ ├── contexts.mdx │ │ │ │ │ ├── function-components/ │ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ │ ├── children.mdx │ │ │ │ │ │ ├── communication.mdx │ │ │ │ │ │ ├── generics.mdx │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── node-refs.mdx │ │ │ │ │ │ ├── properties.mdx │ │ │ │ │ │ ├── pure-components.mdx │ │ │ │ │ │ └── state.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── classes.mdx │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── events.mdx │ │ │ │ │ │ ├── fragments.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ ├── router.mdx │ │ │ │ │ └── suspense.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ ├── editor-setup.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── introduction.mdx │ │ │ │ ├── migration-guides/ │ │ │ │ │ ├── yew/ │ │ │ │ │ │ └── from-0_22_0-to-0_23_0.mdx │ │ │ │ │ ├── yew-agent/ │ │ │ │ │ │ └── from-0_4_0-to-0_5_0.mdx │ │ │ │ │ └── yew-router/ │ │ │ │ │ └── from-0_19_0-to-0_20_0.mdx │ │ │ │ ├── more/ │ │ │ │ │ ├── css.mdx │ │ │ │ │ ├── debugging.mdx │ │ │ │ │ ├── deployment.mdx │ │ │ │ │ ├── roadmap.mdx │ │ │ │ │ └── testing.mdx │ │ │ │ └── tutorial/ │ │ │ │ └── index.mdx │ │ │ ├── current.json │ │ │ ├── version-0.20/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ └── refs.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ └── router.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ └── examples.mdx │ │ │ │ └── more/ │ │ │ │ ├── css.mdx │ │ │ │ ├── debugging.mdx │ │ │ │ ├── roadmap.mdx │ │ │ │ └── testing.mdx │ │ │ ├── version-0.20.json │ │ │ ├── version-0.21/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── immutable.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ ├── portals.mdx │ │ │ │ │ ├── server-side-rendering.md │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── hoc.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── refs.mdx │ │ │ │ │ └── scope.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── contexts.mdx │ │ │ │ │ ├── function-components/ │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ └── properties.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ └── router.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ └── examples.mdx │ │ │ │ └── more/ │ │ │ │ ├── css.mdx │ │ │ │ ├── debugging.mdx │ │ │ │ ├── roadmap.mdx │ │ │ │ └── testing.mdx │ │ │ ├── version-0.21.json │ │ │ ├── version-0.22/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── immutable.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ ├── portals.mdx │ │ │ │ │ ├── server-side-rendering.mdx │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── hoc.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── refs.mdx │ │ │ │ │ └── scope.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── basic-web-technologies/ │ │ │ │ │ │ ├── css.mdx │ │ │ │ │ │ ├── html.mdx │ │ │ │ │ │ ├── js.mdx │ │ │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ │ │ └── web-sys.mdx │ │ │ │ │ ├── contexts.mdx │ │ │ │ │ ├── function-components/ │ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ │ ├── children.mdx │ │ │ │ │ │ ├── communication.mdx │ │ │ │ │ │ ├── generics.mdx │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── node-refs.mdx │ │ │ │ │ │ ├── properties.mdx │ │ │ │ │ │ ├── pure-components.mdx │ │ │ │ │ │ └── state.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── classes.mdx │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── events.mdx │ │ │ │ │ │ ├── fragments.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ ├── router.mdx │ │ │ │ │ └── suspense.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ ├── editor-setup.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── introduction.mdx │ │ │ │ ├── more/ │ │ │ │ │ ├── css.mdx │ │ │ │ │ ├── debugging.mdx │ │ │ │ │ ├── deployment.mdx │ │ │ │ │ ├── roadmap.mdx │ │ │ │ │ └── testing.mdx │ │ │ │ └── tutorial/ │ │ │ │ └── index.mdx │ │ │ ├── version-0.22.json │ │ │ ├── version-0.23/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── immutable.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ ├── portals.mdx │ │ │ │ │ ├── server-side-rendering.mdx │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── hoc.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── refs.mdx │ │ │ │ │ └── scope.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── basic-web-technologies/ │ │ │ │ │ │ ├── css.mdx │ │ │ │ │ │ ├── html.mdx │ │ │ │ │ │ ├── js.mdx │ │ │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ │ │ └── web-sys.mdx │ │ │ │ │ ├── contexts.mdx │ │ │ │ │ ├── function-components/ │ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ │ ├── children.mdx │ │ │ │ │ │ ├── communication.mdx │ │ │ │ │ │ ├── generics.mdx │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── node-refs.mdx │ │ │ │ │ │ ├── properties.mdx │ │ │ │ │ │ ├── pure-components.mdx │ │ │ │ │ │ └── state.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── classes.mdx │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── events.mdx │ │ │ │ │ │ ├── fragments.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ ├── router.mdx │ │ │ │ │ └── suspense.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ ├── editor-setup.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── introduction.mdx │ │ │ │ ├── migration-guides/ │ │ │ │ │ ├── yew/ │ │ │ │ │ │ └── from-0_22_0-to-0_23_0.mdx │ │ │ │ │ ├── yew-agent/ │ │ │ │ │ │ └── from-0_4_0-to-0_5_0.mdx │ │ │ │ │ └── yew-router/ │ │ │ │ │ └── from-0_19_0-to-0_20_0.mdx │ │ │ │ ├── more/ │ │ │ │ │ ├── css.mdx │ │ │ │ │ ├── debugging.mdx │ │ │ │ │ ├── deployment.mdx │ │ │ │ │ ├── roadmap.mdx │ │ │ │ │ └── testing.mdx │ │ │ │ └── tutorial/ │ │ │ │ └── index.mdx │ │ │ └── version-0.23.json │ │ ├── docusaurus-plugin-content-docs-community/ │ │ │ └── current.json │ │ ├── docusaurus-plugin-content-docs-router/ │ │ │ └── current.json │ │ ├── docusaurus-plugin-content-pages/ │ │ │ └── index.mdx │ │ └── docusaurus-theme-classic/ │ │ ├── footer.json │ │ └── navbar.json │ ├── zh-Hans/ │ │ ├── code.json │ │ ├── docusaurus-plugin-content-blog/ │ │ │ └── options.json │ │ ├── docusaurus-plugin-content-docs/ │ │ │ ├── current/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── immutable.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ ├── portals.mdx │ │ │ │ │ ├── server-side-rendering.mdx │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── hoc.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── refs.mdx │ │ │ │ │ └── scope.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── basic-web-technologies/ │ │ │ │ │ │ ├── css.mdx │ │ │ │ │ │ ├── html.mdx │ │ │ │ │ │ ├── js.mdx │ │ │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ │ │ └── web-sys.mdx │ │ │ │ │ ├── contexts.mdx │ │ │ │ │ ├── function-components/ │ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ │ ├── children.mdx │ │ │ │ │ │ ├── communication.mdx │ │ │ │ │ │ ├── generics.mdx │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── node-refs.mdx │ │ │ │ │ │ ├── properties.mdx │ │ │ │ │ │ ├── pure-components.mdx │ │ │ │ │ │ └── state.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── classes.mdx │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── events.mdx │ │ │ │ │ │ ├── fragments.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ ├── router.mdx │ │ │ │ │ └── suspense.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ ├── editor-setup.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── introduction.mdx │ │ │ │ ├── migration-guides/ │ │ │ │ │ ├── yew/ │ │ │ │ │ │ └── from-0_22_0-to-0_23_0.mdx │ │ │ │ │ ├── yew-agent/ │ │ │ │ │ │ └── from-0_4_0-to-0_5_0.mdx │ │ │ │ │ └── yew-router/ │ │ │ │ │ └── from-0_19_0-to-0_20_0.mdx │ │ │ │ ├── more/ │ │ │ │ │ ├── css.mdx │ │ │ │ │ ├── debugging.mdx │ │ │ │ │ ├── deployment.mdx │ │ │ │ │ ├── roadmap.mdx │ │ │ │ │ └── testing.mdx │ │ │ │ └── tutorial/ │ │ │ │ └── index.mdx │ │ │ ├── current.json │ │ │ ├── version-0.20/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ └── refs.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── function-components/ │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ └── custom-hooks.mdx │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ └── router.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ └── examples.mdx │ │ │ │ └── more/ │ │ │ │ ├── css.mdx │ │ │ │ ├── debugging.mdx │ │ │ │ ├── roadmap.mdx │ │ │ │ └── testing.mdx │ │ │ ├── version-0.20.json │ │ │ ├── version-0.21/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── immutable.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ ├── portals.mdx │ │ │ │ │ ├── server-side-rendering.md │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── hoc.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── refs.mdx │ │ │ │ │ └── scope.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── contexts.mdx │ │ │ │ │ ├── function-components/ │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ └── custom-hooks.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ └── properties.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ └── router.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ └── examples.mdx │ │ │ │ └── more/ │ │ │ │ ├── css.mdx │ │ │ │ ├── debugging.mdx │ │ │ │ ├── roadmap.mdx │ │ │ │ └── testing.mdx │ │ │ ├── version-0.21.json │ │ │ ├── version-0.22/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── immutable.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ ├── portals.mdx │ │ │ │ │ ├── server-side-rendering.mdx │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── hoc.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── refs.mdx │ │ │ │ │ └── scope.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── basic-web-technologies/ │ │ │ │ │ │ ├── css.mdx │ │ │ │ │ │ ├── html.mdx │ │ │ │ │ │ ├── js.mdx │ │ │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ │ │ └── web-sys.mdx │ │ │ │ │ ├── contexts.mdx │ │ │ │ │ ├── function-components/ │ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ │ ├── children.mdx │ │ │ │ │ │ ├── communication.mdx │ │ │ │ │ │ ├── generics.mdx │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── node-refs.mdx │ │ │ │ │ │ ├── properties.mdx │ │ │ │ │ │ ├── pure-components.mdx │ │ │ │ │ │ └── state.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── classes.mdx │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── events.mdx │ │ │ │ │ │ ├── fragments.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ ├── router.mdx │ │ │ │ │ └── suspense.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ ├── editor-setup.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── introduction.mdx │ │ │ │ ├── more/ │ │ │ │ │ ├── css.mdx │ │ │ │ │ ├── debugging.mdx │ │ │ │ │ ├── deployment.mdx │ │ │ │ │ ├── roadmap.mdx │ │ │ │ │ └── testing.mdx │ │ │ │ └── tutorial/ │ │ │ │ └── index.mdx │ │ │ ├── version-0.22.json │ │ │ ├── version-0.23/ │ │ │ │ ├── advanced-topics/ │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── how-it-works.mdx │ │ │ │ │ ├── immutable.mdx │ │ │ │ │ ├── optimizations.mdx │ │ │ │ │ ├── portals.mdx │ │ │ │ │ ├── server-side-rendering.mdx │ │ │ │ │ └── struct-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── hoc.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lifecycle.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── refs.mdx │ │ │ │ │ └── scope.mdx │ │ │ │ ├── concepts/ │ │ │ │ │ ├── agents.mdx │ │ │ │ │ ├── basic-web-technologies/ │ │ │ │ │ │ ├── css.mdx │ │ │ │ │ │ ├── html.mdx │ │ │ │ │ │ ├── js.mdx │ │ │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ │ │ └── web-sys.mdx │ │ │ │ │ ├── contexts.mdx │ │ │ │ │ ├── function-components/ │ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ │ ├── children.mdx │ │ │ │ │ │ ├── communication.mdx │ │ │ │ │ │ ├── generics.mdx │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── node-refs.mdx │ │ │ │ │ │ ├── properties.mdx │ │ │ │ │ │ ├── pure-components.mdx │ │ │ │ │ │ └── state.mdx │ │ │ │ │ ├── html/ │ │ │ │ │ │ ├── classes.mdx │ │ │ │ │ │ ├── components.mdx │ │ │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ │ │ ├── elements.mdx │ │ │ │ │ │ ├── events.mdx │ │ │ │ │ │ ├── fragments.mdx │ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ │ ├── lists.mdx │ │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ │ ├── router.mdx │ │ │ │ │ └── suspense.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ │ ├── editor-setup.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── introduction.mdx │ │ │ │ ├── migration-guides/ │ │ │ │ │ ├── yew/ │ │ │ │ │ │ └── from-0_22_0-to-0_23_0.mdx │ │ │ │ │ ├── yew-agent/ │ │ │ │ │ │ └── from-0_4_0-to-0_5_0.mdx │ │ │ │ │ └── yew-router/ │ │ │ │ │ └── from-0_19_0-to-0_20_0.mdx │ │ │ │ ├── more/ │ │ │ │ │ ├── css.mdx │ │ │ │ │ ├── debugging.mdx │ │ │ │ │ ├── deployment.mdx │ │ │ │ │ ├── roadmap.mdx │ │ │ │ │ └── testing.mdx │ │ │ │ └── tutorial/ │ │ │ │ └── index.mdx │ │ │ └── version-0.23.json │ │ ├── docusaurus-plugin-content-docs-community/ │ │ │ └── current.json │ │ ├── docusaurus-plugin-content-docs-router/ │ │ │ └── current.json │ │ ├── docusaurus-plugin-content-pages/ │ │ │ └── index.mdx │ │ └── docusaurus-theme-classic/ │ │ ├── footer.json │ │ └── navbar.json │ └── zh-Hant/ │ ├── code.json │ ├── docusaurus-plugin-content-blog/ │ │ └── options.json │ ├── docusaurus-plugin-content-docs/ │ │ ├── current/ │ │ │ ├── advanced-topics/ │ │ │ │ ├── children.mdx │ │ │ │ ├── how-it-works.mdx │ │ │ │ ├── immutable.mdx │ │ │ │ ├── optimizations.mdx │ │ │ │ ├── portals.mdx │ │ │ │ ├── server-side-rendering.mdx │ │ │ │ └── struct-components/ │ │ │ │ ├── callbacks.mdx │ │ │ │ ├── hoc.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── lifecycle.mdx │ │ │ │ ├── properties.mdx │ │ │ │ ├── refs.mdx │ │ │ │ └── scope.mdx │ │ │ ├── concepts/ │ │ │ │ ├── agents.mdx │ │ │ │ ├── basic-web-technologies/ │ │ │ │ │ ├── css.mdx │ │ │ │ │ ├── html.mdx │ │ │ │ │ ├── js.mdx │ │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ │ └── web-sys.mdx │ │ │ │ ├── contexts.mdx │ │ │ │ ├── function-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── communication.mdx │ │ │ │ │ ├── generics.mdx │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── node-refs.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── pure-components.mdx │ │ │ │ │ └── state.mdx │ │ │ │ ├── html/ │ │ │ │ │ ├── classes.mdx │ │ │ │ │ ├── components.mdx │ │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ │ ├── elements.mdx │ │ │ │ │ ├── events.mdx │ │ │ │ │ ├── fragments.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lists.mdx │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ ├── router.mdx │ │ │ │ └── suspense.mdx │ │ │ ├── getting-started/ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ ├── editor-setup.mdx │ │ │ │ ├── examples.mdx │ │ │ │ └── introduction.mdx │ │ │ ├── migration-guides/ │ │ │ │ ├── yew/ │ │ │ │ │ └── from-0_22_0-to-0_23_0.mdx │ │ │ │ ├── yew-agent/ │ │ │ │ │ └── from-0_4_0-to-0_5_0.mdx │ │ │ │ └── yew-router/ │ │ │ │ └── from-0_19_0-to-0_20_0.mdx │ │ │ ├── more/ │ │ │ │ ├── css.mdx │ │ │ │ ├── debugging.mdx │ │ │ │ ├── deployment.mdx │ │ │ │ ├── roadmap.mdx │ │ │ │ └── testing.mdx │ │ │ └── tutorial/ │ │ │ └── index.mdx │ │ ├── current.json │ │ ├── version-0.20/ │ │ │ ├── advanced-topics/ │ │ │ │ ├── how-it-works.mdx │ │ │ │ ├── optimizations.mdx │ │ │ │ └── struct-components/ │ │ │ │ ├── callbacks.mdx │ │ │ │ ├── lifecycle.mdx │ │ │ │ ├── properties.mdx │ │ │ │ └── refs.mdx │ │ │ ├── concepts/ │ │ │ │ ├── agents.mdx │ │ │ │ ├── html/ │ │ │ │ │ ├── components.mdx │ │ │ │ │ ├── elements.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lists.mdx │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ └── router.mdx │ │ │ ├── getting-started/ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ └── examples.mdx │ │ │ └── more/ │ │ │ ├── css.mdx │ │ │ ├── debugging.mdx │ │ │ ├── roadmap.mdx │ │ │ └── testing.mdx │ │ ├── version-0.20.json │ │ ├── version-0.21/ │ │ │ ├── advanced-topics/ │ │ │ │ ├── children.mdx │ │ │ │ ├── how-it-works.mdx │ │ │ │ ├── immutable.mdx │ │ │ │ ├── optimizations.mdx │ │ │ │ ├── portals.mdx │ │ │ │ ├── server-side-rendering.md │ │ │ │ └── struct-components/ │ │ │ │ ├── callbacks.mdx │ │ │ │ ├── hoc.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── lifecycle.mdx │ │ │ │ ├── properties.mdx │ │ │ │ ├── refs.mdx │ │ │ │ └── scope.mdx │ │ │ ├── concepts/ │ │ │ │ ├── agents.mdx │ │ │ │ ├── contexts.mdx │ │ │ │ ├── function-components/ │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ └── properties.mdx │ │ │ │ ├── html/ │ │ │ │ │ ├── components.mdx │ │ │ │ │ ├── elements.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lists.mdx │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ └── router.mdx │ │ │ ├── getting-started/ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ └── examples.mdx │ │ │ └── more/ │ │ │ ├── css.mdx │ │ │ ├── debugging.mdx │ │ │ ├── roadmap.mdx │ │ │ └── testing.mdx │ │ ├── version-0.21.json │ │ ├── version-0.22/ │ │ │ ├── advanced-topics/ │ │ │ │ ├── children.mdx │ │ │ │ ├── how-it-works.mdx │ │ │ │ ├── immutable.mdx │ │ │ │ ├── optimizations.mdx │ │ │ │ ├── portals.mdx │ │ │ │ ├── server-side-rendering.mdx │ │ │ │ └── struct-components/ │ │ │ │ ├── callbacks.mdx │ │ │ │ ├── hoc.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── lifecycle.mdx │ │ │ │ ├── properties.mdx │ │ │ │ ├── refs.mdx │ │ │ │ └── scope.mdx │ │ │ ├── concepts/ │ │ │ │ ├── agents.mdx │ │ │ │ ├── basic-web-technologies/ │ │ │ │ │ ├── css.mdx │ │ │ │ │ ├── html.mdx │ │ │ │ │ ├── js.mdx │ │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ │ └── web-sys.mdx │ │ │ │ ├── contexts.mdx │ │ │ │ ├── function-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── communication.mdx │ │ │ │ │ ├── generics.mdx │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── node-refs.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── pure-components.mdx │ │ │ │ │ └── state.mdx │ │ │ │ ├── html/ │ │ │ │ │ ├── classes.mdx │ │ │ │ │ ├── components.mdx │ │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ │ ├── elements.mdx │ │ │ │ │ ├── events.mdx │ │ │ │ │ ├── fragments.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lists.mdx │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ ├── router.mdx │ │ │ │ └── suspense.mdx │ │ │ ├── getting-started/ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ ├── editor-setup.mdx │ │ │ │ ├── examples.mdx │ │ │ │ └── introduction.mdx │ │ │ ├── more/ │ │ │ │ ├── css.mdx │ │ │ │ ├── debugging.mdx │ │ │ │ ├── deployment.mdx │ │ │ │ ├── roadmap.mdx │ │ │ │ └── testing.mdx │ │ │ └── tutorial/ │ │ │ └── index.mdx │ │ ├── version-0.22.json │ │ ├── version-0.23/ │ │ │ ├── advanced-topics/ │ │ │ │ ├── children.mdx │ │ │ │ ├── how-it-works.mdx │ │ │ │ ├── immutable.mdx │ │ │ │ ├── optimizations.mdx │ │ │ │ ├── portals.mdx │ │ │ │ ├── server-side-rendering.mdx │ │ │ │ └── struct-components/ │ │ │ │ ├── callbacks.mdx │ │ │ │ ├── hoc.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── lifecycle.mdx │ │ │ │ ├── properties.mdx │ │ │ │ ├── refs.mdx │ │ │ │ └── scope.mdx │ │ │ ├── concepts/ │ │ │ │ ├── agents.mdx │ │ │ │ ├── basic-web-technologies/ │ │ │ │ │ ├── css.mdx │ │ │ │ │ ├── html.mdx │ │ │ │ │ ├── js.mdx │ │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ │ └── web-sys.mdx │ │ │ │ ├── contexts.mdx │ │ │ │ ├── function-components/ │ │ │ │ │ ├── callbacks.mdx │ │ │ │ │ ├── children.mdx │ │ │ │ │ ├── communication.mdx │ │ │ │ │ ├── generics.mdx │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ │ └── introduction.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── node-refs.mdx │ │ │ │ │ ├── properties.mdx │ │ │ │ │ ├── pure-components.mdx │ │ │ │ │ └── state.mdx │ │ │ │ ├── html/ │ │ │ │ │ ├── classes.mdx │ │ │ │ │ ├── components.mdx │ │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ │ ├── elements.mdx │ │ │ │ │ ├── events.mdx │ │ │ │ │ ├── fragments.mdx │ │ │ │ │ ├── introduction.mdx │ │ │ │ │ ├── lists.mdx │ │ │ │ │ └── literals-and-expressions.mdx │ │ │ │ ├── router.mdx │ │ │ │ └── suspense.mdx │ │ │ ├── getting-started/ │ │ │ │ ├── build-a-sample-app.mdx │ │ │ │ ├── editor-setup.mdx │ │ │ │ ├── examples.mdx │ │ │ │ └── introduction.mdx │ │ │ ├── migration-guides/ │ │ │ │ ├── yew/ │ │ │ │ │ └── from-0_22_0-to-0_23_0.mdx │ │ │ │ ├── yew-agent/ │ │ │ │ │ └── from-0_4_0-to-0_5_0.mdx │ │ │ │ └── yew-router/ │ │ │ │ └── from-0_19_0-to-0_20_0.mdx │ │ │ ├── more/ │ │ │ │ ├── css.mdx │ │ │ │ ├── debugging.mdx │ │ │ │ ├── deployment.mdx │ │ │ │ ├── roadmap.mdx │ │ │ │ └── testing.mdx │ │ │ └── tutorial/ │ │ │ └── index.mdx │ │ └── version-0.23.json │ ├── docusaurus-plugin-content-docs-community/ │ │ └── current.json │ ├── docusaurus-plugin-content-docs-router/ │ │ └── current.json │ ├── docusaurus-plugin-content-pages/ │ │ └── index.mdx │ └── docusaurus-theme-classic/ │ ├── footer.json │ └── navbar.json ├── package.json ├── sidebars/ │ ├── community.js │ └── docs.js ├── src/ │ ├── constants.js │ ├── css/ │ │ └── custom.css │ ├── pages/ │ │ ├── index.module.scss │ │ └── index.tsx │ └── theme/ │ └── NavbarItem/ │ └── DefaultNavbarItem.tsx ├── static/ │ ├── .nojekyll │ └── tutorial/ │ └── data.json ├── tsconfig.json ├── versioned_docs/ │ ├── version-0.20/ │ │ ├── advanced-topics/ │ │ │ ├── children.mdx │ │ │ ├── how-it-works.mdx │ │ │ ├── immutable.mdx │ │ │ ├── optimizations.mdx │ │ │ ├── portals.mdx │ │ │ ├── server-side-rendering.md │ │ │ └── struct-components/ │ │ │ ├── callbacks.mdx │ │ │ ├── hoc.mdx │ │ │ ├── introduction.mdx │ │ │ ├── lifecycle.mdx │ │ │ ├── properties.mdx │ │ │ ├── refs.mdx │ │ │ └── scope.mdx │ │ ├── concepts/ │ │ │ ├── agents.mdx │ │ │ ├── basic-web-technologies/ │ │ │ │ ├── css.mdx │ │ │ │ ├── html.mdx │ │ │ │ ├── js.mdx │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ └── web-sys.mdx │ │ │ ├── contexts.mdx │ │ │ ├── function-components/ │ │ │ │ ├── callbacks.mdx │ │ │ │ ├── children.mdx │ │ │ │ ├── communication.mdx │ │ │ │ ├── generics.mdx │ │ │ │ ├── hooks/ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ └── introduction.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── node-refs.mdx │ │ │ │ ├── properties.mdx │ │ │ │ ├── pure-components.mdx │ │ │ │ └── state.mdx │ │ │ ├── html/ │ │ │ │ ├── classes.mdx │ │ │ │ ├── components.mdx │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ ├── elements.mdx │ │ │ │ ├── events.mdx │ │ │ │ ├── fragments.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── lists.mdx │ │ │ │ └── literals-and-expressions.mdx │ │ │ ├── router.mdx │ │ │ └── suspense.mdx │ │ ├── getting-started/ │ │ │ ├── build-a-sample-app.mdx │ │ │ ├── editor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── introduction.mdx │ │ ├── migration-guides/ │ │ │ ├── yew/ │ │ │ │ ├── from-0_18_0-to-0_19_0.mdx │ │ │ │ └── from-0_19_0-to-0_20_0.mdx │ │ │ ├── yew-agent/ │ │ │ │ ├── from-0_0_0-to-0_1_0.mdx │ │ │ │ └── from-0_1_0-to-0_2_0.mdx │ │ │ └── yew-router/ │ │ │ ├── from-0_15_0-to-0_16_0.mdx │ │ │ └── from-0_16_0-to-0_17_0.mdx │ │ ├── more/ │ │ │ ├── css.mdx │ │ │ ├── debugging.mdx │ │ │ ├── deployment.mdx │ │ │ ├── roadmap.mdx │ │ │ └── testing.mdx │ │ └── tutorial/ │ │ └── index.mdx │ ├── version-0.21/ │ │ ├── advanced-topics/ │ │ │ ├── children.mdx │ │ │ ├── how-it-works.mdx │ │ │ ├── immutable.mdx │ │ │ ├── optimizations.mdx │ │ │ ├── portals.mdx │ │ │ ├── server-side-rendering.md │ │ │ └── struct-components/ │ │ │ ├── callbacks.mdx │ │ │ ├── hoc.mdx │ │ │ ├── introduction.mdx │ │ │ ├── lifecycle.mdx │ │ │ ├── properties.mdx │ │ │ ├── refs.mdx │ │ │ └── scope.mdx │ │ ├── concepts/ │ │ │ ├── agents.mdx │ │ │ ├── basic-web-technologies/ │ │ │ │ ├── css.mdx │ │ │ │ ├── html.mdx │ │ │ │ ├── js.mdx │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ └── web-sys.mdx │ │ │ ├── contexts.mdx │ │ │ ├── function-components/ │ │ │ │ ├── callbacks.mdx │ │ │ │ ├── children.mdx │ │ │ │ ├── communication.mdx │ │ │ │ ├── generics.mdx │ │ │ │ ├── hooks/ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ └── introduction.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── node-refs.mdx │ │ │ │ ├── properties.mdx │ │ │ │ ├── pure-components.mdx │ │ │ │ └── state.mdx │ │ │ ├── html/ │ │ │ │ ├── classes.mdx │ │ │ │ ├── components.mdx │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ ├── elements.mdx │ │ │ │ ├── events.mdx │ │ │ │ ├── fragments.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── lists.mdx │ │ │ │ └── literals-and-expressions.mdx │ │ │ ├── router.mdx │ │ │ └── suspense.mdx │ │ ├── getting-started/ │ │ │ ├── build-a-sample-app.mdx │ │ │ ├── editor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── introduction.mdx │ │ ├── migration-guides/ │ │ │ ├── yew/ │ │ │ │ ├── from-0_18_0-to-0_19_0.mdx │ │ │ │ ├── from-0_19_0-to-0_20_0.mdx │ │ │ │ ├── from-0_20_0-to-0_21_0.mdx │ │ │ │ └── from-0_20_0-to-next.mdx │ │ │ ├── yew-agent/ │ │ │ │ ├── from-0_0_0-to-0_1_0.mdx │ │ │ │ └── from-0_1_0-to-0_2_0.mdx │ │ │ └── yew-router/ │ │ │ ├── from-0_15_0-to-0_16_0.mdx │ │ │ └── from-0_16_0-to-0_17_0.mdx │ │ ├── more/ │ │ │ ├── css.mdx │ │ │ ├── debugging.mdx │ │ │ ├── deployment.mdx │ │ │ ├── roadmap.mdx │ │ │ └── testing.mdx │ │ └── tutorial/ │ │ └── index.mdx │ ├── version-0.22/ │ │ ├── advanced-topics/ │ │ │ ├── children.mdx │ │ │ ├── how-it-works.mdx │ │ │ ├── immutable.mdx │ │ │ ├── optimizations.mdx │ │ │ ├── portals.mdx │ │ │ ├── server-side-rendering.mdx │ │ │ └── struct-components/ │ │ │ ├── callbacks.mdx │ │ │ ├── hoc.mdx │ │ │ ├── introduction.mdx │ │ │ ├── lifecycle.mdx │ │ │ ├── properties.mdx │ │ │ ├── refs.mdx │ │ │ └── scope.mdx │ │ ├── concepts/ │ │ │ ├── agents.mdx │ │ │ ├── basic-web-technologies/ │ │ │ │ ├── css.mdx │ │ │ │ ├── html.mdx │ │ │ │ ├── js.mdx │ │ │ │ ├── wasm-bindgen.mdx │ │ │ │ └── web-sys.mdx │ │ │ ├── contexts.mdx │ │ │ ├── function-components/ │ │ │ │ ├── callbacks.mdx │ │ │ │ ├── children.mdx │ │ │ │ ├── communication.mdx │ │ │ │ ├── generics.mdx │ │ │ │ ├── hooks/ │ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ │ └── introduction.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── node-refs.mdx │ │ │ │ ├── properties.mdx │ │ │ │ ├── pure-components.mdx │ │ │ │ └── state.mdx │ │ │ ├── html/ │ │ │ │ ├── classes.mdx │ │ │ │ ├── components.mdx │ │ │ │ ├── conditional-rendering.mdx │ │ │ │ ├── elements.mdx │ │ │ │ ├── events.mdx │ │ │ │ ├── fragments.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── lists.mdx │ │ │ │ └── literals-and-expressions.mdx │ │ │ ├── router.mdx │ │ │ └── suspense.mdx │ │ ├── getting-started/ │ │ │ ├── build-a-sample-app.mdx │ │ │ ├── editor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── introduction.mdx │ │ ├── migration-guides/ │ │ │ ├── yew/ │ │ │ │ ├── from-0_18_0-to-0_19_0.mdx │ │ │ │ ├── from-0_19_0-to-0_20_0.mdx │ │ │ │ ├── from-0_20_0-to-0_21_0.mdx │ │ │ │ └── from-0_21_0-to-0_22_0.mdx │ │ │ ├── yew-agent/ │ │ │ │ ├── from-0_0_0-to-0_1_0.mdx │ │ │ │ ├── from-0_1_0-to-0_2_0.mdx │ │ │ │ └── from-0_3_0-to-0_4_0.mdx │ │ │ └── yew-router/ │ │ │ ├── from-0_15_0-to-0_16_0.mdx │ │ │ └── from-0_16_0-to-0_17_0.mdx │ │ ├── more/ │ │ │ ├── css.mdx │ │ │ ├── debugging.mdx │ │ │ ├── deployment.mdx │ │ │ ├── roadmap.mdx │ │ │ └── testing.mdx │ │ └── tutorial/ │ │ └── index.mdx │ └── version-0.23/ │ ├── advanced-topics/ │ │ ├── children.mdx │ │ ├── how-it-works.mdx │ │ ├── immutable.mdx │ │ ├── optimizations.mdx │ │ ├── portals.mdx │ │ ├── server-side-rendering.mdx │ │ └── struct-components/ │ │ ├── callbacks.mdx │ │ ├── hoc.mdx │ │ ├── introduction.mdx │ │ ├── lifecycle.mdx │ │ ├── properties.mdx │ │ ├── refs.mdx │ │ └── scope.mdx │ ├── concepts/ │ │ ├── agents.mdx │ │ ├── basic-web-technologies/ │ │ │ ├── css.mdx │ │ │ ├── html.mdx │ │ │ ├── js.mdx │ │ │ ├── wasm-bindgen.mdx │ │ │ └── web-sys.mdx │ │ ├── contexts.mdx │ │ ├── function-components/ │ │ │ ├── callbacks.mdx │ │ │ ├── children.mdx │ │ │ ├── communication.mdx │ │ │ ├── generics.mdx │ │ │ ├── hooks/ │ │ │ │ ├── custom-hooks.mdx │ │ │ │ └── introduction.mdx │ │ │ ├── introduction.mdx │ │ │ ├── node-refs.mdx │ │ │ ├── properties.mdx │ │ │ ├── pure-components.mdx │ │ │ └── state.mdx │ │ ├── html/ │ │ │ ├── classes.mdx │ │ │ ├── components.mdx │ │ │ ├── conditional-rendering.mdx │ │ │ ├── elements.mdx │ │ │ ├── events.mdx │ │ │ ├── fragments.mdx │ │ │ ├── introduction.mdx │ │ │ ├── lists.mdx │ │ │ └── literals-and-expressions.mdx │ │ ├── router.mdx │ │ └── suspense.mdx │ ├── getting-started/ │ │ ├── build-a-sample-app.mdx │ │ ├── editor-setup.mdx │ │ ├── examples.mdx │ │ └── introduction.mdx │ ├── migration-guides/ │ │ ├── yew/ │ │ │ ├── from-0_19_0-to-0_20_0.mdx │ │ │ ├── from-0_20_0-to-0_21_0.mdx │ │ │ ├── from-0_21_0-to-0_22_0.mdx │ │ │ └── from-0_22_0-to-0_23_0.mdx │ │ ├── yew-agent/ │ │ │ ├── from-0_0_0-to-0_1_0.mdx │ │ │ ├── from-0_1_0-to-0_2_0.mdx │ │ │ ├── from-0_3_0-to-0_4_0.mdx │ │ │ └── from-0_4_0-to-0_5_0.mdx │ │ └── yew-router/ │ │ ├── from-0_15_0-to-0_16_0.mdx │ │ ├── from-0_16_0-to-0_17_0.mdx │ │ └── from-0_19_0-to-0_20_0.mdx │ ├── more/ │ │ ├── css.mdx │ │ ├── debugging.mdx │ │ ├── deployment.mdx │ │ ├── roadmap.mdx │ │ └── testing.mdx │ └── tutorial/ │ └── index.mdx ├── versioned_sidebars/ │ ├── version-0.20-sidebars.json │ ├── version-0.21-sidebars.json │ ├── version-0.22-sidebars.json │ └── version-0.23-sidebars.json ├── versions.json └── write-translations.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'] runner = 'wasm-bindgen-test-runner' [target.'cfg(all(target_arch = "wasm32", target_os = "wasi"))'] runner = 'wasmtime -W unknown-imports-trap=y' [target.wasm32-unknown-unknown] rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] ================================================ FILE: .firebaserc ================================================ { "projects": { "default": "yew-rs" }, "targets": { "yew-rs": { "hosting": { "website": [ "yew-rs" ], "examples": [ "yew-rs-examples" ], "api": [ "yew-rs-api" ] } } } } ================================================ FILE: .gitattributes ================================================ *.mdx linguist-detectable=false ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: yew ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve Yew title: '' labels: bug assignees: '' --- **Problem** **Steps To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Environment:** - Yew version: [e.g. v0.17, `master`] - Rust version: [e.g. 1.43.0, `nightly`] - Target, if relevant: [e.g. `wasm32-unknown-emscripten`] - Build tool, if relevant: [e.g. `wasm-pack`, `trunk`] - OS, if relevant: [e.g. MacOS] - Browser and version, if relevant: [e.g. Chrome v83] **Questionnaire** - [ ] I'm interested in fixing this myself but don't know where to start - [ ] I would like to fix and I have a solution - [ ] I don't have time to fix this right now, but maybe later ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Ask question url: https://discord.gg/VQck8X4 about: Looking for a quick answer? Ask the community! - name: Feature proposal url: https://github.com/yewstack/yew/discussions/categories/ideas about: Start a discussion for a feature you would like to see - name: Issue with the Playground? url: https://github.com/yewstack/yew-playground/issues/new about: Open an issue on the yew-playground repository ================================================ FILE: .github/ISSUE_TEMPLATE/documentation.md ================================================ --- name: Documentation about: Report an issue relating to this project's documentation. title: '' labels: documentation assignees: '' --- This is about: - [ ] A typo - [ ] Inaccurate/misleading documentation (e.g. technically incorrect advice) - [ ] Undocumented code - [ ] Outdated documentation - [ ] Other **Problem** **Details about the solution you'd like** _(Optional)_ **Additional context** _(Optional)_ **Questionaire** _(Optional)_ - [ ] I'd like to write this documentation - [ ] I'd like to write this documentation but I'm not sure what's needed - [ ] I don't have time to add this right now, but maybe later ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ #### Description Fixes #0000 #### Checklist - [ ] I have reviewed my own code - [ ] I have added tests ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" day: "friday" open-pull-requests-limit: 2 groups: cargo-deps: patterns: - "*" - package-ecosystem: "npm" directory: "/website" schedule: interval: "monthly" target-branch: "master" groups: website-deps: patterns: - "*" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" target-branch: "master" ================================================ FILE: .github/workflows/auto-approve-maintainer-pr.yml ================================================ name: Auto approve on: pull_request_target: types: - opened - reopened - synchronize - ready_for_review - review_requested jobs: auto-approve: runs-on: ubuntu-latest if: github.event.pull_request.draft == false steps: - name: Check if organization member id: is_organization_member uses: JamesSingleton/is-organization-member@1.1.0 with: organization: "yewstack" username: ${{ github.event.pull_request.user.login }} token: ${{ secrets.GITHUB_TOKEN }} - name: Auto approve uses: hmarr/auto-approve-action@v4 if: ${{ steps.is_organization_member.outputs.result == 'true' || github.actor == 'dependabot[bot]' }} with: github-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/benchmark-core.yml ================================================ name: Benchmark - core on: pull_request: branches: [master] paths: - .github/workflows/benchmark-core.yml - "packages/yew/**" - "tools/benchmark-core/**" jobs: benchmark-core: name: Benchmark - core runs-on: ubuntu-latest steps: - name: Checkout master uses: actions/checkout@v6 with: repository: "yewstack/yew" ref: master path: yew-master - name: Checkout pull request uses: actions/checkout@v6 with: path: current-pr - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Restore Rust cache for yew packages uses: Swatinem/rust-cache@v2 with: workspaces: | yew-master current-pr - name: Run pull request benchmark working-directory: current-pr/tools/benchmark-core run: cargo bench -q > ../output.log - name: Run master benchmark working-directory: yew-master/tools/benchmark-core run: cargo bench -q > ../output.log - name: Write Pull Request ID run: | echo "${{ github.event.number }}" > .PR_NUMBER - name: Upload Artifact uses: actions/upload-artifact@v7 with: name: benchmark-core include-hidden-files: true path: | .PR_NUMBER yew-master/tools/output.log current-pr/tools/output.log retention-days: 1 ================================================ FILE: .github/workflows/benchmark-ssr.yml ================================================ name: Benchmark - SSR on: pull_request: branches: [master] paths: - .github/workflows/benchmark-ssr.yml - "packages/yew/**" - "packages/yew-macro/**" - "packages/yew-router/**" - "packages/yew-router-macro/**" - "examples/function_router/**" - "tools/benchmark-ssr/**" jobs: benchmark-ssr: name: Benchmark - SSR runs-on: ubuntu-latest steps: - name: Checkout master uses: actions/checkout@v6 with: repository: "yewstack/yew" ref: master path: yew-master - name: Checkout pull request uses: actions/checkout@v6 with: path: current-pr - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable targets: wasm32-unknown-unknown - name: Restore Rust cache for yew packages uses: Swatinem/rust-cache@v2 with: workspaces: | yew-master current-pr - name: Run pull request benchmark working-directory: current-pr/tools/benchmark-ssr run: cargo run --profile=bench -- --output-path ../output.json - name: Run master benchmark working-directory: yew-master/tools/benchmark-ssr run: cargo run --profile=bench -- --output-path ../output.json - name: Write Pull Request ID run: | echo "${{ github.event.number }}" > .PR_NUMBER - name: Upload Artifact uses: actions/upload-artifact@v7 with: name: benchmark-ssr include-hidden-files: true path: | .PR_NUMBER yew-master/tools/output.json current-pr/tools/output.json retention-days: 1 ================================================ FILE: .github/workflows/benchmark.yml ================================================ name: Benchmark on: push: paths-ignore: - "website/**" branches: - master pull_request: paths-ignore: - "website/**" types: [labeled, synchronize, opened, reopened] # Cancel outstanding benchmarks on pull requests # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#example-using-a-fallback-value concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: benchmark: if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'performance') runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: path: "yew" - uses: actions/checkout@v6 with: repository: krausest/js-framework-benchmark path: "js-framework-benchmark" - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable targets: wasm32-unknown-unknown - name: Setup wasm-pack uses: jetli/wasm-pack-action@v0.4.0 - name: Setup Node uses: actions/setup-node@v6 with: node-version: "lts/Jod" cache: "npm" cache-dependency-path: js-framework-benchmark/package-lock.json - name: Restore Rust cache for yew packages uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} workspaces: | yew - name: Setup chrome id: setup-chrome uses: browser-actions/setup-chrome@v2 with: install-chromedriver: true - name: Setup js-framework-benchmark working-directory: js-framework-benchmark run: | npm ci npm run install-server npm run install-webdriver-ts - name: Setup benchmark-struct benchmark run: | ls -lauh rm *.js rm *.wasm echo "STRUCT_BUILD_DIR=$PWD" >> $GITHUB_ENV working-directory: js-framework-benchmark/frameworks/keyed/yew/bundled-dist/ - name: Build benchmark-struct app working-directory: yew/tools/benchmark-struct run: | RUSTFLAGS='--cfg getrandom_backend="wasm_js"' wasm-pack build \ --release \ --target web \ --no-typescript \ --out-name js-framework-benchmark-yew \ --out-dir $STRUCT_BUILD_DIR - name: Show built benchmark-struct benchmark files run: | ls -lauh js-framework-benchmark/frameworks/keyed/yew/bundled-dist/ - name: Setup yew-hooks benchmark run: | ls -lauh rm *.js rm *.wasm echo "HOOKS_BUILD_DIR=$PWD" >> $GITHUB_ENV working-directory: js-framework-benchmark/frameworks/keyed/yew-hooks/bundled-dist/ - name: Build benchmark-hooks app working-directory: yew/tools/benchmark-hooks run: | RUSTFLAGS='--cfg getrandom_backend="wasm_js"' wasm-pack build \ --release \ --target web \ --no-typescript \ --out-name js-framework-benchmark-yew-hooks \ --out-dir $HOOKS_BUILD_DIR - name: Show built benchmark-hooks benchmark files run: | ls -lauh js-framework-benchmark/frameworks/keyed/yew-hooks/bundled-dist/ - name: Run js-framework-benchmark server working-directory: js-framework-benchmark run: | npm start & sleep 5 # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md - run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - name: Run js-framework-benchmark/webdriver-ts npm run bench working-directory: js-framework-benchmark/webdriver-ts run: xvfb-run npm run bench -- --framework keyed/yew keyed/yew-hooks --runner playwright --chromeBinary "${{ steps.setup-chrome.outputs.chrome-path }}" - name: Transform results to be fit for display benchmark-action/github-action-benchmark@v1 run: | mkdir artifacts/ jq -s . js-framework-benchmark/webdriver-ts/results/*.json | cargo run --manifest-path yew/Cargo.toml --release -p process-benchmark-results > artifacts/results.json echo "$EVENT_INFO" > artifacts/.PR_INFO env: EVENT_INFO: ${{ toJSON(github.event) }} - name: Upload result artifacts uses: actions/upload-artifact@v7 with: name: results path: artifacts/ if-no-files-found: error include-hidden-files: true retention-days: 1 ================================================ FILE: .github/workflows/build-api-docs.yml ================================================ name: Build API Docs (Rustdoc) on: pull_request: branches: [master] paths: - "packages/**" - "firebase.json" - ".github/workflows/*-docs.yml" push: branches: [master] paths: - "packages/**" - "firebase.json" - ".github/workflows/*-docs.yml" jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: nightly components: rust-docs - name: Run cargo doc env: RUSTDOCFLAGS: --cfg documenting --html-before-content ./api-docs/before-content.html --extend-css ./api-docs/styles.css -Z unstable-options --enable-index-page run: | cargo doc \ --no-deps \ --all-features \ -p yew \ -p yew-macro \ -p yew-router \ -p yew-router-macro \ -p yew-agent - name: Move files in correct directory run: | mkdir -p api-docs/dist/next cp -r target/doc/* api-docs/dist/next - name: Upload build artifact uses: actions/upload-artifact@v7 with: name: api-docs path: api-docs/ retention-days: 1 - if: github.event_name == 'pull_request' name: Build pr info run: | echo "${{ github.event.number }}" > .PR_INFO - if: github.event_name == 'pull_request' name: Upload pr info uses: actions/upload-artifact@v7 with: name: pr-info include-hidden-files: true path: .PR_INFO retention-days: 1 ================================================ FILE: .github/workflows/build-website.yml ================================================ name: Build website on: pull_request: branches: [master] paths: - "website/**" - "firebase.json" - ".github/workflows/*-website.yml" push: branches: [master] paths: - "website/**" - "firebase.json" - ".github/workflows/*-website.yml" jobs: build: name: Build Website runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: node-version: "lts/Jod" package-manager-cache: false - name: Install dependencies run: | cd website npm ci - name: Run prettier id: fmt continue-on-error: true run: | cd website npm run fmt - if: steps.fmt.outcome == 'failure' run: | cd website npm run fmt:write git diff exit 1 - name: Check Translations run: | cd website npm run check-translations - name: Build run: | cd website npm run build - name: Upload build artifact uses: actions/upload-artifact@v7 with: name: website path: website/build/ retention-days: 1 - if: github.event_name == 'pull_request' name: Build pr info run: | echo "${{ github.event.number }}" > .PR_INFO - if: github.event_name == 'pull_request' name: Upload pr info uses: actions/upload-artifact@v7 with: name: pr-info include-hidden-files: true path: .PR_INFO retention-days: 1 ================================================ FILE: .github/workflows/clippy.yml ================================================ name: Clippy on: pull_request: paths: - ".github/workflows/clippy.yml" - "tools/**/*" - "examples/**/*" - "packages/**/*" - "Cargo.toml" - "Cargo.lock" push: branches: [master] jobs: feature-soundness: name: Feature Soundness runs-on: ubuntu-latest # if normal clippy doesn't succeed, do not try to lint feature soundness needs: clippy strategy: fail-fast: false matrix: profile: - dev - release steps: - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable components: clippy - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - uses: taiki-e/install-action@cargo-hack - name: Lint feature soundness if: matrix.profile == 'dev' run: >- cargo hack clippy -p yew -p yew-agent -p yew-router --feature-powerset --no-dev-deps --keep-going -- -D warnings - name: Lint feature soundness if: matrix.profile == 'release' run: >- cargo hack clippy -p yew -p yew-agent -p yew-router --feature-powerset --no-dev-deps --keep-going --release -- -D warnings clippy: name: Clippy Workspace runs-on: ubuntu-latest strategy: fail-fast: false matrix: toolchain: - stable steps: - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} components: clippy - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - name: Run clippy run: | cargo clippy \ --all-targets \ --all-features \ --workspace \ -- -D warnings ================================================ FILE: .github/workflows/fmt.yml ================================================ name: cargo fmt on: pull_request: paths: - "**/*.rs" push: branches: [master] paths: - "**/*.rs" jobs: format: name: cargo fmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: nightly components: rustfmt - name: Run fmt run: cargo +nightly fmt --all -- --check ================================================ FILE: .github/workflows/inspect-next-changelogs.yml ================================================ name: Inspect next changelogs permissions: contents: write on: workflow_dispatch: jobs: generate: name: Generate changelogs runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Build changelog generator run: cargo build --release -p changelog -- - name: Read yew changelog in this step shell: bash run: ./target/release/changelog yew minor -t ${{ secrets.GITHUB_TOKEN }} -f "$(pwd)/CHANGELOG.md" - name: Read yew-router changelog in this step shell: bash run: ./target/release/changelog yew-router minor -t ${{ secrets.GITHUB_TOKEN }} -f "$(pwd)/CHANGELOG.md" - name: Read yew-agent changelog in this step shell: bash run: ./target/release/changelog yew-agent minor -t ${{ secrets.GITHUB_TOKEN }} -f "$(pwd)/CHANGELOG.md" ================================================ FILE: .github/workflows/main-checks.yml ================================================ name: Main Checks on: pull_request: paths: - ".github/workflows/main-checks.yml" - "ci/**" - "packages/**/*" - "examples/**/*" - "tools/**/*" - "Cargo.toml" - "Cargo.lock" push: branches: [master] jobs: spell_check: name: spellcheck runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Check spelling uses: crate-ci/typos@v1.44.0 doc_tests: name: Documentation Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 # for wasm-bindgen-cli, always use stable rust - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: nightly targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - name: Install wasm-bindgen-cli shell: bash run: ./ci/install-wasm-bindgen-cli.sh - uses: browser-actions/setup-geckodriver@latest with: token: ${{ secrets.GITHUB_TOKEN }} - uses: nanasess/setup-chromedriver@v2 - name: Run doctest run: | ls packages | xargs -I {} \ cargo test \ -p {} \ --doc \ --all-features \ --target wasm32-unknown-unknown integration_tests: name: Integration Tests on ${{ matrix.toolchain }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: toolchain: - 1.84.0 - stable steps: - uses: actions/checkout@v6 # for wasm-bindgen-cli, always use stable rust - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - name: Install wasm-bindgen-cli shell: bash run: ./ci/install-wasm-bindgen-cli.sh - uses: browser-actions/setup-geckodriver@latest with: token: ${{ secrets.GITHUB_TOKEN }} - uses: nanasess/setup-chromedriver@v2 - name: Run tests - yew run: | cd packages/yew CHROMEDRIVER=$(which chromedriver) cargo test --features csr,hydration,ssr,test --target wasm32-unknown-unknown GECKODRIVER=$(which geckodriver) cargo test --features csr,hydration,ssr,test --target wasm32-unknown-unknown - name: Run tests - yew-router run: | cd packages/yew-router CHROMEDRIVER=$(which chromedriver) cargo test --target wasm32-unknown-unknown GECKODRIVER=$(which geckodriver) cargo test --target wasm32-unknown-unknown unit_tests: name: Unit Tests on ${{ matrix.toolchain }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: toolchain: - 1.84.0 - stable - nightly steps: - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - name: Run native tests env: # workaround for lack of ternary operator # see https://github.com/orgs/community/discussions/25725 RUSTFLAGS: ${{ matrix.toolchain == 'nightly' && '--cfg nightly_yew' || '' }} run: | if [[ "${{ matrix.toolchain }}" == "1.84.0" ]]; then cargo test --all-targets -p yew-agent -p yew-agent-macro -p yew-router else ls packages | grep -v "^yew$" | xargs -I {} cargo test --all-targets -p {} fi - name: Run native tests for yew env: # workaround for lack of ternary operator # see https://github.com/orgs/community/discussions/25725 RUSTFLAGS: ${{ matrix.toolchain == 'nightly' && '--cfg nightly_yew' || '' }} run: cargo test -p yew --all-features test-lints: name: Test lints on nightly runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: nightly - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - name: Run tests env: RUSTFLAGS: --cfg nightly_yew --cfg yew_lints run: cargo test -p yew-macro test_html_lints macro_tests_discovery: name: Discover macro tests runs-on: ubuntu-latest outputs: matrix: ${{ steps.discover.outputs.matrix }} steps: - uses: actions/checkout@v6 - id: discover run: | matrix=$( find packages/*/tests -name '*_test.rs' ! -name 'html_lints_test.rs' -printf '%h/%f\n' \ | while IFS= read -r path; do pkg=$(echo "$path" | cut -d/ -f2) test=$(basename "$path" .rs) printf '{"package":"%s","test":"%s"}\n' "$pkg" "$test" done \ | jq -sc '{include: .}' ) echo "matrix=$matrix" >> "$GITHUB_OUTPUT" macro_tests: name: Macro Tests (${{ matrix.test }}) needs: macro_tests_discovery runs-on: ubuntu-latest strategy: fail-fast: false matrix: ${{ fromJson(needs.macro_tests_discovery.outputs.matrix) }} steps: - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: 1.84.0 - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - name: Run ${{ matrix.test }} run: cargo test -p ${{ matrix.package }} --test ${{ matrix.test }} unit_tests_wasi: name: Unit Tests (WASI) on ${{ matrix.toolchain }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: toolchain: - stable - nightly steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} target: wasm32-wasip1 - name: Install wasmtime run: | wget https://github.com/bytecodealliance/wasmtime/releases/download/v24.0.0/wasmtime-v24.0.0-x86_64-linux.tar.xz tar xf wasmtime-v24.0.0-x86_64-linux.tar.xz mv wasmtime-v24.0.0-x86_64-linux/wasmtime ~/wasmtime rm -rf wasmtime-v24.0.0-x86_64-linux.tar.xz wasmtime-v24.0.0-x86_64-linux chmod +x ~/wasmtime mv ~/wasmtime /usr/local/bin source ~/.bashrc - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - name: Run WASI tests for yew run: | RUST_LOG=info cargo test --features ssr,hydration,test --target wasm32-wasip1 -p yew example-runnable-tests-on-wasi: name: Example Runnable Tests on WASI runs-on: ubuntu-latest strategy: fail-fast: false matrix: package: - wasi_ssr_module toolchain: - stable - nightly steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} target: wasm32-wasip1 - name: Install wasmtime run: | wget https://github.com/bytecodealliance/wasmtime/releases/download/v24.0.0/wasmtime-v24.0.0-x86_64-linux.tar.xz tar xf wasmtime-v24.0.0-x86_64-linux.tar.xz mv wasmtime-v24.0.0-x86_64-linux/wasmtime ~/wasmtime rm -rf wasmtime-v24.0.0-x86_64-linux.tar.xz wasmtime-v24.0.0-x86_64-linux chmod +x ~/wasmtime mv ~/wasmtime /usr/local/bin source ~/.bashrc - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - name: Build and run ${{ matrix.package }} run: | cargo build --target wasm32-wasip1 -p ${{ matrix.package }} wasmtime -W unknown-imports-trap=y target/wasm32-wasip1/debug/${{ matrix.package }}.wasm ssr_e2e_tests: name: SSR E2E Tests (${{ matrix.example }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - example: ssr_router server_bin: ssr_router_server trunk_dir: examples/ssr_router - example: simple_ssr server_bin: simple_ssr_server trunk_dir: examples/simple_ssr steps: - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - name: Install wasm-bindgen-cli shell: bash run: ./ci/install-wasm-bindgen-cli.sh - name: Install trunk uses: jetli/trunk-action@v0.5.1 with: version: "latest" - uses: browser-actions/setup-geckodriver@latest with: token: ${{ secrets.GITHUB_TOKEN }} - name: Run SSR E2E tests run: | GECKODRIVER=$(which geckodriver) cargo run -p ssr-e2e -- \ --trunk-dir ${{ matrix.trunk_dir }} \ --server-cmd "cargo run -p ${{ matrix.example }} --bin ${{ matrix.server_bin }} --features ssr -- --dir ${{ matrix.trunk_dir }}/dist" \ --health-url http://127.0.0.1:8080/ \ --test-pkg ${{ matrix.example }} \ -- --target wasm32-unknown-unknown --test e2e ================================================ FILE: .github/workflows/post-benchmark-core.yml ================================================ name: Post Comment for Benchmark - core on: workflow_run: workflows: ["Benchmark - core"] types: - completed jobs: post-benchmark-core: name: Post Benchmark Comment on Pull Request if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest steps: - name: Download Repository uses: actions/checkout@v6 - name: Download Artifact uses: actions/download-artifact@v8 with: github-token: "${{ secrets.GITHUB_TOKEN }}" run-id: ${{ github.event.workflow_run.id }} name: benchmark-core path: benchmark-core/ - name: Make pull request comment run: | cat - >>comment.txt <>comment.txt cat - >>comment.txt <>comment.txt cat - >>comment.txt <> $GITHUB_ENV - name: Post Comment uses: actions/github-script@v8 with: script: | const fs = require('fs'); const commentInfo = { ...context.repo, issue_number: ${{ env.PR_NUMBER }}, }; const comment = { ...commentInfo, body: fs.readFileSync("comment.txt", 'utf-8'), }; function isCommentByBot(comment) { return comment.user.type === "Bot" && comment.body.includes("### Benchmark - core"); } let commentId = null; const comments = (await github.rest.issues.listComments(commentInfo)).data; for (let i = comments.length; i--; ) { const c = comments[i]; if (isCommentByBot(c)) { commentId = c.id; break; } } if (commentId) { try { await github.rest.issues.updateComment({ ...context.repo, comment_id: commentId, body: comment.body, }); } catch (e) { commentId = null; } } if (!commentId) { await github.rest.issues.createComment(comment); } ================================================ FILE: .github/workflows/post-benchmark-ssr.yml ================================================ name: Post Comment for Benchmark - SSR on: workflow_run: workflows: ["Benchmark - SSR"] types: - completed jobs: post-benchmark-ssr: name: Post Comment on Pull Request if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest steps: - name: Download Repository uses: actions/checkout@v6 - name: Download Artifact uses: actions/download-artifact@v8 with: github-token: "${{ secrets.GITHUB_TOKEN }}" run-id: ${{ github.event.workflow_run.id }} name: benchmark-ssr path: benchmark-ssr/ - name: Make pull request comment run: python3 ci/make_benchmark_ssr_cmt.py - name: Read Pull Request ID run: echo "PR_NUMBER=$(cat benchmark-ssr/.PR_NUMBER)" >> $GITHUB_ENV - name: Post Comment uses: actions/github-script@v8 with: script: | const commentInfo = { ...context.repo, issue_number: ${{ env.PR_NUMBER }}, }; const comment = { ...commentInfo, body: JSON.parse(process.env.YEW_BENCH_SSR), }; function isCommentByBot(comment) { return comment.user.type === "Bot" && comment.body.includes("### Benchmark - SSR"); } let commentId = null; const comments = (await github.rest.issues.listComments(commentInfo)).data; for (let i = comments.length; i--; ) { const c = comments[i]; if (isCommentByBot(c)) { commentId = c.id; break; } } if (commentId) { try { await github.rest.issues.updateComment({ ...context.repo, comment_id: commentId, body: comment.body, }); } catch (e) { commentId = null; } } if (!commentId) { await github.rest.issues.createComment(comment); } ================================================ FILE: .github/workflows/post-benchmark.yml ================================================ name: "Post benchmark results" on: workflow_run: workflows: ["Benchmark"] types: - completed jobs: post-benchmark-results: if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: # Checkout repo for the github-action-benchmark action - uses: actions/checkout@v6 - name: Download result artifacts uses: actions/download-artifact@v8 with: github-token: "${{ secrets.GITHUB_TOKEN }}" run-id: ${{ github.event.workflow_run.id }} name: results path: ./artifacts - name: Test for PR uses: mathiasvr/command-output@v1 id: test-pr with: run: cat artifacts/.PR_INFO # gh-pages branch is updated and pushed automatically with extracted benchmark data - name: Store benchmark result uses: benchmark-action/github-action-benchmark@v1 with: name: "Yew master branch benchmarks (Lower is better)" tool: "customSmallerIsBetter" output-file-path: artifacts/results.json gh-pages-branch: "gh-pages" # Access token to deploy GitHub Pages branch github-token: ${{ secrets.GITHUB_TOKEN }} # Push and deploy GitHub pages branch automatically alert-threshold: "200%" alert-comment-cc-users: "@yewstack/yew" # Only push when this is a non-pr commit that has been benchmarked, i.e. master auto-push: ${{ fromJSON(steps.test-pr.outputs.stdout).number == '' }} save-data-file: ${{ fromJSON(steps.test-pr.outputs.stdout).number == '' }} # Enable job summary summary-always: true comment-always: true ref: ${{ github.event.workflow_run.head_sha }} ================================================ FILE: .github/workflows/post-size-cmp.yml ================================================ name: Post Comment for Size Comparison on: workflow_run: workflows: ["Size Comparison"] types: - completed jobs: post-size-cmp: name: Post Size Comment on Pull Request if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest steps: - name: Download Repository uses: actions/checkout@v6 - name: Download Artifact (master) uses: actions/download-artifact@v8 with: github-token: "${{ secrets.GITHUB_TOKEN }}" run-id: ${{ github.event.workflow_run.id }} name: size-cmp-master-info path: "size-cmp-master-info/" - name: Download Artifact (PR) uses: actions/download-artifact@v8 with: github-token: "${{ secrets.GITHUB_TOKEN }}" run-id: ${{ github.event.workflow_run.id }} name: size-cmp-pr-info path: "size-cmp-pr-info/" - name: Make pull request comment run: python3 ci/make_example_size_cmt.py - name: Post Comment uses: actions/github-script@v8 with: script: | const commentInfo = { ...context.repo, issue_number: ${{ env.PR_NUMBER }}, }; const comment = { ...commentInfo, body: JSON.parse(process.env.YEW_EXAMPLE_SIZES), }; function isCommentByBot(comment) { return comment.user.type === "Bot" && comment.body.includes("### Size Comparison"); } let commentId = null; const comments = (await github.rest.issues.listComments(commentInfo)).data; for (let i = comments.length; i--; ) { const c = comments[i]; if (isCommentByBot(c)) { commentId = c.id; break; } } if (commentId) { try { await github.rest.issues.updateComment({ ...context.repo, comment_id: commentId, body: comment.body, }); } catch (e) { commentId = null; } } if (!commentId) { await github.rest.issues.createComment(comment); } ================================================ FILE: .github/workflows/publish-api-docs.yml ================================================ name: Publish API Docs on: workflow_run: workflows: ["Build API Docs (Rustdoc)"] types: - completed jobs: publish: if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: # need to checkout to get "firebase.json", ".firebaserc" - uses: actions/checkout@v6 - name: Download build artifact uses: actions/download-artifact@v8 with: github-token: "${{ secrets.GITHUB_TOKEN }}" run-id: ${{ github.event.workflow_run.id }} name: api-docs path: api-docs/ - if: github.event.workflow_run.event == 'pull_request' name: Download pr info uses: actions/download-artifact@v8 with: github-token: "${{ secrets.GITHUB_TOKEN }}" run-id: ${{ github.event.workflow_run.id }} name: pr-info path: artifacts - if: github.event.workflow_run.event == 'pull_request' name: Apply pull request environment run: | pr_number=$(cat "artifacts/.PR_INFO") if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then echo "pr number invalid" exit 1 fi echo "PR_NUMBER=$pr_number" >> $GITHUB_ENV echo "PR_BRANCH=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_ENV echo "COMMIT_SHA=${{ github.event.workflow_run.head_sha }}" >> $GITHUB_ENV - if: github.event.workflow_run.event == 'push' name: Apply push environment run: | echo "CHANNEL_ID=live" >> $GITHUB_ENV - name: Deploy to Firebase uses: siku2/action-hosting-deploy@v1 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseToken: "${{ secrets.FIREBASE_TOKEN }}" targets: api channelId: "${{ env.CHANNEL_ID }}" # link to the next version because that's what we care about commentURLPath: "/next/yew" # PR information prNumber: "${{ env.PR_NUMBER }}" prBranchName: "${{ env.PR_BRANCH }}" commitSHA: "${{ env.COMMIT_SHA }}" ================================================ FILE: .github/workflows/publish-examples.yml ================================================ name: Publish Examples on: push: branches: [master] paths: - 'tools/build-examples/**' - 'examples/**' jobs: publish: runs-on: ubuntu-latest env: # leave empty for / PUBLIC_URL_PREFIX: "" RUSTUP_TOOLCHAIN: nightly steps: - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: nightly targets: wasm32-unknown-unknown components: rust-src - uses: Swatinem/rust-cache@v2 - uses: jetli/trunk-action@v0.5.1 with: version: 'latest' - name: Get latest wasm-opt version id: wasm-opt uses: pozetroninc/github-action-get-latest-release@master with: repository: WebAssembly/binaryen excludes: prerelease, draft token: ${{ secrets.GITHUB_TOKEN }} - name: Build examples run: cargo run -p build-examples --bin build-examples env: LATEST_WASM_OPT_VERSION: ${{ steps.wasm-opt.outputs.release }} - name: Deploy to Firebase uses: siku2/action-hosting-deploy@v1 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseToken: "${{ secrets.FIREBASE_TOKEN }}" channelId: live targets: examples ================================================ FILE: .github/workflows/publish-website.yml ================================================ name: Publish website on: workflow_run: workflows: ["Build website"] types: - completed jobs: publish: if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: # need to checkout to get "firebase.json", ".firebaserc" - uses: actions/checkout@v6 - name: Download build artifact uses: actions/download-artifact@v8 with: github-token: "${{ secrets.GITHUB_TOKEN }}" run-id: ${{ github.event.workflow_run.id }} name: website path: website/build - if: github.event.workflow_run.event == 'pull_request' name: Download pr info uses: actions/download-artifact@v8 with: github-token: "${{ secrets.GITHUB_TOKEN }}" run-id: ${{ github.event.workflow_run.id }} name: pr-info path: artifacts - if: github.event.workflow_run.event == 'pull_request' name: Apply pull request environment run: | pr_number=$(cat "artifacts/.PR_INFO") if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then echo "pr number invalid" exit 1 fi echo "PR_NUMBER=$pr_number" >> $GITHUB_ENV echo "PR_BRANCH=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_ENV echo "COMMIT_SHA=${{ github.event.workflow_run.head_sha }}" >> $GITHUB_ENV - if: github.event.workflow_run.event == 'push' name: Apply push environment run: | echo "CHANNEL_ID=live" >> $GITHUB_ENV - name: Deploy to Firebase uses: siku2/action-hosting-deploy@v1 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseToken: "${{ secrets.FIREBASE_TOKEN }}" targets: website channelId: "${{ env.CHANNEL_ID }}" # link to the next version because that's what we care about commentURLPath: "/docs/next" # PR information prNumber: "${{ env.PR_NUMBER }}" prBranchName: "${{ env.PR_BRANCH }}" commitSHA: "${{ env.COMMIT_SHA }}" ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish yew package(s) permissions: contents: write on: workflow_dispatch: inputs: level: description: "Version Level major|minor|patch" required: true type: choice options: - patch - minor - major packages: description: "List of packages to publish (space separated)" required: true type: string jobs: publish: name: Publish yew runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v6 with: token: "${{ secrets.YEWTEMPBOT_TOKEN }}" fetch-depth: 0 - name: Config Git uses: oleksiyrudenko/gha-git-credentials@v2.1.2 with: token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Install cargo binary dependencies uses: baptiste0928/cargo-install@v3 with: crate: cargo-release version: =1.1.1 - name: Cargo login run: cargo login ${{ secrets.CRATES_IO_TOKEN }} - name: Build command shell: bash env: PACKAGES: ${{ github.event.inputs.packages }} run: | output="" for pkg in ${{ github.event.inputs.packages }} do output+="--package $pkg " done echo "pkg=$output" >> $GITHUB_ENV - name: Release yew run: cargo release ${{ github.event.inputs.level }} --execute --no-confirm ${{ env.pkg }} - name: Collect release info id: releaseinfo run: cargo run -p collect-release-info -- ${{ github.event.inputs.packages }} - name: Create a version branch if: github.event.inputs.level != 'patch' uses: peterjgrainger/action-create-branch@v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: branch: ${{ steps.releaseinfo.outputs.version_branch }} - name: Create releases env: GH_TOKEN: ${{ secrets.YEWTEMPBOT_TOKEN }} RELEASES: ${{ steps.releaseinfo.outputs.releases }} run: | echo "$RELEASES" | jq -c '.[]' | while read -r release; do tag=$(echo "$release" | jq -r '.tag') name=$(echo "$release" | jq -r '.name') body=$(echo "$release" | jq -r '.body') gh release create "$tag" --title "$name" --notes "$body" done ================================================ FILE: .github/workflows/size-cmp.yml ================================================ name: Size Comparison on: pull_request: branches: [master] paths: - .github/workflows/size-cmp.yml - "ci/**" - "packages/**" - "examples/**" - "Cargo.toml" jobs: size-cmp: name: Collect ${{ matrix.target }} Size runs-on: ubuntu-latest strategy: matrix: target: ["master", "pr"] steps: - name: Checkout master uses: actions/checkout@v6 if: ${{ matrix.target == 'master' }} with: repository: "yewstack/yew" ref: master - name: Checkout pull request uses: actions/checkout@v6 if: ${{ matrix.target == 'pr' }} - name: Write Optimisation Flags run: | if [ -x ci/write-min-size-flags.sh ] ; then ci/write-min-size-flags.sh else # this branch is a fallback used only for compatibility with earlier commits # in the repository and other branches and can be removed in the future. echo 'build-std = ["std", "panic_abort"]' >> .cargo/config.toml echo '[build]' >> .cargo/config.toml echo 'rustflags = ["-Cpanic=abort"]' >> .cargo/config.toml fi - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: nightly components: rust-src targets: wasm32-unknown-unknown - name: Restore Rust cache uses: Swatinem/rust-cache@v2 - name: Setup Trunk uses: jetli/trunk-action@v0.5.1 with: version: "latest" - name: Get latest wasm-opt version id: wasm-opt uses: pozetroninc/github-action-get-latest-release@master with: repository: WebAssembly/binaryen excludes: prerelease, draft token: ${{ secrets.GITHUB_TOKEN }} - name: Build examples run: cargo run -p build-examples --bin build-examples env: LATEST_WASM_OPT_VERSION: ${{ steps.wasm-opt.outputs.release }} - name: Collect size information run: python3 ci/collect_sizes.py env: ISSUE_NUMBER: ${{ github.event.number }} - name: Upload Artifact uses: actions/upload-artifact@v7 with: name: size-cmp-${{ matrix.target }}-info include-hidden-files: true path: ".SIZE_CMP_INFO" retention-days: 1 ================================================ FILE: .github/workflows/test-website.yml ================================================ name: "Test Website" on: pull_request: paths: - ".github/workflows/test-website.yml" - "packages/**/*" - "website/**/*" push: branches: [master] jobs: website_tests: name: Tests Website Snippets runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 # for wasm-bindgen-cli, always use stable rust - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Install wasm-bindgen-cli shell: bash run: ./ci/install-wasm-bindgen-cli.sh - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: toolchain: nightly targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/master' }} - uses: browser-actions/setup-geckodriver@latest with: token: ${{ secrets.GITHUB_TOKEN }} - uses: nanasess/setup-chromedriver@v2 - name: Run website code snippet tests run: cargo test -p website-test --target wasm32-unknown-unknown ================================================ FILE: .gitignore ================================================ target/ dist/ # backup files generated by rustfmt **/*.rs.bk # editor config files and directories *.iml /.idea/ /.vscode/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## ✨ yew **0.23.0** *(2026-03-10)* bumping from 0.22 should require no code changes for most users. ### 🚨 Breaking changes - Performance: use_reducer now skips re-rendering for the same Rc. [[@Pascal Sommer](https://github.com/Pascal-So), [#3945](https://github.com/yewstack/yew/pull/3945)] NOTE: Whether this is breaking is arguable. It merely breaks the promise that a dispatch will always cause a re-render. For code that wishes to force re-render, [use_force_update](https://docs.rs/yew/latest/yew/functional/fn.use_force_update.html) helps. Please refer to [the migration guide](https://yew.rs/docs/next/migration-guides/yew/from-0_22_0-to-0_23_0) for details. ### ⚡️ Features - `&str` and `String` can now be used for props of type `Option`. [[@Cashew](https://github.com/Casheeew), [#4020](https://github.com/yewstack/yew/pull/4020)] - Added a `scheduler::flush` function to reliably finish rendering. Useful in testing as a replacement for timeouts. [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4044](https://github.com/yewstack/yew/pull/4044)] ### 🛠 Fixes - No more broken child re-renders while setting parents' states. [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4060](https://github.com/yewstack/yew/pull/4060)] - Ergonomics: Bare `None`s are now allowed for `Option` props in the `html!` macro. [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4021](https://github.com/yewstack/yew/pull/4021)] ### ⚙️ Improvements - Yew's scheduler now yields to the main thread from time to time. This fix will make the web page more responsive and reduce warnings about long tasks in the console. [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4033](https://github.com/yewstack/yew/pull/4033)] ## ✨ yew-router **0.20.0** *(2026-03-10)* Yew pinned to 0.23 now. ### 🛠 Fixes - '/' is no longer wrongly encoded in wildcard route segments. [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4056](https://github.com/yewstack/yew/pull/4056)] - Fixed a url corruption issue causing redirection to `/basename//basename` resulting in a 404. [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4030](https://github.com/yewstack/yew/pull/4030)] ## ✨ yew-agent **0.5.0** *(2026-03-10)* No changes. Yew pinned to 0.23 now. ## ✨ yew **0.22.1** *(2026-02-28)* ### 🛠 Fixes - Domslot hydration panic caused by suspension [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4003](https://github.com/yewstack/yew/pull/4003)] - Some `Option` and `&T` types can be used as children again. e.g. `Option` [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4005](https://github.com/yewstack/yew/pull/4005)] - Custom hooks now compiles in edition 2024. [[@Siyuan Yan](https://github.com/Madoshakalaka), [#3992](https://github.com/yewstack/yew/pull/3992)] - No more stale states in callbacks when multiple events fire rapidly. [[@Siyuan Yan](https://github.com/Madoshakalaka), [#3988](https://github.com/yewstack/yew/pull/3988)] - Fixed invisible svg issue on Chrome when included with `from_html_unchecked` [[@Jason Heard](https://github.com/101100), [#3970](https://github.com/yewstack/yew/pull/3970)] - Fixed documentation typo in introduction.mdx. [[@devfbe](https://github.com/devfbe), [#3417](https://github.com/yewstack/yew/pull/3417)] ### ⚙️ Improvements - Improved SSR example with meta rendering. [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4011](https://github.com/yewstack/yew/pull/4011)] - Replaced once_cell with std equivalents (LazyLock, OnceLock). [[@Siyuan Yan](https://github.com/Madoshakalaka), [#4010](https://github.com/yewstack/yew/pull/4010)] - Updated rust dependencies. ## ✨ yew **0.22.0** *(2025-12-08)* ### 🚨 Breaking changes - **MSRV raised to 1.84.0.** [[@Siyuan Yan](https://github.com/Madoshakalaka), [#3900](https://github.com/yewstack/yew/pull/3900)] - Allow setting JsValue as properties. [[@Elina](https://github.com/ranile), [#3458](https://github.com/yewstack/yew/pull/3458)] - Remove deprecated `class=(...)` syntax. [[@Tim Kurdov](https://github.com/its-the-shrimp), [#3497](https://github.com/yewstack/yew/pull/3497)] - Remove ToHtml trait. [[@Elina](https://github.com/ranile), [#3453](https://github.com/yewstack/yew/pull/3453)] - Make `"); } VTagInner::Other { tag, children } => { let lowercase_tag = tag.to_ascii_lowercase(); if !VOID_ELEMENTS.contains(&lowercase_tag.as_ref()) { children .render_into_stream(w, parent_scope, hydratable, tag.into()) .await; let _ = w.write_str(""); } else { // We don't write children of void elements nor closing tags. debug_assert!( match children { VNode::VList(m) => m.is_empty(), _ => false, }, "{tag} cannot have any children!" ); } } } } } } #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] #[cfg(feature = "ssr")] #[cfg(test)] mod ssr_tests { use tokio::test; use crate::prelude::*; use crate::LocalServerRenderer as ServerRenderer; #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_simple_tag() { #[component] fn Comp() -> Html { html! {
} } let s = ServerRenderer::::new() .hydratable(false) .render() .await; assert_eq!(s, "
"); } #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_simple_tag_with_attr() { #[component] fn Comp() -> Html { html! {
} } let s = ServerRenderer::::new() .hydratable(false) .render() .await; assert_eq!(s, r#"
"#); } #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_simple_tag_with_content() { #[component] fn Comp() -> Html { html! {
{"Hello!"}
} } let s = ServerRenderer::::new() .hydratable(false) .render() .await; assert_eq!(s, r#"
Hello!
"#); } #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_simple_tag_with_nested_tag_and_input() { #[component] fn Comp() -> Html { html! {
{"Hello!"}
} } let s = ServerRenderer::::new() .hydratable(false) .render() .await; assert_eq!(s, r#"
Hello!
"#); } #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_textarea() { #[component] fn Comp() -> Html { html! { "#); } #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_textarea_w_defaultvalue() { #[component] fn Comp() -> Html { html! { "#); } #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_value_precedence_over_defaultvalue() { #[component] fn Comp() -> Html { html! { "#); } #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_escaping_in_style_tag() { #[component] fn Comp() -> Html { html! { } } let s = ServerRenderer::::new() .hydratable(false) .render() .await; assert_eq!(s, r#""#); } #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_escaping_in_script_tag() { #[component] fn Comp() -> Html { html! { } } let s = ServerRenderer::::new() .hydratable(false) .render() .await; assert_eq!(s, r#""#); } #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_multiple_vtext_in_style_tag() { #[component] fn Comp() -> Html { let one = "html { background: black } "; let two = "body > a { color: white } "; html! { } } let s = ServerRenderer::::new() .hydratable(false) .render() .await; assert_eq!( s, r#""# ); } } ================================================ FILE: packages/yew/src/virtual_dom/vtext.rs ================================================ //! This module contains the implementation of a virtual text node `VText`. use std::cmp::PartialEq; use super::AttrValue; use crate::html::ImplicitClone; /// A type for a virtual /// [`TextNode`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode) /// representation. #[derive(Clone, ImplicitClone)] pub struct VText { /// Contains a text of the node. pub text: AttrValue, } impl VText { /// Creates new virtual text node with a content. pub fn new(text: impl Into) -> Self { VText { text: text.into() } } } impl std::fmt::Debug for VText { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "VText {{ text: \"{}\" }}", self.text) } } impl PartialEq for VText { fn eq(&self, other: &VText) -> bool { self.text == other.text } } impl From for VText { fn from(value: T) -> Self { VText::new(value.to_string()) } } #[cfg(feature = "ssr")] mod feat_ssr { use std::fmt::Write; use super::*; use crate::feat_ssr::VTagKind; use crate::html::AnyScope; use crate::platform::fmt::BufWriter; impl VText { pub(crate) async fn render_into_stream( &self, w: &mut BufWriter, _parent_scope: &AnyScope, _hydratable: bool, parent_vtag_kind: VTagKind, ) { _ = w.write_str(&match parent_vtag_kind { VTagKind::Style => html_escape::encode_style(&self.text), VTagKind::Script => html_escape::encode_script(&self.text), VTagKind::Other => html_escape::encode_text(&self.text), }) } } } #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] #[cfg(feature = "ssr")] #[cfg(test)] mod ssr_tests { use tokio::test; use crate::prelude::*; use crate::LocalServerRenderer as ServerRenderer; #[cfg_attr(not(target_os = "wasi"), test)] #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))] async fn test_simple_str() { #[component] fn Comp() -> Html { html! { "abc" } } let s = ServerRenderer::::new() .hydratable(false) .render() .await; assert_eq!(s, r#"abc"#); } } ================================================ FILE: packages/yew/tests/common/mod.rs ================================================ #![allow(dead_code)] pub fn obtain_result() -> String { gloo::utils::document() .get_element_by_id("result") .expect("No result found. Most likely, the application crashed and burned") .inner_html() } pub fn obtain_result_by_id(id: &str) -> String { gloo::utils::document() .get_element_by_id(id) .expect("No result found. Most likely, the application crashed and burned") .inner_html() } pub fn output_element() -> web_sys::Element { gloo::utils::document().get_element_by_id("output").unwrap() } ================================================ FILE: packages/yew/tests/hydration.rs ================================================ #![cfg(feature = "hydration")] #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] use std::ops::Range; use std::rc::Rc; use std::time::Duration; mod common; use common::{obtain_result, obtain_result_by_id}; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; use wasm_bindgen_test::*; use web_sys::{HtmlElement, HtmlTextAreaElement}; use yew::platform::time::sleep; use yew::prelude::*; use yew::suspense::{use_future, Suspension, SuspensionResult}; use yew::virtual_dom::VNode; use yew::{component, scheduler, Renderer, ServerRenderer}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); // If any of the assertions fail due to a modification to hydration logic, cargo will suggest the // expected result and you can copy it into the test to fix it. #[wasm_bindgen_test] async fn hydration_works() { #[component] fn Comp() -> Html { let ctr = use_state_eq(|| 0); let onclick = { let ctr = ctr.clone(); Callback::from(move |_| { ctr.set(*ctr + 1); }) }; html! {
{"Counter: "}{*ctr}
} } #[component] fn App() -> Html { html! {
} } let s = ServerRenderer::::new().render().await; gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); scheduler::flush().await; let result = obtain_result_by_id("output"); // no placeholders, hydration is successful. assert_eq!( result, r#"
Counter: 0
"# ); gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); scheduler::flush().await; let result = obtain_result_by_id("output"); assert_eq!( result, r#"
Counter: 1
"# ); } #[wasm_bindgen_test] async fn hydration_with_raw() { #[component(Content)] fn content() -> Html { let vnode = VNode::from_html_unchecked("

Hello World

".into()); html! {
{vnode}
} } #[component(App)] fn app() -> Html { html! {
} } let s = ServerRenderer::::new().render().await; gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); sleep(Duration::from_millis(10)).await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); let result = obtain_result(); // still hydrating, during hydration, the server rendered result is shown. assert_eq!( result.as_str(), r#"

Hello World

"# ); sleep(Duration::from_millis(50)).await; let result = obtain_result(); // hydrated. assert_eq!( result.as_str(), r#"

Hello World

"# ); } #[wasm_bindgen_test] async fn hydration_with_suspense() { #[derive(PartialEq)] pub struct SleepState { s: Suspension, } impl SleepState { fn new() -> Self { let (s, handle) = Suspension::new(); spawn_local(async move { sleep(Duration::from_millis(50)).await; handle.resume(); }); Self { s } } } impl Reducible for SleepState { type Action = (); fn reduce(self: Rc, _action: Self::Action) -> Rc { Self::new().into() } } #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); if sleep_state.s.resumed() { Ok(Rc::new(move || sleep_state.dispatch(()))) } else { Err(sleep_state.s.clone()) } } #[component(Content)] fn content() -> HtmlResult { let resleep = use_sleep()?; let value = use_state(|| 0); let on_increment = { let value = value.clone(); Callback::from(move |_: MouseEvent| { value.set(*value + 1); }) }; let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); Ok(html! {
{*value}
}) } #[component(App)] fn app() -> Html { let fallback = html! {
{"wait..."}
}; html! {
} } let s = ServerRenderer::::new().render().await; gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); // still hydrating, during hydration, the server rendered result is shown. assert_eq!( result.as_str(), r#"
0
"# ); sleep(Duration::from_millis(50)).await; let result = obtain_result(); // hydrated. assert_eq!( result.as_str(), r#"
0
"# ); gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
1
"# ); gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
1
"# ); } #[wasm_bindgen_test] async fn hydration_with_suspense_not_suspended_at_start() { #[derive(PartialEq)] pub struct SleepState { s: Option, } impl SleepState { fn new() -> Self { Self { s: None } } } impl Reducible for SleepState { type Action = (); fn reduce(self: Rc, _action: Self::Action) -> Rc { let (s, handle) = Suspension::new(); spawn_local(async move { sleep(Duration::from_millis(50)).await; handle.resume(); }); Self { s: Some(s) }.into() } } #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); let s = match sleep_state.s.clone() { Some(m) => m, None => return Ok(Rc::new(move || sleep_state.dispatch(()))), }; if s.resumed() { Ok(Rc::new(move || sleep_state.dispatch(()))) } else { Err(s) } } #[component(Content)] fn content() -> HtmlResult { let resleep = use_sleep()?; let value = use_state(|| "I am writing a long story...".to_string()); let on_text_input = { let value = value.clone(); Callback::from(move |e: InputEvent| { let input: HtmlTextAreaElement = e.target_unchecked_into(); value.set(input.value()); }) }; let on_take_a_break = Callback::from(move |_| (resleep.clone())()); Ok(html! {
"# ); gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
"# ); } #[wasm_bindgen_test] async fn hydration_nested_suspense_works() { #[derive(PartialEq)] pub struct SleepState { s: Suspension, } impl SleepState { fn new() -> Self { let (s, handle) = Suspension::new(); spawn_local(async move { sleep(Duration::from_millis(50)).await; handle.resume(); }); Self { s } } } impl Reducible for SleepState { type Action = (); fn reduce(self: Rc, _action: Self::Action) -> Rc { Self::new().into() } } #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); if sleep_state.s.resumed() { Ok(Rc::new(move || sleep_state.dispatch(()))) } else { Err(sleep_state.s.clone()) } } #[component(InnerContent)] fn inner_content() -> HtmlResult { let resleep = use_sleep()?; let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); Ok(html! {
}) } #[component(Content)] fn content() -> HtmlResult { let resleep = use_sleep()?; let fallback = html! {
{"wait...(inner)"}
}; let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); Ok(html! {
}) } #[component(App)] fn app() -> Html { let fallback = html! {
{"wait...(outer)"}
}; html! {
} } let s = ServerRenderer::::new().render().await; gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); // outer suspense is hydrating... sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
"# ); sleep(Duration::from_millis(50)).await; // inner suspense is hydrating... let result = obtain_result(); assert_eq!( result.as_str(), r#"
"# ); sleep(Duration::from_millis(50)).await; // hydrated. let result = obtain_result(); assert_eq!( result.as_str(), r#"
"# ); gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...(outer)
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
"# ); gloo::utils::document() .query_selector(".take-a-break2") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
wait...(inner)
"# ); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
"# ); } #[wasm_bindgen_test] async fn hydration_node_ref_works() { #[component(App)] pub fn app() -> Html { let size = use_state(|| 4); let callback = { let size = size.clone(); Callback::from(move |_| { size.set(10); }) }; html! {
} } #[derive(Properties, PartialEq)] struct ListProps { size: u32, } #[component(Test1)] fn test1() -> Html { html! { {"test"} } } #[component(Test2)] fn test2() -> Html { html! { } } #[component(List)] fn list(props: &ListProps) -> Html { let elems = 0..props.size; html! { <> { for elems.map(|_| html! { } )} } } let s = ServerRenderer::::new().render().await; gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); scheduler::flush().await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), r#"
testtesttesttest
"# ); gloo::utils::document() .query_selector("span") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); scheduler::flush().await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), r#"
testtesttesttesttesttesttesttesttesttest
"# ); } #[wasm_bindgen_test] async fn hydration_list_order_works() { #[component(App)] pub fn app() -> Html { let elems = 0..10; html! { <> { for elems.map(|number| html! { } )} } } #[derive(Properties, PartialEq)] struct NumberProps { number: u32, } #[component(Number)] fn number(props: &NumberProps) -> Html { html! {
{props.number.to_string()}
} } #[component(SuspendedNumber)] fn suspended_number(props: &NumberProps) -> HtmlResult { use_suspend()?; Ok(html! {
{props.number.to_string()}
}) } #[component(ToSuspendOrNot)] fn suspend_or_not(props: &NumberProps) -> Html { let number = props.number; html! { if number % 3 == 0 { } else { } } } #[hook] pub fn use_suspend() -> SuspensionResult<()> { use_future(|| async {})?; Ok(()) } let s = ServerRenderer::::new().render().await; gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); // Wait until all suspended components becomes revealed. scheduler::flush().await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), // Until all components become revealed, there will be component markers. // As long as there's no component markers all components have become unsuspended. r#"
0
1
2
3
4
5
6
7
8
9
"# ); } #[wasm_bindgen_test] async fn hydration_suspense_no_flickering() { #[component(App)] pub fn app() -> Html { let fallback = html! {

{"Loading..."}

}; html! { } } #[derive(Properties, PartialEq, Clone)] struct NumberProps { number: u32, } #[component(SuspendedNumber)] fn suspended_number(props: &NumberProps) -> HtmlResult { use_suspend()?; Ok(html! { }) } #[component(Number)] fn number(props: &NumberProps) -> Html { html! {
{props.number.to_string()}
} } #[component(Suspended)] fn suspended() -> HtmlResult { use_suspend()?; Ok(html! { { for (0..10).map(|number| html! { } )} }) } #[hook] pub fn use_suspend() -> SuspensionResult<()> { use_future(|| async { yew::platform::time::sleep(std::time::Duration::from_millis(200)).await; })?; Ok(()) } let s = ServerRenderer::::new().render().await; gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); // Wait until all suspended components becomes revealed. scheduler::flush().await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), // outer still suspended. r#"
0
1
2
3
4
5
6
7
8
9
"# ); sleep(Duration::from_millis(103)).await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), r#"
0
1
2
3
4
5
6
7
8
9
"# ); sleep(Duration::from_millis(103)).await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), r#"
0
1
2
3
4
5
6
7
8
9
"# ); sleep(Duration::from_millis(103)).await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), // outer revealed, inner still suspended, outer remains. r#"
0
1
2
3
4
5
6
7
8
9
"# ); sleep(Duration::from_millis(103)).await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), // inner revealed. r#"
0
1
2
3
4
5
6
7
8
9
"# ); } #[wasm_bindgen_test] async fn hydration_order_issue_nested_suspense() { #[component(App)] pub fn app() -> Html { let elems = (0..10).map(|number: u32| { html! { } }); html! { { for elems } } } #[derive(Properties, PartialEq)] struct NumberProps { number: u32, } #[component(Number)] fn number(props: &NumberProps) -> Html { html! {
{props.number.to_string()}
} } #[component(SuspendedNumber)] fn suspended_number(props: &NumberProps) -> HtmlResult { use_suspend()?; Ok(html! {
{props.number.to_string()}
}) } #[component(ToSuspendOrNot)] fn suspend_or_not(props: &NumberProps) -> HtmlResult { let number = props.number; Ok(html! { if number % 3 == 0 { } else { } }) } #[hook] pub fn use_suspend() -> SuspensionResult<()> { use_future(|| async {})?; Ok(()) } let s = ServerRenderer::::new().render().await; gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); // Wait until all suspended components becomes revealed. scheduler::flush().await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), // Until all components become revealed, there will be component markers. // As long as there's no component markers all components have become unsuspended. r#"
0
1
2
3
4
5
6
7
8
9
"# ); } #[wasm_bindgen_test] async fn hydration_props_blocked_until_hydrated() { #[component(App)] pub fn app() -> Html { let range = use_state(|| 0u32..2); { let range = range.clone(); use_effect_with((), move |_| { range.set(0..3); || () }); } html! { } } #[derive(Properties, PartialEq)] struct ToSuspendProps { range: Range, } #[component(ToSuspend)] fn to_suspend(ToSuspendProps { range }: &ToSuspendProps) -> HtmlResult { use_suspend(Duration::from_millis(100))?; Ok(html! { { for range.clone().map(|i| html!{
{i}
} )} }) } #[hook] pub fn use_suspend(_dur: Duration) -> SuspensionResult<()> { yew::suspense::use_future(|| async move { sleep(_dur).await; })?; Ok(()) } let s = ServerRenderer::::new().render().await; let output_element = gloo::utils::document() .query_selector("#output") .unwrap() .unwrap(); output_element.set_inner_html(&s); Renderer::::with_root(output_element).hydrate(); sleep(Duration::from_millis(150)).await; let result = obtain_result_by_id("output"); assert_eq!(result.as_str(), r#"
0
1
2
"#); } #[wasm_bindgen_test] async fn hydrate_empty() { #[component] fn Updating() -> Html { let trigger = use_state(|| false); { let trigger = trigger.clone(); use_effect_with((), move |_| { trigger.set(true); || {} }); } if *trigger { html! {
{"after"}
} } else { html! {
{"before"}
} } } #[component] fn Empty() -> Html { html! { <> } } #[component] fn App() -> Html { html! { <> } } let s = ServerRenderer::::new().render().await; let output_element = gloo::utils::document() .query_selector("#output") .unwrap() .unwrap(); output_element.set_inner_html(&s); Renderer::::with_root(output_element).hydrate(); sleep(Duration::from_millis(50)).await; let result = obtain_result_by_id("output"); assert_eq!(result.as_str(), r#"
after
after
"#); } #[wasm_bindgen_test] async fn hydrate_flicker() { // This components renders the same on the server and client during the first render, // but then immediately changes the order of keyed elements in the next render. // This should not lead to any hydration failures #[derive(Properties, PartialEq)] struct InnerCompProps { text: String, } #[component] fn InnerComp(InnerCompProps { text }: &InnerCompProps) -> Html { html! {

{text.clone()}

} } #[component] fn Flickering() -> Html { let trigger = use_state(|| false); let is_first = !*trigger; if is_first { trigger.set(true); html! { <> } } else { html! { <> } } } let s = ServerRenderer::::new().render().await; let output_element = gloo::utils::document() .query_selector("#output") .unwrap() .unwrap(); output_element.set_inner_html(&s); Renderer::::with_root(output_element).hydrate(); sleep(Duration::from_millis(50)).await; let result = obtain_result_by_id("output"); assert_eq!(result.as_str(), r#"

2

1

"#); } #[wasm_bindgen_test] async fn hydration_with_camelcase_svg_elements() { #[function_component] fn SvgWithCamelCase() -> Html { html! { <@{"linearGradient"} id="gradient1"> <@{"radialGradient"} id="gradient2"> <@{"clipPath"} id="clip1"> <@{"feDropShadow"} dx="2" dy="2" stdDeviation="2" /> } } #[function_component] fn App() -> Html { let counter = use_state(|| 0); let onclick = { let counter = counter.clone(); Callback::from(move |_| counter.set(*counter + 1)) }; html! {
{"Count: "}{*counter}
} } // Server render let s = ServerRenderer::::new().render().await; // Set HTML gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; // Hydrate - this should not panic Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(10)).await; // Verify the SVG elements are present and properly cased let svg = gloo::utils::document() .query_selector("svg") .unwrap() .unwrap(); let linear_gradient = svg.query_selector("linearGradient").unwrap().unwrap(); assert_eq!(linear_gradient.tag_name(), "linearGradient"); let radial_gradient = svg.query_selector("radialGradient").unwrap().unwrap(); assert_eq!(radial_gradient.tag_name(), "radialGradient"); let clip_path = svg.query_selector("clipPath").unwrap().unwrap(); assert_eq!(clip_path.tag_name(), "clipPath"); // Test interactivity still works after hydration gloo::utils::document() .query_selector(".increment") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(10)).await; let counter_text = gloo::utils::document() .query_selector(".counter") .unwrap() .unwrap() .text_content() .unwrap(); assert_eq!(counter_text, "Count: 1"); } #[wasm_bindgen_test] async fn hydration_suspended_child_does_not_trap_sibling_slot() { #[hook] fn use_suspend() -> SuspensionResult<()> { use_future(|| async {})?; Ok(()) } #[component(SuspendingChild)] fn suspending_child() -> HtmlResult { use_suspend()?; Ok(html! {
{"child"}
}) } #[component(App)] fn app() -> Html { let trigger = use_state(|| false); { let trigger = trigger.clone(); use_effect_with((), move |_| { trigger.set(true); }); } html! {
{"Loading..."}
}}> if *trigger {

{"new sibling"}

} } } let s = ServerRenderer::::new().render().await; gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); scheduler::flush().await; let result = obtain_result(); assert_eq!( result.as_str(), r#"

new sibling

child
"#, ); } ================================================ FILE: packages/yew/tests/layout.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] mod common; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); use std::time::Duration; use common::obtain_result; use wasm_bindgen_futures::spawn_local; use wasm_bindgen_test::*; use yew::platform::time::sleep; use yew::prelude::*; #[wasm_bindgen_test] async fn change_nested_after_append() { #[component] fn Nested() -> Html { let delayed_trigger = use_state(|| true); { let delayed_trigger = delayed_trigger.clone(); use_effect_with((), move |_| { spawn_local(async move { sleep(Duration::from_millis(50)).await; delayed_trigger.set(false); }); || {} }); } if *delayed_trigger { html! {
{"failure"}
} } else { html! { <>{"success"} } } } #[component] fn Top() -> Html { html! { } } #[component] fn App() -> Html { let show_bottom = use_state_eq(|| false); { let show_bottom = show_bottom.clone(); use_effect_with((), move |_| { show_bottom.set(true); || {} }); } html! { <> if *show_bottom {
{"
Bottom
"}
} } } yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); sleep(Duration::from_millis(100)).await; let result = obtain_result(); assert_eq!(result.as_str(), "success"); } ================================================ FILE: packages/yew/tests/mod.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] mod common; use common::obtain_result; use wasm_bindgen_test::*; use yew::prelude::*; use yew::scheduler; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn props_are_passed() { #[derive(Properties, Clone, PartialEq)] struct PropsPassedFunctionProps { value: String, } #[component] fn PropsComponent(props: &PropsPassedFunctionProps) -> Html { assert_eq!(&props.value, "props"); html! {
{"done"}
} } yew::Renderer::::with_root_and_props( gloo::utils::document().get_element_by_id("output").unwrap(), PropsPassedFunctionProps { value: "props".to_string(), }, ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "done"); } ================================================ FILE: packages/yew/tests/raw_html.rs ================================================ mod common; #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] use wasm_bindgen::JsCast; #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] use wasm_bindgen_test::wasm_bindgen_test as test; use yew::prelude::*; #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] use tokio::test; macro_rules! create_test { ($name:ident, $html:expr) => { create_test!($name, $html, $html); }; ($name:ident, $raw:expr, $expected:expr) => { #[test] async fn $name() { #[component] fn App() -> Html { let raw = Html::from_html_unchecked(AttrValue::from($raw)); html! {
{raw}
} } #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] { use std::time::Duration; use yew::platform::time::sleep; yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); // wait for render to finish sleep(Duration::from_millis(100)).await; let e = gloo::utils::document() .get_element_by_id("raw-container") .unwrap(); assert_eq!(e.inner_html(), $expected); } #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] { let actual = yew::LocalServerRenderer::::new() .hydratable(false) .render() .await; assert_eq!( actual, format!(r#"
{}
"#, $expected) ); } } }; } create_test!(empty_string, ""); create_test!(one_node, "text"); create_test!( one_but_nested_node, r#"

one link more paragraph

"# ); create_test!( multi_node, r#"

paragraph

link"# ); #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] macro_rules! create_update_html_test { ($name:ident, $initial:expr, $updated:expr) => { #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] #[test] async fn $name() { #[component] fn App() -> Html { let raw_html = use_state(|| ($initial)); let onclick = { let raw_html = raw_html.clone(); move |_| raw_html.set($updated) }; let raw = Html::from_html_unchecked(AttrValue::from(*raw_html)); html! { <>
{raw}
} } use std::time::Duration; use yew::platform::time::sleep; yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); // wait for render to finish sleep(Duration::from_millis(100)).await; let e = gloo::utils::document() .get_element_by_id("raw-container") .unwrap(); assert_eq!(e.inner_html(), $initial); gloo::utils::document() .get_element_by_id("click-me-btn") .unwrap() .unchecked_into::() .click(); sleep(Duration::from_millis(100)).await; let e = gloo::utils::document() .get_element_by_id("raw-container") .unwrap(); assert_eq!(e.inner_html(), $updated); } }; } #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] create_update_html_test!( set_new_html_string, "first", "second" ); #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] create_update_html_test!( set_new_html_string_multiple_children, "firstsecond", "second" ); #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] create_update_html_test!( clear_html_string_multiple_children, "firstsecond", "" ); #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] create_update_html_test!( nothing_changes, "firstsecond", "firstsecond" ); #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] #[test] async fn change_vnode_types_from_other_to_vraw() { #[component] fn App() -> Html { let node = use_state(|| html!("text")); let onclick = { let node = node.clone(); move |_| { node.set(Html::from_html_unchecked(AttrValue::from( "second", ))) } }; html! { <>
{(*node).clone()}
} } use std::time::Duration; use yew::platform::time::sleep; yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); // wait for render to finish sleep(Duration::from_millis(100)).await; let e = gloo::utils::document() .get_element_by_id("raw-container") .unwrap(); assert_eq!(e.inner_html(), "text"); gloo::utils::document() .get_element_by_id("click-me-btn") .unwrap() .unchecked_into::() .click(); sleep(Duration::from_millis(100)).await; let e = gloo::utils::document() .get_element_by_id("raw-container") .unwrap(); assert_eq!(e.inner_html(), "second"); } #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] #[test] async fn change_vnode_types_from_vraw_to_other() { #[component] fn App() -> Html { let node = use_state(|| Html::from_html_unchecked(AttrValue::from("second"))); let onclick = { let node = node.clone(); move |_| node.set(html!("text")) }; html! { <>
{(*node).clone()}
} } use std::time::Duration; use yew::platform::time::sleep; yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); // wait for render to finish sleep(Duration::from_millis(100)).await; let e = gloo::utils::document() .get_element_by_id("raw-container") .unwrap(); assert_eq!(e.inner_html(), "second"); gloo::utils::document() .get_element_by_id("click-me-btn") .unwrap() .unchecked_into::() .click(); sleep(Duration::from_millis(100)).await; let e = gloo::utils::document() .get_element_by_id("raw-container") .unwrap(); assert_eq!(e.inner_html(), "text"); } ================================================ FILE: packages/yew/tests/suspense.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] mod common; use std::cell::RefCell; use std::rc::Rc; use std::time::Duration; use common::obtain_result; use wasm_bindgen::JsCast; use wasm_bindgen_test::*; use web_sys::{HtmlElement, HtmlTextAreaElement}; use yew::platform::spawn_local; use yew::platform::time::sleep; use yew::prelude::*; use yew::suspense::{use_future, use_future_with, Suspension, SuspensionResult}; use yew::{scheduler, UseStateHandle}; wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn suspense_works() { #[derive(PartialEq)] pub struct SleepState { s: Suspension, } impl SleepState { fn new() -> Self { let (s, handle) = Suspension::new(); spawn_local(async move { sleep(Duration::from_millis(50)).await; handle.resume(); }); Self { s } } } impl Reducible for SleepState { type Action = (); fn reduce(self: Rc, _action: Self::Action) -> Rc { Self::new().into() } } #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); if sleep_state.s.resumed() { Ok(Rc::new(move || sleep_state.dispatch(()))) } else { Err(sleep_state.s.clone()) } } #[component(Content)] fn content() -> HtmlResult { let resleep = use_sleep()?; let value = use_state(|| 0); let on_increment = { let value = value.clone(); Callback::from(move |_: MouseEvent| { value.set(*value + 1); }) }; let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); Ok(html! {
{*value}
}) } #[component(App)] fn app() -> Html { let fallback = html! {
{"wait..."}
}; html! {
} } yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
0
"# ); sleep(Duration::from_millis(10)).await; gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); scheduler::flush().await; gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(1)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
2
"# ); gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
2
"# ); } #[wasm_bindgen_test] async fn suspense_not_suspended_at_start() { #[derive(PartialEq)] pub struct SleepState { s: Option, } impl SleepState { fn new() -> Self { Self { s: None } } } impl Reducible for SleepState { type Action = (); fn reduce(self: Rc, _action: Self::Action) -> Rc { let (s, handle) = Suspension::new(); spawn_local(async move { sleep(Duration::from_millis(50)).await; handle.resume(); }); Self { s: Some(s) }.into() } } #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); let s = match sleep_state.s.clone() { Some(m) => m, None => return Ok(Rc::new(move || sleep_state.dispatch(()))), }; if s.resumed() { Ok(Rc::new(move || sleep_state.dispatch(()))) } else { Err(s) } } #[component(Content)] fn content() -> HtmlResult { let resleep = use_sleep()?; let value = use_state(|| "I am writing a long story...".to_string()); let on_text_input = { let value = value.clone(); Callback::from(move |e: InputEvent| { let input: HtmlTextAreaElement = e.target_unchecked_into(); value.set(input.value()); }) }; let on_take_a_break = Callback::from(move |_| (resleep.clone())()); Ok(html! {
"# ); gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
"# ); } #[wasm_bindgen_test] async fn suspense_nested_suspense_works() { #[derive(PartialEq)] pub struct SleepState { s: Suspension, } impl SleepState { fn new() -> Self { let (s, handle) = Suspension::new(); spawn_local(async move { sleep(Duration::from_millis(50)).await; handle.resume(); }); Self { s } } } impl Reducible for SleepState { type Action = (); fn reduce(self: Rc, _action: Self::Action) -> Rc { Self::new().into() } } #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); if sleep_state.s.resumed() { Ok(Rc::new(move || sleep_state.dispatch(()))) } else { Err(sleep_state.s.clone()) } } #[component(InnerContent)] fn inner_content() -> HtmlResult { let resleep = use_sleep()?; let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); Ok(html! {
}) } #[component(Content)] fn content() -> HtmlResult { let resleep = use_sleep()?; let fallback = html! {
{"wait...(inner)"}
}; let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); Ok(html! {
}) } #[component(App)] fn app() -> Html { let fallback = html! {
{"wait...(outer)"}
}; html! {
} } yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...(outer)
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
wait...(inner)
"# ); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
"# ); gloo::utils::document() .query_selector(".take-a-break2") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
wait...(inner)
"# ); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
"# ); } #[wasm_bindgen_test] async fn effects_not_run_when_suspended() { #[derive(PartialEq)] pub struct SleepState { s: Suspension, } impl SleepState { fn new() -> Self { let (s, handle) = Suspension::new(); spawn_local(async move { sleep(Duration::from_millis(50)).await; handle.resume(); }); Self { s } } } impl Reducible for SleepState { type Action = (); fn reduce(self: Rc, _action: Self::Action) -> Rc { Self::new().into() } } #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); if sleep_state.s.resumed() { Ok(Rc::new(move || sleep_state.dispatch(()))) } else { Err(sleep_state.s.clone()) } } #[derive(Properties, Clone)] struct Props { counter: Rc>, } impl PartialEq for Props { fn eq(&self, _rhs: &Self) -> bool { true } } #[component(Content)] fn content(props: &Props) -> HtmlResult { { let counter = props.counter.clone(); use_effect(move || { let mut counter = counter.borrow_mut(); *counter += 1; || {} }); } let resleep = use_sleep()?; let value = use_state(|| 0); let on_increment = { let value = value.clone(); Callback::from(move |_: MouseEvent| { value.set(*value + 1); }) }; let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); Ok(html! {
{*value}
}) } #[component(App)] fn app(props: &Props) -> Html { let fallback = html! {
{"wait..."}
}; html! {
} } let counter = Rc::new(RefCell::new(0_u64)); let props = Props { counter: counter.clone(), }; yew::Renderer::::with_root_and_props( gloo::utils::document().get_element_by_id("output").unwrap(), props, ) .render(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); assert_eq!(*counter.borrow(), 0); // effects not called. sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
0
"# ); assert_eq!(*counter.borrow(), 1); // effects ran 1 time. sleep(Duration::from_millis(10)).await; gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); scheduler::flush().await; gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); scheduler::flush().await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
2
"# ); assert_eq!(*counter.borrow(), 3); // effects ran 3 times. gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() .dyn_into::() .unwrap() .click(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); assert_eq!(*counter.borrow(), 3); // effects ran 3 times. sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
2
"# ); assert_eq!(*counter.borrow(), 4); // effects ran 4 times. } #[wasm_bindgen_test] async fn use_suspending_future_works() { #[component(Content)] fn content() -> HtmlResult { let _sleep_handle = use_future(|| async move { sleep(Duration::from_millis(50)).await; })?; Ok(html! {
{"Content"}
}) } #[component(App)] fn app() -> Html { let fallback = html! {
{"wait..."}
}; html! {
} } yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!(result.as_str(), r#"
Content
"#); } #[wasm_bindgen_test] async fn use_suspending_future_with_deps_works() { #[derive(PartialEq, Properties)] struct ContentProps { delay_millis: u64, } #[component(Content)] fn content(ContentProps { delay_millis }: &ContentProps) -> HtmlResult { let delayed_result = use_future_with(*delay_millis, |delay_millis| async move { sleep(Duration::from_millis(*delay_millis)).await; 42 })?; Ok(html! {
{*delayed_result}
}) } #[component(App)] fn app() -> Html { let fallback = html! {
{"wait..."}
}; html! {
} } yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!(result.as_str(), r#"
42
"#); } #[wasm_bindgen_test] async fn test_suspend_forever() { /// A component that its suspension never resumes. /// We test that this can be used with to trigger a suspension and unsuspend upon unmount. #[component] fn SuspendForever() -> HtmlResult { let (s, handle) = Suspension::new(); use_state(move || handle); Err(s.into()) } #[component] fn App() -> Html { let page = use_state(|| 1); { let page_setter = page.setter(); use_effect_with((), move |_| { spawn_local(async move { sleep(Duration::from_secs(1)).await; page_setter.set(2); }); }); } let content = if *page == 1 { html! { } } else { html! {
{"OK"}
} }; html! { {"Loading..."}}}> {content} } } yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); sleep(Duration::from_millis(1500)).await; let result = obtain_result(); assert_eq!(result.as_str(), r#"OK"#); } #[wasm_bindgen_test] async fn resume_after_unmount() { #[derive(Clone, Properties, PartialEq)] struct ContentProps { state: UseStateHandle, } #[component(Content)] fn content(ContentProps { state }: &ContentProps) -> HtmlResult { let state = state.clone(); let _sleep_handle = use_future(|| async move { sleep(Duration::from_millis(50)).await; state.set(false); sleep(Duration::from_millis(50)).await; })?; Ok(html! {
{"Content"}
}) } #[component(App)] fn app() -> Html { let fallback = html! {
{"wait..."}
}; let state = use_state(|| true); html! {
if *state { } else {
{"Content replacement"}
}
} } yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); sleep(Duration::from_millis(25)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
wait...
"); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
Content replacement
"); } #[wasm_bindgen_test] async fn test_duplicate_suspension() { use yew::html::ChildrenProps; #[component] fn FetchingProvider(props: &ChildrenProps) -> HtmlResult { use_future(|| async { scheduler::flush().await; })?; Ok(html! { <>{props.children.clone()} }) } #[component] fn Child() -> Html { html! {
{"hello!"}
} } #[component] fn App() -> Html { let fallback = Html::default(); html! { } } yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!(result.as_str(), "hello!"); } ================================================ FILE: packages/yew/tests/use_callback.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] use std::sync::atomic::{AtomicBool, Ordering}; mod common; use common::obtain_result; use wasm_bindgen_test::*; use yew::prelude::*; use yew::scheduler; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_callback_works() { #[derive(Properties, PartialEq)] struct Props { callback: Callback, } #[component(MyComponent)] fn my_component(props: &Props) -> Html { let greeting = props.callback.emit("Yew".to_string()); static CTR: AtomicBool = AtomicBool::new(false); if CTR.swap(true, Ordering::Relaxed) { panic!("multiple times rendered!"); } html! {
{"The test output is: "}
{&greeting}
{"\n"}
} } #[component(UseCallbackComponent)] fn use_callback_comp() -> Html { let state = use_state(|| 0); let callback = use_callback((), move |name, _| format!("Hello, {}!", name)); use_effect(move || { if *state < 5 { state.set(*state + 1); } || {} }); html! {
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "Hello, Yew!"); } ================================================ FILE: packages/yew/tests/use_context.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] mod common; use std::rc::Rc; use common::obtain_result_by_id; use wasm_bindgen_test::*; use yew::prelude::*; use yew::scheduler; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_context_scoping_works() { #[derive(Clone, Debug, PartialEq)] struct ExampleContext(String); #[component] fn ExpectNoContextComponent() -> Html { let example_context = use_context::(); if example_context.is_some() { console_log!( "Context should be None here, but was {:?}!", example_context ); }; html! {
} } #[component] fn UseContextComponent() -> Html { type ExampleContextProvider = ContextProvider; html! {
{"ignored"}
{"ignored"}
{"ignored"}
} } #[component] fn UseContextComponentInner() -> Html { let context = use_context::(); html! {
{ &context.unwrap().0 }
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result: String = obtain_result_by_id("result"); assert_eq!("correct", result); } #[wasm_bindgen_test] async fn use_context_works_with_multiple_types() { #[derive(Clone, Debug, PartialEq)] struct ContextA(u32); #[derive(Clone, Debug, PartialEq)] struct ContextB(u32); #[component] fn Test1() -> Html { let ctx_a = use_context::(); let ctx_b = use_context::(); assert_eq!(ctx_a, Some(ContextA(2))); assert_eq!(ctx_b, Some(ContextB(1))); html! {} } #[component] fn Test2() -> Html { let ctx_a = use_context::(); let ctx_b = use_context::(); assert_eq!(ctx_a, Some(ContextA(0))); assert_eq!(ctx_b, Some(ContextB(1))); html! {} } #[component] fn Test3() -> Html { let ctx_a = use_context::(); let ctx_b = use_context::(); assert_eq!(ctx_a, Some(ContextA(0))); assert_eq!(ctx_b, None); html! {} } #[component] fn Test4() -> Html { let ctx_a = use_context::(); let ctx_b = use_context::(); assert_eq!(ctx_a, None); assert_eq!(ctx_b, None); html! {} } #[component] fn TestComponent() -> Html { type ContextAProvider = ContextProvider; type ContextBProvider = ContextProvider; html! {
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; } #[wasm_bindgen_test] async fn use_context_update_works() { #[derive(Clone, Debug, PartialEq)] struct MyContext(String); #[derive(Clone, Debug, PartialEq, Properties)] struct RenderCounterProps { id: String, children: Children, } #[component] fn RenderCounter(props: &RenderCounterProps) -> Html { let counter = use_mut_ref(|| 0); *counter.borrow_mut() += 1; html! { <>
{ format!("total: {}", counter.borrow()) }
{ props.children.clone() } } } #[derive(Clone, Debug, PartialEq, Properties)] struct ContextOutletProps { id: String, #[prop_or_default] magic: usize, } #[component] fn ContextOutlet(props: &ContextOutletProps) -> Html { let counter = use_mut_ref(|| 0); *counter.borrow_mut() += 1; let ctx = use_context::>().expect("context not passed down"); html! { <>
{ format!("magic: {}\n", props.magic) }
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) }
} } #[component] fn TestComponent() -> Html { type MyContextProvider = ContextProvider>; let ctx = use_state(|| MyContext("hello".into())); let rendered = use_mut_ref(|| 0); // this is used to force an update specific to test-2 let magic_rc = use_state(|| 0); let magic: usize = *magic_rc; { let ctx = ctx.clone(); use_effect(move || { let count = *rendered.borrow(); match count { 0 => { ctx.set(MyContext("world".into())); *rendered.borrow_mut() += 1; } 1 => { // force test-2 to re-render. magic_rc.set(1); *rendered.borrow_mut() += 1; } 2 => { ctx.set(MyContext("hello world!".into())); *rendered.borrow_mut() += 1; } _ => (), }; || {} }); } html! { } } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; // 1 initial render + 1 magic assert_eq!(obtain_result_by_id("test-0"), "total: 2"); // 1 initial + 2 context update assert_eq!( obtain_result_by_id("test-1"), "current: hello world!, total: 3" ); // 1 initial + 1 context update + 1 magic update + 1 context update assert_eq!( obtain_result_by_id("test-2"), "current: hello world!, total: 4" ); } ================================================ FILE: packages/yew/tests/use_effect.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] mod common; use std::ops::{Deref, DerefMut}; use std::rc::Rc; use common::obtain_result; use wasm_bindgen_test::*; use yew::prelude::*; use yew::scheduler; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_effect_destroys_on_component_drop() { #[derive(Properties, Clone)] struct WrapperProps { destroy_called: Rc, } impl PartialEq for WrapperProps { fn eq(&self, _other: &Self) -> bool { false } } #[derive(Properties, Clone)] struct FunctionProps { effect_called: Rc, destroy_called: Rc, } impl PartialEq for FunctionProps { fn eq(&self, _other: &Self) -> bool { false } } #[component(UseEffectComponent)] fn use_effect_comp(props: &FunctionProps) -> Html { let effect_called = props.effect_called.clone(); let destroy_called = props.destroy_called.clone(); use_effect_with((), move |_| { effect_called(); #[allow(clippy::redundant_closure)] // Otherwise there is a build error move || destroy_called() }); html! {} } #[component(UseEffectWrapperComponent)] fn use_effect_wrapper_comp(props: &WrapperProps) -> Html { let show = use_state(|| true); if *show { let effect_called: Rc = { Rc::new(move || show.set(false)) }; html! { } } else { html! {
{ "EMPTY" }
} } } let destroy_counter = Rc::new(std::cell::RefCell::new(0)); let destroy_counter_c = destroy_counter.clone(); yew::Renderer::::with_root_and_props( gloo::utils::document().get_element_by_id("output").unwrap(), WrapperProps { destroy_called: Rc::new(move || *destroy_counter_c.borrow_mut().deref_mut() += 1), }, ) .render(); scheduler::flush().await; assert_eq!(1, *destroy_counter.borrow().deref()); } #[wasm_bindgen_test] async fn use_effect_works_many_times() { #[component(UseEffectComponent)] fn use_effect_comp() -> Html { let counter = use_state(|| 0); let counter_clone = counter.clone(); use_effect_with(*counter, move |_| { if *counter_clone < 4 { counter_clone.set(*counter_clone + 1); } || {} }); html! {
{ "The test result is" }
{ *counter }
{ "\n" }
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "4"); } #[wasm_bindgen_test] async fn use_effect_works_once() { #[component(UseEffectComponent)] fn use_effect_comp() -> Html { let counter = use_state(|| 0); let counter_clone = counter.clone(); use_effect_with((), move |_| { counter_clone.set(*counter_clone + 1); || panic!("Destructor should not have been called") }); html! {
{ "The test result is" }
{ *counter }
{ "\n" }
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "1"); } #[wasm_bindgen_test] async fn use_effect_refires_on_dependency_change() { #[component(UseEffectComponent)] fn use_effect_comp() -> Html { let number_ref = use_mut_ref(|| 0); let number_ref_c = number_ref.clone(); let number_ref2 = use_mut_ref(|| 0); let number_ref2_c = number_ref2.clone(); let arg = *number_ref.borrow_mut().deref_mut(); let counter = use_state(|| 0); use_effect_with(arg, move |dep| { let mut ref_mut = number_ref_c.borrow_mut(); let inner_ref_mut = ref_mut.deref_mut(); if *inner_ref_mut < 1 { *inner_ref_mut += 1; assert_eq!(dep, &0); } else { assert_eq!(dep, &1); } counter.set(10); // we just need to make sure it does not panic move || { counter.set(11); *number_ref2_c.borrow_mut().deref_mut() += 1; } }); html! {
{"The test result is"}
{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}
{"\n"}
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result: String = obtain_result(); assert_eq!(result.as_str(), "11"); } ================================================ FILE: packages/yew/tests/use_memo.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] use std::sync::atomic::{AtomicBool, Ordering}; mod common; use common::obtain_result; use wasm_bindgen_test::*; use yew::prelude::*; use yew::scheduler; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_memo_works() { #[component(UseMemoComponent)] fn use_memo_comp() -> Html { let state = use_state(|| 0); let memoed_val = use_memo((), |_| { static CTR: AtomicBool = AtomicBool::new(false); if CTR.swap(true, Ordering::Relaxed) { panic!("multiple times rendered!"); } "true" }); use_effect(move || { if *state < 5 { state.set(*state + 1); } || {} }); html! {
{"The test output is: "}
{*memoed_val}
{"\n"}
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "true"); } ================================================ FILE: packages/yew/tests/use_prepared_state.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] #![cfg(feature = "hydration")] #![cfg_attr(nightly_yew, feature(async_closure))] use std::time::Duration; mod common; use common::obtain_result_by_id; use wasm_bindgen_test::*; use yew::platform::time::sleep; use yew::prelude::*; use yew::{scheduler, Renderer, ServerRenderer}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_prepared_state_works() { #[component] fn Comp() -> HtmlResult { let ctr = use_prepared_state!((), |_| -> u32 { 12345 })?.unwrap_or_default(); Ok(html! {
{*ctr}
}) } #[component] fn App() -> Html { html! {
} } let s = ServerRenderer::::new().render().await; assert_eq!( s, r#"
12345
"# ); gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(200)).await; let result = obtain_result_by_id("output"); // no placeholders, hydration is successful and state 12345 is preserved. assert_eq!(result, r#"
12345
"#); } #[wasm_bindgen_test] async fn use_prepared_state_with_suspension_works() { #[component] fn Comp() -> HtmlResult { let ctr = use_prepared_state!((), async move |_| -> u32 { 12345 })?.unwrap_or_default(); Ok(html! {
{*ctr}
}) } #[component] fn App() -> Html { html! {
} } let s = ServerRenderer::::new().render().await; assert_eq!( s, r#"
12345
"# ); gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(200)).await; let result = obtain_result_by_id("output"); // no placeholders, hydration is successful and state 12345 is preserved. assert_eq!(result, r#"
12345
"#); } ================================================ FILE: packages/yew/tests/use_reducer.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] use std::collections::HashSet; use std::rc::Rc; use gloo::utils::document; use wasm_bindgen::JsCast; use wasm_bindgen_test::*; use web_sys::HtmlElement; use yew::prelude::*; use yew::scheduler; mod common; use common::obtain_result; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[derive(Debug)] struct CounterState { counter: i32, } impl Reducible for CounterState { type Action = i32; fn reduce(self: Rc, action: Self::Action) -> Rc { Self { counter: self.counter + action, } .into() } } #[wasm_bindgen_test] async fn use_reducer_works() { #[component(UseReducerComponent)] fn use_reducer_comp() -> Html { let counter = use_reducer(|| CounterState { counter: 10 }); let counter_clone = counter.clone(); use_effect_with((), move |_| { counter_clone.dispatch(1); || {} }); html! {
{"The test result is"}
{counter.counter}
{"\n"}
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "11"); } #[derive(Debug, Clone, PartialEq)] struct ContentState { content: HashSet, } impl Reducible for ContentState { type Action = String; fn reduce(self: Rc, action: Self::Action) -> Rc { let mut self_: Self = (*self).clone(); self_.content.insert(action); self_.into() } } #[wasm_bindgen_test] async fn use_reducer_eq_works() { #[component(UseReducerComponent)] fn use_reducer_comp() -> Html { let content = use_reducer_eq(|| ContentState { content: HashSet::default(), }); let render_count = use_mut_ref(|| 0); let render_count = { let mut render_count = render_count.borrow_mut(); *render_count += 1; *render_count }; let add_content_a = { let content = content.clone(); Callback::from(move |_| content.dispatch("A".to_string())) }; let add_content_b = Callback::from(move |_| content.dispatch("B".to_string())); html! { <>
{"This component has been rendered: "}{render_count}{" Time(s)."}
} } yew::Renderer::::with_root( document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "1"); document() .get_element_by_id("add-a") .unwrap() .unchecked_into::() .click(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "2"); document() .get_element_by_id("add-a") .unwrap() .unchecked_into::() .click(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "2"); document() .get_element_by_id("add-b") .unwrap() .unchecked_into::() .click(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "3"); document() .get_element_by_id("add-b") .unwrap() .unchecked_into::() .click(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "3"); } enum SometimesChangeAction { /// If this action is sent, the state will remain the same Keep, /// If this action is sent, the state will change Change, } /// A state that does not implement PartialEq #[derive(Clone)] struct SometimesChangingState { value: i32, } impl Reducible for SometimesChangingState { type Action = SometimesChangeAction; fn reduce(self: Rc, action: Self::Action) -> Rc { use SometimesChangeAction::*; match action { Keep => self, Change => { let mut self_: Self = (*self).clone(); self_.value += 1; self_.into() } } } } #[wasm_bindgen_test] async fn use_reducer_does_not_rerender_when_rc_is_reused() { #[component(UseReducerComponent)] fn use_reducer_comp() -> Html { let state = use_reducer(|| SometimesChangingState { value: 0 }); let render_count = use_mut_ref(|| 0); let render_count = { let mut render_count = render_count.borrow_mut(); *render_count += 1; *render_count }; let keep_state = { let state = state.clone(); Callback::from(move |_| state.dispatch(SometimesChangeAction::Keep)) }; let change_state = Callback::from(move |_| state.dispatch(SometimesChangeAction::Change)); html! { <>
{"This component has been rendered: "}{render_count}{" Time(s)."}
} } yew::Renderer::::with_root( document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "1"); document() .get_element_by_id("change-state") .unwrap() .unchecked_into::() .click(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "2"); document() .get_element_by_id("keep-state") .unwrap() .unchecked_into::() .click(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "2"); } ================================================ FILE: packages/yew/tests/use_ref.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] mod common; use std::ops::DerefMut; use common::obtain_result; use wasm_bindgen_test::*; use yew::prelude::*; use yew::scheduler; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_ref_works() { #[component(UseRefComponent)] fn use_ref_comp() -> Html { let ref_example = use_mut_ref(|| 0); *ref_example.borrow_mut().deref_mut() += 1; let counter = use_state(|| 0); if *counter < 5 { counter.set(*counter + 1) } html! {
{"The test output is: "}
{*ref_example.borrow_mut().deref_mut() > 4}
{"\n"}
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "true"); } ================================================ FILE: packages/yew/tests/use_state.rs ================================================ #![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] mod common; use common::obtain_result; use wasm_bindgen_test::*; use yew::prelude::*; use yew::scheduler; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_state_works() { #[component(UseComponent)] fn use_state_comp() -> Html { let counter = use_state(|| 0); if *counter < 5 { counter.set(*counter + 1) } html! {
{"Test Output: "}
{*counter}
{"\n"}
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "5"); } #[wasm_bindgen_test] async fn multiple_use_state_setters() { #[component(UseComponent)] fn use_state_comp() -> Html { let counter = use_state(|| 0); let counter_clone = counter.clone(); use_effect_with((), move |_| { // 1st location counter_clone.set(*counter_clone + 1); || {} }); let another_scope = { let counter = counter.clone(); move || { if *counter < 11 { // 2nd location counter.set(*counter + 10) } } }; another_scope(); html! {
{ "Test Output: " } // expected output
{ *counter }
{ "\n" }
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "11"); } #[wasm_bindgen_test] async fn use_state_eq_works() { use std::sync::atomic::{AtomicUsize, Ordering}; static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0); #[component(UseComponent)] fn use_state_comp() -> Html { RENDER_COUNT.fetch_add(1, Ordering::Relaxed); let counter = use_state_eq(|| 0); counter.set(1); html! {
{"Test Output: "}
{*counter}
{"\n"}
} } yew::Renderer::::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); scheduler::flush().await; let result = obtain_result(); assert_eq!(result.as_str(), "1"); assert_eq!(RENDER_COUNT.load(Ordering::Relaxed), 2); } /// Exercises the exact pattern that causes use-after-free in the original PR #3963 /// fix, where `UseReducerHandle::deref()` drops the `Ref` guard but returns a /// pointer derived from it. /// /// The dangerous sequence within a single callback: /// 1. `handle.set(v1)` — dispatch puts a *new* `Rc` (refcount=1) in the shared `RefCell`, /// replacing the one from render time. /// 2. `let r: &T = &*handle` — `deref()` borrows the RefCell, grabs a raw pointer into the Rc /// (refcount still 1), and **drops the `Ref` guard**. /// 3. `handle.set(v2)` — dispatch replaces that Rc. Because its refcount was 1, it is freed. `r` /// is now dangling. /// 4. Allocate objects of similar size to encourage the allocator to reuse the freed memory, /// overwriting the old `T`. /// 5. Read through `r` — **use-after-free**. /// /// With the `deref_history` fix, step 2 clones the Rc into a `Vec` kept alive by /// the handle, bumping the refcount to 2. Step 3 only drops it to 1, so the /// allocation survives and `r` remains valid. #[wasm_bindgen_test] async fn deref_remains_valid_across_multiple_dispatches_in_callback() { use std::cell::RefCell; use gloo::utils::document; use wasm_bindgen::JsCast; use web_sys::HtmlElement; thread_local! { static DEREF_RESULT: RefCell> = const { RefCell::new(None) }; } #[component(UBTestComponent)] fn ub_test_comp() -> Html { let state = use_state(|| "initial".to_string()); let trigger = { let state = state.clone(); Callback::from(move |_| { // Step 1: dispatch. The RefCell now contains a *new* Rc whose only // owner is the RefCell itself (refcount = 1). state.set("first_dispatch".to_string()); // Step 2: deref. In the original fix the Ref guard is dropped // immediately, leaving us with a bare pointer into the refcount-1 // Rc. With deref_history, the Rc is cloned into the Vec so the // refcount is bumped to 2. let borrowed: &String = &*state; // Step 3: dispatch again. The RefCell's old Rc is replaced. // Original fix: refcount was 1 → drops to 0 → freed → `borrowed` // dangles. // deref_history fix: refcount was 2 → drops to 1 (still in Vec) // → allocation survives → `borrowed` is valid. state.set("second_dispatch".to_string()); // Step 4: churn the allocator. Create and drop many heap objects // of ~32 bytes (the size of the freed Rc+UseStateReducer+String // struct on wasm32) to maximize the chance that the allocator // hands out the freed address to one of these, overwriting the // memory `borrowed` points into. for _ in 0..256 { // Each Box<[u8; 32]> is roughly the same size as the freed Rc // allocation containing UseStateReducer. let overwrite = Box::new([0xFFu8; 32]); std::hint::black_box(&*overwrite); drop(overwrite); } // Also allocate Strings whose *buffers* might reuse the freed // String buffer from step 1. let _noise: Vec = (0..64).map(|i| format!("noise_{:032}", i)).collect(); // Step 5: read through the potentially-dangling reference. // With the original fix this is UB: the memory behind `borrowed` // may have been reused by the allocations above, so `.clone()` // could read a garbage ptr/len/cap triple and trap, or silently // return corrupted data. // With deref_history, this always reads "first_dispatch". let value = borrowed.clone(); DEREF_RESULT.with(|r| { *r.borrow_mut() = Some(value); }); }) }; html! {
{(*state).clone()}
} } yew::Renderer::::with_root(document().get_element_by_id("output").unwrap()) .render(); scheduler::flush().await; // Fire the callback document() .get_element_by_id("ub-trigger") .unwrap() .unchecked_into::() .click(); scheduler::flush().await; // The reference obtained between the two dispatches must still read the // value from the first dispatch, not garbage or "second_dispatch". let captured = DEREF_RESULT.with(|r| r.borrow().clone()); assert_eq!( captured, Some("first_dispatch".to_string()), "deref() reference must remain valid across subsequent dispatches" ); } /// Regression test for issue #3796 /// Tests that state handles always read the latest value even when accessed /// from callbacks before a rerender occurs. /// /// The bug occurred when: /// 1. State A is updated via set() /// 2. State B is updated via set() /// 3. A callback reads both states before rerender /// 4. The callback would see stale value for B because the handle was caching a snapshot instead of /// reading from the shared RefCell #[wasm_bindgen_test] async fn use_state_handles_read_latest_value_issue_3796() { use std::cell::RefCell; use gloo::utils::document; use wasm_bindgen::JsCast; use web_sys::HtmlElement; // Shared storage for the values read by the submit handler thread_local! { static CAPTURED_VALUES: RefCell> = const { RefCell::new(None) }; } #[component(FormComponent)] fn form_comp() -> Html { let field_a = use_state(String::new); let field_b = use_state(String::new); let update_a = { let field_a = field_a.clone(); Callback::from(move |_| { field_a.set("value_a".to_string()); }) }; let update_b = { let field_b = field_b.clone(); Callback::from(move |_| { field_b.set("value_b".to_string()); }) }; // This callback reads both states - the bug caused field_b to be stale let submit = { let field_a = field_a.clone(); let field_b = field_b.clone(); Callback::from(move |_| { let a = (*field_a).clone(); let b = (*field_b).clone(); CAPTURED_VALUES.with(|v| { *v.borrow_mut() = Some((a.clone(), b.clone())); }); }) }; html! {
{format!("a={}, b={}", *field_a, *field_b)}
} } yew::Renderer::::with_root(document().get_element_by_id("output").unwrap()) .render(); scheduler::flush().await; // Initial state let result = obtain_result(); assert_eq!(result.as_str(), "a=, b="); // Click update-a, then update-b, then submit WITHOUT waiting for rerender. // This simulates rapid user interaction (like the Firefox bug in issue #3796). document() .get_element_by_id("update-a") .unwrap() .unchecked_into::() .click(); document() .get_element_by_id("update-b") .unwrap() .unchecked_into::() .click(); document() .get_element_by_id("submit") .unwrap() .unchecked_into::() .click(); // Now wait for rerenders to complete scheduler::flush().await; // Check the values captured by the submit handler. // Before the fix, field_b would be empty because the callback captured a stale handle. let captured = CAPTURED_VALUES.with(|v| v.borrow().clone()); assert_eq!( captured, Some(("value_a".to_string(), "value_b".to_string())), "Submit handler should see latest values for both fields" ); // Also verify the DOM shows correct values after rerender let result = obtain_result(); assert_eq!(result.as_str(), "a=value_a, b=value_b"); } /// Regression test for issue #4058 /// /// When a UseStateHandle is passed as a prop to a child component, updating the /// state should cause the child to re-render. After the deref_history change /// (PR #3988), UseReducerHandle::eq dereferences both old and new handles, but /// since Deref now always reads from the shared RefCell, both sides resolve to /// the latest value, making eq always return true and preventing child re-renders. #[wasm_bindgen_test] async fn use_state_handle_as_prop_triggers_child_rerender_issue_4058() { use std::sync::atomic::{AtomicUsize, Ordering}; use gloo::utils::document; use wasm_bindgen::JsCast; use web_sys::HtmlElement; static CHILD_RENDER_COUNT: AtomicUsize = AtomicUsize::new(0); #[derive(Properties, PartialEq)] struct ChildProps { handle: UseStateHandle, } #[component(ChildComponent)] fn child_comp(props: &ChildProps) -> Html { CHILD_RENDER_COUNT.fetch_add(1, Ordering::Relaxed); let onclick = { let handle = props.handle.clone(); Callback::from(move |_| { handle.set(*handle + 1); }) }; html! {
{ *props.handle }
} } #[component(ParentComponent)] fn parent_comp() -> Html { let state = use_state(|| 0); html! { } } CHILD_RENDER_COUNT.store(0, Ordering::Relaxed); yew::Renderer::::with_root(document().get_element_by_id("output").unwrap()) .render(); scheduler::flush().await; // Initial render: child should show 0 let result = obtain_result(); assert_eq!(result.as_str(), "0"); assert_eq!(CHILD_RENDER_COUNT.load(Ordering::Relaxed), 1); // Click the increment button in the child document() .get_element_by_id("child-increment") .unwrap() .unchecked_into::() .click(); scheduler::flush().await; // After increment: child should re-render and show 1 let result = obtain_result(); assert_eq!( result.as_str(), "1", "Child component must re-render when UseStateHandle prop changes (issue #4058)" ); assert!( CHILD_RENDER_COUNT.load(Ordering::Relaxed) >= 2, "Child must have re-rendered at least twice, but rendered {} times", CHILD_RENDER_COUNT.load(Ordering::Relaxed) ); } ================================================ FILE: packages/yew/tests/use_transitive_state.rs ================================================ #![cfg(feature = "hydration")] #![cfg(target_arch = "wasm32")] use std::time::Duration; mod common; use common::obtain_result_by_id; use wasm_bindgen_test::*; use yew::platform::time::sleep; use yew::prelude::*; use yew::{scheduler, Renderer, ServerRenderer}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_transitive_state_works() { #[component] fn Comp() -> HtmlResult { let ctr = use_transitive_state!((), |_| -> u32 { 12345 })?.unwrap_or_default(); Ok(html! {
{*ctr}
}) } #[component] fn App() -> Html { html! {
} } let s = ServerRenderer::::new().render().await; assert_eq!( s, // div text content should be 0 but state should be 12345. r#"
0
"# ); gloo::utils::document() .query_selector("#output") .unwrap() .unwrap() .set_inner_html(&s); scheduler::flush().await; Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(200)).await; let result = obtain_result_by_id("output"); // no placeholders, hydration is successful and div text content now becomes 12345. assert_eq!(result, r#"
12345
"#); } ================================================ FILE: packages/yew-agent/Cargo.toml ================================================ [package] name = "yew-agent" version = "0.5.0" authors = ["Hamza "] repository = "https://github.com/yewstack/yew" homepage = "https://yew.rs" documentation = "https://docs.rs/yew/" edition = "2021" readme = "../../README.md" description = "Agents for Yew" license = "MIT OR Apache-2.0" rust-version = "1.84.0" [dependencies] yew = { version = "0.23.0", path = "../yew" } wasm-bindgen.workspace = true js-sys.workspace = true pinned = "0.1.0" thiserror.workspace = true bincode = { workspace = true } wasm-bindgen-futures.workspace = true serde = { workspace = true, features = ["derive"] } futures.workspace = true yew-agent-macro = { version = "0.4", path = "../yew-agent-macro" } [dependencies.web-sys] workspace = true features = [ "Blob", "BlobPropertyBag", "DedicatedWorkerGlobalScope", "MessageEvent", "Url", "Worker", "WorkerOptions", "WorkerType" ] [dev-dependencies] serde = { workspace = true } ================================================ FILE: packages/yew-agent/README.md ================================================ # Yew Agent This module contains Yew's web worker implementation. ## Types There're a couple kinds of agents: ### Oneshot A kind of agent that for each input, a single output is returned. #### Reactor A kind of agent that can send many inputs and receive many outputs over a single bridge. #### Worker The low-level implementation of agents that provides an actor model and communicates with multiple bridges. ## Reachability When an agent is spawned, each agent is associated with a reachability. ### Private Each time a bridge is created, a new instance of agent is spawned. This allows parallel computing between agents. #### Public Public agents are shared among all children of a provider. Only 1 instance will be spawned for each public agents provider. ### Provider Each Agent requires a provider to provide communications and maintain bridges. All hooks must be called within a provider. ## Communications with Agents Hooks provides means to communicate with agent instances. ### Bridge See: [`use_worker_bridge`](worker::use_worker_bridge), [`use_reactor_bridge`](reactor::use_reactor_bridge) A bridge takes a callback to receive outputs from agents and provides a handle to send inputs to agents. #### Subscription See: [`use_worker_subscription`](worker::use_worker_subscription), [`use_reactor_subscription`](reactor::use_reactor_subscription) Similar to bridges, a subscription produces a handle to send inputs to agents. However, instead of notifying the receiver with a callback, it collect all outputs into a slice. #### Runner See: [`use_oneshot_runner`](oneshot::use_oneshot_runner) Unlike other agents, oneshot bridges provide a `use_oneshot_runner` hook to execute oneshot agents on demand. ================================================ FILE: packages/yew-agent/src/codec.rs ================================================ //! Submodule providing the `Codec` trait and its default implementation using `bincode`. use js_sys::Uint8Array; use serde::{Deserialize, Serialize}; use wasm_bindgen::JsValue; /// Message Encoding and Decoding Format pub trait Codec { /// Encode an input to JsValue fn encode(input: I) -> JsValue where I: Serialize; /// Decode a message to a type fn decode(input: JsValue) -> O where O: for<'de> Deserialize<'de>; } /// Default message encoding with [bincode]. #[derive(Debug)] pub struct Bincode; impl Codec for Bincode { fn encode(input: I) -> JsValue where I: Serialize, { let buf = bincode::serde::encode_to_vec(&input, bincode::config::standard()) .expect("can't serialize an worker message"); Uint8Array::from(buf.as_slice()).into() } fn decode(input: JsValue) -> O where O: for<'de> Deserialize<'de>, { let data = Uint8Array::from(input).to_vec(); let (result, _) = bincode::serde::decode_from_slice(&data, bincode::config::standard()) .expect("can't deserialize an worker message"); result } } ================================================ FILE: packages/yew-agent/src/lib.rs ================================================ #![doc = include_str!("../README.md")] #![deny( clippy::all, missing_docs, missing_debug_implementations, bare_trait_objects, anonymous_parameters, elided_lifetimes_in_paths )] extern crate self as yew_agent; pub mod codec; pub mod oneshot; pub mod reactor; pub mod worker; pub use codec::{Bincode, Codec}; pub mod traits; pub use traits::{Registrable, Spawnable}; mod reach; pub mod scope_ext; pub use reach::Reach; mod utils; #[doc(hidden)] pub mod __vendored { pub use futures; } pub mod prelude { //! Prelude module to be imported when working with `yew-agent`. //! //! This module re-exports the frequently used types from the crate. pub use crate::oneshot::{oneshot, use_oneshot_runner, UseOneshotRunnerHandle}; pub use crate::reach::Reach; pub use crate::reactor::{ reactor, use_reactor_bridge, use_reactor_subscription, ReactorEvent, ReactorScope, UseReactorBridgeHandle, UseReactorSubscriptionHandle, }; pub use crate::scope_ext::{AgentScopeExt, ReactorBridgeHandle, WorkerBridgeHandle}; pub use crate::worker::{ use_worker_bridge, use_worker_subscription, UseWorkerBridgeHandle, UseWorkerSubscriptionHandle, WorkerScope, }; pub use crate::{Registrable, Spawnable}; } ================================================ FILE: packages/yew-agent/src/oneshot/bridge.rs ================================================ use futures::stream::StreamExt; use pinned::mpsc; use pinned::mpsc::UnboundedReceiver; use super::traits::Oneshot; use super::worker::OneshotWorker; use crate::codec::Codec; use crate::worker::{WorkerBridge, WorkerSpawner}; /// A connection manager for components interaction with oneshot workers. #[derive(Debug)] pub struct OneshotBridge where N: Oneshot + 'static, { inner: WorkerBridge>, rx: UnboundedReceiver, } impl OneshotBridge where N: Oneshot + 'static, { #[inline(always)] pub(crate) fn new( inner: WorkerBridge>, rx: UnboundedReceiver, ) -> Self { Self { inner, rx } } #[inline(always)] pub(crate) fn register_callback( spawner: &mut WorkerSpawner, CODEC>, ) -> UnboundedReceiver where CODEC: Codec, { let (tx, rx) = mpsc::unbounded(); spawner.callback(move |output| { let _ = tx.send_now(output); }); rx } /// Forks the bridge. /// /// This method creates a new bridge that can be used to execute tasks on the same worker /// instance. pub fn fork(&self) -> Self { let (tx, rx) = mpsc::unbounded(); let inner = self.inner.fork(Some(move |output| { let _ = tx.send_now(output); })); Self { inner, rx } } /// Run the current oneshot worker once in the current worker instance. pub async fn run(&mut self, input: N::Input) -> N::Output { // &mut self guarantees that the bridge will be // exclusively borrowed during the time the oneshot worker is running. self.inner.send(input); // For each bridge, there can only be 1 active task running on the worker instance. // The next output will be the output for the input that we just sent. self.rx .next() .await .expect("failed to receive result from worker") } } ================================================ FILE: packages/yew-agent/src/oneshot/hooks.rs ================================================ use yew::prelude::*; use super::provider::OneshotProviderState; use super::Oneshot; /// Hook handle for [`use_oneshot_runner`] #[derive(Debug)] pub struct UseOneshotRunnerHandle where T: Oneshot + 'static, { state: OneshotProviderState, } impl UseOneshotRunnerHandle where T: Oneshot + 'static, { /// Runs an oneshot agent. pub async fn run(&self, input: T::Input) -> T::Output { self.state.create_bridge().run(input).await } } impl Clone for UseOneshotRunnerHandle where T: Oneshot + 'static, { fn clone(&self) -> Self { Self { state: self.state.clone(), } } } impl PartialEq for UseOneshotRunnerHandle where T: Oneshot, { fn eq(&self, rhs: &Self) -> bool { self.state == rhs.state } } /// A hook to create a runner to an oneshot agent. #[hook] pub fn use_oneshot_runner() -> UseOneshotRunnerHandle where T: Oneshot + 'static, { let state = use_context::>().expect("failed to find worker context"); UseOneshotRunnerHandle { state } } ================================================ FILE: packages/yew-agent/src/oneshot/mod.rs ================================================ //! This module provides task agent implementation. mod bridge; mod hooks; mod provider; mod registrar; mod spawner; mod traits; mod worker; pub use bridge::OneshotBridge; pub use hooks::{use_oneshot_runner, UseOneshotRunnerHandle}; pub use provider::OneshotProvider; pub(crate) use provider::OneshotProviderState; pub use registrar::OneshotRegistrar; pub use spawner::OneshotSpawner; pub use traits::Oneshot; /// A procedural macro to create oneshot agents. pub use yew_agent_macro::oneshot; ================================================ FILE: packages/yew-agent/src/oneshot/provider.rs ================================================ use core::fmt; use std::any::type_name; use std::cell::RefCell; use std::rc::Rc; use serde::{Deserialize, Serialize}; use yew::prelude::*; use super::{Oneshot, OneshotBridge, OneshotSpawner}; use crate::utils::get_next_id; use crate::worker::WorkerProviderProps; use crate::{Bincode, Codec, Reach}; pub(crate) struct OneshotProviderState where T: Oneshot + 'static, { id: usize, spawn_bridge_fn: Rc OneshotBridge>, reach: Reach, held_bridge: Rc>>>, } impl fmt::Debug for OneshotProviderState where T: Oneshot, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(type_name::()) } } impl OneshotProviderState where T: Oneshot, { fn get_held_bridge(&self) -> OneshotBridge { let mut held_bridge = self.held_bridge.borrow_mut(); match held_bridge.as_mut() { Some(m) => m.fork(), None => { let bridge = (self.spawn_bridge_fn)(); *held_bridge = Some(bridge.fork()); bridge } } } /// Creates a bridge, uses "fork" for public agents. pub fn create_bridge(&self) -> OneshotBridge { match self.reach { Reach::Public => { let held_bridge = self.get_held_bridge(); held_bridge.fork() } Reach::Private => (self.spawn_bridge_fn)(), } } } impl Clone for OneshotProviderState where T: Oneshot, { fn clone(&self) -> Self { Self { id: self.id, spawn_bridge_fn: self.spawn_bridge_fn.clone(), reach: self.reach, held_bridge: self.held_bridge.clone(), } } } impl PartialEq for OneshotProviderState where T: Oneshot, { fn eq(&self, rhs: &Self) -> bool { self.id == rhs.id } } /// The Oneshot Agent Provider. /// /// This component provides its children access to an oneshot agent. #[component] pub fn OneshotProvider(props: &WorkerProviderProps) -> Html where T: Oneshot + 'static, T::Input: Serialize + for<'de> Deserialize<'de> + 'static, T::Output: Serialize + for<'de> Deserialize<'de> + 'static, C: Codec + 'static, { let WorkerProviderProps { children, path, lazy, module, reach, } = props.clone(); // Creates a spawning function so Codec is can be erased from contexts. let spawn_bridge_fn: Rc OneshotBridge> = { let path = path.clone(); Rc::new(move || { OneshotSpawner::::new() .as_module(module) .encoding::() .spawn(&path) }) }; let state = { use_memo((path, lazy, reach), move |(_path, lazy, reach)| { let state = OneshotProviderState:: { id: get_next_id(), spawn_bridge_fn, reach: *reach, held_bridge: Rc::default(), }; if *reach == Reach::Public && !*lazy { state.get_held_bridge(); } state }) }; html! { > context={(*state).clone()}> {children} >> } } ================================================ FILE: packages/yew-agent/src/oneshot/registrar.rs ================================================ use std::fmt; use serde::de::Deserialize; use serde::ser::Serialize; use super::traits::Oneshot; use super::worker::OneshotWorker; use crate::codec::{Bincode, Codec}; use crate::traits::Registrable; use crate::worker::WorkerRegistrar; /// A registrar for oneshot workers. pub struct OneshotRegistrar where T: Oneshot + 'static, CODEC: Codec + 'static, { inner: WorkerRegistrar, CODEC>, } impl Default for OneshotRegistrar where T: Oneshot + 'static, CODEC: Codec + 'static, { fn default() -> Self { Self::new() } } impl OneshotRegistrar where N: Oneshot + 'static, CODEC: Codec + 'static, { /// Creates a new Oneshot Registrar. pub fn new() -> Self { Self { inner: OneshotWorker::::registrar().encoding::(), } } /// Sets the encoding. pub fn encoding(&self) -> OneshotRegistrar where C: Codec + 'static, { OneshotRegistrar { inner: self.inner.encoding::(), } } /// Registers the worker. pub fn register(&self) where N::Input: Serialize + for<'de> Deserialize<'de>, N::Output: Serialize + for<'de> Deserialize<'de>, { self.inner.register() } } impl fmt::Debug for OneshotRegistrar where T: Oneshot + 'static, CODEC: Codec + 'static, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("OneshotRegistrar<_>").finish() } } ================================================ FILE: packages/yew-agent/src/oneshot/spawner.rs ================================================ use serde::de::Deserialize; use serde::ser::Serialize; use super::bridge::OneshotBridge; use super::traits::Oneshot; use super::worker::OneshotWorker; use crate::codec::{Bincode, Codec}; use crate::worker::WorkerSpawner; /// A spawner to create oneshot workers. #[derive(Debug, Default)] pub struct OneshotSpawner where N: Oneshot + 'static, CODEC: Codec, { inner: WorkerSpawner, CODEC>, } impl OneshotSpawner where N: Oneshot + 'static, CODEC: Codec, { /// Creates a [OneshotSpawner]. pub const fn new() -> Self { Self { inner: WorkerSpawner::, CODEC>::new(), } } /// Sets a new message encoding. pub const fn encoding(&self) -> OneshotSpawner where C: Codec, { OneshotSpawner { inner: WorkerSpawner::, C>::new(), } } /// Indicates that [`spawn`](WorkerSpawner#method.spawn) should expect a /// `path` to a loader shim script (e.g. when using Trunk, created by using /// the [`data-loader-shim`](https://trunkrs.dev/assets/#link-asset-types) /// asset type) and one does not need to be generated. `false` by default. pub fn with_loader(mut self, with_loader: bool) -> Self { self.inner.with_loader(with_loader); self } /// Determines whether the worker will be spawned with /// [`options.type`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker#type) /// set to `module`. `true` by default. /// /// This option should be un-set if the worker was created with the /// `--target no-modules` flag of `wasm-bindgen`. If using Trunk, see the /// [`data-bindgen-target`](https://trunkrs.dev/assets/#link-asset-types) /// asset type. pub fn as_module(mut self, as_module: bool) -> Self { self.inner.as_module(as_module); self } /// Spawns a Oneshot Worker. pub fn spawn(mut self, path: &str) -> OneshotBridge where N::Input: Serialize + for<'de> Deserialize<'de>, N::Output: Serialize + for<'de> Deserialize<'de>, { let rx = OneshotBridge::register_callback(&mut self.inner); let inner = self.inner.spawn(path); OneshotBridge::new(inner, rx) } } ================================================ FILE: packages/yew-agent/src/oneshot/traits.rs ================================================ use std::future::Future; /// A future-based worker that for each input, one output is produced. pub trait Oneshot: Future { /// Incoming message type. type Input; /// Creates an oneshot worker. fn create(input: Self::Input) -> Self; } ================================================ FILE: packages/yew-agent/src/oneshot/worker.rs ================================================ use super::traits::Oneshot; use crate::worker::{HandlerId, Worker, WorkerDestroyHandle, WorkerScope}; pub(crate) enum Message where T: Oneshot, { Finished { handler_id: HandlerId, output: T::Output, }, } pub(crate) struct OneshotWorker where T: 'static + Oneshot, { running_tasks: usize, destruct_handle: Option>, } impl Worker for OneshotWorker where T: 'static + Oneshot, { type Input = T::Input; type Message = Message; type Output = T::Output; fn create(_scope: &WorkerScope) -> Self { Self { running_tasks: 0, destruct_handle: None, } } fn update(&mut self, scope: &WorkerScope, msg: Self::Message) { let Message::Finished { handler_id, output } = msg; self.running_tasks -= 1; scope.respond(handler_id, output); if self.running_tasks == 0 { self.destruct_handle = None; } } fn received(&mut self, scope: &WorkerScope, input: Self::Input, handler_id: HandlerId) { self.running_tasks += 1; scope.send_future(async move { let output = T::create(input).await; Message::Finished { handler_id, output } }); } fn destroy(&mut self, _scope: &WorkerScope, destruct: WorkerDestroyHandle) { if self.running_tasks > 0 { self.destruct_handle = Some(destruct); } } } ================================================ FILE: packages/yew-agent/src/reach.rs ================================================ /// The reachability of an agent. #[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)] pub enum Reach { /// Public Reachability. Public, /// Private Reachability. Private, } ================================================ FILE: packages/yew-agent/src/reactor/bridge.rs ================================================ use std::fmt; use std::pin::Pin; use std::task::{Context, Poll}; use futures::sink::Sink; use futures::stream::{FusedStream, Stream}; use pinned::mpsc; use pinned::mpsc::{UnboundedReceiver, UnboundedSender}; use thiserror::Error; use super::messages::{ReactorInput, ReactorOutput}; use super::scope::ReactorScoped; use super::traits::Reactor; use super::worker::ReactorWorker; use crate::worker::{WorkerBridge, WorkerSpawner}; use crate::Codec; /// A connection manager for components interaction with oneshot workers. /// /// As this type implements [Stream] + [Sink], it can be split with [`StreamExt::split`]. pub struct ReactorBridge where R: Reactor + 'static, { inner: WorkerBridge>, rx: UnboundedReceiver<::Output>, } impl fmt::Debug for ReactorBridge where R: Reactor, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("ReactorBridge<_>") } } impl ReactorBridge where R: Reactor + 'static, { #[inline(always)] pub(crate) fn new( inner: WorkerBridge>, rx: UnboundedReceiver<::Output>, ) -> Self { Self { inner, rx } } pub(crate) fn output_callback( tx: &UnboundedSender<::Output>, output: ReactorOutput<::Output>, ) { match output { ReactorOutput::Output(m) => { let _ = tx.send_now(m); } ReactorOutput::Finish => { tx.close_now(); } } } #[inline(always)] pub(crate) fn register_callback( spawner: &mut WorkerSpawner, CODEC>, ) -> UnboundedReceiver<::Output> where CODEC: Codec, { let (tx, rx) = mpsc::unbounded(); spawner.callback(move |output| Self::output_callback(&tx, output)); rx } /// Forks the bridge. /// /// This method creates a new bridge connected to a new reactor on the same worker instance. pub fn fork(&self) -> Self { let (tx, rx) = mpsc::unbounded(); let inner = self .inner .fork(Some(move |output| Self::output_callback(&tx, output))); Self { inner, rx } } /// Sends an input to the current reactor. pub fn send_input(&self, msg: ::Input) { self.inner.send(ReactorInput::Input(msg)); } } impl Stream for ReactorBridge where R: Reactor + 'static, { type Item = ::Output; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.rx).poll_next(cx) } fn size_hint(&self) -> (usize, Option) { self.rx.size_hint() } } impl FusedStream for ReactorBridge where R: Reactor + 'static, { fn is_terminated(&self) -> bool { self.rx.is_terminated() } } /// An error type for bridge sink. #[derive(Error, Clone, PartialEq, Eq, Debug)] pub enum ReactorBridgeSinkError { /// A bridge is an RAII Guard, it can only be closed by dropping the value. #[error("attempting to close the bridge via the sink")] AttemptClosure, } impl Sink<::Input> for ReactorBridge where R: Reactor + 'static, { type Error = ReactorBridgeSinkError; fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Err(ReactorBridgeSinkError::AttemptClosure)) } fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn start_send( self: Pin<&mut Self>, item: ::Input, ) -> Result<(), Self::Error> { self.send_input(item); Ok(()) } } ================================================ FILE: packages/yew-agent/src/reactor/hooks.rs ================================================ use std::any::type_name; use std::fmt; use std::ops::Deref; use std::rc::Rc; use futures::sink::SinkExt; use futures::stream::{SplitSink, StreamExt}; use wasm_bindgen::UnwrapThrowExt; use yew::platform::pinned::RwLock; use yew::platform::spawn_local; use yew::prelude::*; use super::provider::ReactorProviderState; use super::{Reactor, ReactorBridge, ReactorScoped}; use crate::utils::{BridgeIdState, OutputsAction, OutputsState}; type ReactorTx = Rc, <::Scope as ReactorScoped>::Input>>>; /// A type that represents events from a reactor. pub enum ReactorEvent where R: Reactor, { /// The reactor agent has sent an output. Output(::Output), /// The reactor agent has exited. Finished, } impl fmt::Debug for ReactorEvent where R: Reactor, ::Output: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Output(m) => f.debug_tuple("ReactorEvent::Output").field(&m).finish(), Self::Finished => f.debug_tuple("ReactorEvent::Finished").finish(), } } } /// Hook handle for the [`use_reactor_bridge`] hook. pub struct UseReactorBridgeHandle where R: 'static + Reactor, { tx: ReactorTx, ctr: UseReducerDispatcher, } impl fmt::Debug for UseReactorBridgeHandle where R: 'static + Reactor, ::Input: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(type_name::()) .field("inner", &self.tx) .finish() } } impl Clone for UseReactorBridgeHandle where R: 'static + Reactor, { fn clone(&self) -> Self { Self { tx: self.tx.clone(), ctr: self.ctr.clone(), } } } impl UseReactorBridgeHandle where R: 'static + Reactor, { /// Send an input to a reactor agent. pub fn send(&self, msg: ::Input) { let tx = self.tx.clone(); spawn_local(async move { let mut tx = tx.write().await; let _ = tx.send(msg).await; }); } /// Reset the bridge. /// /// Disconnect the old bridge and re-connects the agent with a new bridge. pub fn reset(&self) { self.ctr.dispatch(()); } } impl PartialEq for UseReactorBridgeHandle where R: 'static + Reactor, { fn eq(&self, rhs: &Self) -> bool { self.ctr == rhs.ctr } } /// A hook to bridge to a [`Reactor`]. /// /// This hooks will only bridge the reactor once over the entire component lifecycle. /// /// Takes a callback as the argument. /// /// The callback will be updated on every render to make sure captured values (if any) are up to /// date. #[hook] pub fn use_reactor_bridge(on_output: F) -> UseReactorBridgeHandle where R: 'static + Reactor, F: Fn(ReactorEvent) + 'static, { let ctr = use_reducer(BridgeIdState::default); let worker_state = use_context::>() .expect_throw("cannot find a provider for current agent."); let on_output = Rc::new(on_output); let on_output_ref = { let on_output = on_output.clone(); use_mut_ref(move || on_output) }; // Refresh the callback on every render. { let mut on_output_ref = on_output_ref.borrow_mut(); *on_output_ref = on_output; } let tx = use_memo((worker_state, ctr.inner), |(state, _ctr)| { let bridge = state.create_bridge(); let (tx, mut rx) = bridge.split(); spawn_local(async move { while let Some(m) = rx.next().await { let on_output = on_output_ref.borrow().clone(); on_output(ReactorEvent::::Output(m)); } let on_output = on_output_ref.borrow().clone(); on_output(ReactorEvent::::Finished); }); RwLock::new(tx) }); UseReactorBridgeHandle { tx: tx.clone(), ctr: ctr.dispatcher(), } } /// Hook handle for the [`use_reactor_subscription`] hook. pub struct UseReactorSubscriptionHandle where R: 'static + Reactor, { bridge: UseReactorBridgeHandle, outputs: Vec::Output>>, finished: bool, ctr: usize, } impl UseReactorSubscriptionHandle where R: 'static + Reactor, { /// Send an input to a reactor agent. pub fn send(&self, msg: ::Input) { self.bridge.send(msg); } /// Returns whether the current bridge has received a finish message. pub fn finished(&self) -> bool { self.finished } /// Reset the subscription. /// /// This disconnects the old bridge and re-connects the agent with a new bridge. /// Existing outputs stored in the subscription will also be cleared. pub fn reset(&self) { self.bridge.reset(); } } impl Clone for UseReactorSubscriptionHandle where R: 'static + Reactor, { fn clone(&self) -> Self { Self { bridge: self.bridge.clone(), outputs: self.outputs.clone(), ctr: self.ctr, finished: self.finished, } } } impl fmt::Debug for UseReactorSubscriptionHandle where R: 'static + Reactor, ::Input: fmt::Debug, ::Output: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(type_name::()) .field("bridge", &self.bridge) .field("outputs", &self.outputs) .finish() } } impl Deref for UseReactorSubscriptionHandle where R: 'static + Reactor, { type Target = [Rc<::Output>]; fn deref(&self) -> &Self::Target { &self.outputs } } impl PartialEq for UseReactorSubscriptionHandle where R: 'static + Reactor, { fn eq(&self, rhs: &Self) -> bool { self.bridge == rhs.bridge && self.ctr == rhs.ctr } } /// A hook to subscribe to the outputs of a [Reactor] agent. /// /// All outputs sent to current bridge will be collected into a slice. #[hook] pub fn use_reactor_subscription() -> UseReactorSubscriptionHandle where R: 'static + Reactor, { let outputs = use_reducer(OutputsState::<::Output>::default); let bridge = { let outputs = outputs.clone(); use_reactor_bridge::(move |output| { outputs.dispatch(match output { ReactorEvent::Output(m) => OutputsAction::Push(m.into()), ReactorEvent::Finished => OutputsAction::Close, }) }) }; { let outputs = outputs.clone(); use_effect_with(bridge.clone(), move |_| { outputs.dispatch(OutputsAction::Reset); || {} }); } UseReactorSubscriptionHandle { bridge, outputs: outputs.inner.clone(), ctr: outputs.ctr, finished: outputs.closed, } } ================================================ FILE: packages/yew-agent/src/reactor/messages.rs ================================================ use serde::{Deserialize, Serialize}; /// The Bridge Input. #[derive(Debug, Serialize, Deserialize)] pub(crate) enum ReactorInput { /// An input message. Input(I), } /// The Bridge Output. #[derive(Debug, Serialize, Deserialize)] pub enum ReactorOutput { /// An output message has been received. Output(O), /// Reactor for current bridge has exited. Finish, } ================================================ FILE: packages/yew-agent/src/reactor/mod.rs ================================================ //! This module contains the reactor agent implementation. //! //! Reactor agents are agents that receive multiple inputs and send multiple outputs over a single //! bridge. A reactor is defined as an async function that takes a [ReactorScope] //! as the argument. //! //! The reactor scope is a stream that produces inputs from the bridge and a //! sink that implements an additional send method to send outputs to the connected bridge. //! When the bridge disconnects, the output stream and input sink will be closed. //! //! # Example //! //! ``` //! # use serde::{Serialize, Deserialize}; //! # #[derive(Serialize, Deserialize)] //! # pub struct ReactorInput {} //! # #[derive(Serialize, Deserialize)] //! # pub struct ReactorOutput {} //! # //! use futures::sink::SinkExt; //! use futures::stream::StreamExt; //! use yew_agent::reactor::{reactor, ReactorScope}; //! #[reactor(MyReactor)] //! pub async fn my_reactor(mut scope: ReactorScope) { //! while let Some(input) = scope.next().await { //! // handles each input. //! // ... //! # let output = ReactorOutput { /* ... */ }; //! //! // sends output //! if scope.send(output).await.is_err() { //! // sender closed, the bridge is disconnected //! break; //! } //! } //! } //! ``` mod bridge; mod hooks; mod messages; mod provider; mod registrar; mod scope; mod spawner; mod traits; mod worker; pub use bridge::{ReactorBridge, ReactorBridgeSinkError}; pub use hooks::{ use_reactor_bridge, use_reactor_subscription, ReactorEvent, UseReactorBridgeHandle, UseReactorSubscriptionHandle, }; pub use provider::ReactorProvider; pub(crate) use provider::ReactorProviderState; pub use registrar::ReactorRegistrar; pub use scope::{ReactorScope, ReactorScoped}; pub use spawner::ReactorSpawner; pub use traits::Reactor; /// A procedural macro to create reactor agents. pub use yew_agent_macro::reactor; ================================================ FILE: packages/yew-agent/src/reactor/provider.rs ================================================ use std::any::type_name; use std::cell::RefCell; use std::fmt; use std::rc::Rc; use serde::{Deserialize, Serialize}; use yew::prelude::*; use super::{Reactor, ReactorBridge, ReactorScoped, ReactorSpawner}; use crate::utils::get_next_id; use crate::worker::WorkerProviderProps; use crate::{Bincode, Codec, Reach}; pub(crate) struct ReactorProviderState where T: Reactor + 'static, { id: usize, spawn_bridge_fn: Rc ReactorBridge>, reach: Reach, held_bridge: Rc>>>, } impl fmt::Debug for ReactorProviderState where T: Reactor, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(type_name::()) } } impl ReactorProviderState where T: Reactor, { fn get_held_bridge(&self) -> ReactorBridge { let mut held_bridge = self.held_bridge.borrow_mut(); match held_bridge.as_mut() { Some(m) => m.fork(), None => { let bridge = (self.spawn_bridge_fn)(); *held_bridge = Some(bridge.fork()); bridge } } } /// Creates a bridge, uses "fork" for public agents. pub fn create_bridge(&self) -> ReactorBridge { match self.reach { Reach::Public => { let held_bridge = self.get_held_bridge(); held_bridge.fork() } Reach::Private => (self.spawn_bridge_fn)(), } } } impl Clone for ReactorProviderState where T: Reactor, { fn clone(&self) -> Self { Self { id: self.id, spawn_bridge_fn: self.spawn_bridge_fn.clone(), reach: self.reach, held_bridge: self.held_bridge.clone(), } } } impl PartialEq for ReactorProviderState where T: Reactor, { fn eq(&self, rhs: &Self) -> bool { self.id == rhs.id } } /// The Reactor Agent Provider. /// /// This component provides its children access to a reactor agent. #[component] pub fn ReactorProvider(props: &WorkerProviderProps) -> Html where R: 'static + Reactor, <::Scope as ReactorScoped>::Input: Serialize + for<'de> Deserialize<'de> + 'static, <::Scope as ReactorScoped>::Output: Serialize + for<'de> Deserialize<'de> + 'static, C: Codec + 'static, { let WorkerProviderProps { children, path, lazy, module, reach, } = props.clone(); // Creates a spawning function so Codec is can be erased from contexts. let spawn_bridge_fn: Rc ReactorBridge> = { let path = path.clone(); Rc::new(move || { ReactorSpawner::::new() .as_module(module) .encoding::() .spawn(&path) }) }; let state = { use_memo((path, lazy, reach), move |(_path, lazy, reach)| { let state = ReactorProviderState:: { id: get_next_id(), spawn_bridge_fn, reach: *reach, held_bridge: Rc::default(), }; if *reach == Reach::Public && !*lazy { state.get_held_bridge(); } state }) }; html! { > context={(*state).clone()}> {children} >> } } ================================================ FILE: packages/yew-agent/src/reactor/registrar.rs ================================================ use std::fmt; use serde::de::Deserialize; use serde::ser::Serialize; use super::scope::ReactorScoped; use super::traits::Reactor; use super::worker::ReactorWorker; use crate::codec::{Bincode, Codec}; use crate::traits::Registrable; use crate::worker::WorkerRegistrar; /// A registrar for reactor workers. pub struct ReactorRegistrar where R: Reactor + 'static, CODEC: Codec + 'static, { inner: WorkerRegistrar, CODEC>, } impl Default for ReactorRegistrar where R: Reactor + 'static, CODEC: Codec + 'static, { fn default() -> Self { Self::new() } } impl ReactorRegistrar where R: Reactor + 'static, CODEC: Codec + 'static, { /// Creates a new reactor registrar. pub fn new() -> Self { Self { inner: ReactorWorker::::registrar().encoding::(), } } /// Sets the encoding. pub fn encoding(&self) -> ReactorRegistrar where C: Codec + 'static, { ReactorRegistrar { inner: self.inner.encoding::(), } } /// Registers the worker. pub fn register(&self) where ::Input: Serialize + for<'de> Deserialize<'de>, ::Output: Serialize + for<'de> Deserialize<'de>, { self.inner.register() } } impl fmt::Debug for ReactorRegistrar where R: Reactor + 'static, CODEC: Codec + 'static, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ReactorRegistrar<_>").finish() } } ================================================ FILE: packages/yew-agent/src/reactor/scope.rs ================================================ use std::convert::Infallible; use std::fmt; use std::pin::Pin; use futures::stream::{FusedStream, Stream}; use futures::task::{Context, Poll}; use futures::Sink; /// A handle to communicate with bridges. pub struct ReactorScope { input_stream: Pin>>, output_sink: Pin>>, } impl fmt::Debug for ReactorScope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ReactorScope<_>").finish() } } impl Stream for ReactorScope { type Item = I; #[inline(always)] fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.input_stream).poll_next(cx) } #[inline(always)] fn size_hint(&self) -> (usize, Option) { self.input_stream.size_hint() } } impl FusedStream for ReactorScope { #[inline(always)] fn is_terminated(&self) -> bool { self.input_stream.is_terminated() } } /// A helper trait to extract the input and output type from a [ReactorStream]. pub trait ReactorScoped: Stream + FusedStream { /// The Input Message. type Input; /// The Output Message. type Output; /// Creates a ReactorReceiver. fn new(input_stream: IS, output_sink: OS) -> Self where IS: Stream + FusedStream + 'static, OS: Sink + 'static; } impl ReactorScoped for ReactorScope { type Input = I; type Output = O; #[inline] fn new(input_stream: IS, output_sink: OS) -> Self where IS: Stream + FusedStream + 'static, OS: Sink + 'static, { Self { input_stream: Box::pin(input_stream), output_sink: Box::pin(output_sink), } } } impl Sink for ReactorScope { type Error = Infallible; fn start_send(mut self: Pin<&mut Self>, item: O) -> Result<(), Self::Error> { Pin::new(&mut self.output_sink).start_send(item) } fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.output_sink).poll_close(cx) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.output_sink).poll_flush(cx) } fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.output_sink).poll_flush(cx) } } ================================================ FILE: packages/yew-agent/src/reactor/spawner.rs ================================================ use serde::de::Deserialize; use serde::ser::Serialize; use super::bridge::ReactorBridge; use super::scope::ReactorScoped; use super::traits::Reactor; use super::worker::ReactorWorker; use crate::codec::{Bincode, Codec}; use crate::worker::WorkerSpawner; /// A spawner to create oneshot workers. #[derive(Debug, Default)] pub struct ReactorSpawner where R: Reactor + 'static, CODEC: Codec, { inner: WorkerSpawner, CODEC>, } impl ReactorSpawner where R: Reactor + 'static, CODEC: Codec, { /// Creates a ReactorSpawner. pub const fn new() -> Self { Self { inner: WorkerSpawner::, CODEC>::new(), } } /// Sets a new message encoding. pub const fn encoding(&self) -> ReactorSpawner where C: Codec, { ReactorSpawner { inner: WorkerSpawner::, C>::new(), } } /// Indicates that [`spawn`](WorkerSpawner#method.spawn) should expect a /// `path` to a loader shim script (e.g. when using Trunk, created by using /// the [`data-loader-shim`](https://trunkrs.dev/assets/#link-asset-types) /// asset type) and one does not need to be generated. `false` by default. pub fn with_loader(mut self, with_loader: bool) -> Self { self.inner.with_loader(with_loader); self } /// Determines whether the worker will be spawned with /// [`options.type`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker#type) /// set to `module`. `true` by default. /// /// This option should be un-set if the worker was created with the /// `--target no-modules` flag of `wasm-bindgen`. If using Trunk, see the /// [`data-bindgen-target`](https://trunkrs.dev/assets/#link-asset-types) /// asset type. pub fn as_module(mut self, as_module: bool) -> Self { self.inner.as_module(as_module); self } /// Spawns a reactor worker. pub fn spawn(mut self, path: &str) -> ReactorBridge where ::Input: Serialize + for<'de> Deserialize<'de>, ::Output: Serialize + for<'de> Deserialize<'de>, { let rx = ReactorBridge::register_callback(&mut self.inner); let inner = self.inner.spawn(path); ReactorBridge::new(inner, rx) } } ================================================ FILE: packages/yew-agent/src/reactor/traits.rs ================================================ use std::future::Future; use super::scope::ReactorScoped; /// A reactor worker. pub trait Reactor: Future { /// The Reactor Scope type Scope: ReactorScoped; /// Creates a reactor worker. fn create(scope: Self::Scope) -> Self; } ================================================ FILE: packages/yew-agent/src/reactor/worker.rs ================================================ use std::collections::HashMap; use std::convert::Infallible; use futures::sink; use futures::stream::StreamExt; use pinned::mpsc; use pinned::mpsc::UnboundedSender; use wasm_bindgen_futures::spawn_local; use super::messages::{ReactorInput, ReactorOutput}; use super::scope::ReactorScoped; use super::traits::Reactor; use crate::worker::{HandlerId, Worker, WorkerDestroyHandle, WorkerScope}; pub(crate) enum Message { ReactorExited(HandlerId), } pub(crate) struct ReactorWorker where R: 'static + Reactor, { senders: HashMap::Input>>, destruct_handle: Option>, } impl Worker for ReactorWorker where R: 'static + Reactor, { type Input = ReactorInput<::Input>; type Message = Message; type Output = ReactorOutput<::Output>; fn create(_scope: &WorkerScope) -> Self { Self { senders: HashMap::new(), destruct_handle: None, } } fn update(&mut self, scope: &WorkerScope, msg: Self::Message) { match msg { Self::Message::ReactorExited(id) => { scope.respond(id, ReactorOutput::Finish); self.senders.remove(&id); } } // All reactors have closed themselves, the worker can now close. if self.destruct_handle.is_some() && self.senders.is_empty() { self.destruct_handle = None; } } fn connected(&mut self, scope: &WorkerScope, id: HandlerId) { let from_bridge = { let (tx, rx) = mpsc::unbounded(); self.senders.insert(id, tx); rx }; let to_bridge = { let scope_ = scope.clone(); let (tx, mut rx) = mpsc::unbounded(); spawn_local(async move { while let Some(m) = rx.next().await { scope_.respond(id, ReactorOutput::Output(m)); } }); sink::unfold((), move |_, item: ::Output| { let tx = tx.clone(); async move { let _ = tx.send_now(item); Ok::<(), Infallible>(()) } }) }; let reactor_scope = ReactorScoped::new(from_bridge, to_bridge); let reactor = R::create(reactor_scope); scope.send_future(async move { reactor.await; Message::ReactorExited(id) }); } fn received(&mut self, _scope: &WorkerScope, input: Self::Input, id: HandlerId) { match input { Self::Input::Input(input) => { if let Some(m) = self.senders.get_mut(&id) { let _result = m.send_now(input); } } } } fn disconnected(&mut self, _scope: &WorkerScope, id: HandlerId) { // We close this channel, but drop it when the reactor has exited itself. if let Some(m) = self.senders.get_mut(&id) { m.close_now(); } } fn destroy(&mut self, _scope: &WorkerScope, destruct: WorkerDestroyHandle) { if !self.senders.is_empty() { self.destruct_handle = Some(destruct); } } } ================================================ FILE: packages/yew-agent/src/scope_ext.rs ================================================ //! This module contains extensions to the component scope for agent access. use std::any::type_name; use std::fmt; use std::rc::Rc; use futures::stream::SplitSink; use futures::{SinkExt, StreamExt}; use wasm_bindgen::UnwrapThrowExt; use yew::html::Scope; use yew::platform::pinned::RwLock; use yew::platform::spawn_local; use yew::prelude::*; use crate::oneshot::{Oneshot, OneshotProviderState}; use crate::reactor::{Reactor, ReactorBridge, ReactorEvent, ReactorProviderState, ReactorScoped}; use crate::worker::{Worker, WorkerBridge, WorkerProviderState}; /// A Worker Bridge Handle. #[derive(Debug)] pub struct WorkerBridgeHandle where W: Worker, { inner: WorkerBridge, } impl WorkerBridgeHandle where W: Worker, { /// Sends a message to the worker agent. pub fn send(&self, input: W::Input) { self.inner.send(input) } } type ReactorTx = Rc, <::Scope as ReactorScoped>::Input>>>; /// A Reactor Bridge Handle. pub struct ReactorBridgeHandle where R: Reactor + 'static, { tx: ReactorTx, } impl fmt::Debug for ReactorBridgeHandle where R: Reactor + 'static, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(type_name::()).finish_non_exhaustive() } } impl ReactorBridgeHandle where R: Reactor + 'static, { /// Sends a message to the reactor agent. pub fn send(&self, input: ::Input) { let tx = self.tx.clone(); spawn_local(async move { let mut tx = tx.write().await; let _ = tx.send(input).await; }); } } /// An extension to [`Scope`](yew::html::Scope) that provides communication mechanism to agents. /// /// You can access them on `ctx.link()` pub trait AgentScopeExt { /// Bridges to a Worker Agent. fn bridge_worker(&self, callback: Callback) -> WorkerBridgeHandle where W: Worker + 'static; /// Bridges to a Reactor Agent. fn bridge_reactor(&self, callback: Callback>) -> ReactorBridgeHandle where R: Reactor + 'static, ::Output: 'static; /// Runs an oneshot in an Oneshot Agent. fn run_oneshot(&self, input: T::Input, callback: Callback) where T: Oneshot + 'static; } impl AgentScopeExt for Scope where COMP: Component, { fn bridge_worker(&self, callback: Callback) -> WorkerBridgeHandle where W: Worker + 'static, { let inner = self .context::>>((|_| {}).into()) .expect_throw("failed to bridge to agent.") .0 .create_bridge(callback); WorkerBridgeHandle { inner } } fn bridge_reactor(&self, callback: Callback>) -> ReactorBridgeHandle where R: Reactor + 'static, ::Output: 'static, { let (tx, mut rx) = self .context::>((|_| {}).into()) .expect_throw("failed to bridge to agent.") .0 .create_bridge() .split(); spawn_local(async move { while let Some(m) = rx.next().await { callback.emit(ReactorEvent::::Output(m)); } callback.emit(ReactorEvent::::Finished); }); let tx = Rc::new(RwLock::new(tx)); ReactorBridgeHandle { tx } } fn run_oneshot(&self, input: T::Input, callback: Callback) where T: Oneshot + 'static, { let (inner, _) = self .context::>((|_| {}).into()) .expect_throw("failed to bridge to agent."); spawn_local(async move { callback.emit(inner.create_bridge().run(input).await) }); } } ================================================ FILE: packages/yew-agent/src/traits.rs ================================================ //! Submodule providing the `Spawnable` and `Registrable` traits. /// A Worker that can be spawned by a spawner. pub trait Spawnable { /// Spawner Type. type Spawner; /// Creates a spawner. fn spawner() -> Self::Spawner; } /// A trait to enable public workers being registered in a web worker. pub trait Registrable { /// Registrar Type. type Registrar; /// Creates a registrar for the current worker. fn registrar() -> Self::Registrar; } ================================================ FILE: packages/yew-agent/src/utils.rs ================================================ use std::rc::Rc; use std::sync::atomic::{AtomicUsize, Ordering}; use wasm_bindgen::UnwrapThrowExt; use yew::Reducible; /// Convenience function to avoid repeating expect logic. pub fn window() -> web_sys::Window { web_sys::window().expect_throw("Can't find the global Window") } /// Gets a unique worker id pub(crate) fn get_next_id() -> usize { static CTR: AtomicUsize = AtomicUsize::new(0); CTR.fetch_add(1, Ordering::SeqCst) } #[derive(Default, PartialEq)] pub(crate) struct BridgeIdState { pub inner: usize, } impl Reducible for BridgeIdState { type Action = (); fn reduce(self: Rc, _: Self::Action) -> Rc { Self { inner: self.inner + 1, } .into() } } pub(crate) enum OutputsAction { Push(Rc), Close, Reset, } pub(crate) struct OutputsState { pub ctr: usize, pub inner: Vec>, pub closed: bool, } impl Clone for OutputsState { fn clone(&self) -> Self { Self { ctr: self.ctr, inner: self.inner.clone(), closed: self.closed, } } } impl Reducible for OutputsState { type Action = OutputsAction; fn reduce(mut self: Rc, action: Self::Action) -> Rc { { let this = Rc::make_mut(&mut self); this.ctr += 1; match action { OutputsAction::Push(m) => this.inner.push(m), OutputsAction::Close => { this.closed = true; } OutputsAction::Reset => { this.closed = false; this.inner = Vec::new(); } } } self } } impl Default for OutputsState { fn default() -> Self { Self { ctr: 0, inner: Vec::new(), closed: false, } } } ================================================ FILE: packages/yew-agent/src/worker/bridge.rs ================================================ use std::cell::RefCell; use std::collections::HashMap; use std::fmt; use std::marker::PhantomData; use std::rc::{Rc, Weak}; use serde::{Deserialize, Serialize}; use super::handler_id::HandlerId; use super::messages::ToWorker; use super::native_worker::NativeWorkerExt; use super::traits::Worker; use super::{Callback, Shared}; use crate::codec::Codec; pub(crate) type ToWorkerQueue = Vec>; pub(crate) type CallbackMap = HashMap::Output)>>; struct WorkerBridgeInner where W: Worker, { // When worker is loaded, queue becomes None. pending_queue: Shared>>, callbacks: Shared>, post_msg: Rc)>, } impl fmt::Debug for WorkerBridgeInner where W: Worker, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("WorkerBridgeInner<_>") } } impl WorkerBridgeInner where W: Worker, { /// Send a message to the worker, queuing the message if necessary fn send_message(&self, msg: ToWorker) { let mut pending_queue = self.pending_queue.borrow_mut(); match pending_queue.as_mut() { Some(m) => { m.push(msg); } None => { (self.post_msg)(msg); } } } } impl Drop for WorkerBridgeInner where W: Worker, { fn drop(&mut self) { let destroy = ToWorker::Destroy; self.send_message(destroy); } } /// A connection manager for components interaction with workers. pub struct WorkerBridge where W: Worker, { inner: Rc>, id: HandlerId, _worker: PhantomData, _cb: Option>, } impl WorkerBridge where W: Worker, { fn init(&self) { self.inner.send_message(ToWorker::Connected(self.id)); } pub(crate) fn new( id: HandlerId, native_worker: web_sys::Worker, pending_queue: Rc>>>, callbacks: Rc>>, callback: Option>, ) -> Self where CODEC: Codec, W::Input: Serialize + for<'de> Deserialize<'de>, { let post_msg = move |msg: ToWorker| native_worker.post_packed_message::<_, CODEC>(msg); let self_ = Self { inner: WorkerBridgeInner { pending_queue, callbacks, post_msg: Rc::new(post_msg), } .into(), id, _worker: PhantomData, _cb: callback, }; self_.init(); self_ } /// Send a message to the current worker. pub fn send(&self, msg: W::Input) { let msg = ToWorker::ProcessInput(self.id, msg); self.inner.send_message(msg); } /// Forks the bridge with a different callback. /// /// This creates a new [HandlerID] that helps the worker to differentiate bridges. pub fn fork(&self, cb: Option) -> Self where F: 'static + Fn(W::Output), { let cb = cb.map(|m| Rc::new(m) as Rc); let handler_id = HandlerId::new(); if let Some(cb_weak) = cb.as_ref().map(Rc::downgrade) { self.inner .callbacks .borrow_mut() .insert(handler_id, cb_weak); } let self_ = Self { inner: self.inner.clone(), id: handler_id, _worker: PhantomData, _cb: cb, }; self_.init(); self_ } } impl Drop for WorkerBridge where W: Worker, { fn drop(&mut self) { let disconnected = ToWorker::Disconnected(self.id); self.inner.send_message(disconnected); } } impl fmt::Debug for WorkerBridge where W: Worker, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("WorkerBridge<_>") } } impl PartialEq for WorkerBridge where W: Worker, { fn eq(&self, rhs: &Self) -> bool { self.id == rhs.id } } ================================================ FILE: packages/yew-agent/src/worker/handler_id.rs ================================================ use std::sync::atomic::{AtomicUsize, Ordering}; use serde::{Deserialize, Serialize}; /// Identifier to send output to bridges. #[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone, Copy)] pub struct HandlerId(usize); impl HandlerId { pub(crate) fn new() -> Self { static CTR: AtomicUsize = AtomicUsize::new(0); let id = CTR.fetch_add(1, Ordering::SeqCst); HandlerId(id) } } ================================================ FILE: packages/yew-agent/src/worker/hooks.rs ================================================ use std::any::type_name; use std::fmt; use std::ops::Deref; use std::rc::Rc; use wasm_bindgen::prelude::*; use yew::prelude::*; use crate::utils::{BridgeIdState, OutputsAction, OutputsState}; use crate::worker::provider::WorkerProviderState; use crate::worker::{Worker, WorkerBridge}; /// Hook handle for the [`use_worker_bridge`] hook. pub struct UseWorkerBridgeHandle where T: Worker, { inner: Rc>, ctr: UseReducerDispatcher, } impl UseWorkerBridgeHandle where T: Worker, { /// Send an input to a worker agent. pub fn send(&self, msg: T::Input) { self.inner.send(msg); } /// Reset the bridge. /// /// Disconnect the old bridge and re-connects the agent with a new bridge. pub fn reset(&self) { self.ctr.dispatch(()); } } impl Clone for UseWorkerBridgeHandle where T: Worker, { fn clone(&self) -> Self { Self { inner: self.inner.clone(), ctr: self.ctr.clone(), } } } impl fmt::Debug for UseWorkerBridgeHandle where T: Worker, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(type_name::()) .field("inner", &self.inner) .finish() } } impl PartialEq for UseWorkerBridgeHandle where T: Worker, { fn eq(&self, rhs: &Self) -> bool { self.inner == rhs.inner } } /// A hook to bridge to a [`Worker`]. /// /// This hooks will only bridge the worker once over the entire component lifecycle. /// /// Takes a callback as the argument. /// /// The callback will be updated on every render to make sure captured values (if any) are up to /// date. #[hook] pub fn use_worker_bridge(on_output: F) -> UseWorkerBridgeHandle where T: Worker + 'static, F: Fn(T::Output) + 'static, { let ctr = use_reducer(BridgeIdState::default); let worker_state = use_context::>>() .expect_throw("cannot find a provider for current agent."); let on_output = Rc::new(on_output); let on_output_clone = on_output.clone(); let on_output_ref = use_mut_ref(move || on_output_clone); // Refresh the callback on every render. { let mut on_output_ref = on_output_ref.borrow_mut(); *on_output_ref = on_output; } let bridge = use_memo((worker_state, ctr.inner), |(state, _ctr)| { state.create_bridge(Callback::from(move |output| { let on_output = on_output_ref.borrow().clone(); on_output(output); })) }); UseWorkerBridgeHandle { inner: bridge, ctr: ctr.dispatcher(), } } /// Hook handle for the [`use_worker_subscription`] hook. pub struct UseWorkerSubscriptionHandle where T: Worker, { bridge: UseWorkerBridgeHandle, outputs: Vec>, ctr: usize, } impl UseWorkerSubscriptionHandle where T: Worker, { /// Send an input to a worker agent. pub fn send(&self, msg: T::Input) { self.bridge.send(msg); } /// Reset the subscription. /// /// This disconnects the old bridge and re-connects the agent with a new bridge. /// Existing outputs stored in the subscription will also be cleared. pub fn reset(&self) { self.bridge.reset(); } } impl Clone for UseWorkerSubscriptionHandle where T: Worker, { fn clone(&self) -> Self { Self { bridge: self.bridge.clone(), outputs: self.outputs.clone(), ctr: self.ctr, } } } impl fmt::Debug for UseWorkerSubscriptionHandle where T: Worker, T::Output: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(type_name::()) .field("bridge", &self.bridge) .field("outputs", &self.outputs) .finish() } } impl Deref for UseWorkerSubscriptionHandle where T: Worker, { type Target = [Rc]; fn deref(&self) -> &[Rc] { &self.outputs } } impl PartialEq for UseWorkerSubscriptionHandle where T: Worker, { fn eq(&self, rhs: &Self) -> bool { self.bridge == rhs.bridge && self.ctr == rhs.ctr } } /// A hook to subscribe to the outputs of a [Worker] agent. /// /// All outputs sent to current bridge will be collected into a slice. #[hook] pub fn use_worker_subscription() -> UseWorkerSubscriptionHandle where T: Worker + 'static, { let outputs = use_reducer(OutputsState::default); let bridge = { let outputs = outputs.clone(); use_worker_bridge::(move |output| { outputs.dispatch(OutputsAction::Push(Rc::new(output))) }) }; { let outputs_dispatcher = outputs.dispatcher(); use_effect_with(bridge.clone(), move |_| { outputs_dispatcher.dispatch(OutputsAction::Reset); || {} }); } UseWorkerSubscriptionHandle { bridge, outputs: outputs.inner.clone(), ctr: outputs.ctr, } } ================================================ FILE: packages/yew-agent/src/worker/lifecycle.rs ================================================ use wasm_bindgen::prelude::*; use super::messages::ToWorker; use super::native_worker::{DedicatedWorker, WorkerSelf}; use super::scope::{WorkerDestroyHandle, WorkerScope}; use super::traits::Worker; use super::Shared; pub(crate) struct WorkerState where W: Worker, { worker: Option<(W, WorkerScope)>, to_destroy: bool, } impl WorkerState where W: Worker, { pub fn new() -> Self { WorkerState { worker: None, to_destroy: false, } } } /// Internal Worker lifecycle events pub(crate) enum WorkerLifecycleEvent { /// Request to create the scope Create(WorkerScope), /// Internal Worker message Message(W::Message), /// External Messages from bridges Remote(ToWorker), /// Destroy the Worker Destroy, } pub(crate) struct WorkerRunnable { pub state: Shared>, pub event: WorkerLifecycleEvent, } impl WorkerRunnable where W: Worker + 'static, { pub fn run(self) { let mut state = self.state.borrow_mut(); // We should block all event other than message after a worker is destroyed. match self.event { WorkerLifecycleEvent::Create(scope) => { if state.to_destroy { return; } state.worker = Some((W::create(&scope), scope)); } WorkerLifecycleEvent::Message(msg) => { if let Some((worker, scope)) = state.worker.as_mut() { worker.update(scope, msg); } } WorkerLifecycleEvent::Remote(ToWorker::Connected(id)) => { if state.to_destroy { return; } let (worker, scope) = state .worker .as_mut() .expect_throw("worker was not created to process connected messages"); worker.connected(scope, id); } WorkerLifecycleEvent::Remote(ToWorker::ProcessInput(id, inp)) => { if state.to_destroy { return; } let (worker, scope) = state .worker .as_mut() .expect_throw("worker was not created to process inputs"); worker.received(scope, inp, id); } WorkerLifecycleEvent::Remote(ToWorker::Disconnected(id)) => { if state.to_destroy { return; } let (worker, scope) = state .worker .as_mut() .expect_throw("worker was not created to process disconnected messages"); worker.disconnected(scope, id); } WorkerLifecycleEvent::Remote(ToWorker::Destroy) => { if state.to_destroy { return; } state.to_destroy = true; let (worker, scope) = state .worker .as_mut() .expect_throw("trying to destroy not existent worker"); let destruct = WorkerDestroyHandle::new(scope.clone()); worker.destroy(scope, destruct); } WorkerLifecycleEvent::Destroy => { state .worker .take() .expect_throw("worker is not initialised or already destroyed"); DedicatedWorker::worker_self().close(); } } } } ================================================ FILE: packages/yew-agent/src/worker/messages.rs ================================================ use serde::{Deserialize, Serialize}; use super::handler_id::HandlerId; use super::traits::Worker; /// Serializable messages to worker #[derive(Serialize, Deserialize, Debug)] pub(crate) enum ToWorker where W: Worker, { /// Client is connected Connected(HandlerId), /// Incoming message to Worker ProcessInput(HandlerId, W::Input), /// Client is disconnected Disconnected(HandlerId), /// Worker should be terminated Destroy, } /// Serializable messages sent by worker to consumer #[derive(Serialize, Deserialize, Debug)] pub(crate) enum FromWorker where W: Worker, { /// Worker sends this message when `wasm` bundle has loaded. WorkerLoaded, /// Outgoing message to consumer ProcessOutput(HandlerId, W::Output), } ================================================ FILE: packages/yew-agent/src/worker/mod.rs ================================================ //! This module contains the worker agent implementation. //! //! This is a low-level implementation that uses an actor model. //! //! # Example //! //! ``` //! # mod example { //! use serde::{Deserialize, Serialize}; //! use yew::prelude::*; //! use yew_agent::worker::{use_worker_bridge, UseWorkerBridgeHandle}; //! //! // This would usually live in the same file as your worker //! #[derive(Serialize, Deserialize)] //! pub enum WorkerResponseType { //! IncrementCounter, //! } //! # mod my_worker_mod { //! # use yew_agent::worker::{HandlerId, WorkerScope}; //! # use super::WorkerResponseType; //! # pub struct MyWorker {} //! # //! # impl yew_agent::worker::Worker for MyWorker { //! # type Input = (); //! # type Output = WorkerResponseType; //! # type Message = (); //! # //! # fn create(scope: &WorkerScope) -> Self { //! # MyWorker {} //! # } //! # //! # fn update(&mut self, scope: &WorkerScope, _msg: Self::Message) { //! # // do nothing //! # } //! # //! # fn received(&mut self, scope: &WorkerScope, _msg: Self::Input, id: HandlerId) { //! # scope.respond(id, WorkerResponseType::IncrementCounter); //! # } //! # } //! # } //! use my_worker_mod::MyWorker; // note that ::Output == WorkerResponseType //! #[component(UseWorkerBridge)] //! fn bridge() -> Html { //! let counter = use_state(|| 0); //! //! // a scoped block to clone the state in //! { //! let counter = counter.clone(); //! // response will be of type MyWorker::Output, i.e. WorkerResponseType //! let bridge: UseWorkerBridgeHandle = use_worker_bridge(move |response| match response { //! WorkerResponseType::IncrementCounter => { //! counter.set(*counter + 1); //! } //! }); //! } //! //! html! { //!
//! {*counter} //!
//! } //! } //! # } //! ``` mod bridge; mod handler_id; mod hooks; mod lifecycle; mod messages; mod native_worker; mod provider; mod registrar; mod scope; mod spawner; mod traits; use std::cell::RefCell; use std::rc::Rc; pub use bridge::WorkerBridge; pub use handler_id::HandlerId; pub use hooks::{ use_worker_bridge, use_worker_subscription, UseWorkerBridgeHandle, UseWorkerSubscriptionHandle, }; pub(crate) use provider::WorkerProviderState; pub use provider::{WorkerProvider, WorkerProviderProps}; pub use registrar::WorkerRegistrar; pub use scope::{WorkerDestroyHandle, WorkerScope}; pub use spawner::WorkerSpawner; pub use traits::Worker; /// Alias for `Rc>` type Shared = Rc>; /// Alias for `Rc` type Callback = Rc; ================================================ FILE: packages/yew-agent/src/worker/native_worker.rs ================================================ use serde::{Deserialize, Serialize}; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; use wasm_bindgen::{JsCast, JsValue}; pub(crate) use web_sys::Worker as DedicatedWorker; use web_sys::{DedicatedWorkerGlobalScope, MessageEvent}; use crate::codec::Codec; pub(crate) trait WorkerSelf { type GlobalScope; fn worker_self() -> Self::GlobalScope; } impl WorkerSelf for DedicatedWorker { type GlobalScope = DedicatedWorkerGlobalScope; fn worker_self() -> Self::GlobalScope { JsValue::from(js_sys::global()).into() } } pub(crate) trait NativeWorkerExt { fn set_on_packed_message(&self, handler: F) where T: Serialize + for<'de> Deserialize<'de>, CODEC: Codec, F: 'static + Fn(T); fn post_packed_message(&self, data: T) where T: Serialize + for<'de> Deserialize<'de>, CODEC: Codec; } macro_rules! worker_ext_impl { ($($type:path),+) => {$( impl NativeWorkerExt for $type { fn set_on_packed_message(&self, handler: F) where T: Serialize + for<'de> Deserialize<'de>, CODEC: Codec, F: 'static + Fn(T) { let handler = move |message: MessageEvent| { let msg = CODEC::decode(message.data()); handler(msg); }; let closure = Closure::wrap(Box::new(handler) as Box).into_js_value(); self.set_onmessage(Some(closure.as_ref().unchecked_ref())); } fn post_packed_message(&self, data: T) where T: Serialize + for<'de> Deserialize<'de>, CODEC: Codec { self.post_message(&CODEC::encode(data)) .expect_throw("failed to post message"); } } )+}; } worker_ext_impl! { DedicatedWorker, DedicatedWorkerGlobalScope } ================================================ FILE: packages/yew-agent/src/worker/provider.rs ================================================ use std::any::type_name; use std::cell::RefCell; use std::fmt; use std::rc::Rc; use serde::{Deserialize, Serialize}; use yew::prelude::*; use super::{Worker, WorkerBridge}; use crate::reach::Reach; use crate::utils::get_next_id; use crate::{Bincode, Codec, Spawnable}; /// Properties for [WorkerProvider]. #[derive(Debug, Properties, PartialEq, Clone)] pub struct WorkerProviderProps { /// The path to an agent. pub path: AttrValue, /// The reachability of an agent. /// /// Default: [`Public`](Reach::Public). #[prop_or(Reach::Public)] pub reach: Reach, /// Whether the agent should be created /// with type `Module`. #[prop_or(false)] pub module: bool, /// Lazily spawn the agent. /// /// The agent will be spawned when the first time a hook requests a bridge. /// /// Does not affect private agents. /// /// Default: `true` #[prop_or(true)] pub lazy: bool, /// Children of the provider. #[prop_or_default] pub children: Html, } pub(crate) struct WorkerProviderState where W: Worker, { id: usize, spawn_bridge_fn: Rc WorkerBridge>, reach: Reach, held_bridge: RefCell>>>, } impl fmt::Debug for WorkerProviderState where W: Worker, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(type_name::()) } } impl WorkerProviderState where W: Worker, W::Output: 'static, { fn get_held_bridge(&self) -> Rc> { let mut held_bridge = self.held_bridge.borrow_mut(); match held_bridge.as_mut() { Some(m) => m.clone(), None => { let bridge = Rc::new((self.spawn_bridge_fn)()); *held_bridge = Some(bridge.clone()); bridge } } } /// Creates a bridge, uses "fork" for public agents. pub fn create_bridge(&self, cb: Callback) -> WorkerBridge { match self.reach { Reach::Public => { let held_bridge = self.get_held_bridge(); held_bridge.fork(Some(move |m| cb.emit(m))) } Reach::Private => (self.spawn_bridge_fn)(), } } } impl PartialEq for WorkerProviderState where W: Worker, { fn eq(&self, rhs: &Self) -> bool { self.id == rhs.id } } /// The Worker Agent Provider. /// /// This component provides its children access to a worker agent. #[component] pub fn WorkerProvider(props: &WorkerProviderProps) -> Html where W: Worker + 'static, W::Input: Serialize + for<'de> Deserialize<'de> + 'static, W::Output: Serialize + for<'de> Deserialize<'de> + 'static, C: Codec + 'static, { let WorkerProviderProps { children, path, lazy, module, reach, } = props.clone(); // Creates a spawning function so Codec is can be erased from contexts. let spawn_bridge_fn: Rc WorkerBridge> = { let path = path.clone(); Rc::new(move || W::spawner().as_module(module).encoding::().spawn(&path)) }; let state = { use_memo((path, lazy, reach), move |(_path, lazy, reach)| { let state = WorkerProviderState:: { id: get_next_id(), spawn_bridge_fn, reach: *reach, held_bridge: Default::default(), }; if *reach == Reach::Public && !*lazy { state.get_held_bridge(); } state }) }; html! { >> context={state.clone()}> {children} >>> } } ================================================ FILE: packages/yew-agent/src/worker/registrar.rs ================================================ use std::fmt; use std::marker::PhantomData; use serde::de::Deserialize; use serde::ser::Serialize; use super::lifecycle::WorkerLifecycleEvent; use super::messages::{FromWorker, ToWorker}; use super::native_worker::{DedicatedWorker, NativeWorkerExt, WorkerSelf}; use super::scope::WorkerScope; use super::traits::Worker; use crate::codec::{Bincode, Codec}; /// A Worker Registrar. pub struct WorkerRegistrar where W: Worker, CODEC: Codec, { _marker: PhantomData<(W, CODEC)>, } impl fmt::Debug for WorkerRegistrar { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("WorkerRegistrar<_>") } } impl WorkerRegistrar where W: Worker + 'static, CODEC: Codec, { pub(crate) fn new() -> Self { Self { _marker: PhantomData, } } /// Sets a new message encoding. pub fn encoding(&self) -> WorkerRegistrar where C: Codec, { WorkerRegistrar::new() } /// Executes an worker in the current environment. pub fn register(&self) where CODEC: Codec, W::Input: Serialize + for<'de> Deserialize<'de>, W::Output: Serialize + for<'de> Deserialize<'de>, { let scope = WorkerScope::::new::(); let upd = WorkerLifecycleEvent::Create(scope.clone()); scope.send(upd); let handler = move |msg: ToWorker| { let upd = WorkerLifecycleEvent::Remote(msg); scope.send(upd); }; let loaded: FromWorker = FromWorker::WorkerLoaded; let worker = DedicatedWorker::worker_self(); worker.set_on_packed_message::<_, CODEC, _>(handler); worker.post_packed_message::<_, CODEC>(loaded); } } ================================================ FILE: packages/yew-agent/src/worker/scope.rs ================================================ use std::cell::RefCell; use std::fmt; use std::future::Future; use std::rc::Rc; use serde::de::Deserialize; use serde::ser::Serialize; use wasm_bindgen_futures::spawn_local; use super::handler_id::HandlerId; use super::lifecycle::{WorkerLifecycleEvent, WorkerRunnable, WorkerState}; use super::messages::FromWorker; use super::native_worker::{DedicatedWorker, NativeWorkerExt, WorkerSelf}; use super::traits::Worker; use super::Shared; use crate::codec::Codec; /// A handle that closes the worker when it is dropped. pub struct WorkerDestroyHandle where W: Worker + 'static, { scope: WorkerScope, } impl fmt::Debug for WorkerDestroyHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("WorkerDestroyHandle<_>") } } impl WorkerDestroyHandle where W: Worker, { pub(crate) fn new(scope: WorkerScope) -> Self { Self { scope } } } impl Drop for WorkerDestroyHandle where W: Worker, { fn drop(&mut self) { self.scope.send(WorkerLifecycleEvent::Destroy); } } /// This struct holds a reference to a component and to a global scheduler. pub struct WorkerScope { state: Shared>, post_msg: Rc)>, } impl fmt::Debug for WorkerScope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("WorkerScope<_>") } } impl Clone for WorkerScope { fn clone(&self) -> Self { WorkerScope { state: self.state.clone(), post_msg: self.post_msg.clone(), } } } impl WorkerScope where W: Worker + 'static, { /// Create worker scope pub(crate) fn new() -> Self where CODEC: Codec, W::Output: Serialize + for<'de> Deserialize<'de>, { let post_msg = move |msg: FromWorker| { DedicatedWorker::worker_self().post_packed_message::<_, CODEC>(msg) }; let state = Rc::new(RefCell::new(WorkerState::new())); WorkerScope { post_msg: Rc::new(post_msg), state, } } /// Schedule message for sending to worker pub(crate) fn send(&self, event: WorkerLifecycleEvent) { let state = self.state.clone(); // We can implement a custom scheduler, // but it's easier to borrow the one from wasm-bindgen-futures. spawn_local(async move { WorkerRunnable { state, event }.run(); }); } /// Send response to a worker bridge. pub fn respond(&self, id: HandlerId, output: W::Output) { let msg = FromWorker::::ProcessOutput(id, output); (self.post_msg)(msg); } /// Send a message to the worker pub fn send_message(&self, msg: T) where T: Into, { self.send(WorkerLifecycleEvent::Message(msg.into())); } /// Create a callback which will send a message to the worker when invoked. pub fn callback(&self, function: F) -> Rc where M: Into, F: Fn(IN) -> M + 'static, { let scope = self.clone(); let closure = move |input| { let output = function(input).into(); scope.send(WorkerLifecycleEvent::Message(output)); }; Rc::new(closure) } /// This method creates a callback which returns a Future which /// returns a message to be sent back to the worker /// /// # Panics /// If the future panics, then the promise will not resolve, and /// will leak. pub fn callback_future(&self, function: FN) -> Rc where M: Into, FU: Future + 'static, FN: Fn(IN) -> FU + 'static, { let scope = self.clone(); let closure = move |input: IN| { let future: FU = function(input); scope.send_future(future); }; Rc::new(closure) } /// This method processes a Future that returns a message and sends it back to the worker. /// /// # Panics /// If the future panics, then the promise will not resolve, and will leak. pub fn send_future(&self, future: F) where M: Into, F: Future + 'static, { let scope = self.clone(); let js_future = async move { let message: W::Message = future.await.into(); let cb = scope.callback(|m: W::Message| m); (*cb)(message); }; wasm_bindgen_futures::spawn_local(js_future); } } ================================================ FILE: packages/yew-agent/src/worker/spawner.rs ================================================ use std::cell::RefCell; use std::collections::HashMap; use std::fmt; use std::marker::PhantomData; use std::rc::{Rc, Weak}; use js_sys::Array; use serde::de::Deserialize; use serde::ser::Serialize; use web_sys::{Blob, BlobPropertyBag, Url, WorkerOptions, WorkerType}; use super::bridge::{CallbackMap, WorkerBridge}; use super::handler_id::HandlerId; use super::messages::FromWorker; use super::native_worker::{DedicatedWorker, NativeWorkerExt}; use super::traits::Worker; use super::{Callback, Shared}; use crate::codec::{Bincode, Codec}; use crate::utils::window; /// A spawner to create workers. #[derive(Clone)] pub struct WorkerSpawner where W: Worker, CODEC: Codec, { _marker: PhantomData<(W, CODEC)>, callback: Option>, with_loader: bool, as_module: bool, } impl fmt::Debug for WorkerSpawner where W: Worker, CODEC: Codec, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("WorkerScope<_>") } } impl Default for WorkerSpawner where W: Worker + 'static, CODEC: Codec, { fn default() -> Self { Self::new() } } impl WorkerSpawner where W: Worker + 'static, CODEC: Codec, { /// Creates a [WorkerSpawner]. pub const fn new() -> Self { Self { _marker: PhantomData, callback: None, with_loader: false, as_module: false, } } /// Sets a new message encoding. pub fn encoding(&mut self) -> WorkerSpawner where C: Codec, { WorkerSpawner { _marker: PhantomData, callback: self.callback.clone(), with_loader: self.with_loader, as_module: self.as_module, } } /// Sets a callback. pub fn callback(&mut self, cb: F) -> &mut Self where F: 'static + Fn(W::Output), { self.callback = Some(Rc::new(cb)); self } /// Indicates that [`spawn`](WorkerSpawner#method.spawn) should expect a /// `path` to a loader shim script (e.g. when using Trunk, created by using /// the [`data-loader-shim`](https://trunkrs.dev/assets/#link-asset-types) /// asset type) and one does not need to be generated. `false` by default. pub fn with_loader(&mut self, with_loader: bool) -> &mut Self { self.with_loader = with_loader; self } /// Determines whether the worker will be spawned with /// [`options.type`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker#type) /// set to `module`. `true` by default. /// /// This option should be un-set if the worker was created with the /// `--target no-modules` flag of `wasm-bindgen`. If using Trunk, see the /// [`data-bindgen-target`](https://trunkrs.dev/assets/#link-asset-types) /// asset type. pub fn as_module(&mut self, as_module: bool) -> &mut Self { self.as_module = as_module; self } /// Spawns a Worker. pub fn spawn(&self, path: &str) -> WorkerBridge where W::Input: Serialize + for<'de> Deserialize<'de>, W::Output: Serialize + for<'de> Deserialize<'de>, { let worker = self.create_worker(path).expect("failed to spawn worker"); self.spawn_inner(worker) } fn create_worker(&self, path: &str) -> Option { let path = if self.with_loader { std::borrow::Cow::Borrowed(path) } else { let js_shim_url = Url::new_with_base( path, &window().location().href().expect("failed to read href."), ) .expect("failed to create url for javascript entrypoint") .to_string(); let wasm_url = js_shim_url.replace(".js", "_bg.wasm"); let array = Array::new(); let shim = if self.as_module { format!(r#"import init from '{js_shim_url}';await init();"#) } else { format!(r#"importScripts("{js_shim_url}");wasm_bindgen("{wasm_url}");"#) }; array.push(&shim.into()); let blob_property = BlobPropertyBag::new(); blob_property.set_type("application/javascript"); let blob = Blob::new_with_str_sequence_and_options(&array, &blob_property).unwrap(); let url = Url::create_object_url_with_blob(&blob).unwrap(); std::borrow::Cow::Owned(url) }; let path = path.as_ref(); if self.as_module { let options = WorkerOptions::new(); options.set_type(WorkerType::Module); DedicatedWorker::new_with_options(path, &options).ok() } else { DedicatedWorker::new(path).ok() } } fn spawn_inner(&self, worker: DedicatedWorker) -> WorkerBridge where W::Input: Serialize + for<'de> Deserialize<'de>, W::Output: Serialize + for<'de> Deserialize<'de>, { let pending_queue = Rc::new(RefCell::new(Some(Vec::new()))); let handler_id = HandlerId::new(); let mut callbacks = HashMap::new(); if let Some(m) = self.callback.as_ref().map(Rc::downgrade) { callbacks.insert(handler_id, m); } let callbacks: Shared> = Rc::new(RefCell::new(callbacks)); let handler = { let pending_queue = pending_queue.clone(); let callbacks = callbacks.clone(); let worker = worker.clone(); move |msg: FromWorker| match msg { FromWorker::WorkerLoaded => { if let Some(pending_queue) = pending_queue.borrow_mut().take() { for to_worker in pending_queue.into_iter() { worker.post_packed_message::<_, CODEC>(to_worker); } } } FromWorker::ProcessOutput(id, output) => { let mut callbacks = callbacks.borrow_mut(); if let Some(m) = callbacks.get(&id) { if let Some(m) = Weak::upgrade(m) { m(output); } else { callbacks.remove(&id); } } } } }; worker.set_on_packed_message::<_, CODEC, _>(handler); WorkerBridge::::new::( handler_id, worker, pending_queue, callbacks, self.callback.clone(), ) } } ================================================ FILE: packages/yew-agent/src/worker/traits.rs ================================================ use super::handler_id::HandlerId; use super::registrar::WorkerRegistrar; use super::scope::{WorkerDestroyHandle, WorkerScope}; use super::spawner::WorkerSpawner; use crate::traits::{Registrable, Spawnable}; /// Declares the behaviour of a worker. pub trait Worker: Sized { /// Update message type. type Message; /// Incoming message type. type Input; /// Outgoing message type. type Output; /// Creates an instance of a worker. fn create(scope: &WorkerScope) -> Self; /// Receives an update. /// /// This method is called when the worker send messages to itself via /// [`WorkerScope::send_message`]. fn update(&mut self, scope: &WorkerScope, msg: Self::Message); /// New bridge created. /// /// When a new bridge is created by [`WorkerSpawner::spawn`](crate::WorkerSpawner) /// or [`WorkerBridge::fork`](crate::WorkerBridge::fork), /// the worker will be notified the [`HandlerId`] of the created bridge via this method. fn connected(&mut self, scope: &WorkerScope, id: HandlerId) { let _scope = scope; let _id = id; } /// Receives an input from a connected bridge. /// /// When a bridge sends an input via [`WorkerBridge::send`](crate::WorkerBridge::send), the /// worker will receive the input via this method. fn received(&mut self, scope: &WorkerScope, msg: Self::Input, id: HandlerId); /// Existing bridge destroyed. /// /// When a bridge is dropped, the worker will be notified with this method. fn disconnected(&mut self, scope: &WorkerScope, id: HandlerId) { let _scope = scope; let _id = id; } /// Destroys the current worker. /// /// When all bridges are dropped, the method will be invoked. /// /// This method is provided a destroy handle where when it is dropped, the worker is closed. /// If the worker is closed immediately, then it can ignore the destroy handle. /// Otherwise hold the destroy handle until the clean up task is finished. /// /// # Note /// /// This method will only be called after all bridges are disconnected. /// Attempting to send messages after this method is called will have no effect. fn destroy(&mut self, scope: &WorkerScope, destruct: WorkerDestroyHandle) { let _scope = scope; let _destruct = destruct; } } impl Spawnable for W where W: Worker + 'static, { type Spawner = WorkerSpawner; fn spawner() -> WorkerSpawner { WorkerSpawner::new() } } impl Registrable for W where W: Worker + 'static, { type Registrar = WorkerRegistrar; fn registrar() -> WorkerRegistrar { WorkerRegistrar::new() } } ================================================ FILE: packages/yew-agent-macro/Cargo.toml ================================================ [package] name = "yew-agent-macro" version = "0.4.0" edition = "2021" rust-version = "1.84.0" authors = ["Kaede Hoshikawa "] repository = "https://github.com/yewstack/yew" homepage = "https://yew.rs" documentation = "https://docs.rs/yew/" readme = "../../README.md" description = "Macro Support for Yew Agents" license = "MIT OR Apache-2.0" [lib] proc-macro = true [dependencies] proc-macro2.workspace = true quote.workspace = true syn = { workspace = true, features = ["full", "extra-traits"] } [dev-dependencies] rustversion.workspace = true trybuild = { workspace = true } yew-agent = { path = "../yew-agent" } ================================================ FILE: packages/yew-agent-macro/release.toml ================================================ tag = false ================================================ FILE: packages/yew-agent-macro/src/agent_fn.rs ================================================ use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{Attribute, FnArg, Generics, Ident, Item, ItemFn, Signature, Type, Visibility}; pub trait AgentFnType { type RecvType; type OutputType; fn attr_name() -> &'static str; fn agent_type_name() -> &'static str; fn parse_recv_type(sig: &Signature) -> syn::Result; fn parse_output_type(sig: &Signature) -> syn::Result; fn extract_fn_arg_type(arg: &FnArg) -> syn::Result { let ty = match arg { FnArg::Typed(arg) => arg.ty.clone(), FnArg::Receiver(_) => { return Err(syn::Error::new_spanned( arg, format!("{} agents can't accept a receiver", Self::agent_type_name()), )); } }; Ok(*ty) } fn assert_no_left_argument(rest_inputs: I, expected_len: usize) -> syn::Result<()> where I: ExactSizeIterator + IntoIterator, T: ToTokens, { // Checking after param parsing may make it a little inefficient // but that's a requirement for better error messages in case of receivers // `>0` because first one is already consumed. if rest_inputs.len() > 0 { let params: TokenStream = rest_inputs .into_iter() .map(|it| it.to_token_stream()) .collect(); return Err(syn::Error::new_spanned( params, format!( "{} agent can accept at most {} argument{}", Self::agent_type_name(), expected_len, if expected_len > 1 { "s" } else { "" } ), )); } Ok(()) } } #[derive(Clone)] pub struct AgentFn where F: AgentFnType + 'static, { pub recv_type: F::RecvType, pub output_type: F::OutputType, pub generics: Generics, pub vis: Visibility, pub attrs: Vec, pub name: Ident, pub agent_name: Option, pub is_async: bool, pub func: ItemFn, } impl Parse for AgentFn where F: AgentFnType + 'static, { fn parse(input: ParseStream) -> syn::Result { let parsed: Item = input.parse()?; let func = match parsed { Item::Fn(m) => m, item => { return Err(syn::Error::new_spanned( item, format!( "`{}` attribute can only be applied to functions", F::attr_name() ), )) } }; let ItemFn { attrs, vis, sig, .. } = func.clone(); if sig.generics.lifetimes().next().is_some() { return Err(syn::Error::new_spanned( sig.generics, format!( "{} agents can't have generic lifetime parameters", F::agent_type_name() ), )); } if sig.constness.is_some() { return Err(syn::Error::new_spanned( sig.constness, format!("const functions can't be {} agents", F::agent_type_name()), )); } if sig.abi.is_some() { return Err(syn::Error::new_spanned( sig.abi, format!("extern functions can't be {} agents", F::agent_type_name()), )); } let recv_type = F::parse_recv_type(&sig)?; let output_type = F::parse_output_type(&sig)?; let is_async = sig.asyncness.is_some(); Ok(Self { recv_type, output_type, generics: sig.generics, is_async, vis, attrs, name: sig.ident, agent_name: None, func, }) } } impl AgentFn where F: AgentFnType + 'static, { /// Filters attributes that should be copied to agent definition. pub fn filter_attrs_for_agent_struct(&self) -> Vec { self.attrs .iter() .filter_map(|m| { m.path() .get_ident() .and_then(|ident| match ident.to_string().as_str() { "doc" | "allow" => Some(m.clone()), _ => None, }) }) .collect() } /// Filters attributes that should be copied to the agent impl block. pub fn filter_attrs_for_agent_impl(&self) -> Vec { self.attrs .iter() .filter_map(|m| { m.path() .get_ident() .and_then(|ident| match ident.to_string().as_str() { "allow" => Some(m.clone()), _ => None, }) }) .collect() } pub fn phantom_generics(&self) -> Punctuated { self.generics .type_params() .map(|ty_param| ty_param.ident.clone()) // create a new Punctuated sequence without any type bounds .collect::>() } pub fn merge_agent_name(&mut self, name: AgentName) -> syn::Result<()> { if let Some(ref m) = name.agent_name { if m == &self.name { return Err(syn::Error::new_spanned( m, format!( "the {} must not have the same name as the function", F::agent_type_name() ), )); } } self.agent_name = name.agent_name; Ok(()) } pub fn inner_fn_ident(&self) -> Ident { if self.agent_name.is_some() { self.name.clone() } else { Ident::new("inner", Span::mixed_site()) } } pub fn agent_name(&self) -> Ident { self.agent_name.clone().unwrap_or_else(|| self.name.clone()) } pub fn print_inner_fn(&self) -> ItemFn { let mut func = self.func.clone(); func.sig.ident = self.inner_fn_ident(); func.vis = Visibility::Inherited; func } } pub struct AgentName { agent_name: Option, } impl Parse for AgentName { fn parse(input: ParseStream) -> syn::Result { if input.is_empty() { return Ok(Self { agent_name: None }); } let agent_name = input.parse()?; Ok(Self { agent_name: Some(agent_name), }) } } ================================================ FILE: packages/yew-agent-macro/src/lib.rs ================================================ use proc_macro::TokenStream; use syn::parse_macro_input; mod agent_fn; mod oneshot; mod reactor; use agent_fn::{AgentFn, AgentName}; use oneshot::{oneshot_impl, OneshotFn}; use reactor::{reactor_impl, ReactorFn}; #[proc_macro_attribute] pub fn reactor(attr: TokenStream, item: TokenStream) -> TokenStream { let item = parse_macro_input!(item as AgentFn); let attr = parse_macro_input!(attr as AgentName); reactor_impl(attr, item) .unwrap_or_else(|err| err.to_compile_error()) .into() } #[proc_macro_attribute] pub fn oneshot(attr: TokenStream, item: TokenStream) -> TokenStream { let item = parse_macro_input!(item as AgentFn); let attr = parse_macro_input!(attr as AgentName); oneshot_impl(attr, item) .unwrap_or_else(|err| err.to_compile_error()) .into() } ================================================ FILE: packages/yew-agent-macro/src/oneshot.rs ================================================ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{parse_quote, Ident, ReturnType, Signature, Type}; use crate::agent_fn::{AgentFn, AgentFnType, AgentName}; pub struct OneshotFn {} impl AgentFnType for OneshotFn { type OutputType = Type; type RecvType = Type; fn attr_name() -> &'static str { "oneshot" } fn agent_type_name() -> &'static str { "oneshot" } fn parse_recv_type(sig: &Signature) -> syn::Result { let mut inputs = sig.inputs.iter(); let arg = inputs .next() .ok_or_else(|| syn::Error::new_spanned(&sig.ident, "expected 1 argument"))?; let ty = Self::extract_fn_arg_type(arg)?; Self::assert_no_left_argument(inputs, 1)?; Ok(ty) } fn parse_output_type(sig: &Signature) -> syn::Result { let ty = match &sig.output { ReturnType::Default => { parse_quote! { () } } ReturnType::Type(_, ty) => *ty.clone(), }; Ok(ty) } } pub fn oneshot_impl(name: AgentName, mut agent_fn: AgentFn) -> syn::Result { agent_fn.merge_agent_name(name)?; let struct_attrs = agent_fn.filter_attrs_for_agent_struct(); let oneshot_impl_attrs = agent_fn.filter_attrs_for_agent_impl(); let phantom_generics = agent_fn.phantom_generics(); let oneshot_name = agent_fn.agent_name(); let fn_name = agent_fn.inner_fn_ident(); let inner_fn = agent_fn.print_inner_fn(); let AgentFn { recv_type: input_type, generics, output_type, vis, is_async, .. } = agent_fn; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let fn_generics = ty_generics.as_turbofish(); let in_ident = Ident::new("_input", Span::mixed_site()); let fn_call = if is_async { quote! { #fn_name #fn_generics (#in_ident).await } } else { quote! { #fn_name #fn_generics (#in_ident) } }; let crate_name = quote! { yew_agent }; let quoted = quote! { #(#struct_attrs)* #[allow(unused_parens)] #vis struct #oneshot_name #generics #where_clause { inner: ::std::pin::Pin<::std::boxed::Box>>, _marker: ::std::marker::PhantomData<(#phantom_generics)>, } // we cannot disable any lints here because it will be applied to the function body // as well. #(#oneshot_impl_attrs)* impl #impl_generics ::#crate_name::oneshot::Oneshot for #oneshot_name #ty_generics #where_clause { type Input = #input_type; fn create(#in_ident: Self::Input) -> Self { #inner_fn Self { inner: ::std::boxed::Box::pin( async move { #fn_call } ), _marker: ::std::marker::PhantomData, } } } impl #impl_generics ::std::future::Future for #oneshot_name #ty_generics #where_clause { type Output = #output_type; fn poll(mut self: ::std::pin::Pin<&mut Self>, cx: &mut ::std::task::Context<'_>) -> ::std::task::Poll { ::std::future::Future::poll(::std::pin::Pin::new(&mut self.inner), cx) } } impl #impl_generics ::#crate_name::Registrable for #oneshot_name #ty_generics #where_clause { type Registrar = ::#crate_name::oneshot::OneshotRegistrar; fn registrar() -> Self::Registrar { ::#crate_name::oneshot::OneshotRegistrar::::new() } } impl #impl_generics ::#crate_name::Spawnable for #oneshot_name #ty_generics #where_clause { type Spawner = ::#crate_name::oneshot::OneshotSpawner; fn spawner() -> Self::Spawner { ::#crate_name::oneshot::OneshotSpawner::::new() } } }; Ok(quoted) } ================================================ FILE: packages/yew-agent-macro/src/reactor.rs ================================================ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{Ident, ReturnType, Signature, Type}; use crate::agent_fn::{AgentFn, AgentFnType, AgentName}; pub struct ReactorFn {} impl AgentFnType for ReactorFn { type OutputType = (); type RecvType = Type; fn attr_name() -> &'static str { "reactor" } fn agent_type_name() -> &'static str { "reactor" } fn parse_recv_type(sig: &Signature) -> syn::Result { let mut inputs = sig.inputs.iter(); let arg = inputs .next() .ok_or_else(|| syn::Error::new_spanned(&sig.ident, "expected 1 argument"))?; let ty = Self::extract_fn_arg_type(arg)?; Self::assert_no_left_argument(inputs, 1)?; Ok(ty) } fn parse_output_type(sig: &Signature) -> syn::Result { match &sig.output { ReturnType::Default => {} ReturnType::Type(_, ty) => { return Err(syn::Error::new_spanned( ty, "reactor agents cannot return any value", )) } } Ok(()) } } pub fn reactor_impl(name: AgentName, mut agent_fn: AgentFn) -> syn::Result { agent_fn.merge_agent_name(name)?; if !agent_fn.is_async { return Err(syn::Error::new_spanned( &agent_fn.name, "reactor agents must be asynchronous", )); } let struct_attrs = agent_fn.filter_attrs_for_agent_struct(); let reactor_impl_attrs = agent_fn.filter_attrs_for_agent_impl(); let phantom_generics = agent_fn.phantom_generics(); let reactor_name = agent_fn.agent_name(); let fn_name = agent_fn.inner_fn_ident(); let inner_fn = agent_fn.print_inner_fn(); let AgentFn { recv_type, generics, vis, .. } = agent_fn; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let fn_generics = ty_generics.as_turbofish(); let scope_ident = Ident::new("_scope", Span::mixed_site()); let fn_call = quote! { #fn_name #fn_generics (#scope_ident).await }; let crate_name = quote! { yew_agent }; let quoted = quote! { #(#struct_attrs)* #[allow(unused_parens)] #vis struct #reactor_name #generics #where_clause { inner: ::std::pin::Pin<::std::boxed::Box>>, _marker: ::std::marker::PhantomData<(#phantom_generics)>, } // we cannot disable any lints here because it will be applied to the function body // as well. #(#reactor_impl_attrs)* impl #impl_generics ::#crate_name::reactor::Reactor for #reactor_name #ty_generics #where_clause { type Scope = #recv_type; fn create(#scope_ident: Self::Scope) -> Self { #inner_fn Self { inner: ::std::boxed::Box::pin( async move { #fn_call } ), _marker: ::std::marker::PhantomData, } } } impl #impl_generics ::std::future::Future for #reactor_name #ty_generics #where_clause { type Output = (); fn poll(mut self: ::std::pin::Pin<&mut Self>, cx: &mut ::std::task::Context<'_>) -> ::std::task::Poll { ::std::future::Future::poll(::std::pin::Pin::new(&mut self.inner), cx) } } impl #impl_generics ::#crate_name::Registrable for #reactor_name #ty_generics #where_clause { type Registrar = ::#crate_name::reactor::ReactorRegistrar; fn registrar() -> Self::Registrar { ::#crate_name::reactor::ReactorRegistrar::::new() } } impl #impl_generics ::#crate_name::Spawnable for #reactor_name #ty_generics #where_clause { type Spawner = ::#crate_name::reactor::ReactorSpawner; fn spawner() -> Self::Spawner { ::#crate_name::reactor::ReactorSpawner::::new() } } }; Ok(quoted) } ================================================ FILE: packages/yew-macro/Cargo.toml ================================================ [package] name = "yew-macro" version = "0.23.0" edition = "2021" authors = ["Justin Starry "] repository = "https://github.com/yewstack/yew" homepage = "https://github.com/yewstack/yew" documentation = "https://docs.rs/yew-macro/" license = "MIT OR Apache-2.0" keywords = ["web", "wasm", "frontend", "webasm", "webassembly"] categories = ["gui", "web-programming", "wasm"] description = "A framework for making client-side single-page apps" rust-version = "1.84.0" [lib] proc-macro = true [dependencies] proc-macro-error = "1" proc-macro2.workspace = true quote.workspace = true syn = { workspace = true, features = ["full", "extra-traits", "visit-mut"] } prettyplease = "0.2" rustversion.workspace = true # testing [dev-dependencies] trybuild = { workspace = true } yew = { path = "../yew" } implicit-clone = { workspace = true } [lints] workspace = true ================================================ FILE: packages/yew-macro/Makefile.toml ================================================ [tasks.test] clear = true toolchain = "1.84.0" command = "cargo" # test target can be optionally specified like `cargo make test html_macro`, args = ["test", "${@}"] [tasks.test-lint] clear = true toolchain = "nightly" command = "cargo" args = ["test", "test_html_lints", "--features", "lints"] [tasks.test-overwrite] extend = "test" env = { TRYBUILD = "overwrite" } ================================================ FILE: packages/yew-macro/release.toml ================================================ tag = false ================================================ FILE: packages/yew-macro/src/classes/mod.rs ================================================ use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::{Expr, ExprLit, Lit, LitStr, Token}; /// List of HTML classes. pub struct Classes(Punctuated); impl Parse for Classes { fn parse(input: ParseStream) -> syn::Result { input .parse_terminated(ClassExpr::parse, Token![,]) .map(Self) } } impl ToTokens for Classes { fn to_tokens(&self, tokens: &mut TokenStream) { let n = self.0.len(); let push_classes = self.0.iter().map(|x| match x { ClassExpr::Lit(class) => quote! { unsafe { __yew_classes.unchecked_push(#class) }; }, ClassExpr::Expr(class) => quote_spanned! {class.span()=> __yew_classes.push(#class); }, }); tokens.extend(quote! { { let mut __yew_classes = ::yew::html::Classes::with_capacity(#n); #(#push_classes)* __yew_classes } }); } } enum ClassExpr { Lit(LitStr), Expr(Box), } impl Parse for ClassExpr { fn parse(input: ParseStream) -> syn::Result { match input.parse()? { Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => { let value = lit_str.value(); let classes = value.split_whitespace().collect::>(); if classes.len() > 1 { let fix = classes .into_iter() .map(|class| format!("\"{class}\"")) .collect::>() .join(", "); let msg = format!( "string literals must not contain more than one class (hint: use `{fix}`)" ); Err(syn::Error::new(lit_str.span(), msg)) } else { Ok(Self::Lit(lit_str)) } } expr => Ok(Self::Expr(Box::new(expr))), } } } ================================================ FILE: packages/yew-macro/src/derive_props/builder.rs ================================================ //! The `PropsBuilder` constructs props in alphabetical order and enforces that required props have //! been set before allowing the build to complete. Each property has a corresponding method in the //! builder. Required property builder methods advance the builder to the next step, optional //! properties can be added or skipped with no effect on the build step. Once all of required //! properties have been set, the builder moves to the final build step which implements the //! `build()` method. use proc_macro2::{Ident, Span}; use quote::{format_ident, quote, ToTokens}; use syn::{parse_quote_spanned, Attribute, GenericParam}; use super::generics::to_arguments; use super::DerivePropsInput; use crate::derive_props::generics::push_type_param; pub struct PropsBuilder<'a> { builder_name: &'a Ident, props: &'a DerivePropsInput, wrapper_name: &'a Ident, check_all_props_name: &'a Ident, extra_attrs: &'a [Attribute], } impl ToTokens for PropsBuilder<'_> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { builder_name, props, wrapper_name, .. } = self; let DerivePropsInput { vis, generics, .. } = props; let assert_all_props = self.impl_assert_props(); let (_, ty_generics, where_clause) = generics.split_for_impl(); let builder = quote! { #[doc(hidden)] #vis struct #builder_name #generics #where_clause { wrapped: ::std::boxed::Box<#wrapper_name #ty_generics>, } #assert_all_props }; tokens.extend(builder); } } impl<'a> PropsBuilder<'_> { pub fn new( name: &'a Ident, props: &'a DerivePropsInput, wrapper_name: &'a Ident, check_all_props_name: &'a Ident, extra_attrs: &'a [Attribute], ) -> PropsBuilder<'a> { PropsBuilder { builder_name: name, props, wrapper_name, check_all_props_name, extra_attrs, } } } impl PropsBuilder<'_> { fn set_fields(&self) -> impl Iterator { self.props.prop_fields.iter().map(|pf| pf.to_field_setter()) } fn impl_assert_props(&self) -> proc_macro2::TokenStream { let Self { builder_name, check_all_props_name, extra_attrs, .. } = self; let DerivePropsInput { vis, generics, props_name, prop_fields, .. } = self.props; let set_fields = self.set_fields(); let prop_fns = prop_fields .iter() .map(|pf| pf.to_build_step_fn(vis, props_name)); let (builder_impl_generics, ty_generics, builder_where_clause) = generics.split_for_impl(); let turbofish_generics = ty_generics.as_turbofish(); let generic_args = to_arguments(generics); let mut assert_impl_generics = generics.clone(); let token_arg: GenericParam = parse_quote_spanned! {Span::mixed_site()=> __YewToken }; push_type_param(&mut assert_impl_generics, token_arg.clone()); let assert_impl_generics = assert_impl_generics; let (impl_generics, _, where_clause) = assert_impl_generics.split_for_impl(); let props_mod_name = format_ident!("_{}", props_name, span = Span::mixed_site()); let mut check_impl_generics = assert_impl_generics.clone(); let mut check_args = vec![]; let mut check_props = proc_macro2::TokenStream::new(); let prop_field_decls = prop_fields .iter() .map(|pf| pf.to_field_check(props_name, vis, &token_arg)) .collect::>(); let prop_name_decls = prop_field_decls.iter().map(|pf| pf.to_fake_prop_decl()); for pf in prop_field_decls.iter() { check_props.extend(pf.to_stream( &mut check_impl_generics, &mut check_args, &props_mod_name, )); } let (check_impl_generics, _, check_where_clause) = check_impl_generics.split_for_impl(); quote! { #[automatically_derived] #( #extra_attrs )* impl #builder_impl_generics #builder_name<#generic_args> #builder_where_clause { #( #prop_fns )* } #[doc(hidden)] #[allow(non_snake_case)] #vis mod #props_mod_name { #( #prop_name_decls )* } #check_props #[doc(hidden)] #vis struct #check_all_props_name(::std::marker::PhantomData); #[automatically_derived] #[diagnostic::do_not_recommend] impl ::yew::html::HasProp> for #check_all_props_name where B: ::yew::html::HasProp {} #[automatically_derived] #[diagnostic::do_not_recommend] impl #check_impl_generics ::yew::html::HasAllProps< #props_name #ty_generics , ( #( #check_args , )* ), > for #check_all_props_name< #token_arg > #check_where_clause { } #[automatically_derived] impl #impl_generics ::yew::html::Buildable< #token_arg > for #builder_name<#generic_args> #where_clause { type Output = #props_name #ty_generics; type WrappedToken = #check_all_props_name< #token_arg >; fn build(this: Self) -> Self::Output { #props_name #turbofish_generics { #(#set_fields)* } } } } } } ================================================ FILE: packages/yew-macro/src/derive_props/field.rs ================================================ use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; use std::convert::TryFrom; use proc_macro2::{Ident, Span}; use quote::{format_ident, quote, quote_spanned}; use syn::parse::Result; use syn::spanned::Spanned; use syn::{ parse_quote, Attribute, Error, Expr, Field, GenericArgument, GenericParam, Generics, PathArguments, Type, Visibility, }; use super::should_preserve_attr; use crate::derive_props::generics::push_type_param; fn is_option_type(ty: &Type) -> bool { if let Type::Path(type_path) = ty { if let Some(segment) = type_path.path.segments.last() { if segment.ident == "Option" { if let PathArguments::AngleBracketed(args) = &segment.arguments { return args.args.len() == 1 && matches!(args.args.first(), Some(GenericArgument::Type(_))); } } } } false } #[allow(clippy::large_enum_variant)] #[derive(PartialEq, Eq)] pub enum PropAttr { Required { wrapped_name: Ident }, PropOr(Expr), PropOrElse(Expr), PropOrDefault, } #[derive(Eq)] pub struct PropField { pub ty: Type, name: Ident, pub attr: PropAttr, extra_attrs: Vec, } impl PropField { /// All required property fields are wrapped in an `Option` pub fn is_required(&self) -> bool { matches!(self.attr, PropAttr::Required { .. }) } /// This check name is descriptive to help a developer realize they missed a required prop fn to_check_name(&self, props_name: &Ident) -> Ident { format_ident!("Has{}{}", props_name, self.name, span = Span::mixed_site()) } /// This check name is descriptive to help a developer realize they missed a required prop fn to_check_arg_name(&self, props_name: &Ident) -> GenericParam { let ident = format_ident!("How{}{}", props_name, self.name, span = Span::mixed_site()); GenericParam::Type(ident.into()) } /// Ident of the wrapped field name fn wrapped_name(&self) -> &Ident { match &self.attr { PropAttr::Required { wrapped_name } => wrapped_name, _ => &self.name, } } pub fn to_field_check<'a>( &'a self, props_name: &'a Ident, vis: &'a Visibility, token: &'a GenericParam, ) -> PropFieldCheck<'a> { let check_struct = self.to_check_name(props_name); let check_arg = self.to_check_arg_name(props_name); PropFieldCheck { this: self, vis, token, check_struct, check_arg, } } /// Used to transform the `PropWrapper` struct into `Properties` pub fn to_field_setter(&self) -> proc_macro2::TokenStream { let name = &self.name; let setter = match &self.attr { PropAttr::Required { wrapped_name } => { quote! { #name: ::std::option::Option::unwrap(this.wrapped.#wrapped_name), } } PropAttr::PropOr(value) => { quote_spanned! {value.span()=> #name: ::std::option::Option::unwrap_or(this.wrapped.#name, #value), } } PropAttr::PropOrElse(func) => { quote_spanned! {func.span()=> #name: ::std::option::Option::unwrap_or_else(this.wrapped.#name, #func), } } PropAttr::PropOrDefault => { quote! { #name: ::std::option::Option::unwrap_or_default(this.wrapped.#name), } } }; let extra_attrs = &self.extra_attrs; quote! { #( #extra_attrs )* #setter } } /// Wrap all required props in `Option` pub fn to_field_def(&self) -> proc_macro2::TokenStream { let ty = &self.ty; let extra_attrs = &self.extra_attrs; let wrapped_name = self.wrapped_name(); quote! { #( #extra_attrs )* #wrapped_name: ::std::option::Option<#ty>, } } /// All optional props must implement the `Default` trait pub fn to_default_setter(&self) -> proc_macro2::TokenStream { let wrapped_name = self.wrapped_name(); let extra_attrs = &self.extra_attrs; quote! { #( #extra_attrs )* #wrapped_name: ::std::option::Option::None, } } /// Each field is set using a builder method pub fn to_build_step_fn( &self, vis: &Visibility, props_name: &Ident, ) -> proc_macro2::TokenStream { let Self { name, ty, attr, .. } = self; let token_ty = Ident::new("__YewTokenTy", Span::mixed_site()); let none_fn_name = format_ident!("{}_none", name, span = Span::mixed_site()); let build_fn = match attr { PropAttr::Required { wrapped_name } => { let check_struct = self.to_check_name(props_name); let none_setter = if is_option_type(ty) { quote! { #[doc(hidden)] #vis fn #none_fn_name<#token_ty>( &mut self, token: #token_ty, ) -> #check_struct< #token_ty > { self.wrapped.#wrapped_name = ::std::option::Option::Some(::std::option::Option::None); #check_struct ( ::std::marker::PhantomData ) } } } else { quote! {} }; quote! { #[doc(hidden)] #vis fn #name<#token_ty>( &mut self, token: #token_ty, value: impl ::yew::html::IntoPropValue<#ty>, ) -> #check_struct< #token_ty > { self.wrapped.#wrapped_name = ::std::option::Option::Some(value.into_prop_value()); #check_struct ( ::std::marker::PhantomData ) } #none_setter } } _ => { let none_setter = if is_option_type(ty) { quote! { #[doc(hidden)] #vis fn #none_fn_name<#token_ty>( &mut self, token: #token_ty, ) -> #token_ty { self.wrapped.#name = ::std::option::Option::Some(::std::option::Option::None); token } } } else { quote! {} }; quote! { #[doc(hidden)] #vis fn #name<#token_ty>( &mut self, token: #token_ty, value: impl ::yew::html::IntoPropValue<#ty>, ) -> #token_ty { self.wrapped.#name = ::std::option::Option::Some(value.into_prop_value()); token } #none_setter } } }; let extra_attrs = &self.extra_attrs; quote! { #( #extra_attrs )* #build_fn } } // Detect Properties 2.0 attributes fn attribute(named_field: &Field) -> Result { let attr = named_field.attrs.iter().find(|attr| { attr.path().is_ident("prop_or") || attr.path().is_ident("prop_or_else") || attr.path().is_ident("prop_or_default") }); if let Some(attr) = attr { if attr.path().is_ident("prop_or") { Ok(PropAttr::PropOr(attr.parse_args()?)) } else if attr.path().is_ident("prop_or_else") { Ok(PropAttr::PropOrElse(attr.parse_args()?)) } else if attr.path().is_ident("prop_or_default") { Ok(PropAttr::PropOrDefault) } else { unreachable!() } } else { let ident = named_field.ident.as_ref().unwrap(); let wrapped_name = format_ident!("{}_wrapper", ident, span = Span::mixed_site()); Ok(PropAttr::Required { wrapped_name }) } } } pub struct PropFieldCheck<'a> { this: &'a PropField, vis: &'a Visibility, token: &'a GenericParam, check_struct: Ident, check_arg: GenericParam, } impl PropFieldCheck<'_> { pub fn to_fake_prop_decl(&self) -> proc_macro2::TokenStream { let Self { this, .. } = self; if !this.is_required() { return Default::default(); } let mut prop_check_name = this.name.clone(); prop_check_name.set_span(Span::mixed_site()); quote! { #[allow(non_camel_case_types)] pub struct #prop_check_name; } } pub fn to_stream( &self, type_generics: &mut Generics, check_args: &mut Vec, prop_name_mod: &Ident, ) -> proc_macro2::TokenStream { let Self { this, vis, token, check_struct, check_arg, } = self; if !this.is_required() { return Default::default(); } let mut prop_check_name = this.name.clone(); prop_check_name.set_span(Span::mixed_site()); check_args.push(check_arg.clone()); push_type_param(type_generics, check_arg.clone()); let where_clause = type_generics.make_where_clause(); where_clause.predicates.push(parse_quote! { #token: ::yew::html::HasProp< #prop_name_mod :: #prop_check_name, #check_arg > }); quote! { #[doc(hidden)] #[allow(non_camel_case_types)] #vis struct #check_struct(::std::marker::PhantomData); #[automatically_derived] #[diagnostic::do_not_recommend] impl ::yew::html::HasProp< #prop_name_mod :: #prop_check_name, #check_struct> for #check_struct {} #[automatically_derived] #[diagnostic::do_not_recommend] impl ::yew::html::HasProp> for #check_struct where B: ::yew::html::HasProp {} } } } impl TryFrom for PropField { type Error = Error; fn try_from(field: Field) -> Result { let extra_attrs = field .attrs .iter() .filter(|a| should_preserve_attr(a)) .cloned() .collect(); Ok(PropField { attr: Self::attribute(&field)?, extra_attrs, ty: field.ty, name: field.ident.unwrap(), }) } } impl PartialOrd for PropField { fn partial_cmp(&self, other: &PropField) -> Option { Some(self.cmp(other)) } } impl Ord for PropField { fn cmp(&self, other: &PropField) -> Ordering { if self.name == other.name { Ordering::Equal } else if self.name == "children" { Ordering::Greater } else if other.name == "children" { Ordering::Less } else { self.name.cmp(&other.name) } } } impl PartialEq for PropField { fn eq(&self, other: &Self) -> bool { self.name == other.name } } ================================================ FILE: packages/yew-macro/src/derive_props/generics.rs ================================================ use proc_macro2::Ident; use syn::punctuated::Punctuated; use syn::{GenericArgument, GenericParam, Generics, Path, Token, Type, TypePath}; /// Alias for a comma-separated list of `GenericArgument` pub type GenericArguments = Punctuated; /// Finds the index of the first generic param with a default value or a const generic. fn first_default_or_const_param_position(generics: &Generics) -> Option { generics.params.iter().position(|param| match param { GenericParam::Type(param) => param.default.is_some(), GenericParam::Const(_) => true, _ => false, }) } /// Push a type GenericParam into a Generics pub fn push_type_param(generics: &mut Generics, type_param: GenericParam) { if let Some(idx) = first_default_or_const_param_position(generics) { generics.params.insert(idx, type_param) } else { generics.params.push(type_param) } } /// Converts `GenericParams` into `GenericArguments`. pub fn to_arguments(generics: &Generics) -> GenericArguments { let mut args: GenericArguments = Punctuated::new(); args.extend(generics.params.iter().map(|param| match param { GenericParam::Type(type_param) => new_generic_type_arg(type_param.ident.clone()), GenericParam::Lifetime(lifetime_param) => { GenericArgument::Lifetime(lifetime_param.lifetime.clone()) } GenericParam::Const(const_param) => new_generic_type_arg(const_param.ident.clone()), })); args } // Creates a `GenericArgument` from an `Ident` fn new_generic_type_arg(ident: Ident) -> GenericArgument { GenericArgument::Type(Type::Path(TypePath { path: Path::from(ident), qself: None, })) } ================================================ FILE: packages/yew-macro/src/derive_props/mod.rs ================================================ mod builder; mod field; mod generics; mod wrapper; use std::convert::TryInto; use builder::PropsBuilder; use field::PropField; use proc_macro2::{Ident, Span}; use quote::{format_ident, quote, ToTokens}; use syn::parse::{Parse, ParseStream, Result}; use syn::punctuated::Pair; use syn::visit_mut::VisitMut; use syn::{ AngleBracketedGenericArguments, Attribute, ConstParam, DeriveInput, GenericArgument, GenericParam, Generics, Path, PathArguments, PathSegment, Type, TypeParam, TypePath, Visibility, }; use wrapper::PropsWrapper; use self::field::PropAttr; use self::generics::to_arguments; pub struct DerivePropsInput { vis: Visibility, generics: Generics, props_name: Ident, prop_fields: Vec, preserved_attrs: Vec, } /// AST visitor that replaces all occurrences of the keyword `Self` with `new_self` struct Normaliser<'ast> { new_self: &'ast Ident, generics: &'ast Generics, /// `Option` for one-time initialisation new_self_full: Option, } impl<'ast> Normaliser<'ast> { pub fn new(new_self: &'ast Ident, generics: &'ast Generics) -> Self { Self { new_self, generics, new_self_full: None, } } fn get_new_self(&mut self) -> PathSegment { self.new_self_full .get_or_insert_with(|| { PathSegment { ident: self.new_self.clone(), arguments: if self.generics.lt_token.is_some() { PathArguments::AngleBracketed(AngleBracketedGenericArguments { colon2_token: Some(Default::default()), lt_token: Default::default(), args: self .generics .params .pairs() .map(|pair| { let (value, punct) = pair.cloned().into_tuple(); let value = match value { GenericParam::Lifetime(param) => { GenericArgument::Lifetime(param.lifetime) } GenericParam::Type(TypeParam { ident, .. }) | GenericParam::Const(ConstParam { ident, .. }) => { GenericArgument::Type(Type::Path(TypePath { qself: None, path: ident.into(), })) } }; Pair::new(value, punct) }) .collect(), gt_token: Default::default(), }) } else { // if no generics were defined for the struct PathArguments::None }, } }) .clone() } } impl VisitMut for Normaliser<'_> { fn visit_path_mut(&mut self, path: &mut Path) { if let Some(first) = path.segments.first_mut() { if first.ident == "Self" { *first = self.get_new_self(); } syn::visit_mut::visit_path_mut(self, path) } } } /// Some attributes on the original struct are to be preserved and added to the builder struct, /// in order to avoid warnings (sometimes reported as errors) in the output. fn should_preserve_attr(attr: &Attribute) -> bool { // #[cfg(...)]: does not usually appear in macro inputs, but rust-analyzer seems to generate it // sometimes. If not preserved, results in "no-such-field" errors generating // the field setter for `build` #[allow(...)]: silences warnings from clippy, such as // dead_code etc. #[deny(...)]: enable additional warnings from clippy let path = attr.path(); path.is_ident("allow") || path.is_ident("deny") || path.is_ident("cfg") } impl Parse for DerivePropsInput { fn parse(input: ParseStream) -> Result { let input: DeriveInput = input.parse()?; let prop_fields = match input.data { syn::Data::Struct(data) => match data.fields { syn::Fields::Named(fields) => { let mut prop_fields: Vec = fields .named .into_iter() .map(|f| f.try_into()) .collect::>>()?; // Alphabetize prop_fields.sort(); prop_fields } syn::Fields::Unit => Vec::new(), _ => unimplemented!("only structs are supported"), }, _ => unimplemented!("only structs are supported"), }; let preserved_attrs = input .attrs .iter() .filter(|a| should_preserve_attr(a)) .cloned() .collect(); Ok(Self { vis: input.vis, props_name: input.ident, generics: input.generics, prop_fields, preserved_attrs, }) } } impl DerivePropsInput { /// Replaces all occurrences of `Self` in the struct with the actual name of the struct. /// Must be called before tokenising the struct. pub fn normalise(&mut self) { let mut normaliser = Normaliser::new(&self.props_name, &self.generics); for field in &mut self.prop_fields { normaliser.visit_type_mut(&mut field.ty); if let PropAttr::PropOr(expr) | PropAttr::PropOrElse(expr) = &mut field.attr { normaliser.visit_expr_mut(expr) } } } } impl ToTokens for DerivePropsInput { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { generics, props_name, prop_fields, preserved_attrs, .. } = self; // The wrapper is a new struct which wraps required props in `Option` let wrapper_name = format_ident!("{}Wrapper", props_name, span = Span::mixed_site()); let wrapper = PropsWrapper::new(&wrapper_name, generics, prop_fields, preserved_attrs); tokens.extend(wrapper.into_token_stream()); // The builder will only build if all required props have been set let builder_name = format_ident!("{}Builder", props_name, span = Span::mixed_site()); let check_all_props_name = format_ident!("Check{}All", props_name, span = Span::mixed_site()); let builder = PropsBuilder::new( &builder_name, self, &wrapper_name, &check_all_props_name, preserved_attrs, ); let generic_args = to_arguments(generics); tokens.extend(builder.into_token_stream()); // The properties trait has a `builder` method which creates the props builder let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let properties = quote! { impl #impl_generics ::yew::html::Properties for #props_name #ty_generics #where_clause { type Builder = #builder_name<#generic_args>; fn builder() -> Self::Builder { #builder_name { wrapped: ::std::boxed::Box::new(::std::default::Default::default()), } } } }; tokens.extend(properties); } } ================================================ FILE: packages/yew-macro/src/derive_props/wrapper.rs ================================================ use proc_macro2::Ident; use quote::{quote, ToTokens}; use syn::{Attribute, Generics}; use super::PropField; pub struct PropsWrapper<'a> { wrapper_name: &'a Ident, generics: &'a Generics, prop_fields: &'a [PropField], extra_attrs: &'a [Attribute], } impl ToTokens for PropsWrapper<'_> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { generics, wrapper_name, extra_attrs, .. } = self; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let turbofish_generics = ty_generics.as_turbofish(); let wrapper_field_defs = self.field_defs(); let wrapper_default_setters = self.default_setters(); let wrapper = quote! { #[doc(hidden)] #(#extra_attrs)* struct #wrapper_name #generics #where_clause { #(#wrapper_field_defs)* } #[automatically_derived] impl #impl_generics ::std::default::Default for #wrapper_name #ty_generics #where_clause { fn default() -> Self { #wrapper_name #turbofish_generics { #(#wrapper_default_setters)* } } } }; wrapper.to_tokens(tokens); } } impl<'a> PropsWrapper<'a> { pub fn new( name: &'a Ident, generics: &'a Generics, prop_fields: &'a [PropField], extra_attrs: &'a [Attribute], ) -> Self { PropsWrapper { wrapper_name: name, generics, prop_fields, extra_attrs, } } fn field_defs(&self) -> impl Iterator { self.prop_fields.iter().map(|pf| pf.to_field_def()) } fn default_setters(&self) -> impl Iterator { self.prop_fields.iter().map(|pf| pf.to_default_setter()) } } ================================================ FILE: packages/yew-macro/src/function_component.rs ================================================ use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::token::{Comma, Fn}; use syn::{ parse_quote, parse_quote_spanned, visit_mut, Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, LitStr, ReturnType, Type, Visibility, }; use crate::hook::BodyRewriter; #[derive(Clone)] pub struct FunctionComponent { block: Box, props_type: Box, arg: FnArg, generics: Generics, vis: Visibility, attrs: Vec, name: Ident, return_type: Box, fn_token: Fn, component_name: Option, } impl Parse for FunctionComponent { fn parse(input: ParseStream) -> syn::Result { let parsed: Item = input.parse()?; let func = match parsed { Item::Fn(m) => m, item => { return Err(syn::Error::new_spanned( item, "`component` attribute can only be applied to functions", )) } }; let ItemFn { attrs, vis, sig, block, } = func; if sig.generics.lifetimes().next().is_some() { return Err(syn::Error::new_spanned( sig.generics, "function components can't have generic lifetime parameters", )); } if sig.asyncness.is_some() { return Err(syn::Error::new_spanned( sig.asyncness, "function components can't be async", )); } if sig.constness.is_some() { return Err(syn::Error::new_spanned( sig.constness, "const functions can't be function components", )); } if sig.abi.is_some() { return Err(syn::Error::new_spanned( sig.abi, "extern functions can't be function components", )); } let return_type = match sig.output { ReturnType::Default => { return Err(syn::Error::new_spanned( sig, "function components must return `yew::Html` or `yew::HtmlResult`", )) } ReturnType::Type(_, ty) => ty, }; let mut inputs = sig.inputs.into_iter(); let arg = inputs .next() .unwrap_or_else(|| syn::parse_quote! { _: &() }); let ty = match &arg { FnArg::Typed(arg) => match &*arg.ty { Type::Reference(ty) => { if ty.lifetime.is_some() { return Err(syn::Error::new_spanned( &ty.lifetime, "reference must not have a lifetime", )); } if ty.mutability.is_some() { return Err(syn::Error::new_spanned( ty.mutability, "reference must not be mutable", )); } ty.elem.clone() } ty => { let msg = format!( "expected a reference to a `Properties` type (try: `&{}`)", ty.to_token_stream() ); return Err(syn::Error::new_spanned(ty, msg)); } }, FnArg::Receiver(_) => { return Err(syn::Error::new_spanned( arg, "function components can't accept a receiver", )); } }; // Checking after param parsing may make it a little inefficient // but that's a requirement for better error messages in case of receivers // `>0` because first one is already consumed. if inputs.len() > 0 { let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect(); return Err(syn::Error::new_spanned( params, "function components can accept at most one parameter for the props", )); } Ok(Self { props_type: ty, block, arg, generics: sig.generics, vis, attrs, name: sig.ident, return_type, fn_token: sig.fn_token, component_name: None, }) } } impl FunctionComponent { /// Filters attributes that should be copied to component definition. fn filter_attrs_for_component_struct(&self) -> Vec { self.attrs .iter() .filter_map(|m| { m.path() .get_ident() .and_then(|ident| match ident.to_string().as_str() { "doc" | "allow" => Some(m.clone()), _ => None, }) }) .collect() } /// Filters attributes that should be copied to the component impl block. fn filter_attrs_for_component_impl(&self) -> Vec { self.attrs .iter() .filter_map(|m| { m.path() .get_ident() .and_then(|ident| match ident.to_string().as_str() { "allow" => Some(m.clone()), _ => None, }) }) .collect() } fn phantom_generics(&self) -> Punctuated { self.generics .type_params() .map(|ty_param| ty_param.ident.clone()) // create a new Punctuated sequence without any type bounds .collect::>() } fn merge_component_name(&mut self, name: FunctionComponentName) -> syn::Result<()> { if let Some(ref m) = name.component_name { if m == &self.name { return Err(syn::Error::new_spanned( m, "the component must not have the same name as the function", )); } } self.component_name = name.component_name; Ok(()) } fn inner_fn_ident(&self) -> Ident { if self.component_name.is_some() { self.name.clone() } else { Ident::new("inner", Span::mixed_site()) } } fn component_name(&self) -> Ident { self.component_name .clone() .unwrap_or_else(|| self.name.clone()) } // We need to cast 'static on all generics for base component. fn create_static_component_generics(&self) -> Generics { let mut generics = self.generics.clone(); let where_clause = generics.make_where_clause(); for ty_generic in self.generics.type_params() { let ident = &ty_generic.ident; let bound = parse_quote_spanned! { ident.span() => #ident: 'static }; where_clause.predicates.push(bound); } where_clause.predicates.push(parse_quote! { Self: 'static }); generics } /// Prints the impl fn. fn print_inner_fn(&self) -> TokenStream { let name = self.inner_fn_ident(); let FunctionComponent { ref fn_token, ref attrs, ref block, ref return_type, ref generics, ref arg, .. } = self; let mut block = *block.clone(); let (impl_generics, _ty_generics, where_clause) = generics.split_for_impl(); // We use _ctx here so if the component does not use any hooks, the unused_vars lint will // not be triggered. let ctx_ident = Ident::new("_ctx", Span::mixed_site()); let mut body_rewriter = BodyRewriter::new(ctx_ident.clone()); visit_mut::visit_block_mut(&mut body_rewriter, &mut block); quote! { #(#attrs)* #fn_token #name #impl_generics (#ctx_ident: &mut ::yew::functional::HookContext, #arg) -> #return_type #where_clause { #block } } } fn print_base_component_impl(&self) -> TokenStream { let component_name = self.component_name(); let props_type = &self.props_type; let static_comp_generics = self.create_static_component_generics(); let (impl_generics, ty_generics, where_clause) = static_comp_generics.split_for_impl(); // TODO: replace with blanket implementation when specialisation becomes stable. quote! { #[automatically_derived] impl #impl_generics ::yew::html::BaseComponent for #component_name #ty_generics #where_clause { type Message = (); type Properties = #props_type; #[inline] fn create(ctx: &::yew::html::Context) -> Self { Self { _marker: ::std::marker::PhantomData, function_component: ::yew::functional::FunctionComponent::::new(ctx), } } #[inline] fn update(&mut self, _ctx: &::yew::html::Context, _msg: Self::Message) -> ::std::primitive::bool { true } #[inline] fn changed(&mut self, _ctx: &::yew::html::Context, _old_props: &Self::Properties) -> ::std::primitive::bool { true } #[inline] fn view(&self, ctx: &::yew::html::Context) -> ::yew::html::HtmlResult { ::yew::functional::FunctionComponent::::render( &self.function_component, ::yew::html::Context::::props(ctx) ) } #[inline] fn rendered(&mut self, _ctx: &::yew::html::Context, _first_render: ::std::primitive::bool) { ::yew::functional::FunctionComponent::::rendered(&self.function_component) } #[inline] fn destroy(&mut self, _ctx: &::yew::html::Context) { ::yew::functional::FunctionComponent::::destroy(&self.function_component) } #[inline] fn prepare_state(&self) -> ::std::option::Option<::std::string::String> { ::yew::functional::FunctionComponent::::prepare_state(&self.function_component) } } } } fn print_debug_impl(&self) -> TokenStream { let component_name = self.component_name(); let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); let component_name_lit = LitStr::new(&format!("{component_name}<_>"), Span::mixed_site()); quote! { #[automatically_derived] impl #impl_generics ::std::fmt::Debug for #component_name #ty_generics #where_clause { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { ::std::write!(f, #component_name_lit) } } } } fn print_fn_provider_impl(&self) -> TokenStream { let func = self.print_inner_fn(); let component_impl_attrs = self.filter_attrs_for_component_impl(); let component_name = self.component_name(); let fn_name = self.inner_fn_ident(); let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); let props_type = &self.props_type; let fn_generics = ty_generics.as_turbofish(); let component_props = Ident::new("props", Span::mixed_site()); let ctx_ident = Ident::new("ctx", Span::mixed_site()); quote! { // we cannot disable any lints here because it will be applied to the function body // as well. #(#component_impl_attrs)* impl #impl_generics ::yew::functional::FunctionProvider for #component_name #ty_generics #where_clause { type Properties = #props_type; fn run(#ctx_ident: &mut ::yew::functional::HookContext, #component_props: &Self::Properties) -> ::yew::html::HtmlResult { #func ::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#ctx_ident, #component_props)) } } } } fn print_struct_def(&self) -> TokenStream { let component_attrs = self.filter_attrs_for_component_struct(); let component_name = self.component_name(); let generics = &self.generics; let (_impl_generics, _ty_generics, where_clause) = self.generics.split_for_impl(); let phantom_generics = self.phantom_generics(); let vis = &self.vis; quote! { #(#component_attrs)* #[allow(unused_parens)] #vis struct #component_name #generics #where_clause { _marker: ::std::marker::PhantomData<(#phantom_generics)>, function_component: ::yew::functional::FunctionComponent, } } } } pub struct FunctionComponentName { component_name: Option, } impl Parse for FunctionComponentName { fn parse(input: ParseStream) -> syn::Result { if input.is_empty() { return Ok(Self { component_name: None, }); } let component_name = input.parse()?; Ok(Self { component_name: Some(component_name), }) } } pub fn function_component_impl( name: FunctionComponentName, mut component: FunctionComponent, ) -> syn::Result { component.merge_component_name(name)?; let base_comp_impl = component.print_base_component_impl(); let debug_impl = component.print_debug_impl(); let provider_fn_impl = component.print_fn_provider_impl(); let struct_def = component.print_struct_def(); let quoted = quote! { #struct_def #provider_fn_impl #debug_impl #base_comp_impl }; Ok(quoted) } ================================================ FILE: packages/yew-macro/src/hook/body.rs ================================================ use std::sync::{Arc, Mutex}; use proc_macro_error::emit_error; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{ parse_quote_spanned, visit_mut, Expr, ExprCall, ExprClosure, ExprForLoop, ExprIf, ExprLoop, ExprMatch, ExprWhile, Ident, Item, }; #[derive(Debug)] pub struct BodyRewriter { branch_lock: Arc>, ctx_ident: Ident, } impl BodyRewriter { pub fn new(ctx_ident: Ident) -> Self { Self { branch_lock: Arc::default(), ctx_ident, } } fn is_branched(&self) -> bool { self.branch_lock.try_lock().is_err() } fn with_branch(&mut self, f: F) -> O where F: FnOnce(&mut BodyRewriter) -> O, { let branch_lock = self.branch_lock.clone(); let _branched = branch_lock.try_lock(); f(self) } } impl VisitMut for BodyRewriter { fn visit_expr_call_mut(&mut self, i: &mut ExprCall) { let ctx_ident = &self.ctx_ident; // Only rewrite hook calls. if let Expr::Path(ref m) = &*i.func { if let Some(m) = m.path.segments.last().as_ref().map(|m| &m.ident) { if m.to_string().starts_with("use_") { if self.is_branched() { emit_error!( m, "hooks cannot be called at this position."; help = "move hooks to the top-level of your function."; note = "see: https://yew.rs/docs/next/concepts/function-components/hooks" ); } else { *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #ctx_ident) }; } return; } } } visit_mut::visit_expr_call_mut(self, i); } fn visit_expr_mut(&mut self, i: &mut Expr) { let ctx_ident = &self.ctx_ident; match &mut *i { Expr::Macro(m) => { if let Some(ident) = m.mac.path.segments.last().as_ref().map(|m| &m.ident) { if ident.to_string().starts_with("use_") { if self.is_branched() { emit_error!( ident, "hooks cannot be called at this position."; help = "move hooks to the top-level of your function."; note = "see: https://yew.rs/docs/next/concepts/function-components/hooks" ); } else { *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #ctx_ident) }; } } else { visit_mut::visit_expr_macro_mut(self, m); } } } _ => visit_mut::visit_expr_mut(self, i), } } fn visit_expr_closure_mut(&mut self, i: &mut ExprClosure) { self.with_branch(move |m| visit_mut::visit_expr_closure_mut(m, i)) } fn visit_expr_if_mut(&mut self, i: &mut ExprIf) { for it in &mut i.attrs { visit_mut::visit_attribute_mut(self, it); } visit_mut::visit_expr_mut(self, &mut i.cond); self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.then_branch)); if let Some(it) = &mut i.else_branch { self.with_branch(|m| visit_mut::visit_expr_mut(m, &mut (it).1)); } } fn visit_expr_loop_mut(&mut self, i: &mut ExprLoop) { self.with_branch(|m| visit_mut::visit_expr_loop_mut(m, i)); } fn visit_expr_for_loop_mut(&mut self, i: &mut ExprForLoop) { for it in &mut i.attrs { visit_mut::visit_attribute_mut(self, it); } if let Some(it) = &mut i.label { visit_mut::visit_label_mut(self, it); } visit_mut::visit_pat_mut(self, &mut i.pat); visit_mut::visit_expr_mut(self, &mut i.expr); self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.body)); } fn visit_expr_match_mut(&mut self, i: &mut ExprMatch) { for it in &mut i.attrs { visit_mut::visit_attribute_mut(self, it); } visit_mut::visit_expr_mut(self, &mut i.expr); self.with_branch(|m| { for it in &mut i.arms { visit_mut::visit_arm_mut(m, it); } }); } fn visit_expr_while_mut(&mut self, i: &mut ExprWhile) { for it in &mut i.attrs { visit_mut::visit_attribute_mut(self, it); } if let Some(it) = &mut i.label { visit_mut::visit_label_mut(self, it); } self.with_branch(|m| visit_mut::visit_expr_mut(m, &mut i.cond)); self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.body)); } fn visit_item_mut(&mut self, _i: &mut Item) { // We don't do anything for items. // for components / hooks in other components / hooks, apply the attribute again. } } ================================================ FILE: packages/yew-macro/src/hook/lifetime.rs ================================================ use std::sync::{Arc, Mutex}; use proc_macro2::Span; use syn::visit_mut::{self, VisitMut}; use syn::{ GenericArgument, Lifetime, ParenthesizedGenericArguments, Receiver, TypeBareFn, TypeImplTrait, TypeParamBound, TypeReference, TypeTraitObject, }; // borrowed from the awesome async-trait crate. pub struct CollectLifetimes { pub elided: Vec, pub name: &'static str, pub default_span: Span, pub type_trait_obj_lock: Arc>, pub impl_trait_lock: Arc>, pub impl_fn_lock: Arc>, } impl CollectLifetimes { pub fn new(name: &'static str, default_span: Span) -> Self { CollectLifetimes { elided: Vec::new(), name, default_span, impl_trait_lock: Arc::default(), type_trait_obj_lock: Arc::default(), impl_fn_lock: Arc::default(), } } fn is_impl_trait(&self) -> bool { self.impl_trait_lock.try_lock().is_err() } fn is_type_trait_obj(&self) -> bool { self.type_trait_obj_lock.try_lock().is_err() } fn is_impl_fn(&self) -> bool { self.impl_fn_lock.try_lock().is_err() } fn visit_opt_lifetime(&mut self, lifetime: &mut Option) { match lifetime { None => *lifetime = Some(self.next_lifetime(None)), Some(lifetime) => self.visit_lifetime(lifetime), } } fn visit_lifetime(&mut self, lifetime: &mut Lifetime) { if lifetime.ident == "_" { *lifetime = self.next_lifetime(lifetime.span()); } } fn next_lifetime>>(&mut self, span: S) -> Lifetime { let name = format!("{}{}", self.name, self.elided.len()); let span = span.into().unwrap_or(self.default_span); let life = Lifetime::new(&name, span); self.elided.push(life.clone()); life } } impl VisitMut for CollectLifetimes { fn visit_receiver_mut(&mut self, arg: &mut Receiver) { if let Some((_, lifetime)) = &mut arg.reference { self.visit_opt_lifetime(lifetime); } } fn visit_type_reference_mut(&mut self, ty: &mut TypeReference) { // We don't rewrite references in the impl FnOnce(&arg) or fn(&arg) if self.is_impl_fn() { return; } self.visit_opt_lifetime(&mut ty.lifetime); visit_mut::visit_type_reference_mut(self, ty); } fn visit_generic_argument_mut(&mut self, gen: &mut GenericArgument) { // We don't rewrite types in the impl FnOnce(&arg) -> Type<'_> if self.is_impl_fn() { return; } if let GenericArgument::Lifetime(lifetime) = gen { self.visit_lifetime(lifetime); } visit_mut::visit_generic_argument_mut(self, gen); } fn visit_type_impl_trait_mut(&mut self, impl_trait: &mut TypeImplTrait) { let impl_trait_lock = self.impl_trait_lock.clone(); let _locked = impl_trait_lock.try_lock(); impl_trait .bounds .insert(0, TypeParamBound::Lifetime(self.next_lifetime(None))); visit_mut::visit_type_impl_trait_mut(self, impl_trait); } fn visit_type_trait_object_mut(&mut self, type_trait_obj: &mut TypeTraitObject) { let type_trait_obj_lock = self.type_trait_obj_lock.clone(); let _locked = type_trait_obj_lock.try_lock(); visit_mut::visit_type_trait_object_mut(self, type_trait_obj); } fn visit_parenthesized_generic_arguments_mut( &mut self, generic_args: &mut ParenthesizedGenericArguments, ) { let impl_fn_lock = self.impl_fn_lock.clone(); let _maybe_locked = (self.is_impl_trait() || self.is_type_trait_obj()).then(|| impl_fn_lock.try_lock()); visit_mut::visit_parenthesized_generic_arguments_mut(self, generic_args); } fn visit_type_bare_fn_mut(&mut self, i: &mut TypeBareFn) { let impl_fn_lock = self.impl_fn_lock.clone(); let _locked = impl_fn_lock.try_lock(); visit_mut::visit_type_bare_fn_mut(self, i); } } ================================================ FILE: packages/yew-macro/src/hook/mod.rs ================================================ use proc_macro2::{Span, TokenStream}; use proc_macro_error::emit_error; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{ visit_mut, AttrStyle, Attribute, Block, Expr, ExprPath, File, Ident, Item, ItemFn, LitStr, Meta, MetaNameValue, ReturnType, Signature, Stmt, Token, Type, }; mod body; mod lifetime; mod signature; pub use body::BodyRewriter; use signature::HookSignature; #[derive(Clone)] pub struct HookFn { inner: ItemFn, } impl Parse for HookFn { fn parse(input: ParseStream) -> syn::Result { let func: ItemFn = input.parse()?; let sig = func.sig.clone(); if sig.asyncness.is_some() { emit_error!(sig.asyncness, "async functions can't be hooks"); } if sig.constness.is_some() { emit_error!(sig.constness, "const functions can't be hooks"); } if sig.abi.is_some() { emit_error!(sig.abi, "extern functions can't be hooks"); } if sig.unsafety.is_some() { emit_error!(sig.unsafety, "unsafe functions can't be hooks"); } if !sig.ident.to_string().starts_with("use_") { emit_error!(sig.ident, "hooks must have a name starting with `use_`"); } Ok(Self { inner: func }) } } impl HookFn { fn doc_attr(&self) -> Attribute { let span = self.inner.span(); let sig_formatted = prettyplease::unparse(&File { shebang: None, attrs: vec![], items: vec![Item::Fn(ItemFn { block: Box::new(Block { brace_token: Default::default(), stmts: vec![Stmt::Expr( Expr::Path(ExprPath { attrs: vec![], qself: None, path: Ident::new("__yew_macro_dummy_function_body__", span).into(), }), None, )], }), ..self.inner.clone() })], }); let literal = LitStr::new( &format!( r#" # Note When used in function components and hooks, this hook is equivalent to: ``` {} ``` "#, sig_formatted.replace( "__yew_macro_dummy_function_body__", "/* implementation omitted */" ) ), span, ); Attribute { pound_token: Default::default(), style: AttrStyle::Outer, bracket_token: Default::default(), meta: Meta::NameValue(MetaNameValue { path: Ident::new("doc", span).into(), eq_token: Token![=](span), value: Expr::Lit(syn::ExprLit { attrs: vec![], lit: literal.into(), }), }), } } } pub fn hook_impl(hook: HookFn) -> syn::Result { let doc_attr = hook.doc_attr(); let HookFn { inner: original_fn } = hook; let ItemFn { ref vis, ref sig, ref block, ref attrs, } = original_fn; let mut block = *block.clone(); let hook_sig = HookSignature::rewrite(sig); let Signature { ref fn_token, ref ident, ref inputs, output: ref hook_return_type, ref generics, .. } = hook_sig.sig; let output_type = &hook_sig.output_type; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let call_generics = hook_sig.call_generics(); // We use _ctx so that if a hook does not use other hooks, it will not trigger unused_vars. let ctx_ident = Ident::new("_ctx", Span::mixed_site()); let mut body_rewriter = BodyRewriter::new(ctx_ident.clone()); visit_mut::visit_block_mut(&mut body_rewriter, &mut block); let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); let input_args = hook_sig.input_args(); let inner_fn_rt = match &sig.output { ReturnType::Default => None, ReturnType::Type(rarrow, _) => Some(quote! { #rarrow #output_type }), }; let output_is_impl_trait = matches!(hook_sig.output_type, Type::ImplTrait(_)); let inner_fn = if output_is_impl_trait { quote! {} } else { quote! { fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) #inner_fn_rt #where_clause #block } }; let inner_type_impl = if hook_sig.needs_boxing { let hook_lifetime = &hook_sig.hook_lifetime; let boxed_inner_ident = Ident::new("boxed_inner", Span::mixed_site()); if output_is_impl_trait { quote! { let #boxed_inner_ident = ::std::boxed::Box::new( move |#ctx_ident: &mut ::yew::functional::HookContext| #block ); ::yew::functional::BoxedHook::<#hook_lifetime,>::new(#boxed_inner_ident) } } else { let hook_lifetime_plus = quote! { #hook_lifetime + }; let boxed_fn_type = quote! { ::std::boxed::Box }; let as_boxed_fn = quote! { as #boxed_fn_type }; let generic_types = generics.type_params().map(|t| &t.ident); quote! { let #boxed_inner_ident = ::std::boxed::Box::new( move |#ctx_ident: &mut ::yew::functional::HookContext| #inner_fn_rt { #inner_fn_ident :: <#(#generic_types,)*> (#ctx_ident, #(#input_args,)*) } ) #as_boxed_fn; ::yew::functional::BoxedHook::<#hook_lifetime, #output_type>::new(#boxed_inner_ident) } } } else { let input_types = hook_sig.input_types(); let args_ident = Ident::new("args", Span::mixed_site()); let hook_struct_name = Ident::new("HookProvider", Span::mixed_site()); let phantom_types = hook_sig.phantom_types(); let phantom_lifetimes = hook_sig.phantom_lifetimes(); quote! { struct #hook_struct_name #generics #where_clause { _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>, #args_ident: (#(#input_types,)*), } #[automatically_derived] impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { type Output = #output_type; fn run(mut self, #ctx_ident: &mut ::yew::functional::HookContext) -> Self::Output { let (#(#input_args,)*) = self.#args_ident; #inner_fn_ident #call_generics (#ctx_ident, #(#input_args,)*) } } #[automatically_derived] impl #impl_generics #hook_struct_name #ty_generics #where_clause { fn new(#inputs) -> Self { #hook_struct_name { _marker: ::std::marker::PhantomData, #args_ident: (#(#input_args,)*), } } } #hook_struct_name #call_generics ::new(#(#input_args,)*) } }; // There're some weird issues with doc tests that it cannot detect return types properly. // So we print original implementation instead. let output = quote! { #[cfg(not(doctest))] #(#attrs)* #doc_attr #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { #inner_fn #inner_type_impl } #[cfg(doctest)] #original_fn }; Ok(output) } ================================================ FILE: packages/yew-macro/src/hook/signature.rs ================================================ use std::iter::once; use std::mem::take; use proc_macro2::{Span, TokenStream}; use proc_macro_error::emit_error; use quote::{quote, ToTokens}; use syn::punctuated::{Pair, Punctuated}; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{ parse_quote, parse_quote_spanned, visit_mut, FnArg, GenericParam, Ident, Lifetime, LifetimeParam, Pat, Receiver, ReturnType, Signature, Type, TypeImplTrait, TypeParam, TypeParamBound, TypeReference, WherePredicate, }; use super::lifetime; fn type_is_generic(ty: &Type, param: &TypeParam) -> bool { match ty { Type::Path(path) => path.path.is_ident(¶m.ident), _ => false, } } #[derive(Default)] pub struct CollectArgs { needs_boxing: bool, } impl CollectArgs { pub fn new() -> Self { Self::default() } } impl VisitMut for CollectArgs { fn visit_type_impl_trait_mut(&mut self, impl_trait: &mut TypeImplTrait) { self.needs_boxing = true; visit_mut::visit_type_impl_trait_mut(self, impl_trait); } fn visit_receiver_mut(&mut self, recv: &mut Receiver) { emit_error!(recv, "methods cannot be hooks"); visit_mut::visit_receiver_mut(self, recv); } } pub struct HookSignature { pub hook_lifetime: Lifetime, pub sig: Signature, pub output_type: Type, pub needs_boxing: bool, } impl HookSignature { fn rewrite_return_type(hook_lifetime: &Lifetime, rt_type: &ReturnType) -> (ReturnType, Type) { let bound = quote! { #hook_lifetime + }; match rt_type { ReturnType::Default => ( parse_quote! { -> impl #bound ::yew::functional::Hook }, parse_quote! { () }, ), ReturnType::Type(arrow, ref return_type) => { if let Type::Reference(ref m) = &**return_type { if m.lifetime.is_none() { let mut return_type_ref = m.clone(); return_type_ref.lifetime = parse_quote!('hook); let return_type_ref = Type::Reference(return_type_ref); return ( parse_quote_spanned! { return_type.span() => #arrow impl #bound ::yew::functional::Hook }, return_type_ref, ); } } ( parse_quote_spanned! { return_type.span() => #arrow impl #bound ::yew::functional::Hook }, *return_type.clone(), ) } } } /// Rewrites a Hook Signature and extracts information. pub fn rewrite(sig: &Signature) -> Self { let mut sig = sig.clone(); let mut arg_info = CollectArgs::new(); arg_info.visit_signature_mut(&mut sig); let mut lifetimes = lifetime::CollectLifetimes::new("'arg", sig.ident.span()); for arg in sig.inputs.iter_mut() { match arg { FnArg::Receiver(arg) => lifetimes.visit_receiver_mut(arg), FnArg::Typed(arg) => lifetimes.visit_type_mut(&mut arg.ty), } } let Signature { ref mut generics, output: ref return_type, .. } = sig; let hook_lifetime = Lifetime::new("'hook", Span::mixed_site()); let mut params: Punctuated<_, _> = once(hook_lifetime.clone()) .chain(lifetimes.elided) .map(|lifetime| { GenericParam::Lifetime(LifetimeParam { attrs: vec![], lifetime, colon_token: None, bounds: Default::default(), }) }) .map(|param| Pair::new(param, Some(Default::default()))) .chain(take(&mut generics.params).into_pairs()) .collect(); for type_param in params.iter_mut().skip(1) { match type_param { GenericParam::Lifetime(param) => { if let Some(predicate) = generics .where_clause .iter_mut() .flat_map(|c| &mut c.predicates) .find_map(|predicate| match predicate { WherePredicate::Lifetime(p) if p.lifetime == param.lifetime => Some(p), _ => None, }) { predicate.bounds.push(hook_lifetime.clone()); } else { param.colon_token = Some(param.colon_token.unwrap_or_default()); param.bounds.push(hook_lifetime.clone()); } } GenericParam::Type(param) => { if let Some(predicate) = generics .where_clause .iter_mut() .flat_map(|c| &mut c.predicates) .find_map(|predicate| match predicate { WherePredicate::Type(p) if type_is_generic(&p.bounded_ty, param) => { Some(p) } _ => None, }) { predicate .bounds .push(TypeParamBound::Lifetime(hook_lifetime.clone())); } else { param.colon_token = Some(param.colon_token.unwrap_or_default()); param .bounds .push(TypeParamBound::Lifetime(hook_lifetime.clone())); } } GenericParam::Const(_) => {} } } generics.params = params; let (output, output_type) = Self::rewrite_return_type(&hook_lifetime, return_type); sig.output = output; Self { hook_lifetime, sig, output_type, needs_boxing: arg_info.needs_boxing, } } pub fn phantom_types(&self) -> Vec { self.sig .generics .type_params() .map(|ty_param| ty_param.ident.clone()) .collect() } pub fn phantom_lifetimes(&self) -> Vec { self.sig .generics .lifetimes() .map(|life| TypeReference { and_token: Default::default(), lifetime: Some(life.lifetime.clone()), mutability: None, elem: Box::new(Type::Tuple(syn::TypeTuple { paren_token: Default::default(), elems: Default::default(), })), }) .collect() } pub fn input_args(&self) -> Vec { self.sig .inputs .iter() .filter_map(|m| { if let FnArg::Typed(m) = m { if let Pat::Ident(ref m) = *m.pat { return Some(m.ident.clone()); } } None }) .collect() } pub fn input_types(&self) -> Vec { self.sig .inputs .iter() .filter_map(|m| { if let FnArg::Typed(m) = m { return Some(*m.ty.clone()); } None }) .collect() } pub fn call_generics(&self) -> TokenStream { let mut generics = self.sig.generics.clone(); // We need to filter out lifetimes. generics.params = generics .params .into_iter() .filter(|m| !matches!(m, GenericParam::Lifetime(_))) .collect(); let (_impl_generics, ty_generics, _where_clause) = generics.split_for_impl(); ty_generics.as_turbofish().to_token_stream() } } ================================================ FILE: packages/yew-macro/src/html_tree/html_block.rs ================================================ use proc_macro2::Delimiter; use quote::{quote, quote_spanned, ToTokens}; use syn::buffer::Cursor; use syn::parse::{Parse, ParseStream}; use syn::{braced, token}; use super::{HtmlIterable, HtmlNode, ToNodeIterator}; use crate::PeekValue; pub struct HtmlBlock { pub content: BlockContent, brace: token::Brace, } pub enum BlockContent { Node(Box), Iterable(Box), } impl PeekValue<()> for HtmlBlock { fn peek(cursor: Cursor) -> Option<()> { cursor.group(Delimiter::Brace).map(|_| ()) } } impl Parse for HtmlBlock { fn parse(input: ParseStream) -> syn::Result { let content; let brace = braced!(content in input); let content = if HtmlIterable::peek(content.cursor()).is_some() { BlockContent::Iterable(Box::new(content.parse()?)) } else { BlockContent::Node(Box::new(content.parse()?)) }; Ok(HtmlBlock { content, brace }) } } impl ToTokens for HtmlBlock { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let HtmlBlock { content, .. } = self; let new_tokens = match content { BlockContent::Iterable(html_iterable) => quote! {#html_iterable}, BlockContent::Node(html_node) => quote! {#html_node}, }; tokens.extend(quote! {#new_tokens}); } } impl ToNodeIterator for HtmlBlock { fn to_node_iterator_stream(&self) -> Option { let HtmlBlock { content, brace } = self; let new_tokens = match content { BlockContent::Iterable(iterable) => iterable.to_node_iterator_stream(), BlockContent::Node(node) => node.to_node_iterator_stream(), }?; Some(quote_spanned! {brace.span=> #new_tokens}) } fn is_singular(&self) -> bool { match &self.content { BlockContent::Node(node) => node.is_singular(), BlockContent::Iterable(_) => false, } } } ================================================ FILE: packages/yew-macro/src/html_tree/html_component.rs ================================================ use proc_macro2::Span; use quote::{quote, quote_spanned, ToTokens}; use syn::parse::discouraged::Speculative; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{Token, Type}; use super::{HtmlChildrenTree, TagTokens}; use crate::is_ide_completion; use crate::props::ComponentProps; pub struct HtmlComponent { ty: Type, pub props: ComponentProps, children: HtmlChildrenTree, close: Option, } impl Parse for HtmlComponent { fn parse(input: ParseStream) -> syn::Result { // check if the next tokens are (); if !is_ide_completion() { return match close { Ok(close) => Err(syn::Error::new_spanned( close.to_spanned(), "this closing tag has no corresponding opening tag", )), Err(err) => Err(err), }; } } let open = input.parse::()?; // Return early if it's a self-closing tag if open.is_self_closing() { return Ok(HtmlComponent { ty: open.ty, props: open.props, children: HtmlChildrenTree::new(), close: None, }); } let mut children = HtmlChildrenTree::new(); let close = loop { if input.is_empty() { if is_ide_completion() { break None; } return Err(syn::Error::new_spanned( open.to_spanned(), "this opening tag has no corresponding closing tag", )); } if trying_to_close() { fn format_token_stream(ts: impl ToTokens) -> String { let string = ts.to_token_stream().to_string(); // remove unnecessary spaces string.replace(' ', "") } let fork = input.fork(); let close = TagTokens::parse_end_content(&fork, |i_fork, tag| { let ty = i_fork.parse().map_err(|e| { syn::Error::new( e.span(), format!( "expected a valid closing tag for component\nnote: found opening \ tag `{lt}{0}{gt}`\nhelp: try `{lt}/{0}{gt}`", format_token_stream(&open.ty), lt = open.tag.lt.to_token_stream(), gt = open.tag.gt.to_token_stream(), ), ) })?; if ty != open.ty && !is_ide_completion() { let open_ty = &open.ty; Err(syn::Error::new_spanned( quote!(#open_ty #ty), format!( "mismatched closing tags: expected `{}`, found `{}`", format_token_stream(open_ty), format_token_stream(ty) ), )) } else { let close = HtmlComponentClose { tag, ty }; input.advance_to(&fork); Ok(close) } })?; break Some(close); } children.parse_child(input)?; }; if !children.is_empty() { if let Some(children_prop) = open.props.children() { return Err(syn::Error::new_spanned( &children_prop.label, "cannot specify the `children` prop when the component already has children", )); } } Ok(HtmlComponent { ty: open.ty, props: open.props, children, close, }) } } impl ToTokens for HtmlComponent { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { ty, props, children, close, } = self; let ty_span = ty.span().resolved_at(Span::call_site()); let props_ty = quote_spanned!(ty_span=> <#ty as ::yew::html::BaseComponent>::Properties); let children_renderer = children.to_children_renderer_tokens(); let build_props = props.build_properties_tokens(&props_ty, children_renderer); let key = props.special().wrap_key_attr(); let use_close_tag = close .as_ref() .map(|close| { let close_ty = &close.ty; quote_spanned! {close_ty.span()=> let _ = |_:#close_ty| {}; } }) .unwrap_or_default(); tokens.extend(quote_spanned! {ty_span=> { #use_close_tag #[allow(clippy::let_unit_value)] let __yew_props = #build_props; ::yew::virtual_dom::VChild::<#ty>::new(__yew_props, #key) } }); } } struct HtmlComponentOpen { tag: TagTokens, ty: Type, props: ComponentProps, } impl HtmlComponentOpen { fn is_self_closing(&self) -> bool { self.tag.div.is_some() } fn to_spanned(&self) -> impl ToTokens { self.tag.to_spanned() } } impl Parse for HtmlComponentOpen { fn parse(input: ParseStream) -> syn::Result { TagTokens::parse_start_content(input, |input, tag| { let ty = input.parse()?; let props: ComponentProps = input.parse()?; if let Some(ref node_ref) = props.special().node_ref { return Err(syn::Error::new_spanned( &node_ref.label, "cannot use `ref` with components. If you want to specify a property, use \ `r#ref` here instead.", )); } Ok(Self { tag, ty, props }) }) } } struct HtmlComponentClose { tag: TagTokens, ty: Type, } impl HtmlComponentClose { fn to_spanned(&self) -> impl ToTokens { self.tag.to_spanned() } } impl Parse for HtmlComponentClose { fn parse(input: ParseStream) -> syn::Result { TagTokens::parse_end_content(input, |input, tag| { let ty = input.parse()?; Ok(Self { tag, ty }) }) } } ================================================ FILE: packages/yew-macro/src/html_tree/html_dashed_name.rs ================================================ use std::fmt; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use syn::buffer::Cursor; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{LitStr, Token}; use crate::stringify::Stringify; use crate::{non_capitalized_ascii, Peek}; #[derive(Clone, PartialEq, Eq)] pub struct HtmlDashedName { pub name: Ident, pub extended: Vec<(Token![-], Ident)>, } impl HtmlDashedName { /// Checks if this name is equal to the provided item (which can be anything implementing /// `Into`). pub fn eq_ignore_ascii_case(&self, other: S) -> bool where S: Into, { let mut s = other.into(); s.make_ascii_lowercase(); s == self.to_ascii_lowercase_string() } pub fn to_ascii_lowercase_string(&self) -> String { let mut s = self.to_string(); s.make_ascii_lowercase(); s } pub fn to_lit_str(&self) -> LitStr { LitStr::new(&self.to_string(), self.span()) } } impl fmt::Display for HtmlDashedName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; for (_, ident) in &self.extended { write!(f, "-{ident}")?; } Ok(()) } } impl Peek<'_, Self> for HtmlDashedName { fn peek(cursor: Cursor) -> Option<(Self, Cursor)> { let (name, cursor) = cursor.ident()?; if !non_capitalized_ascii(&name.to_string()) { return None; } let mut extended = Vec::new(); let mut cursor = cursor; loop { if let Some((punct, p_cursor)) = cursor.punct() { if punct.as_char() == '-' { let (ident, i_cursor) = p_cursor.ident()?; cursor = i_cursor; extended.push((Token![-](Span::mixed_site()), ident)); continue; } } break; } Some((HtmlDashedName { name, extended }, cursor)) } } impl Parse for HtmlDashedName { fn parse(input: ParseStream) -> syn::Result { let name = input.call(Ident::parse_any)?; let mut extended = Vec::new(); while input.peek(Token![-]) { extended.push((input.parse::()?, input.call(Ident::parse_any)?)); } Ok(HtmlDashedName { name, extended }) } } impl ToTokens for HtmlDashedName { fn to_tokens(&self, tokens: &mut TokenStream) { let HtmlDashedName { name, extended } = self; let dashes = extended.iter().map(|(dash, _)| quote! {#dash}); let idents = extended.iter().map(|(_, ident)| quote! {#ident}); let extended = quote! { #(#dashes #idents)* }; tokens.extend(quote! { #name #extended }); } } impl Stringify for HtmlDashedName { fn try_into_lit(&self) -> Option { Some(self.to_lit_str()) } fn stringify(&self) -> TokenStream { self.to_lit_str().stringify() } } impl From for HtmlDashedName { fn from(name: Ident) -> Self { HtmlDashedName { name, extended: vec![], } } } ================================================ FILE: packages/yew-macro/src/html_tree/html_element.rs ================================================ use proc_macro2::{Delimiter, Group, Span, TokenStream}; use proc_macro_error::emit_warning; use quote::{quote, quote_spanned, ToTokens}; use syn::buffer::Cursor; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{Expr, Ident, Lit, LitStr, Token}; use super::{HtmlChildrenTree, HtmlDashedName, TagTokens}; use crate::props::{ElementProps, Prop, PropDirective}; use crate::stringify::{Stringify, Value}; use crate::{is_ide_completion, non_capitalized_ascii, Peek, PeekValue}; fn is_normalised_element_name(name: &str) -> bool { match name { "animateMotion" | "animateTransform" | "clipPath" | "feBlend" | "feColorMatrix" | "feComponentTransfer" | "feComposite" | "feConvolveMatrix" | "feDiffuseLighting" | "feDisplacementMap" | "feDistantLight" | "feDropShadow" | "feFlood" | "feFuncA" | "feFuncB" | "feFuncG" | "feFuncR" | "feGaussianBlur" | "feImage" | "feMerge" | "feMergeNode" | "feMorphology" | "feOffset" | "fePointLight" | "feSpecularLighting" | "feSpotLight" | "feTile" | "feTurbulence" | "foreignObject" | "glyphRef" | "linearGradient" | "radialGradient" | "textPath" => true, _ => !name.chars().any(|c| c.is_ascii_uppercase()), } } pub struct HtmlElement { pub name: TagName, pub props: ElementProps, pub children: HtmlChildrenTree, } impl PeekValue<()> for HtmlElement { fn peek(cursor: Cursor) -> Option<()> { HtmlElementOpen::peek(cursor) .or_else(|| HtmlElementClose::peek(cursor)) .map(|_| ()) } } impl Parse for HtmlElement { fn parse(input: ParseStream) -> syn::Result { if HtmlElementClose::peek(input.cursor()).is_some() { return match input.parse::() { Ok(close) => Err(syn::Error::new_spanned( close.to_spanned(), "this closing tag has no corresponding opening tag", )), Err(err) => Err(err), }; } let open = input.parse::()?; // Return early if it's a self-closing tag if open.is_self_closing() { return Ok(HtmlElement { name: open.name, props: open.props, children: HtmlChildrenTree::new(), }); } if let TagName::Lit(name) = &open.name { // Void elements should not have children. // See https://html.spec.whatwg.org/multipage/syntax.html#void-elements // // For dynamic tags this is done at runtime! match name.to_ascii_lowercase_string().as_str() { "textarea" => { return Err(syn::Error::new_spanned( open.to_spanned(), "the tag ` } // make sure that capitalization doesn't matter for the void children check html! { }; // no tag name html! { <@> }; html! { <@/> }; // invalid closing tag html! { <@{"test"}> }; // type mismatch html! { <@{55}> }; // Missing curly braces html! { }; html! { }; html! { }; html! { }; html! { }; html! { }; html! { }; html! { }; html! { }; } fn main() {} ================================================ FILE: packages/yew-macro/tests/html_macro/element-fail.stderr ================================================ error: this opening tag has no corresponding closing tag --> tests/html_macro/element-fail.rs:7:13 | 7 | html! {
}; | ^^^^^ error: this opening tag has no corresponding closing tag --> tests/html_macro/element-fail.rs:8:18 | 8 | html! {
}; | ^^^^^ error: this opening tag has no corresponding closing tag --> tests/html_macro/element-fail.rs:9:13 | 9 | html! {
}; | ^^^^^ error: this closing tag has no corresponding opening tag --> tests/html_macro/element-fail.rs:12:13 | 12 | html! {
}; | ^^^^^^ error: this closing tag has no corresponding opening tag --> tests/html_macro/element-fail.rs:13:18 | 13 | html! {
}; | ^^^^^^^ error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<>`) --> tests/html_macro/element-fail.rs:14:20 | 14 | html! { }; | ^^^^^^ error: this closing tag has no corresponding opening tag --> tests/html_macro/element-fail.rs:17:18 | 17 | html! {
}; | ^^^^^^^ error: this closing tag has no corresponding opening tag --> tests/html_macro/element-fail.rs:18:20 | 18 | html! { }; | ^^^^^^^^ error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<>`) --> tests/html_macro/element-fail.rs:21:24 | 21 | html! {
}; | ^^^^^^^^^^^ error: expected a valid html element --> tests/html_macro/element-fail.rs:23:18 | 23 | html! {
Invalid
}; | ^^^^^^^ error: `attr` can only be specified once but is given here again --> tests/html_macro/element-fail.rs:26:27 | 26 | html! { }; | ^^^^ error: `value` can only be specified once but is given here again --> tests/html_macro/element-fail.rs:27:32 | 27 | html! { }; | ^^^^^ error: `kind` can only be specified once but is given here again --> tests/html_macro/element-fail.rs:28:36 | 28 | html! { }; | ^^^^ error: `checked` can only be specified once but is given here again --> tests/html_macro/element-fail.rs:29:33 | 29 | html! { }; | ^^^^^^^ error: `disabled` can only be specified once but is given here again --> tests/html_macro/element-fail.rs:30:34 | 30 | html! { }; | ^^^^^^^^ error: `selected` can only be specified once but is given here again --> tests/html_macro/element-fail.rs:31:35 | 31 | html! {
}; | ^^^^^ error: `ref` can only be specified once --> tests/html_macro/element-fail.rs:33:29 | 33 | html! { }; | ^^^ error: `ref` can only be specified once --> tests/html_macro/element-fail.rs:63:29 | 63 | html! { }; | ^^^ error: the tag `` is a void element and cannot have children (hint: rewrite this as ``) --> tests/html_macro/element-fail.rs:66:13 | 66 | html! { }; | ^^^^^^^^^^^^^^^^^^^ error: the tag ` } | ^^^^^^^^^^ error: the tag `` is a void element and cannot have children (hint: rewrite this as ``) --> tests/html_macro/element-fail.rs:70:13 | 70 | html! { }; | ^^^^^^^^^^^^^^^^^^^ error: this dynamic tag is missing an expression block defining its value --> tests/html_macro/element-fail.rs:73:14 | 73 | html! { <@> }; | ^ error: this dynamic tag is missing an expression block defining its value --> tests/html_macro/element-fail.rs:74:14 | 74 | html! { <@/> }; | ^ error: dynamic closing tags must not have a body (hint: replace it with just ``) --> tests/html_macro/element-fail.rs:77:27 | 77 | html! { <@{"test"}> }; | ^^^^^^^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { attrs: [], paren_token: Paren, elems: [], } --> tests/html_macro/element-fail.rs:82:24 | 82 | html! { }; | ^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { attrs: [], paren_token: Paren, elems: [], } --> tests/html_macro/element-fail.rs:83:24 | 83 | html! { }; | ^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Call { attrs: [], func: Expr::Path { attrs: [], qself: None, path: Path { leading_colon: None, segments: [ PathSegment { ident: Ident { ident: "Some", span: #0 bytes(2628..2632), }, arguments: PathArguments::None, }, ], }, }, paren_token: Paren, args: [ Expr::Lit { attrs: [], lit: Lit::Int { token: 5, }, }, ], } --> tests/html_macro/element-fail.rs:84:28 | 84 | html! { }; | ^^^^^^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Path { attrs: [], qself: None, path: Path { leading_colon: None, segments: [ PathSegment { ident: Ident { ident: "NotToString", span: #0 bytes(2668..2679), }, arguments: PathArguments::None, }, ], }, } --> tests/html_macro/element-fail.rs:85:27 | 85 | html! { }; | ^^^^^^^^^^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Call { attrs: [], func: Expr::Path { attrs: [], qself: None, path: Path { leading_colon: None, segments: [ PathSegment { ident: Ident { ident: "Some", span: #0 bytes(2707..2711), }, arguments: PathArguments::None, }, ], }, }, paren_token: Paren, args: [ Expr::Path { attrs: [], qself: None, path: Path { leading_colon: None, segments: [ PathSegment { ident: Ident { ident: "NotToString", span: #0 bytes(2712..2723), }, arguments: PathArguments::None, }, ], }, }, ], } --> tests/html_macro/element-fail.rs:86:22 | 86 | html! { }; | ^^^^^^^^^^^^^^^^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Call { attrs: [], func: Expr::Path { attrs: [], qself: None, path: Path { leading_colon: None, segments: [ PathSegment { ident: Ident { ident: "Some", span: #0 bytes(2751..2755), }, arguments: PathArguments::None, }, ], }, }, paren_token: Paren, args: [ Expr::Lit { attrs: [], lit: Lit::Int { token: 5, }, }, ], } --> tests/html_macro/element-fail.rs:87:21 | 87 | html! { }; | ^^^^^^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { attrs: [], paren_token: Paren, elems: [], } --> tests/html_macro/element-fail.rs:88:25 | 88 | html! { }; | ^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { attrs: [], paren_token: Paren, elems: [], } --> tests/html_macro/element-fail.rs:89:26 | 89 | html! { }; | ^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Path { attrs: [], qself: None, path: Path { leading_colon: None, segments: [ PathSegment { ident: Ident { ident: "NotToString", span: #0 bytes(2858..2869), }, arguments: PathArguments::None, }, ], }, } --> tests/html_macro/element-fail.rs:90:27 | 90 | html! { }; | ^^^^^^^^^^^ error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:36:28 | 36 | html! { }; | -----------------------^----- | | | | | expected `bool`, found integer | arguments to this enum variant are incorrect | help: the type constructed contains `{integer}` due to the type of the argument passed --> tests/html_macro/element-fail.rs:36:5 | 36 | html! { }; | ^^^^^^^^^^^^^^^^^^^^^^^-^^^^^ | | | this argument influences the type of `{{root}}` note: tuple variant defined here --> $RUST/core/src/option.rs = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:37:29 | 37 | html! { }; | ------------------------^^^^^^^^^^^------ | | | | | expected `bool`, found `Option` | arguments to this enum variant are incorrect | = note: expected type `bool` found enum `Option` help: the type constructed contains `Option` due to the type of the argument passed --> tests/html_macro/element-fail.rs:37:5 | 37 | html! { }; | ^^^^^^^^^^^^^^^^^^^^^^^^-----------^^^^^^ | | | this argument influences the type of `{{root}}` note: tuple variant defined here --> $RUST/core/src/option.rs = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) help: use `Option::is_some` to test if the `Option` has a value | 37 | html! { }; | ++++++++++ error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:38:29 | 38 | html! { }; | ^ | | | expected `bool`, found integer | arguments to this function are incorrect | note: function defined here --> $WORKSPACE/packages/yew/src/utils/mod.rs | | pub fn __ensure_type(_: T) {} | ^^^^^^^^^^^^^ error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:39:30 | 39 | html! { }; | ^^^^^^^^^^ expected `bool`, found `Option` | = note: expected type `bool` found enum `Option` help: use `Option::is_some` to test if the `Option` has a value | 39 | html! { }; | ++++++++++ error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:40:30 | 40 | html! { }; | ^^ the trait `IntoPropValue>` is not implemented for `()` | = help: the trait `IntoPropValue>` is not implemented for `()` but trait `IntoPropValue` is implemented for it = help: for that trait implementation, expected `VNode`, found `Option` error[E0277]: the trait bound `NotToString: IntoPropValue>` is not satisfied --> tests/html_macro/element-fail.rs:46:28 | 46 | html! { }; | ^^^^^^^^^^^ the trait `IntoPropValue>` is not implemented for `NotToString` | = help: the following other types implement trait `IntoPropValue`: `&&String` implements `IntoPropValue>` `&&String` implements `IntoPropValue` `&&str` implements `IntoPropValue>` `&&str` implements `IntoPropValue` `&'static [(K, V)]` implements `IntoPropValue>` `&'static [T]` implements `IntoPropValue>` `&'static str` implements `IntoPropValue` `&'static str` implements `IntoPropValue>` and $N others error[E0277]: the trait bound `Option: IntoPropValue>` is not satisfied --> tests/html_macro/element-fail.rs:47:23 | 47 | html! { }; | ----^^^^^^^^^^^^^ | | | the trait `IntoPropValue>` is not implemented for `Option` | required by a bound introduced by this call | = help: the following other types implement trait `IntoPropValue`: `Option<&String>` implements `IntoPropValue>` `Option<&implicit_clone::unsync::string::IString>` implements `IntoPropValue>` `Option<&str>` implements `IntoPropValue>` `Option<&str>` implements `IntoPropValue>` `Option<&str>` implements `IntoPropValue>` `Option>` implements `IntoPropValue>` `Option>` implements `IntoPropValue>` `Option>` implements `IntoPropValue>` and $N others error[E0277]: the trait bound `Option<{integer}>: IntoPropValue>` is not satisfied --> tests/html_macro/element-fail.rs:48:22 | 48 | html! { }; | ----^^^ | | | the trait `IntoPropValue>` is not implemented for `Option<{integer}>` | required by a bound introduced by this call | = help: the following other types implement trait `IntoPropValue`: `Option<&String>` implements `IntoPropValue>` `Option<&implicit_clone::unsync::string::IString>` implements `IntoPropValue>` `Option<&str>` implements `IntoPropValue>` `Option<&str>` implements `IntoPropValue>` `Option<&str>` implements `IntoPropValue>` `Option>` implements `IntoPropValue>` `Option>` implements `IntoPropValue>` `Option>` implements `IntoPropValue>` and $N others error[E0277]: the trait bound `{integer}: IntoEventCallback` is not satisfied --> tests/html_macro/element-fail.rs:51:28 | 51 | html! { }; | -----------------------^----- | | | | | the trait `Fn(MouseEvent)` is not implemented for `{integer}` | required by a bound introduced by this call | = help: the following other types implement trait `IntoEventCallback`: &yew::Callback Option Option> yew::Callback = note: required for `{integer}` to implement `IntoEventCallback` note: required by a bound in `yew::html::onclick::Wrapper::__macro_new` --> $WORKSPACE/packages/yew/src/html/listener/events.rs | | / impl_short! { | | onauxclick(MouseEvent) | | onclick(MouseEvent) ... | | | ontransitionstart(TransitionEvent) | | } | | ^ | | | | |_required by a bound in this associated function | required by this bound in `Wrapper::__macro_new` = note: this error originates in the macro `impl_action` which comes from the expansion of the macro `impl_short` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `yew::Callback: IntoEventCallback` is not satisfied --> tests/html_macro/element-fail.rs:52:29 | 52 | html! { }; | ------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------ | | | | | the trait `Fn(MouseEvent)` is not implemented for `yew::Callback` | required by a bound introduced by this call | = help: the following other types implement trait `IntoEventCallback`: &yew::Callback yew::Callback = note: required for `yew::Callback` to implement `IntoEventCallback` note: required by a bound in `yew::html::onclick::Wrapper::__macro_new` --> $WORKSPACE/packages/yew/src/html/listener/events.rs | | / impl_short! { | | onauxclick(MouseEvent) | | onclick(MouseEvent) ... | | | ontransitionstart(TransitionEvent) | | } | | ^ | | | | |_required by a bound in this associated function | required by this bound in `Wrapper::__macro_new` = note: this error originates in the macro `impl_action` which comes from the expansion of the macro `impl_short` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `Option<{integer}>: IntoEventCallback` is not satisfied --> tests/html_macro/element-fail.rs:53:29 | 53 | html! { }; | ------------------------^^^^^^^------ | | | | | the trait `IntoEventCallback` is not implemented for `Option<{integer}>` | required by a bound introduced by this call | = help: the following other types implement trait `IntoEventCallback`: Option Option> note: required by a bound in `yew::html::onfocus::Wrapper::__macro_new` --> $WORKSPACE/packages/yew/src/html/listener/events.rs | | / impl_short! { | | onauxclick(MouseEvent) | | onclick(MouseEvent) ... | | | ontransitionstart(TransitionEvent) | | } | | ^ | | | | |_required by a bound in this associated function | required by this bound in `Wrapper::__macro_new` = note: this error originates in the macro `impl_action` which comes from the expansion of the macro `impl_short` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `(): IntoPropValue` is not satisfied --> tests/html_macro/element-fail.rs:56:25 | 56 | html! { }; | ^^ | | | the trait `IntoPropValue` is not implemented for `()` | required by a bound introduced by this call | = help: the trait `IntoPropValue` is not implemented for `()` but trait `IntoPropValue` is implemented for it = help: for that trait implementation, expected `VNode`, found `yew::NodeRef` error[E0277]: the trait bound `Option: IntoPropValue` is not satisfied --> tests/html_macro/element-fail.rs:57:25 | 57 | html! { }; | ----^^^^^^^^^^^^^^^^^^^^ | | | the trait `IntoPropValue` is not implemented for `Option` | required by a bound introduced by this call | = help: the following other types implement trait `IntoPropValue`: `Option<&String>` implements `IntoPropValue>` `Option<&implicit_clone::unsync::string::IString>` implements `IntoPropValue>` `Option<&str>` implements `IntoPropValue>` `Option<&str>` implements `IntoPropValue>` `Option<&str>` implements `IntoPropValue>` `Option>` implements `IntoPropValue>` `Option>` implements `IntoPropValue>` `Option>` implements `IntoPropValue>` and $N others error[E0277]: the trait bound `yew::Callback: IntoEventCallback` is not satisfied --> tests/html_macro/element-fail.rs:58:29 | 58 | html! { }; | ------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------ | | | | | the trait `Fn(MouseEvent)` is not implemented for `yew::Callback` | required by a bound introduced by this call | = help: the following other types implement trait `IntoEventCallback`: &yew::Callback yew::Callback = note: required for `yew::Callback` to implement `IntoEventCallback` note: required by a bound in `yew::html::onclick::Wrapper::__macro_new` --> $WORKSPACE/packages/yew/src/html/listener/events.rs | | / impl_short! { | | onauxclick(MouseEvent) | | onclick(MouseEvent) ... | | | ontransitionstart(TransitionEvent) | | } | | ^ | | | | |_required by a bound in this associated function | required by this bound in `Wrapper::__macro_new` = note: this error originates in the macro `impl_action` which comes from the expansion of the macro `impl_short` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `NotToString: IntoPropValue>` is not satisfied --> tests/html_macro/element-fail.rs:60:28 | 60 | html! { }; | ^^^^^^^^^^^ the trait `IntoPropValue>` is not implemented for `NotToString` | = help: the following other types implement trait `IntoPropValue`: `&&String` implements `IntoPropValue>` `&&String` implements `IntoPropValue` `&&str` implements `IntoPropValue>` `&&str` implements `IntoPropValue` `&'static [(K, V)]` implements `IntoPropValue>` `&'static [T]` implements `IntoPropValue>` `&'static str` implements `IntoPropValue` `&'static str` implements `IntoPropValue>` and $N others error[E0277]: the trait bound `(): IntoPropValue` is not satisfied --> tests/html_macro/element-fail.rs:62:25 | 62 | html! { }; | ^^ | | | the trait `IntoPropValue` is not implemented for `()` | required by a bound introduced by this call | = help: the trait `IntoPropValue` is not implemented for `()` but trait `IntoPropValue` is implemented for it = help: for that trait implementation, expected `VNode`, found `yew::NodeRef` error[E0277]: the trait bound `implicit_clone::unsync::string::IString: From<{integer}>` is not satisfied --> tests/html_macro/element-fail.rs:79:16 | 79 | html! { <@{55}> }; | ^^ the trait `From<{integer}>` is not implemented for `implicit_clone::unsync::string::IString` | = help: the following other types implement trait `From`: `implicit_clone::unsync::string::IString` implements `From<&Classes>` `implicit_clone::unsync::string::IString` implements `From<&implicit_clone::unsync::string::IString>` `implicit_clone::unsync::string::IString` implements `From<&str>` `implicit_clone::unsync::string::IString` implements `From>` `implicit_clone::unsync::string::IString` implements `From>` `implicit_clone::unsync::string::IString` implements `From>` `implicit_clone::unsync::string::IString` implements `From` = note: required for `{integer}` to implement `Into` ================================================ FILE: packages/yew-macro/tests/html_macro/for-fail.rs ================================================ mod smth { const KEY: u32 = 42; } fn main() { _ = ::yew::html!{for x}; _ = ::yew::html!{for x in}; _ = ::yew::html!{for x in 0 .. 10}; _ = ::yew::html!{for (x, y) in 0 .. 10 { {x} }}; _ = ::yew::html!{for _ in 0 .. 10 { {break} }}; _ = ::yew::html!{for _ in 0 .. 10 {
}}; _ = ::yew::html!{for _ in 0 .. 10 {
}}; } ================================================ FILE: packages/yew-macro/tests/html_macro/for-fail.stderr ================================================ error: unexpected end of input, expected `in` --> tests/html_macro/for-fail.rs:6:9 | 6 | _ = ::yew::html!{for x}; | ^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `::yew::html` (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected end of input, expected an expression --> tests/html_macro/for-fail.rs:7:9 | 7 | _ = ::yew::html!{for x in}; | ^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `::yew::html` (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected end of input, expected curly braces --> tests/html_macro/for-fail.rs:8:9 | 8 | _ = ::yew::html!{for x in 0 .. 10}; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `::yew::html` (in Nightly builds, run with -Z macro-backtrace for more info) error: duplicate key for a node in a `for`-loop this will create elements with duplicate keys if the loop iterates more than once --> tests/html_macro/for-fail.rs:18:18 | 18 |
| ^^^^^^^^^^^ error: duplicate key for a node in a `for`-loop this will create elements with duplicate keys if the loop iterates more than once --> tests/html_macro/for-fail.rs:22:19 | 22 |
| ^^^^ error[E0267]: `break` inside of a closure --> tests/html_macro/for-fail.rs:14:16 | 13 | _ = ::yew::html!{for _ in 0 .. 10 { | _________- 14 | | {break} | | ^^^^^ cannot `break` inside of a closure 15 | | }}; | |______- enclosing closure error[E0308]: mismatched types --> tests/html_macro/for-fail.rs:9:26 | 9 | _ = ::yew::html!{for (x, y) in 0 .. 10 { | ^^^^^^ | | | expected integer, found `(_, _)` | expected due to this | = note: expected type `{integer}` found tuple `(_, _)` ================================================ FILE: packages/yew-macro/tests/html_macro/for-pass.rs ================================================ #![no_implicit_prelude] // Shadow primitives #[allow(non_camel_case_types)] pub struct bool; #[allow(non_camel_case_types)] pub struct char; #[allow(non_camel_case_types)] pub struct f32; #[allow(non_camel_case_types)] pub struct f64; #[allow(non_camel_case_types)] pub struct i128; #[allow(non_camel_case_types)] pub struct i16; #[allow(non_camel_case_types)] pub struct i32; #[allow(non_camel_case_types)] pub struct i64; #[allow(non_camel_case_types)] pub struct i8; #[allow(non_camel_case_types)] pub struct isize; #[allow(non_camel_case_types)] pub struct str; #[allow(non_camel_case_types)] pub struct u128; #[allow(non_camel_case_types)] pub struct u16; #[allow(non_camel_case_types)] pub struct u32; #[allow(non_camel_case_types)] pub struct u64; #[allow(non_camel_case_types)] pub struct u8; #[allow(non_camel_case_types)] pub struct usize; fn main() { _ = ::yew::html!{ for i in 0 .. 10 { {i} } }; struct Pair { value1: &'static ::std::primitive::str, value2: ::std::primitive::i32 } _ = ::yew::html! { for Pair { value1, value2 } in ::std::iter::Iterator::map(0 .. 10, |value2| Pair { value1: "Yew", value2 }) { {value1} {value2} } }; fn rand_number() -> ::std::primitive::u32 { 4 // chosen by fair dice roll. guaranteed to be random. } _ = ::yew::html!{ for _ in 0..5 {
{{ loop { let a = rand_number(); if a % 2 == 0 { break a; } } }}
} } } ================================================ FILE: packages/yew-macro/tests/html_macro/generic-component-fail.rs ================================================ use std::marker::PhantomData; use yew::prelude::*; pub struct Generic { marker: PhantomData, } impl Component for Generic where T: 'static, { type Message = (); type Properties = (); fn create(_ctx: &Context) -> Self { unimplemented!() } fn view(&self, _ctx: &Context) -> Html { unimplemented!() } } pub struct Generic2 { marker: PhantomData<(T1, T2)>, } impl Component for Generic2 where T1: 'static, T2: 'static, { type Message = (); type Properties = (); fn create(_ctx: &Context) -> Self { unimplemented!() } fn view(&self, _ctx: &Context) -> Html { unimplemented!() }} fn compile_fail() { #[allow(unused_imports)] use std::path::Path; html! { > }; html! { > }; html! { >>> }; html! { >> }; html! { > }; } fn main() {} ================================================ FILE: packages/yew-macro/tests/html_macro/generic-component-fail.stderr ================================================ error: this opening tag has no corresponding closing tag --> tests/html_macro/generic-component-fail.rs:46:13 | 46 | html! { > }; | ^^^^^^^^^^^^^^^^^ error: mismatched closing tags: expected `Generic`, found `Generic` --> tests/html_macro/generic-component-fail.rs:47:14 | 47 | html! { > }; | ^^^^^^^^^^^^^^^^^^^^^^^^^ error: mismatched closing tags: expected `Generic`, found `Generic>` --> tests/html_macro/generic-component-fail.rs:48:14 | 48 | html! { >>> }; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: mismatched closing tags: expected `Generic`, found `Generic` --> tests/html_macro/generic-component-fail.rs:50:14 | 50 | html! { >> }; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: expected a valid closing tag for component note: found opening tag `>` help: try `>` --> tests/html_macro/generic-component-fail.rs:51:30 | 51 | html! { > }; | ^^^ ================================================ FILE: packages/yew-macro/tests/html_macro/generic-component-pass.rs ================================================ #![no_implicit_prelude] // Shadow primitives #[allow(non_camel_case_types)] pub struct bool; #[allow(non_camel_case_types)] pub struct char; #[allow(non_camel_case_types)] pub struct f32; #[allow(non_camel_case_types)] pub struct f64; #[allow(non_camel_case_types)] pub struct i128; #[allow(non_camel_case_types)] pub struct i16; #[allow(non_camel_case_types)] pub struct i32; #[allow(non_camel_case_types)] pub struct i64; #[allow(non_camel_case_types)] pub struct i8; #[allow(non_camel_case_types)] pub struct isize; #[allow(non_camel_case_types)] pub struct str; #[allow(non_camel_case_types)] pub struct u128; #[allow(non_camel_case_types)] pub struct u16; #[allow(non_camel_case_types)] pub struct u32; #[allow(non_camel_case_types)] pub struct u64; #[allow(non_camel_case_types)] pub struct u8; #[allow(non_camel_case_types)] pub struct usize; pub struct Generic { marker: ::std::marker::PhantomData, } impl ::yew::Component for Generic where T: 'static, { type Message = (); type Properties = (); fn create(_ctx: &::yew::Context) -> Self { ::std::unimplemented!() } fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { ::std::unimplemented!() } } pub struct Generic2 { marker: ::std::marker::PhantomData<(T1, T2)>, } impl ::yew::Component for Generic2 where T1: 'static, T2: 'static, { type Message = (); type Properties = (); fn create(_ctx: &::yew::Context) -> Self { ::std::unimplemented!() } fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { ::std::unimplemented!() } } fn compile_pass() { _ = ::yew::html! { /> }; _ = ::yew::html! { /> }; _ = ::yew::html! { >> }; _ = ::yew::html! { >> }; _ = ::yew::html! { > /> }; _ = ::yew::html! { >>>> }; _ = ::yew::html! { /> }; _ = ::yew::html! { >> }; _ = ::yew::html! { /> }; _ = ::yew::html! { >> }; _ = ::yew::html! { /> }; _ = ::yew::html! { >> }; _ = ::yew::html! { /> }; _ = ::yew::html! { >> }; } fn main() {} ================================================ FILE: packages/yew-macro/tests/html_macro/html-component-fail.stderr ================================================ error: this opening tag has no corresponding closing tag --> $DIR/html-component-fail.rs:78:13 | 78 | html! { }; | ^^^^^^^ error: unexpected end of input, expected identifier --> $DIR/html-component-fail.rs:79:13 | 79 | html! { }; | ^^^^^^^^^^^ error: expected expression following this `with` --> $DIR/html-component-fail.rs:80:20 | 80 | html! { }; | ^^^^ error: `props` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) --> $DIR/html-component-fail.rs:81:20 | 81 | html! { }; | ^^^^^ error: this opening tag has no corresponding closing tag --> $DIR/html-component-fail.rs:82:13 | 82 | html! { }; | ^^^^^^^^^^^^^^^^^^^ error: there are two `with ` definitions for this component (note: you can only define `with ` once) --> $DIR/html-component-fail.rs:84:20 | 84 | html! { }; | ^^^^^^^ error: `ref` can only be set once --> $DIR/html-component-fail.rs:85:38 | 85 | html! { }; | ^^^ error: `ref` can only be set once --> $DIR/html-component-fail.rs:86:38 | 86 | html! { }; | ^^^ error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) --> $DIR/html-component-fail.rs:87:38 | 87 | html! { }; | ^^^^^ error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) --> $DIR/html-component-fail.rs:88:31 | 88 | html! { }; | ^^^^^ error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) --> $DIR/html-component-fail.rs:89:20 | 89 | html! { }; | ^^^^^ error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) --> $DIR/html-component-fail.rs:90:20 | 90 | html! { }; | ^^^^^ error: `ref` can only be set once --> $DIR/html-component-fail.rs:91:27 | 91 | html! { }; | ^^^ error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) --> $DIR/html-component-fail.rs:93:20 | 93 | html! { }; | ^^^^^ error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) --> $DIR/html-component-fail.rs:94:31 | 94 | html! { }; | ^^^^^ error: expected identifier, found keyword `type` --> $DIR/html-component-fail.rs:95:20 | 95 | html! { }; | ^^^^ expected identifier, found keyword | help: you can escape reserved keywords to use them as identifiers | 95 | html! { }; | ^^^^^^ error: expected a valid Rust identifier --> $DIR/html-component-fail.rs:96:20 | 96 | html! { }; | ^^^^^^^^^^^^^^^^^ error: expected an expression following this equals sign --> $DIR/html-component-fail.rs:98:26 | 98 | html! { }; | ^ error: `int` can only be specified once but is given here again --> $DIR/html-component-fail.rs:99:26 | 99 | html! { }; | ^^^ error: `int` can only be specified once but is given here again --> $DIR/html-component-fail.rs:99:32 | 99 | html! { }; | ^^^ error: `ref` can only be specified once --> $DIR/html-component-fail.rs:104:26 | 104 | html! { }; | ^^^ error: this closing tag has no corresponding opening tag --> $DIR/html-component-fail.rs:107:13 | 107 | html! { }; | ^^^^^^^^ error: this opening tag has no corresponding closing tag --> $DIR/html-component-fail.rs:108:13 | 108 | html! { }; | ^^^^^^^ error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<>`) --> $DIR/html-component-fail.rs:109:28 | 109 | html! { }; | ^^^^^^^^^^^^^^^ error: cannot specify the `children` prop when the component already has children --> $DIR/html-component-fail.rs:128:25 | 128 | | ^^^^^^^^ error: this closing tag has no corresponding opening tag --> $DIR/html-component-fail.rs:133:30 | 133 | html! { > }; | ^^^^^^^^^^ error: this closing tag has no corresponding opening tag --> $DIR/html-component-fail.rs:134:30 | 134 | html! { >>> }; | ^^^^^^^^^^^^^^^^^^^^^^^ error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<>`) --> $DIR/html-component-fail.rs:138:9 | 138 | { 2 } | ^^^^^^^^^^^^^^^^^^ error: optional attributes are only supported on elements. Components can use `Option` properties to accomplish the same thing. --> $DIR/html-component-fail.rs:141:28 | 141 | html! { }; | ^^^^^ error[E0425]: cannot find value `blah` in this scope --> $DIR/html-component-fail.rs:92:25 | 92 | html! { }; | ^^^^ not found in this scope error[E0609]: no field `r#type` on type `ChildProperties` --> $DIR/html-component-fail.rs:95:20 | 95 | html! { }; | ^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `r#type` found for struct `ChildPropertiesBuilder` in the current scope --> $DIR/html-component-fail.rs:95:20 | 5 | #[derive(Clone, Properties, PartialEq)] | ---------- method `r#type` not found for this ... 95 | html! { }; | ^^^^ method not found in `ChildPropertiesBuilder` error[E0609]: no field `unknown` on type `ChildProperties` --> $DIR/html-component-fail.rs:97:20 | 97 | html! { }; | ^^^^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `unknown` found for struct `ChildPropertiesBuilder` in the current scope --> $DIR/html-component-fail.rs:97:20 | 5 | #[derive(Clone, Properties, PartialEq)] | ---------- method `unknown` not found for this ... 97 | html! { }; | ^^^^^^^ method not found in `ChildPropertiesBuilder` error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer<(), std::string::String>` is not satisfied --> $DIR/html-component-fail.rs:100:33 | 100 | html! { }; | ^^ the trait `yew::virtual_dom::Transformer<(), std::string::String>` is not implemented for `yew::virtual_dom::vcomp::VComp` | = help: the following implementations were found: > >> >> > and 3 others = note: required by `yew::virtual_dom::Transformer::transform` error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer<{integer}, std::string::String>` is not satisfied --> $DIR/html-component-fail.rs:101:33 | 101 | html! { }; | ^ the trait `yew::virtual_dom::Transformer<{integer}, std::string::String>` is not implemented for `yew::virtual_dom::vcomp::VComp` | = help: the following implementations were found: > >> >> > and 3 others = note: required by `yew::virtual_dom::Transformer::transform` error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer<{integer}, std::string::String>` is not satisfied --> $DIR/html-component-fail.rs:102:33 | 102 | html! { }; | ^^^ the trait `yew::virtual_dom::Transformer<{integer}, std::string::String>` is not implemented for `yew::virtual_dom::vcomp::VComp` | = help: the following implementations were found: > >> >> > and 3 others = note: required by `yew::virtual_dom::Transformer::transform` error[E0308]: mismatched types --> $DIR/html-component-fail.rs:103:30 | 103 | html! { }; | ^^ expected struct `yew::html::NodeRef`, found `()` error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer` is not satisfied --> $DIR/html-component-fail.rs:105:24 | 105 | html! { }; | ^^^^ the trait `yew::virtual_dom::Transformer` is not implemented for `yew::virtual_dom::vcomp::VComp` | = help: the following implementations were found: > >> >> > and 3 others = note: required by `yew::virtual_dom::Transformer::transform` error[E0599]: no method named `string` found for struct `ChildPropertiesBuilder` in the current scope --> $DIR/html-component-fail.rs:106:20 | 5 | #[derive(Clone, Properties, PartialEq)] | ---------- method `string` not found for this ... 106 | html! { }; | ^^^^^^ method not found in `ChildPropertiesBuilder` | = help: items from traits can only be used if the trait is implemented and in scope = note: the following trait defines an item `string`, perhaps you need to implement it: candidate #1: `proc_macro::bridge::server::Literal` error[E0609]: no field `children` on type `ChildProperties` --> $DIR/html-component-fail.rs:110:14 | 110 | html! { { "Not allowed" } }; | ^^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `children` found for struct `ChildPropertiesBuilder` in the current scope --> $DIR/html-component-fail.rs:110:14 | 5 | #[derive(Clone, Properties, PartialEq)] | ---------- method `children` not found for this ... 110 | html! { { "Not allowed" } }; | ^^^^^ method not found in `ChildPropertiesBuilder` error[E0609]: no field `children` on type `ChildProperties` --> $DIR/html-component-fail.rs:114:10 | 114 | | ^^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `build` found for struct `ChildContainerPropertiesBuilder` in the current scope --> $DIR/html-component-fail.rs:119:14 | 31 | #[derive(Clone, Properties)] | ---------- method `build` not found for this ... 119 | html! { }; | ^^^^^^^^^^^^^^ method not found in `ChildContainerPropertiesBuilder` | = help: items from traits can only be used if the trait is implemented and in scope = note: the following trait defines an item `build`, perhaps you need to implement it: candidate #1: `proc_macro::bridge::server::TokenStreamBuilder` error[E0599]: no method named `build` found for struct `ChildContainerPropertiesBuilder` in the current scope --> $DIR/html-component-fail.rs:120:14 | 31 | #[derive(Clone, Properties)] | ---------- method `build` not found for this ... 120 | html! { }; | ^^^^^^^^^^^^^^ method not found in `ChildContainerPropertiesBuilder` | = help: items from traits can only be used if the trait is implemented and in scope = note: the following trait defines an item `build`, perhaps you need to implement it: candidate #1: `proc_macro::bridge::server::TokenStreamBuilder` error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild: std::convert::From` is not satisfied --> $DIR/html-component-fail.rs:121:31 | 121 | html! { { "Not allowed" } }; | ^^^^^^^^^^^^^ the trait `std::convert::From` is not implemented for `yew::virtual_dom::vcomp::VChild` | = note: required because of the requirements on the impl of `std::convert::Into>` for `yew::virtual_dom::vtext::VText` = note: required by `std::convert::Into::into` error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild: std::convert::From` is not satisfied --> $DIR/html-component-fail.rs:122:29 | 122 | html! { <> }; | ^ the trait `std::convert::From` is not implemented for `yew::virtual_dom::vcomp::VChild` | = note: required because of the requirements on the impl of `std::convert::Into>` for `yew::virtual_dom::vnode::VNode` = note: required by `std::convert::Into::into` error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild: std::convert::From` is not satisfied --> $DIR/html-component-fail.rs:123:30 | 123 | html! { }; | ^^^^^ the trait `std::convert::From` is not implemented for `yew::virtual_dom::vcomp::VChild` | = note: required because of the requirements on the impl of `std::convert::Into>` for `yew::virtual_dom::vnode::VNode` = note: required by `std::convert::Into::into` ================================================ FILE: packages/yew-macro/tests/html_macro/html-element-pass.rs ================================================ #![no_implicit_prelude] // Shadow primitives #[allow(non_camel_case_types)] pub struct bool; #[allow(non_camel_case_types)] pub struct char; #[allow(non_camel_case_types)] pub struct f32; #[allow(non_camel_case_types)] pub struct f64; #[allow(non_camel_case_types)] pub struct i128; #[allow(non_camel_case_types)] pub struct i16; #[allow(non_camel_case_types)] pub struct i32; #[allow(non_camel_case_types)] pub struct i64; #[allow(non_camel_case_types)] pub struct i8; #[allow(non_camel_case_types)] pub struct isize; #[allow(non_camel_case_types)] pub struct str; #[allow(non_camel_case_types)] pub struct u128; #[allow(non_camel_case_types)] pub struct u16; #[allow(non_camel_case_types)] pub struct u32; #[allow(non_camel_case_types)] pub struct u64; #[allow(non_camel_case_types)] pub struct u8; #[allow(non_camel_case_types)] pub struct usize; fn compile_pass() { let onclick = <::yew::Callback<::yew::events::MouseEvent> as ::std::convert::From<_>>::from( |_: ::yew::events::MouseEvent| (), ); let parent_ref = <::yew::NodeRef as ::std::default::Default>::default(); let dyn_tag = || <::std::string::String as ::std::convert::From<&::std::primitive::str>>::from("test"); let mut extra_tags_iter = ::std::iter::IntoIterator::into_iter(::std::vec!["a", "b"]); let attr_val_none: ::std::option::Option<::yew::virtual_dom::AttrValue> = ::std::option::Option::None; _ = ::yew::html! {
``` Which would fail to compile, it's customary to write ```html ``` Which would fail to compile, it's customary to write ```html ``` Which would fail to compile, it's customary to write ```html