mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
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:
parent
071f1b28df
commit
7e2542cbf8
27
.github/workflows/pull-request.yml
vendored
27
.github/workflows/pull-request.yml
vendored
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 /> }
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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">{ §ion.title }</h2>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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>),
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ use syn::Expr;
|
||||
|
||||
pub struct HtmlList {
|
||||
open: HtmlListOpen,
|
||||
children: HtmlChildrenTree,
|
||||
pub children: HtmlChildrenTree,
|
||||
close: HtmlListClose,
|
||||
}
|
||||
|
||||
|
||||
108
packages/yew-macro/src/html_tree/lint/mod.rs
Normal file
108
packages/yew-macro/src/html_tree/lint/mod.rs
Normal 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 "
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
17
packages/yew-macro/tests/html_lints/fail.rs
Normal file
17
packages/yew-macro/tests/html_lints/fail.rs
Normal 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");
|
||||
}
|
||||
29
packages/yew-macro/tests/html_lints/fail.stderr
Normal file
29
packages/yew-macro/tests/html_lints/fail.stderr
Normal 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");
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
7
packages/yew-macro/tests/html_lints_test.rs
Normal file
7
packages/yew-macro/tests/html_lints_test.rs
Normal 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");
|
||||
}
|
||||
371
packages/yew-macro/tests/html_macro/html-component-fail.stderr
Normal file
371
packages/yew-macro/tests/html_macro/html-component-fail.stderr
Normal 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`
|
||||
@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user