Add basic lints to the HTML macro. (#1748)

* Add basic lints to the HTML macro.

* f

* Fix the examples.

* Fix nightly warning message tests.

* apply code review suggestions

* update error message

* rebase onto master

* remove unused props

* remove console log

* remove js void

* fix according to PR comments

Co-authored-by: Julius Lungys <32368314+voidpumpkin@users.noreply.github.com>
This commit is contained in:
Teymour Aldridge 2021-11-21 14:54:49 +00:00 committed by GitHub
parent 071f1b28df
commit 7e2542cbf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 736 additions and 87 deletions

View File

@ -215,3 +215,30 @@ jobs:
with:
command: test
args: --all-targets --workspace --exclude yew
test-lints:
name: Test lints on nightly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
profile: minimal
- uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}
restore-keys: |
cargo-${{ runner.os }}-
- name: Run tests
run: |
cd packages/yew-macro
cargo +nightly test test_html_lints --features lints

View File

@ -185,7 +185,7 @@ impl Component for Model {
<div>
<section class="game-container">
<header class="app-header">
<img src="favicon.ico" class="app-logo"/>
<img alt="The app logo" src="favicon.ico" class="app-logo"/>
<h1 class="app-title">{ "Game of Life" }</h1>
</header>
<section class="game-area">

View File

@ -33,7 +33,7 @@ impl Component for AuthorCard {
<div class="media">
<div class="media-left">
<figure class="image is-128x128">
<img src={author.image_url.clone()} />
<img alt="Author's profile picture" src={author.image_url.clone()} />
</figure>
</div>
<div class="media-content">

View File

@ -1,12 +1,22 @@
use serde::Deserialize;
use serde::Serialize;
use yew::prelude::*;
use yew_router::prelude::*;
use crate::Route;
const ELLIPSIS: &str = "\u{02026}";
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct PageQuery {
pub page: u64,
}
#[derive(Clone, Debug, PartialEq, Properties)]
pub struct Props {
pub page: u64,
pub total_pages: u64,
pub on_switch_page: Callback<u64>,
pub route_to_page: Route,
}
pub struct Pagination;
@ -34,18 +44,21 @@ impl Pagination {
fn render_link(&self, to_page: u64, props: &Props) -> Html {
let Props {
page,
ref on_switch_page,
route_to_page,
..
} = *props;
} = props.clone();
let onclick = on_switch_page.reform(move |_| to_page);
let is_current_class = if to_page == page { "is-current" } else { "" };
html! {
<li>
<a class={classes!("pagination-link", is_current_class)} aria-label={format!("Goto page {}", to_page)} {onclick}>
<Link<Route, PageQuery>
classes={classes!("pagination-link", is_current_class)}
to={route_to_page}
query={Some(PageQuery{page: to_page})}
>
{ to_page }
</a>
</Link<Route, PageQuery>>
</li>
}
}
@ -100,23 +113,27 @@ impl Pagination {
let Props {
page,
total_pages,
ref on_switch_page,
} = *props;
route_to_page: to,
} = props.clone();
html! {
<>
<a class="pagination-previous"
<Link<Route, PageQuery>
classes={classes!("pagination-previous")}
disabled={page==1}
onclick={on_switch_page.reform(move |_| page - 1)}
query={Some(PageQuery{page: page - 1})}
to={to.clone()}
>
{ "Previous" }
</a>
<a class="pagination-next"
</Link<Route, PageQuery>>
<Link<Route, PageQuery>
classes={classes!("pagination-next")}
disabled={page==total_pages}
onclick={on_switch_page.reform(move |_| page + 1)}
query={Some(PageQuery{page: page + 1})}
{to}
>
{ "Next page" }
</a>
</Link<Route, PageQuery>>
</>
}
}

View File

@ -30,7 +30,7 @@ impl Component for PostCard {
<div class="card">
<div class="card-image">
<figure class="image is-2by1">
<img src={post.image_url.clone()} loading="lazy" />
<img alt="This post's image" src={post.image_url.clone()} loading="lazy" />
</figure>
</div>
<div class="card-content">

View File

@ -80,22 +80,21 @@ impl Model {
fn view_nav(&self, link: &Scope<Self>) -> Html {
let Self { navbar_active, .. } = *self;
let active_class = if navbar_active { "is-active" } else { "" };
let active_class = if !navbar_active { "is-active" } else { "" };
html! {
<nav class="navbar is-primary" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<h1 class="navbar-item is-size-3">{ "Yew Blog" }</h1>
<a role="button"
class={classes!("navbar-burger", "burger", active_class)}
<button class={classes!("navbar-burger", "burger", active_class)}
aria-label="menu" aria-expanded="false"
onclick={link.callback(|_| Msg::ToggleNavbar)}
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</button>
</div>
<div class={classes!("navbar-menu", active_class)}>
<div class="navbar-start">
@ -107,15 +106,13 @@ impl Model {
</Link<Route>>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
<div class="navbar-link">
{ "More" }
</a>
</div>
<div class="navbar-dropdown">
<a class="navbar-item">
<Link<Route> classes={classes!("navbar-item")} to={Route::Authors}>
{ "Meet the authors" }
</Link<Route>>
</a>
<Link<Route> classes={classes!("navbar-item")} to={Route::Authors}>
{ "Meet the authors" }
</Link<Route>>
</div>
</div>
</div>
@ -126,15 +123,15 @@ impl Model {
}
fn switch(routes: &Route) -> Html {
match routes {
match routes.clone() {
Route::Post { id } => {
html! { <Post seed={*id} /> }
html! { <Post seed={id} /> }
}
Route::Posts => {
html! { <PostList /> }
}
Route::Author { id } => {
html! { <Author seed={*id} /> }
html! { <Author seed={id} /> }
}
Route::Authors => {
html! { <AuthorList /> }

View File

@ -46,7 +46,7 @@ impl Component for Author {
</div>
<div class="tile is-parent">
<figure class="tile is-child image is-square">
<img src={author.image_url.clone()} />
<img alt="The author's profile picture." src={author.image_url.clone()} />
</figure>
</div>
<div class="tile is-parent">

View File

@ -21,7 +21,7 @@ impl Component for Home {
<div class="tile is-child">
<figure class="image is-3by1">
<img src="https://source.unsplash.com/random/1200x400/?yew" />
<img alt="A random image for the input term 'yew'." src="https://source.unsplash.com/random/1200x400/?yew" />
</figure>
</div>

View File

@ -38,7 +38,7 @@ impl Component for Post {
html! {
<>
<section class="hero is-medium is-light has-background">
<img class="hero-background is-transparent" src={post.meta.image_url.clone()} />
<img alt="The hero's background" class="hero-background is-transparent" src={post.meta.image_url.clone()} />
<div class="hero-body">
<div class="container">
<h1 class="title">
@ -69,7 +69,7 @@ impl Post {
<article class="media block box my-6">
<figure class="media-left">
<p class="image is-64x64">
<img src={quote.author.image_url.clone()} loading="lazy" />
<img alt="The author's profile" src={quote.author.image_url.clone()} loading="lazy" />
</p>
</figure>
<div class="media-content">
@ -89,7 +89,7 @@ impl Post {
fn render_section_hero(&self, section: &content::Section) -> Html {
html! {
<section class="hero is-dark has-background mt-6 mb-3">
<img class="hero-background is-transparent" src={section.image_url.clone()} loading="lazy" />
<img alt="This section's image" class="hero-background is-transparent" src={section.image_url.clone()} loading="lazy" />
<div class="hero-body">
<div class="container">
<h2 class="subtitle">{ &section.title }</h2>

View File

@ -1,6 +1,6 @@
use crate::components::pagination::PageQuery;
use crate::components::{pagination::Pagination, post_card::PostCard};
use crate::Route;
use serde::{Deserialize, Serialize};
use yew::prelude::*;
use yew_router::prelude::*;
@ -8,39 +8,45 @@ const ITEMS_PER_PAGE: u64 = 10;
const TOTAL_PAGES: u64 = u64::MAX / ITEMS_PER_PAGE;
pub enum Msg {
ShowPage(u64),
PageUpdated,
}
#[derive(Serialize, Deserialize)]
struct PageQuery {
pub struct PostList {
page: u64,
_listener: HistoryListener,
}
pub struct PostList;
fn current_page(ctx: &Context<PostList>) -> u64 {
let location = ctx.link().location().unwrap();
location.query::<PageQuery>().map(|it| it.page).unwrap_or(1)
}
impl Component for PostList {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self
fn create(ctx: &Context<Self>) -> Self {
let link = ctx.link().clone();
let listener = ctx.link().history().unwrap().listen(move || {
link.send_message(Msg::PageUpdated);
});
Self {
page: current_page(ctx),
_listener: listener,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::ShowPage(page) => {
ctx.link()
.history()
.unwrap()
.push_with_query(Route::Posts, PageQuery { page })
.unwrap();
true
}
Msg::PageUpdated => self.page = current_page(ctx),
}
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let page = self.current_page(ctx);
let page = self.page;
html! {
<div class="section container">
@ -50,15 +56,15 @@ impl Component for PostList {
<Pagination
{page}
total_pages={TOTAL_PAGES}
on_switch_page={ctx.link().callback(Msg::ShowPage)}
route_to_page={Route::Posts}
/>
</div>
}
}
}
impl PostList {
fn view_posts(&self, ctx: &Context<Self>) -> Html {
let start_seed = (self.current_page(ctx) - 1) * ITEMS_PER_PAGE;
fn view_posts(&self, _ctx: &Context<Self>) -> Html {
let start_seed = (self.page - 1) * ITEMS_PER_PAGE;
let mut cards = (0..ITEMS_PER_PAGE).map(|seed_offset| {
html! {
<li class="list-item mb-5">
@ -81,10 +87,4 @@ impl PostList {
</div>
}
}
fn current_page(&self, ctx: &Context<Self>) -> u64 {
let location = ctx.link().location().unwrap();
location.query::<PageQuery>().map(|it| it.page).unwrap_or(1)
}
}

View File

@ -17,6 +17,7 @@ proc-macro = true
[dependencies]
boolinator = "2"
lazy_static = "1"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full", "extra-traits"] }
@ -31,3 +32,4 @@ yew = { path = "../yew" }
[features]
doc_test = []
lints = []

View File

@ -4,6 +4,12 @@ toolchain = "1.51"
command = "cargo"
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" }

View File

@ -7,11 +7,11 @@ use syn::parse::{Parse, ParseStream};
use syn::{braced, token};
pub struct HtmlBlock {
content: BlockContent,
pub content: BlockContent,
brace: token::Brace,
}
enum BlockContent {
pub enum BlockContent {
Node(Box<HtmlNode>),
Iterable(Box<HtmlIterable>),
}

View File

@ -16,6 +16,17 @@ pub struct HtmlDashedName {
}
impl HtmlDashedName {
/// Checks if this name is equal to the provided item (which can be anything implementing
/// `Into<String>`).
pub fn eq_ignore_ascii_case<S>(&self, other: S) -> bool
where
S: Into<String>,
{
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();
@ -81,6 +92,7 @@ impl ToTokens for HtmlDashedName {
tokens.extend(quote! { #name#extended });
}
}
impl Stringify for HtmlDashedName {
fn try_into_lit(&self) -> Option<LitStr> {
Some(self.to_lit_str())

View File

@ -11,9 +11,9 @@ use syn::spanned::Spanned;
use syn::{Block, Expr, Ident, Lit, LitStr, Token};
pub struct HtmlElement {
name: TagName,
props: ElementProps,
children: HtmlChildrenTree,
pub name: TagName,
pub props: ElementProps,
pub children: HtmlChildrenTree,
}
impl PeekValue<()> for HtmlElement {
@ -451,7 +451,7 @@ fn wrap_attr_prop(prop: &Prop) -> TokenStream {
}
}
struct DynamicName {
pub struct DynamicName {
at: Token![@],
expr: Option<Block>,
}
@ -498,7 +498,7 @@ enum TagKey {
Expr,
}
enum TagName {
pub enum TagName {
Lit(HtmlDashedName),
Expr(DynamicName),
}

View File

@ -9,7 +9,7 @@ use syn::Expr;
pub struct HtmlList {
open: HtmlListOpen,
children: HtmlChildrenTree,
pub children: HtmlChildrenTree,
close: HtmlListClose,
}

View File

@ -0,0 +1,108 @@
//! Lints to catch possible misuse of the `html!` macro use. At the moment these are mostly focused
//! on accessibility.
use proc_macro_error::emit_warning;
use syn::spanned::Spanned;
use crate::props::{ElementProps, Prop};
use super::html_element::TagName;
use super::{html_element::HtmlElement, HtmlTree};
/// Lints HTML elements to check if they are well formed. If the element is not well-formed, then
/// use `proc-macro-error` (and the `emit_warning!` macro) to produce a warning. At present, these
/// are only emitted on nightly.
pub trait Lint {
fn lint(element: &HtmlElement);
}
/// Applies all the lints to the HTML tree.
pub fn lint_all(tree: &HtmlTree) {
lint::<AHrefLint>(tree);
lint::<ImgAltLint>(tree);
}
/// Applies a specific lint to the HTML tree.
pub fn lint<L>(tree: &HtmlTree)
where
L: Lint,
{
match tree {
HtmlTree::List(list) => {
for child in &list.children.0 {
lint::<L>(child)
}
}
HtmlTree::Element(el) => L::lint(el),
_ => {}
}
}
/// Retrieves an attribute from an element and returns a reference valid for the lifetime of the
/// element (if that attribute can be found on the prop).
///
/// Attribute names are lowercased before being compared (so pass "href" for `name` and not "HREF").
fn get_attribute<'a>(props: &'a ElementProps, name: &str) -> Option<&'a Prop> {
props
.attributes
.iter()
.find(|item| item.label.eq_ignore_ascii_case(name))
}
/// Lints to check if anchor (`<a>`) tags have valid `href` attributes defined.
pub struct AHrefLint;
impl Lint for AHrefLint {
fn lint(element: &HtmlElement) {
if let TagName::Lit(ref tag_name) = element.name {
if !tag_name.eq_ignore_ascii_case("a") {
return;
};
if let Some(prop) = get_attribute(&element.props, "href") {
if let syn::Expr::Lit(lit) = &prop.value {
if let syn::Lit::Str(href) = &lit.lit {
let href_value = href.value();
match href_value.as_ref() {
"#" | "javascript:void(0)" => emit_warning!(
lit.span(),
format!("'{}' is not a suitable value for the `href` attribute. \
Without a meaningful attribute assistive technologies \
will struggle to understand your webpage. \
https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#onclick_events"
,href_value)),
_ => {}
}
}
};
} else {
emit_warning!(
quote::quote! {#tag_name}.span(),
"All `<a>` elements should have a `href` attribute. This makes it possible \
for assistive technologies to correctly interpret what your links point to. \
https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#more_on_links"
)
}
}
}
}
/// Checks to make sure that images have `alt` attributes defined.
pub struct ImgAltLint;
impl Lint for ImgAltLint {
fn lint(element: &HtmlElement) {
if let super::html_element::TagName::Lit(ref tag_name) = element.name {
if !tag_name.eq_ignore_ascii_case("img") {
return;
};
if get_attribute(&element.props, "alt").is_none() {
emit_warning!(
quote::quote! {#tag_name}.span(),
"All `<img>` tags should have an `alt` attribute which provides a \
human-readable description "
)
}
}
}
}

View File

@ -13,6 +13,7 @@ mod html_element;
mod html_iterable;
mod html_list;
mod html_node;
mod lint;
mod tag;
use html_block::HtmlBlock;
@ -107,6 +108,7 @@ impl HtmlTree {
impl ToTokens for HtmlTree {
fn to_tokens(&self, tokens: &mut TokenStream) {
lint::lint_all(self);
match self {
HtmlTree::Empty => tokens.extend(quote! {
::yew::virtual_dom::VNode::VList(::yew::virtual_dom::VList::new())
@ -164,6 +166,7 @@ impl Parse for HtmlRootVNode {
input.parse().map(Self)
}
}
impl ToTokens for HtmlRootVNode {
fn to_tokens(&self, tokens: &mut TokenStream) {
let new_tokens = self.0.to_token_stream();
@ -192,7 +195,7 @@ impl ToNodeIterator for HtmlTree {
}
}
struct HtmlChildrenTree(Vec<HtmlTree>);
pub struct HtmlChildrenTree(pub Vec<HtmlTree>);
impl HtmlChildrenTree {
pub fn new() -> Self {

View File

@ -96,12 +96,14 @@ pub fn derive_props(input: TokenStream) -> TokenStream {
TokenStream::from(input.into_token_stream())
}
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn html_nested(input: TokenStream) -> TokenStream {
let root = parse_macro_input!(input as HtmlRoot);
TokenStream::from(root.into_token_stream())
}
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn html(input: TokenStream) -> TokenStream {
let root = parse_macro_input!(input as HtmlRootVNode);

View File

@ -0,0 +1,17 @@
use yew::prelude::*;
fn main() {
let bad_a = html! {
<a>{ "I don't have a href attribute" }</a>
};
let bad_a_2 = html! {
<a href="#">{ "I have a malformed href attribute" }</a>
};
let bad_a_3 = html! {
<a href="javascript:void(0)">{ "I have a malformed href attribute" }</a>
};
let bad_img = html! {
<img src="img.jpeg"/>
};
compile_error!("This macro call exists to deliberately fail the compilation of the test so we can verify output of lints");
}

View File

@ -0,0 +1,29 @@
warning: All `<a>` elements should have a `href` attribute. This makes it possible for assistive technologies to correctly interpret what your links point to. https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#more_on_links
--> tests/html_lints/fail.rs:5:10
|
5 | <a>{ "I don't have a href attribute" }</a>
| ^
warning: '#' is not a suitable value for the `href` attribute. Without a meaningful attribute assistive technologies will struggle to understand your webpage. https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#onclick_events
--> tests/html_lints/fail.rs:8:17
|
8 | <a href="#">{ "I have a malformed href attribute" }</a>
| ^^^
warning: 'javascript:void(0)' is not a suitable value for the `href` attribute. Without a meaningful attribute assistive technologies will struggle to understand your webpage. https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#onclick_events
--> tests/html_lints/fail.rs:11:17
|
11 | <a href="javascript:void(0)">{ "I have a malformed href attribute" }</a>
| ^^^^^^^^^^^^^^^^^^^^
warning: All `<img>` tags should have an `alt` attribute which provides a human-readable description
--> tests/html_lints/fail.rs:14:10
|
14 | <img src="img.jpeg"/>
| ^^^
error: This macro call exists to deliberately fail the compilation of the test so we can verify output of lints
--> tests/html_lints/fail.rs:16:5
|
16 | compile_error!("This macro call exists to deliberately fail the compilation of the test so we can verify output of lints");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,7 @@
#[allow(dead_code)]
#[cfg(feature = "lints")]
#[rustversion::attr(nightly, test)]
fn test_html_lints() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/html_lints/fail.rs");
}

View File

@ -0,0 +1,371 @@
error: this opening tag has no corresponding closing tag
--> $DIR/html-component-fail.rs:78:13
|
78 | html! { <Child> };
| ^^^^^^^
error: unexpected end of input, expected identifier
--> $DIR/html-component-fail.rs:79:13
|
79 | html! { <Child:: /> };
| ^^^^^^^^^^^
error: expected expression following this `with`
--> $DIR/html-component-fail.rs:80:20
|
80 | html! { <Child with /> };
| ^^^^
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! { <Child props /> };
| ^^^^^
error: this opening tag has no corresponding closing tag
--> $DIR/html-component-fail.rs:82:13
|
82 | html! { <Child with props > };
| ^^^^^^^^^^^^^^^^^^^
error: there are two `with <props>` definitions for this component (note: you can only define `with <props>` once)
--> $DIR/html-component-fail.rs:84:20
|
84 | html! { <Child with p1 with p2 /> };
| ^^^^^^^
error: `ref` can only be set once
--> $DIR/html-component-fail.rs:85:38
|
85 | html! { <Child with props ref=() ref=() /> };
| ^^^
error: `ref` can only be set once
--> $DIR/html-component-fail.rs:86:38
|
86 | html! { <Child with props ref=() ref=() value=1 /> };
| ^^^
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! { <Child with props ref=() value=1 ref=() /> };
| ^^^^^
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! { <Child with props value=1 ref=() ref=() /> };
| ^^^^^
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! { <Child value=1 with props ref=() ref=() /> };
| ^^^^^
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! { <Child value=1 ref=() with props ref=() /> };
| ^^^^^
error: `ref` can only be set once
--> $DIR/html-component-fail.rs:91:27
|
91 | html! { <Child ref=() ref=() value=1 with props /> };
| ^^^
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! { <Child value=1 with props /> };
| ^^^^^
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! { <Child with props value=1 /> };
| ^^^^^
error: expected identifier, found keyword `type`
--> $DIR/html-component-fail.rs:95:20
|
95 | html! { <Child type=0 /> };
| ^^^^ expected identifier, found keyword
|
help: you can escape reserved keywords to use them as identifiers
|
95 | html! { <Child r#type=0 /> };
| ^^^^^^
error: expected a valid Rust identifier
--> $DIR/html-component-fail.rs:96:20
|
96 | html! { <Child invalid-prop-name=0 /> };
| ^^^^^^^^^^^^^^^^^
error: expected an expression following this equals sign
--> $DIR/html-component-fail.rs:98:26
|
98 | html! { <Child string= /> };
| ^
error: `int` can only be specified once but is given here again
--> $DIR/html-component-fail.rs:99:26
|
99 | html! { <Child int=1 int=2 int=3 /> };
| ^^^
error: `int` can only be specified once but is given here again
--> $DIR/html-component-fail.rs:99:32
|
99 | html! { <Child int=1 int=2 int=3 /> };
| ^^^
error: `ref` can only be specified once
--> $DIR/html-component-fail.rs:104:26
|
104 | html! { <Child int=1 ref=() ref=() /> };
| ^^^
error: this closing tag has no corresponding opening tag
--> $DIR/html-component-fail.rs:107:13
|
107 | html! { </Child> };
| ^^^^^^^^
error: this opening tag has no corresponding closing tag
--> $DIR/html-component-fail.rs:108:13
|
108 | html! { <Child><Child></Child> };
| ^^^^^^^
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! { <Child></Child><Child></Child> };
| ^^^^^^^^^^^^^^^
error: cannot specify the `children` prop when the component already has children
--> $DIR/html-component-fail.rs:128:25
|
128 | <ChildContainer children=children>
| ^^^^^^^^
error: this closing tag has no corresponding opening tag
--> $DIR/html-component-fail.rs:133:30
|
133 | html! { <Generic<String>></Generic> };
| ^^^^^^^^^^
error: this closing tag has no corresponding opening tag
--> $DIR/html-component-fail.rs:134:30
|
134 | html! { <Generic<String>></Generic<Vec<String>>> };
| ^^^^^^^^^^^^^^^^^^^^^^^
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 | <span>{ 2 }</span>
| ^^^^^^^^^^^^^^^^^^
error: optional attributes are only supported on elements. Components can use `Option<T>` properties to accomplish the same thing.
--> $DIR/html-component-fail.rs:141:28
|
141 | html! { <TestComponent value?="not_supported" /> };
| ^^^^^
error[E0425]: cannot find value `blah` in this scope
--> $DIR/html-component-fail.rs:92:25
|
92 | html! { <Child with blah /> };
| ^^^^ not found in this scope
error[E0609]: no field `r#type` on type `ChildProperties`
--> $DIR/html-component-fail.rs:95:20
|
95 | html! { <Child type=0 /> };
| ^^^^ unknown field
|
= note: available fields are: `string`, `int`
error[E0599]: no method named `r#type` found for struct `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` 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! { <Child type=0 /> };
| ^^^^ method not found in `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>`
error[E0609]: no field `unknown` on type `ChildProperties`
--> $DIR/html-component-fail.rs:97:20
|
97 | html! { <Child unknown="unknown" /> };
| ^^^^^^^ unknown field
|
= note: available fields are: `string`, `int`
error[E0599]: no method named `unknown` found for struct `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` in the current scope
--> $DIR/html-component-fail.rs:97:20
|
5 | #[derive(Clone, Properties, PartialEq)]
| ---------- method `unknown` not found for this
...
97 | html! { <Child unknown="unknown" /> };
| ^^^^^^^ method not found in `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>`
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! { <Child int=1 string={} /> };
| ^^ the trait `yew::virtual_dom::Transformer<(), std::string::String>` is not implemented for `yew::virtual_dom::vcomp::VComp`
|
= help: the following implementations were found:
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, T>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, std::option::Option<T>>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::option::Option<std::string::String>>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::string::String>>
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! { <Child int=1 string=3 /> };
| ^ 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:
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, T>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, std::option::Option<T>>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::option::Option<std::string::String>>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::string::String>>
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! { <Child int=1 string={3} /> };
| ^^^ 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:
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, T>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, std::option::Option<T>>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::option::Option<std::string::String>>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::string::String>>
and 3 others
= note: required by `yew::virtual_dom::Transformer::transform`
error[E0308]: mismatched types
--> $DIR/html-component-fail.rs:103:30
|
103 | html! { <Child int=1 ref=() /> };
| ^^ expected struct `yew::html::NodeRef`, found `()`
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer<u32, i32>` is not satisfied
--> $DIR/html-component-fail.rs:105:24
|
105 | html! { <Child int=0u32 /> };
| ^^^^ the trait `yew::virtual_dom::Transformer<u32, i32>` is not implemented for `yew::virtual_dom::vcomp::VComp`
|
= help: the following implementations were found:
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, T>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, std::option::Option<T>>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::option::Option<std::string::String>>>
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::string::String>>
and 3 others
= note: required by `yew::virtual_dom::Transformer::transform`
error[E0599]: no method named `string` found for struct `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` in the current scope
--> $DIR/html-component-fail.rs:106:20
|
5 | #[derive(Clone, Properties, PartialEq)]
| ---------- method `string` not found for this
...
106 | html! { <Child string="abc" /> };
| ^^^^^^ method not found in `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>`
|
= 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! { <Child>{ "Not allowed" }</Child> };
| ^^^^^ unknown field
|
= note: available fields are: `string`, `int`
error[E0599]: no method named `children` found for struct `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` in the current scope
--> $DIR/html-component-fail.rs:110:14
|
5 | #[derive(Clone, Properties, PartialEq)]
| ---------- method `children` not found for this
...
110 | html! { <Child>{ "Not allowed" }</Child> };
| ^^^^^ method not found in `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>`
error[E0609]: no field `children` on type `ChildProperties`
--> $DIR/html-component-fail.rs:114:10
|
114 | <Child with ChildProperties { string: "hello".to_owned(), int: 5 }>
| ^^^^^ unknown field
|
= note: available fields are: `string`, `int`
error[E0599]: no method named `build` found for struct `ChildContainerPropertiesBuilder<ChildContainerPropertiesBuilderStep_missing_required_prop_children>` in the current scope
--> $DIR/html-component-fail.rs:119:14
|
31 | #[derive(Clone, Properties)]
| ---------- method `build` not found for this
...
119 | html! { <ChildContainer /> };
| ^^^^^^^^^^^^^^ method not found in `ChildContainerPropertiesBuilder<ChildContainerPropertiesBuilderStep_missing_required_prop_children>`
|
= 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<ChildContainerPropertiesBuilderStep_missing_required_prop_children>` in the current scope
--> $DIR/html-component-fail.rs:120:14
|
31 | #[derive(Clone, Properties)]
| ---------- method `build` not found for this
...
120 | html! { <ChildContainer></ChildContainer> };
| ^^^^^^^^^^^^^^ method not found in `ChildContainerPropertiesBuilder<ChildContainerPropertiesBuilderStep_missing_required_prop_children>`
|
= 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<Child>: std::convert::From<yew::virtual_dom::vtext::VText>` is not satisfied
--> $DIR/html-component-fail.rs:121:31
|
121 | html! { <ChildContainer>{ "Not allowed" }</ChildContainer> };
| ^^^^^^^^^^^^^ the trait `std::convert::From<yew::virtual_dom::vtext::VText>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
|
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `yew::virtual_dom::vtext::VText`
= note: required by `std::convert::Into::into`
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied
--> $DIR/html-component-fail.rs:122:29
|
122 | html! { <ChildContainer><></></ChildContainer> };
| ^ the trait `std::convert::From<yew::virtual_dom::vnode::VNode>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
|
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `yew::virtual_dom::vnode::VNode`
= note: required by `std::convert::Into::into`
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied
--> $DIR/html-component-fail.rs:123:30
|
123 | html! { <ChildContainer><other /></ChildContainer> };
| ^^^^^ the trait `std::convert::From<yew::virtual_dom::vnode::VNode>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
|
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `yew::virtual_dom::vnode::VNode`
= note: required by `std::convert::Into::into`

View File

@ -1,5 +1,6 @@
use std::marker::PhantomData;
use serde::Serialize;
use wasm_bindgen::UnwrapThrowExt;
use yew::prelude::*;
@ -9,54 +10,93 @@ use crate::Routable;
/// Props for [`Link`]
#[derive(Properties, Clone, PartialEq)]
pub struct LinkProps<R: Routable> {
pub struct LinkProps<R, Q = ()>
where
R: Routable,
Q: Clone + PartialEq + Serialize,
{
/// CSS classes to add to the anchor element (optional).
#[prop_or_default]
pub classes: Classes,
/// Route that will be pushed when the anchor is clicked.
pub to: R,
/// Route query data
#[prop_or_default]
pub query: Option<Q>,
#[prop_or_default]
pub disabled: bool,
#[prop_or_default]
pub children: Children,
}
/// A wrapper around `<a>` tag to be used with [`Router`](crate::Router)
pub struct Link<R: Routable + 'static> {
_data: PhantomData<R>,
pub struct Link<R, Q = ()>
where
R: Routable + 'static,
Q: Clone + PartialEq + Serialize + 'static,
{
_route: PhantomData<R>,
_query: PhantomData<Q>,
}
pub enum Msg {
OnClick,
}
impl<R: Routable + 'static> Component for Link<R> {
impl<R, Q> Component for Link<R, Q>
where
R: Routable + 'static,
Q: Clone + PartialEq + Serialize + 'static,
{
type Message = Msg;
type Properties = LinkProps<R>;
type Properties = LinkProps<R, Q>;
fn create(_ctx: &Context<Self>) -> Self {
Self { _data: PhantomData }
Self {
_route: PhantomData,
_query: PhantomData,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::OnClick => {
ctx.link()
.history()
.expect_throw("failed to read history")
.push(ctx.props().to.clone());
let LinkProps { to, query, .. } = ctx.props();
let history = ctx.link().history().expect_throw("failed to read history");
match query {
None => {
history.push(to.clone());
}
Some(data) => {
history
.push_with_query(to.clone(), data.clone())
.expect_throw("failed push history with query");
}
};
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let LinkProps {
classes,
to,
children,
disabled,
..
} = ctx.props().clone();
let onclick = ctx.link().callback(|e: MouseEvent| {
e.prevent_default();
Msg::OnClick
});
html! {
<a class={ctx.props().classes.clone()}
href={ctx.props().to.to_path()}
onclick={ctx.link().callback(|e: MouseEvent| {
e.prevent_default();
Msg::OnClick
})}
<a class={classes}
href={to.to_path()}
{onclick}
{disabled}
>
{ ctx.props().children.clone() }
{ children }
</a>
}
}

View File

@ -125,6 +125,17 @@ html! {
<!--END_DOCUSAURUS_CODE_TABS-->
## Lints
If you compile Yew using a nightly version of the Rust compiler, the macro will warn you about some
common pitfalls that you might run into. Of course, you may need to use the stable compiler (e.g.
your organization might have a policy mandating it) for release builds, but even if you're using a
stable toolchain, running `cargo +nightly check` might flag some ways that you could improve your
HTML code.
At the moment the lints are mostly accessibility-related. If you have ideas for lints, please feel
free to [chime in on this issue](https://github.com/yewstack/yew/issues/1334).
## Special properties
There are special properties which don't directly influence the DOM but instead act as instructions to Yew's virtual DOM.