Extract Classes to a separate macro (#1601)

* Initial commit

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* Add feature flag for now

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* Add macro classes!()

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* rustfmt

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* rustfmt

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* Not sure why there is a prelude.rs file here...

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* Revert example changes

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* WIP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* CLEANUP

Forked at: 7e6d6c43f0aa98d78ae34654d49e1cf84d252cce
Parent branch: yewstack/master

* Revert back to e2717ae6281691f9563079ef07b56b24b6431ff1

* Put back original class behavior in html!

* Store span and use quote_spanned!

* Deprecation warning

* Better error message

* cargo fmt & fix conflict leftover oops

* Fix clippy: missing import

* What have I done

* Use Token! instead of Comma

* Apply suggestion

* Renamed HtmlClasses to Classes

* Remove clippy attribute & add dedicated tests

* Remove span from Single variant

* Spacing stuff with quote_spanned!

* Removed "use" and use path to Classes

* Some doc

* Replace Vec<Expr> with ExprTuple

* rustfmt

* Move classes to its own module

* Move tests from vtag to classes

* Update classes-fail.stderr

* Check for spaces in string literals

* Use unchecked_push for string literals

* Fixed tests

* More doc

* Tested doc in browser and make boolinator import visible

* Improve doc a tad bit

* Stuff

* Fix due to change with string literal

* Typo

* Moved classes to html

* Update macro new path for Classes

* Test fix

* Example fix

* Update classes-fail.stderr

* Fixed weird error

* Update yew/src/lib.rs

Co-authored-by: Simon <simon@siku2.io>

* Update yew/Cargo.toml

* Update yew/src/html/classes.rs

Co-authored-by: Simon <simon@siku2.io>

* Update docs/concepts/html/classes.md

Co-authored-by: Simon <simon@siku2.io>

* I blame GitHub

* Use syn::Result

* Apply suggestions

* Update yew-macro/src/classes/mod.rs

Co-authored-by: Simon <simon@siku2.io>

* Update yew-macro/tests/macro_test.rs

Co-authored-by: Simon <simon@siku2.io>

* Replaced html! by classes!

* Moved classes tests

* Renamed doc title

* Apply suggestion

* Update yew-macro/src/html_tree/html_element.rs

Co-authored-by: Simon <simon@siku2.io>

* Fixed span badly handled in macro

* no implicit prelude

* Update yew/src/lib.rs

Co-authored-by: Simon <simon@siku2.io>

* Improve documentation

* Yeah... OK

* OMG clippy

* Please squash merge and don't keep my commit messages

* Safety notice

* Change "Yew's macros" to "HTML"

* Unneeded closure

* Boxing stuff 🥊

* Indentation issue

* Removed unneeded block & removed duplicated code

* Renamed "macros" to "html_macro"

* Better span

* Doc: advice to use `classes!`

* Doc: fix error and remove one "and"

* Removed interpolated example

* Remove unnecessary tests and add test for deprecated syntax

* Link to classes!

* Update yew-macro/tests/html_macro_test.rs

Co-authored-by: Simon <simon@siku2.io>

* Renamed fail test

* Apply suggestion

* WIP

* WIP

* cargo make pr-flow passes locally, let's try again

Co-authored-by: Simon <simon@siku2.io>
This commit is contained in:
Cecile Tonglet 2020-11-25 16:32:53 +01:00 committed by GitHub
parent 6ed5365be5
commit 9737fe77db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 760 additions and 725 deletions

View File

@ -1,5 +1,5 @@
---
title: html!
title: HTML
sidebar_label: Introduction
description: The procedural macro for generating HTML and SVG
---

View File

@ -0,0 +1,122 @@
---
title: Classes
description: A handy macro to handle classes
---
## Classes
The struct `Classes` can be used to deal with HTML classes.
When pushing a string to the set, `Classes` ensures that there is one element
for every class even if a single string might contain multiple classes.
`Classes` can also be merged by using `Extend` (i.e.
`classes1.extend(classes2)`) or `push()` (i.e. `classes1.push(classes2)`). In
fact, anything that implements `Into<Classes>` can be used to push new classes
to the set.
The macro `classes!` is a convenient macro that creates one single `Classes`.
Its input accepts a comma separated list of expressions. The only requirement
is that every expression implements `Into<Classes>`.
<!--DOCUSAURUS_CODE_TABS-->
<!--Literal-->
```rust
html! {
<div class=classes!("container")></div>
}
```
<!--Multiple-->
```rust
html! {
<div class=classes!("class-1", "class-2")></div>
}
```
<!--String-->
```rust
let my_classes = String::from("class-1 class-2");
html! {
<div class=classes!(my_classes)></div>
}
```
<!--Optional-->
```rust
html! {
<div class=classes!(Some("class")) />
}
```
<!--Vector-->
```rust
html! {
<div class=classes!(vec!["class-1", "class-2"])></div>
}
```
<!--Array-->
```rust
let my_classes = ["class-1", "class-2"];
html! {
<div class=classes!(&my_classes)></div>
}
```
<!--END_DOCUSAURUS_CODE_TABS-->
## Components that accept classes
```rust
use boolinator::Boolinator;
#[derive(Clone, Properties)]
struct Props {
#[prop_or_default]
class: Classes,
fill: bool,
children: Children,
}
struct MyComponent {
props: Props,
}
impl Component for MyComponent {
type Properties = Props;
// ...
fn view(&self) -> Html {
let Props {
class,
fill,
children,
} = &self.props;
html! {
<div
class=classes!(
"my-container-class",
fill.as_some("my-fill-class"),
class.clone(),
)
>
{ children.clone() }
</div>
}
}
}
```
The example makes use of the [boolinator](https://crates.io/crates/boolinator)
crate to conditionally add the "my-fill-class" class based on the `fill`
boolean attribute.

View File

@ -35,53 +35,6 @@ html! {
If the attribute is set to `None`, the attribute won't be set in the DOM.
## Classes
There are a number of convenient ways to specify classes for an element:
<!--DOCUSAURUS_CODE_TABS-->
<!--Literal-->
```rust
html! {
<div class="container"></div>
}
```
<!--Multiple-->
```rust
html! {
<div class=("class-1", "class-2")></div>
}
```
<!--Optional-->
```rust
html! {
<div class=if condition { Some("class") } else { None } />
}
```
<!--Interpolated-->
```rust
html! {
<div class=format!("{}-container", size) />
}
```
<!--Vector-->
```rust
html! {
<div class=vec!["class-1", "class-2"]></div>
}
```
<!--END_DOCUSAURUS_CODE_TABS-->
## Listeners
Listener attributes need to be passed a `Callback` which is a wrapper around a closure. How you create your callback depends on how you wish your app to react to a listener event:

View File

@ -1,5 +1,7 @@
use crate::Client;
use yew::{html, Callback, Component, ComponentLink, Html, InputData, Properties, ShouldRender};
use yew::{
classes, html, Callback, Component, ComponentLink, Html, InputData, Properties, ShouldRender,
};
#[derive(Debug)]
pub enum Msg {
@ -74,19 +76,19 @@ impl Component for AddClientForm {
<>
<div class="names">
<input
class=("new-client", "firstname")
class=classes!("new-client", "firstname")
placeholder="First name"
value=&client.first_name
oninput=link.callback(|e: InputData| Msg::UpdateFirstName(e.value))
/>
<input
class=("new-client", "lastname")
class=classes!("new-client", "lastname")
placeholder="Last name"
value=&client.last_name
oninput=link.callback(|e: InputData| Msg::UpdateLastName(e.value))
/>
<textarea
class=("new-client", "description")
class=classes!("new-client", "description")
placeholder="Description"
value=&client.description
oninput=link.callback(|e: InputData| Msg::UpdateDescription(e.value))

View File

@ -1,8 +1,8 @@
/// Original author of this code is [Nathan Ringo](https://github.com/remexre)
/// Source: https://github.com/acmumn/mentoring/blob/master/web-client/src/view/markdown.rs
use pulldown_cmark::{Alignment, CodeBlockKind, Event, Options, Parser, Tag};
use yew::virtual_dom::{Classes, VNode, VTag, VText};
use yew::{html, Html};
use yew::virtual_dom::{VNode, VTag, VText};
use yew::{html, Classes, Html};
/// Adds a class to the VTag.
/// You can also provide multiple classes separated by ascii whitespaces.

View File

@ -2,7 +2,7 @@ use cell::Cellule;
use rand::Rng;
use std::time::Duration;
use yew::services::interval::{IntervalService, IntervalTask};
use yew::{html, Component, ComponentLink, Html, ShouldRender};
use yew::{classes, html, Component, ComponentLink, Html, ShouldRender};
mod cell;
@ -96,7 +96,7 @@ impl Model {
}
};
html! {
<div key=idx class=("game-cellule", cellule_status)
<div key=idx class=classes!("game-cellule", cellule_status)
onclick=self.link.callback(move |_| Msg::ToggleCellule(idx))>
</div>
}

View File

@ -103,7 +103,7 @@ impl Component for List {
let onmouseout = self.props.on_hover.reform(|_| Hovered::None);
html! {
<div class="list-container" onmouseout=onmouseout onmouseover=onmouseover>
<div class=("list", inactive)>
<div class=classes!("list", inactive)>
{ self.view_header() }
<div class="items">
{ self.view_items() }

View File

@ -53,7 +53,7 @@ impl Pagination {
html! {
<li>
<a class=("pagination-link", is_current_class) aria-label=format!("Goto page {}", to_page) onclick=onclick>
<a class=classes!("pagination-link", is_current_class) aria-label=format!("Goto page {}", to_page) onclick=onclick>
{ to_page }
</a>
</li>

View File

@ -87,7 +87,7 @@ impl Model {
<h1 class="navbar-item is-size-3">{ "Yew Blog" }</h1>
<a role="button"
class=("navbar-burger", "burger", active_class)
class=classes!("navbar-burger", "burger", active_class)
aria-label="menu" aria-expanded="false"
onclick=link.callback(|_| Msg::ToggleNavbar)
>
@ -96,7 +96,7 @@ impl Model {
<span aria-hidden="true"></span>
</a>
</div>
<div class=("navbar-menu", active_class)>
<div class=classes!("navbar-menu", active_class)>
<div class="navbar-start">
<AppAnchor classes="navbar-item" route=AppRoute::Home>
{ "Home" }

View File

@ -3,8 +3,8 @@ use strum::IntoEnumIterator;
use yew::format::Json;
use yew::services::storage::{Area, StorageService};
use yew::web_sys::HtmlInputElement as InputElement;
use yew::{classes, html, Component, ComponentLink, Html, InputData, NodeRef, ShouldRender};
use yew::{events::KeyboardEvent, Classes};
use yew::{html, Component, ComponentLink, Html, InputData, NodeRef, ShouldRender};
mod state;
@ -134,7 +134,7 @@ impl Component for Model {
<h1>{ "todos" }</h1>
{ self.view_input() }
</header>
<section class=("main", hidden_class)>
<section class=classes!("main", hidden_class)>
<input
type="checkbox"
class="toggle-all"
@ -147,7 +147,7 @@ impl Component for Model {
{ for self.state.entries.iter().filter(|e| self.state.filter.fits(e)).enumerate().map(|e| self.view_entry(e)) }
</ul>
</section>
<footer class=("footer", hidden_class)>
<footer class=classes!("footer", hidden_class)>
<span class="todo-count">
<strong>{ self.state.total() }</strong>
{ " item(s) left" }

View File

@ -41,9 +41,12 @@
"title": "Pre-defined hooks"
},
"concepts/html": {
"title": "html!",
"title": "HTML",
"sidebar_label": "Introduction"
},
"concepts/html/classes": {
"title": "Classes"
},
"concepts/html/components": {
"title": "Components"
},

View File

@ -31,11 +31,12 @@
},
{
"type": "subcategory",
"label": "The HTML macro",
"label": "HTML",
"ids": [
"concepts/html",
"concepts/html/components",
"concepts/html/elements",
"concepts/html/classes",
"concepts/html/lists",
"concepts/html/literals-and-expressions"
]

View File

@ -0,0 +1,71 @@
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<ClassExpr, Token![,]>);
impl Parse for Classes {
fn parse(input: ParseStream) -> syn::Result<Self> {
input.parse_terminated(ClassExpr::parse).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<Expr>),
}
impl Parse for ClassExpr {
fn parse(input: ParseStream) -> syn::Result<Self> {
match input.parse()? {
Expr::Lit(ExprLit {
lit: Lit::Str(lit_str),
..
}) => {
let value = lit_str.value();
let classes = value.split_whitespace().collect::<Vec<_>>();
if classes.len() > 1 {
let fix = classes
.into_iter()
.map(|class| format!("\"{}\"", class))
.collect::<Vec<_>>()
.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))),
}
}
}

View File

@ -234,12 +234,28 @@ impl ToTokens for HtmlElement {
let push_classes = match classes {
Some(ClassesForm::Tuple(classes)) => {
let span = classes.span();
let classes: Vec<_> = classes.elems.iter().collect();
let n = classes.len();
let sr = stringify::stringify_at_runtime(quote! { __yew_classes });
let deprecation_warning = quote_spanned! {span=>
#[deprecated(
note = "the use of `(...)` with the attribute `class` is deprecated and will be removed in version 0.19. Use the `classes!` macro instead."
)]
fn deprecated_use_of_class() {}
if false {
deprecated_use_of_class();
};
};
Some(quote! {
let mut __yew_classes = ::yew::virtual_dom::Classes::with_capacity(#n);
let mut __yew_classes = ::yew::html::Classes::with_capacity(#n);
#(__yew_classes.push(#classes);)*
#deprecation_warning
if !__yew_classes.is_empty() {
#vtag.__macro_push_attribute("class", #sr);
} else {
@ -261,7 +277,7 @@ impl ToTokens for HtmlElement {
None => {
let sr = stringify::stringify_at_runtime(quote! { __yew_classes });
Some(quote! {
let __yew_classes = ::std::convert::Into::<::yew::virtual_dom::Classes>::into(#classes);
let __yew_classes = ::std::convert::Into::<::yew::html::Classes>::into(#classes);
if !__yew_classes.is_empty() {
#vtag.__macro_push_attribute("class", #sr);
} else {

View File

@ -55,6 +55,7 @@
//!
//! Please refer to [https://github.com/yewstack/yew](https://github.com/yewstack/yew) for how to set this up.
mod classes;
mod derive_props;
mod html_tree;
mod props;
@ -119,3 +120,9 @@ pub fn props(input: TokenStream) -> TokenStream {
let props = parse_macro_input!(input as props::PropsMacroInput);
TokenStream::from(props.into_token_stream())
}
#[proc_macro]
pub fn classes(input: TokenStream) -> TokenStream {
let classes = parse_macro_input!(input as classes::Classes);
TokenStream::from(classes.into_token_stream())
}

View File

@ -5,13 +5,13 @@ use syn::parse::{Parse, ParseStream};
use syn::{Expr, ExprTuple};
pub enum ClassesForm {
Tuple(Vec<Expr>),
Tuple(ExprTuple),
Single(Box<Expr>),
}
impl ClassesForm {
fn from_expr(expr: Expr) -> Self {
match expr {
Expr::Tuple(ExprTuple { elems, .. }) => ClassesForm::Tuple(elems.into_iter().collect()),
Expr::Tuple(expr_tuple) => ClassesForm::Tuple(expr_tuple),
expr => ClassesForm::Single(Box::new(expr)),
}
}

View File

@ -0,0 +1,21 @@
use yew::prelude::*;
fn compile_pass() {
classes!(42);
classes!(42.0);
classes!("one" "two");
classes!(vec![42]);
let some = Some(42);
let none: Option<u32> = None;
classes!(some);
classes!(none);
classes!("one", 42);
classes!("one", "two three", "four");
}
fn main() {}

View File

@ -0,0 +1,101 @@
error: expected `,`
--> $DIR/classes-fail.rs:7:20
|
7 | classes!("one" "two");
| ^^^^^
error: string literals must not contain more than one class (hint: use `"two", "three"`)
--> $DIR/classes-fail.rs:18:21
|
18 | classes!("one", "two three", "four");
| ^^^^^^^^^^^
error[E0277]: the trait bound `yew::html::classes::Classes: std::convert::From<{integer}>` is not satisfied
--> $DIR/classes-fail.rs:4:14
|
4 | classes!(42);
| ^^ the trait `std::convert::From<{integer}>` is not implemented for `yew::html::classes::Classes`
|
= help: the following implementations were found:
<yew::html::classes::Classes as std::convert::From<&'static str>>
<yew::html::classes::Classes as std::convert::From<&[T]>>
<yew::html::classes::Classes as std::convert::From<&std::option::Option<T>>>
<yew::html::classes::Classes as std::convert::From<&std::string::String>>
and 4 others
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::classes::Classes>` for `{integer}`
error[E0277]: the trait bound `yew::html::classes::Classes: std::convert::From<{float}>` is not satisfied
--> $DIR/classes-fail.rs:5:14
|
5 | classes!(42.0);
| ^^^^ the trait `std::convert::From<{float}>` is not implemented for `yew::html::classes::Classes`
|
= help: the following implementations were found:
<yew::html::classes::Classes as std::convert::From<&'static str>>
<yew::html::classes::Classes as std::convert::From<&[T]>>
<yew::html::classes::Classes as std::convert::From<&std::option::Option<T>>>
<yew::html::classes::Classes as std::convert::From<&std::string::String>>
and 4 others
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::classes::Classes>` for `{float}`
error[E0277]: the trait bound `yew::html::classes::Classes: std::convert::From<{integer}>` is not satisfied
--> $DIR/classes-fail.rs:9:14
|
9 | classes!(vec![42]);
| ^^^ the trait `std::convert::From<{integer}>` is not implemented for `yew::html::classes::Classes`
|
= help: the following implementations were found:
<yew::html::classes::Classes as std::convert::From<&'static str>>
<yew::html::classes::Classes as std::convert::From<&[T]>>
<yew::html::classes::Classes as std::convert::From<&std::option::Option<T>>>
<yew::html::classes::Classes as std::convert::From<&std::string::String>>
and 4 others
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::classes::Classes>` for `{integer}`
= note: required because of the requirements on the impl of `std::convert::From<std::vec::Vec<{integer}>>` for `yew::html::classes::Classes`
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::classes::Classes>` for `std::vec::Vec<{integer}>`
error[E0277]: the trait bound `yew::html::classes::Classes: std::convert::From<{integer}>` is not satisfied
--> $DIR/classes-fail.rs:13:14
|
13 | classes!(some);
| ^^^^ the trait `std::convert::From<{integer}>` is not implemented for `yew::html::classes::Classes`
|
= help: the following implementations were found:
<yew::html::classes::Classes as std::convert::From<&'static str>>
<yew::html::classes::Classes as std::convert::From<&[T]>>
<yew::html::classes::Classes as std::convert::From<&std::option::Option<T>>>
<yew::html::classes::Classes as std::convert::From<&std::string::String>>
and 4 others
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::classes::Classes>` for `{integer}`
= note: required because of the requirements on the impl of `std::convert::From<std::option::Option<{integer}>>` for `yew::html::classes::Classes`
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::classes::Classes>` for `std::option::Option<{integer}>`
error[E0277]: the trait bound `yew::html::classes::Classes: std::convert::From<u32>` is not satisfied
--> $DIR/classes-fail.rs:14:14
|
14 | classes!(none);
| ^^^^ the trait `std::convert::From<u32>` is not implemented for `yew::html::classes::Classes`
|
= help: the following implementations were found:
<yew::html::classes::Classes as std::convert::From<&'static str>>
<yew::html::classes::Classes as std::convert::From<&[T]>>
<yew::html::classes::Classes as std::convert::From<&std::option::Option<T>>>
<yew::html::classes::Classes as std::convert::From<&std::string::String>>
and 4 others
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::classes::Classes>` for `u32`
= note: required because of the requirements on the impl of `std::convert::From<std::option::Option<u32>>` for `yew::html::classes::Classes`
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::classes::Classes>` for `std::option::Option<u32>`
error[E0277]: the trait bound `yew::html::classes::Classes: std::convert::From<{integer}>` is not satisfied
--> $DIR/classes-fail.rs:16:21
|
16 | classes!("one", 42);
| ^^ the trait `std::convert::From<{integer}>` is not implemented for `yew::html::classes::Classes`
|
= help: the following implementations were found:
<yew::html::classes::Classes as std::convert::From<&'static str>>
<yew::html::classes::Classes as std::convert::From<&[T]>>
<yew::html::classes::Classes as std::convert::From<&std::option::Option<T>>>
<yew::html::classes::Classes as std::convert::From<&std::string::String>>
and 4 others
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::classes::Classes>` for `{integer}`

View File

@ -0,0 +1,29 @@
#![no_implicit_prelude]
fn compile_pass() {
// multiple literals
::yew::classes!("one", "two");
// single literal
::yew::classes!("one");
// empty
::yew::classes!();
// multiple expressions
::yew::classes!(::std::vec!["one"], ::std::vec!["two"]);
// single expression
::yew::classes!(::std::vec!["one", "two"]);
// optional classes
::yew::classes!(
::std::option::Option::Some("one"),
::std::option::Option::None::<&'static str>,
);
// mixed types
{
use ::std::borrow::ToOwned;
::yew::classes!("one".to_owned(), "two", ::std::vec!["three"]);
}
}
fn main() {}

View File

@ -0,0 +1,7 @@
#[allow(dead_code)]
#[rustversion::attr(stable(1.45), test)]
fn classes_macro() {
let t = trybuild::TestCases::new();
t.pass("tests/classes_macro/*-pass.rs");
t.compile_fail("tests/classes_macro/*-fail.rs");
}

View File

@ -1,6 +1,6 @@
#[allow(dead_code)]
#[rustversion::attr(stable(1.45), test)]
fn tests() {
fn derive_props() {
let t = trybuild::TestCases::new();
t.pass("tests/derive_props/pass.rs");
t.compile_fail("tests/derive_props/fail.rs");

View File

@ -1,5 +1,5 @@
error[E0599]: no function or associated item named `new` found for struct `yew::virtual_dom::vcomp::VChild<Unimplemented>` in the current scope
--> $DIR/html-component-fail-unimplemented.rs:8:14
--> $DIR/html-component-unimplemented-fail.rs:8:14
|
5 | struct Unimplemented;
| --------------------- doesn't satisfy `Unimplemented: yew::html::Component`
@ -11,7 +11,7 @@ error[E0599]: no function or associated item named `new` found for struct `yew::
`Unimplemented: yew::html::Component`
error[E0277]: the trait bound `Unimplemented: yew::html::Component` is not satisfied
--> $DIR/html-component-fail-unimplemented.rs:8:14
--> $DIR/html-component-unimplemented-fail.rs:8:14
|
8 | html! { <Unimplemented /> };
| ^^^^^^^^^^^^^ the trait `yew::html::Component` is not implemented for `Unimplemented`

View File

@ -56,10 +56,11 @@ fn compile_fail() {
html! { <a href?="href" /> };
html! { <a href?=Some(NotToString) /> };
html! { <input checked?=Some(false) /> };
html! { <input class?=() /> };
html! { <input ref?=() /> };
html! { <input onfocus?=Some(5) /> };
html! { <input onfocus?=Callback::from(|_| ()) /> };
html! { <div class=("deprecated", "warning") /> };
}
fn main() {}

View File

@ -148,18 +148,20 @@ error: `checked` does not support being used as an optional attribute
58 | html! { <input checked?=Some(false) /> };
| ^^^^^^^^^
error: `class` does not support being used as an optional attribute
error: `ref` does not support being used as an optional attribute
--> $DIR/html-element-fail.rs:59:20
|
59 | html! { <input class?=() /> };
| ^^^^^^^
error: `ref` does not support being used as an optional attribute
--> $DIR/html-element-fail.rs:60:20
|
60 | html! { <input ref?=() /> };
59 | html! { <input ref?=() /> };
| ^^^^^
warning: use of deprecated item 'compile_fail::deprecated_use_of_class': the use of `(...)` with the attribute `class` is deprecated and will be removed in version 0.19. Use the `classes!` macro instead.
--> $DIR/html-element-fail.rs:63:24
|
63 | html! { <div class=("deprecated", "warning") /> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(deprecated)]` on by default
error[E0308]: mismatched types
--> $DIR/html-element-fail.rs:25:28
|
@ -353,18 +355,18 @@ error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
= note: required by `std::string::ToString::to_string`
error[E0308]: mismatched types
--> $DIR/html-element-fail.rs:61:20
--> $DIR/html-element-fail.rs:60:20
|
61 | html! { <input onfocus?=Some(5) /> };
60 | html! { <input onfocus?=Some(5) /> };
| ^^^^^^^ expected enum `yew::callback::Callback`, found integer
|
= note: expected enum `yew::callback::Callback<web_sys::features::gen_FocusEvent::FocusEvent>`
found type `{integer}`
error[E0308]: mismatched types
--> $DIR/html-element-fail.rs:62:29
--> $DIR/html-element-fail.rs:61:29
|
62 | html! { <input onfocus?=Callback::from(|_| ()) /> };
61 | html! { <input onfocus?=Callback::from(|_| ()) /> };
| ^^^^^^^^^^^^^^^^^^^^^^
| |
| expected enum `std::option::Option`, found enum `yew::callback::Callback`

View File

@ -37,7 +37,7 @@ fn compile_pass() {
</filter>
</defs>
</svg>
<img class=("avatar", "hidden") src="http://pic.com" />
<img class=classes!("avatar", "hidden") src="http://pic.com" />
<img class="avatar hidden" />
<button onclick=&onclick onclick=onclick />
<a href="http://google.com" />

View File

@ -0,0 +1,30 @@
use yew::html;
#[allow(dead_code)]
#[rustversion::attr(stable(1.45), test)]
fn html_macro() {
let t = trybuild::TestCases::new();
t.pass("tests/html_macro/*-pass.rs");
t.compile_fail("tests/html_macro/*-fail.rs");
}
#[test]
#[should_panic(
expected = "a dynamic tag tried to create a `<br>` tag with children. `<br>` is a void element which can't have any children."
)]
fn dynamic_tags_catch_void_elements() {
html! {
<@{"br"}>
<span>{ "No children allowed" }</span>
</@>
};
}
#[test]
#[should_panic(expected = "a dynamic tag returned a tag name containing non ASCII characters: `❤`")]
fn dynamic_tags_catch_non_ascii() {
html! {
<@{""}/>
};
}

View File

@ -1,46 +0,0 @@
use yew::html;
#[allow(dead_code)]
#[rustversion::attr(stable(1.45), test)]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/macro/html-block-pass.rs");
t.compile_fail("tests/macro/html-block-fail.rs");
t.pass("tests/macro/html-component-pass.rs");
t.compile_fail("tests/macro/html-component-fail.rs");
t.compile_fail("tests/macro/html-component-fail-unimplemented.rs");
t.pass("tests/macro/html-iterable-pass.rs");
t.compile_fail("tests/macro/html-iterable-fail.rs");
t.pass("tests/macro/html-list-pass.rs");
t.compile_fail("tests/macro/html-list-fail.rs");
t.pass("tests/macro/html-node-pass.rs");
t.compile_fail("tests/macro/html-node-fail.rs");
t.pass("tests/macro/html-element-pass.rs");
t.compile_fail("tests/macro/html-element-fail.rs");
}
#[test]
#[should_panic(
expected = "a dynamic tag tried to create a `<br>` tag with children. `<br>` is a void element which can't have any children."
)]
fn dynamic_tags_catch_void_elements() {
html! {
<@{"br"}>
<span>{ "No children allowed" }</span>
</@>
};
}
#[test]
#[should_panic(expected = "a dynamic tag returned a tag name containing non ASCII characters: `❤`")]
fn dynamic_tags_catch_non_ascii() {
html! {
<@{""}/>
};
}

View File

@ -1,6 +1,6 @@
#[allow(dead_code)]
#[rustversion::attr(stable(1.45), test)]
fn tests() {
fn props_macro() {
let t = trybuild::TestCases::new();
t.pass("tests/props_macro/*-pass.rs");
t.compile_fail("tests/props_macro/*-fail.rs");

252
yew/src/html/classes.rs Normal file
View File

@ -0,0 +1,252 @@
use indexmap::IndexSet;
use std::{
borrow::{Borrow, Cow},
iter::FromIterator,
};
/// A set of classes.
///
/// The preferred way of creating this is using the [`classes!`][yew::classes!] macro.
#[derive(Debug, Clone, Default)]
pub struct Classes {
set: IndexSet<Cow<'static, str>>,
}
impl Classes {
/// Creates an empty set of classes. (Does not allocate.)
pub fn new() -> Self {
Self {
set: IndexSet::new(),
}
}
/// Creates an empty set of classes with capacity for n elements. (Does not allocate if n is
/// zero.)
pub fn with_capacity(n: usize) -> Self {
Self {
set: IndexSet::with_capacity(n),
}
}
/// Adds a class to a set.
///
/// If the provided class has already been added, this method will ignore it.
pub fn push<T: Into<Self>>(&mut self, class: T) {
let classes_to_add: Self = class.into();
self.set.extend(classes_to_add.set);
}
/// Adds a class to a set.
///
/// If the provided class has already been added, this method will ignore it.
///
/// This method won't check if there are multiple classes in the input string.
///
/// # Safety
///
/// This function will not split the string into multiple classes. Please do not use it unless
/// you are absolutely certain that the string does not contain any whitespace. Using `push()`
/// is preferred.
pub unsafe fn unchecked_push<T: Into<Cow<'static, str>>>(&mut self, class: T) {
self.set.insert(class.into());
}
/// Check the set contains a class.
pub fn contains<T: AsRef<str>>(&self, class: T) -> bool {
self.set.contains(class.as_ref())
}
/// Check the set is empty.
pub fn is_empty(&self) -> bool {
self.set.is_empty()
}
}
impl<T: Into<Classes>> Extend<T> for Classes {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
let classes = iter
.into_iter()
.map(Into::into)
.flat_map(|classes| classes.set);
self.set.extend(classes);
}
}
impl<T: Into<Classes>> FromIterator<T> for Classes {
fn from_iter<IT: IntoIterator<Item = T>>(iter: IT) -> Self {
let mut classes = Self::new();
classes.extend(iter);
classes
}
}
impl IntoIterator for Classes {
type Item = Cow<'static, str>;
type IntoIter = indexmap::set::IntoIter<Cow<'static, str>>;
fn into_iter(self) -> Self::IntoIter {
self.set.into_iter()
}
}
impl ToString for Classes {
fn to_string(&self) -> String {
self.set
.iter()
.map(Borrow::borrow)
.collect::<Vec<_>>()
.join(" ")
}
}
impl From<Cow<'static, str>> for Classes {
fn from(t: Cow<'static, str>) -> Self {
match t {
Cow::Borrowed(x) => Self::from(x),
Cow::Owned(x) => Self::from(x),
}
}
}
impl From<&'static str> for Classes {
fn from(t: &'static str) -> Self {
let set = t.split_whitespace().map(Cow::Borrowed).collect();
Self { set }
}
}
impl From<String> for Classes {
fn from(t: String) -> Self {
Self::from(&t)
}
}
impl From<&String> for Classes {
fn from(t: &String) -> Self {
let set = t
.split_whitespace()
.map(ToOwned::to_owned)
.map(Cow::Owned)
.collect();
Self { set }
}
}
impl<T: Into<Classes>> From<Option<T>> for Classes {
fn from(t: Option<T>) -> Self {
t.map(|x| x.into()).unwrap_or_default()
}
}
impl<T: Into<Classes> + Clone> From<&Option<T>> for Classes {
fn from(t: &Option<T>) -> Self {
Self::from(t.clone())
}
}
impl<T: Into<Classes>> From<Vec<T>> for Classes {
fn from(t: Vec<T>) -> Self {
Self::from_iter(t)
}
}
impl<T: Into<Classes> + Clone> From<&[T]> for Classes {
fn from(t: &[T]) -> Self {
Self::from_iter(t.iter().cloned())
}
}
impl PartialEq for Classes {
fn eq(&self, other: &Self) -> bool {
self.set.len() == other.set.len() && self.set.iter().eq(other.set.iter())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestClass;
impl TestClass {
fn as_class(&self) -> &'static str {
"test-class"
}
}
impl From<TestClass> for Classes {
fn from(x: TestClass) -> Self {
Classes::from(x.as_class())
}
}
#[test]
fn it_is_initially_empty() {
let subject = Classes::new();
assert!(subject.is_empty());
}
#[test]
fn it_pushes_value() {
let mut subject = Classes::new();
subject.push("foo");
assert!(!subject.is_empty());
assert!(subject.contains("foo"));
}
#[test]
fn it_adds_values_via_extend() {
let mut other = Classes::new();
other.push("bar");
let mut subject = Classes::new();
subject.extend(other);
assert!(subject.contains("bar"));
}
#[test]
fn it_contains_both_values() {
let mut other = Classes::new();
other.push("bar");
let mut subject = Classes::new();
subject.extend(other);
subject.push("foo");
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
#[test]
fn it_splits_class_with_spaces() {
let mut subject = Classes::new();
subject.push("foo bar");
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
#[test]
fn push_and_contains_can_be_used_with_other_objects() {
let mut subject = Classes::new();
subject.push(TestClass);
let other_class: Option<TestClass> = None;
subject.push(other_class);
assert!(subject.contains(TestClass.as_class()));
}
#[test]
fn can_be_extended_with_another_class() {
let mut other = Classes::new();
other.push("foo");
other.push("bar");
let mut subject = Classes::new();
subject.extend(other);
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
#[test]
fn can_be_collected() {
let classes = vec!["foo", "bar"];
let subject = classes.into_iter().collect::<Classes>();
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
}

View File

@ -3,9 +3,11 @@
//! Also this module contains declaration of `Component` trait which used
//! to create own UI-components.
mod classes;
mod listener;
mod scope;
pub use classes::*;
pub use listener::*;
pub use scope::{AnyScope, Scope, SendAsMessage};
pub(crate) use scope::{ComponentUpdate, Scoped};

View File

@ -98,6 +98,28 @@
#![recursion_limit = "512"]
extern crate self as yew;
/// This macro provides a convenient way to create [`Classes`].
///
/// The macro takes a list of items similar to the [`vec!`] macro and returns a [`Classes`] instance.
/// Each item can be of any type that implements `Into<Classes>` (See the implementations on [`Classes`] to learn what types can be used).
///
/// # Example
///
/// ```
/// # use yew::prelude::*;
/// # fn test() {
/// let conditional_class = Some("my-other-class");
/// let vec_of_classes = vec!["one-bean", "two-beans", "three-beans", "a-very-small-casserole"];
///
/// html! {
/// <div class=classes!("my-container-class", conditional_class, vec_of_classes)>
/// // ...
/// </div>
/// };
/// # }
/// ```
pub use yew_macro::classes;
/// This macro implements JSX-like templates.
///
/// This macro always returns [`Html`].
@ -252,6 +274,7 @@ pub use yew_macro::props;
/// This module contains macros which implements html! macro and JSX-like templates
pub mod macros {
pub use crate::classes;
pub use crate::html;
pub use crate::html_nested;
pub use crate::props;
@ -354,11 +377,10 @@ pub mod prelude {
pub use crate::callback::Callback;
pub use crate::events::*;
pub use crate::html::{
Children, ChildrenWithProps, Component, ComponentLink, Html, NodeRef, Properties,
Children, ChildrenWithProps, Classes, Component, ComponentLink, Html, NodeRef, Properties,
ShouldRender,
};
pub use crate::macros::{html, html_nested};
pub use crate::virtual_dom::Classes;
pub use crate::macros::{classes, html, html_nested};
/// Prelude module for creating worker.
#[cfg(feature = "agent")]

View File

View File

@ -15,16 +15,8 @@ pub mod vtext;
use crate::html::{AnyScope, NodeRef};
use cfg_if::cfg_if;
use indexmap::{IndexMap, IndexSet};
use std::{
borrow::{Borrow, Cow},
collections::HashMap,
fmt,
hint::unreachable_unchecked,
iter::{self, FromIterator},
mem,
rc::Rc,
};
use indexmap::IndexMap;
use std::{borrow::Cow, collections::HashMap, fmt, hint::unreachable_unchecked, iter, mem, rc::Rc};
cfg_if! {
if #[cfg(feature = "std_web")] {
use crate::html::EventListener;
@ -318,147 +310,6 @@ impl Default for Attributes {
}
}
/// A set of classes.
#[derive(Debug, Clone, Default)]
pub struct Classes {
set: IndexSet<Cow<'static, str>>,
}
impl Classes {
/// Creates an empty set of classes. (Does not allocate.)
pub fn new() -> Self {
Self {
set: IndexSet::new(),
}
}
/// Creates an empty set of classes with capacity for n elements. (Does not allocate if n is
/// zero.)
pub fn with_capacity(n: usize) -> Self {
Self {
set: IndexSet::with_capacity(n),
}
}
/// Adds a class to a set.
///
/// If the provided class has already been added, this method will ignore it.
pub fn push<T: Into<Self>>(&mut self, class: T) {
let classes_to_add: Self = class.into();
self.set.extend(classes_to_add.set);
}
/// Check the set contains a class.
pub fn contains<T: AsRef<str>>(&self, class: T) -> bool {
self.set.contains(class.as_ref())
}
/// Check the set is empty.
pub fn is_empty(&self) -> bool {
self.set.is_empty()
}
}
impl<T: Into<Classes>> Extend<T> for Classes {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
let classes = iter
.into_iter()
.map(Into::into)
.flat_map(|classes| classes.set);
self.set.extend(classes);
}
}
impl<T: Into<Classes>> FromIterator<T> for Classes {
fn from_iter<IT: IntoIterator<Item = T>>(iter: IT) -> Self {
let mut classes = Self::new();
classes.extend(iter);
classes
}
}
impl IntoIterator for Classes {
type Item = Cow<'static, str>;
type IntoIter = indexmap::set::IntoIter<Cow<'static, str>>;
fn into_iter(self) -> Self::IntoIter {
self.set.into_iter()
}
}
impl ToString for Classes {
fn to_string(&self) -> String {
self.set
.iter()
.map(Borrow::borrow)
.collect::<Vec<_>>()
.join(" ")
}
}
impl From<Cow<'static, str>> for Classes {
fn from(t: Cow<'static, str>) -> Self {
match t {
Cow::Borrowed(x) => Self::from(x),
Cow::Owned(x) => Self::from(x),
}
}
}
impl From<&'static str> for Classes {
fn from(t: &'static str) -> Self {
let set = t.split_whitespace().map(Cow::Borrowed).collect();
Self { set }
}
}
impl From<String> for Classes {
fn from(t: String) -> Self {
Self::from(&t)
}
}
impl From<&String> for Classes {
fn from(t: &String) -> Self {
let set = t
.split_whitespace()
.map(ToOwned::to_owned)
.map(Cow::Owned)
.collect();
Self { set }
}
}
impl<T: Into<Classes>> From<Option<T>> for Classes {
fn from(t: Option<T>) -> Self {
t.map(|x| x.into()).unwrap_or_default()
}
}
impl<T: Into<Classes> + Clone> From<&Option<T>> for Classes {
fn from(t: &Option<T>) -> Self {
Self::from(t.clone())
}
}
impl<T: Into<Classes>> From<Vec<T>> for Classes {
fn from(t: Vec<T>) -> Self {
Self::from_iter(t)
}
}
impl<T: Into<Classes> + Clone> From<&[T]> for Classes {
fn from(t: &[T]) -> Self {
Self::from_iter(t.iter().cloned())
}
}
impl PartialEq for Classes {
fn eq(&self, other: &Self) -> bool {
self.set.len() == other.set.len() && self.set.iter().eq(other.set.iter())
}
}
/// Patch for DOM node modification.
#[derive(Debug, PartialEq)]
enum Patch<ID, T> {
@ -537,87 +388,6 @@ pub trait Transformer<FROM, TO> {
fn transform(from: FROM) -> TO;
}
#[cfg(test)]
mod tests {
use super::*;
struct TestClass;
impl TestClass {
fn as_class(&self) -> &'static str {
"test-class"
}
}
impl From<TestClass> for Classes {
fn from(x: TestClass) -> Self {
Classes::from(x.as_class())
}
}
#[test]
fn it_is_initially_empty() {
let subject = Classes::new();
assert!(subject.is_empty());
}
#[test]
fn it_pushes_value() {
let mut subject = Classes::new();
subject.push("foo");
assert!(!subject.is_empty());
assert!(subject.contains("foo"));
}
#[test]
fn it_adds_values_via_extend() {
let mut other = Classes::new();
other.push("bar");
let mut subject = Classes::new();
subject.extend(other);
assert!(subject.contains("bar"));
}
#[test]
fn it_contains_both_values() {
let mut other = Classes::new();
other.push("bar");
let mut subject = Classes::new();
subject.extend(other);
subject.push("foo");
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
#[test]
fn it_splits_class_with_spaces() {
let mut subject = Classes::new();
subject.push("foo bar");
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
#[test]
fn push_and_contains_can_be_used_with_other_objects() {
let mut subject = Classes::new();
subject.push(TestClass);
let other_class: Option<TestClass> = None;
subject.push(other_class);
assert!(subject.contains(TestClass.as_class()));
}
#[test]
fn can_be_extended_with_another_class() {
let mut other = Classes::new();
other.push("foo");
other.push("bar");
let mut subject = Classes::new();
subject.extend(other);
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
}
// stdweb lacks the `inner_html` method
#[cfg(all(test, feature = "web_sys"))]
mod layout_tests {

View File

@ -680,184 +680,6 @@ mod tests {
assert_eq!(c, d);
}
#[test]
fn classes_from_local_variables() {
let a = html! {
<div class=("class-1", "class-2")></div>
};
let class_2 = "class-2";
let b = html! {
<div class=("class-1", class_2)></div>
};
let class_2_fmt = format!("class-{}", 2);
let c = html! {
<div class=("class-1", class_2_fmt)></div>
};
assert_eq!(a, b);
assert_eq!(a, c);
}
/// Returns the class attribute as str reference, or "" if the attribute is not set.
fn get_class_str(vtag: &VTag) -> &str {
vtag.attributes
.iter()
.find(|(k, _)| k == &"class")
.map(|(_, v)| AsRef::as_ref(v))
.unwrap_or("")
}
/// Note: Compares to "" if the class attribute is not set.
fn assert_class(vnode: VNode, class: &str) {
if let VNode::VTag(ref vtag) = vnode {
assert_eq!(get_class_str(vtag), class);
} else {
panic!("expected VTag");
}
}
#[test]
fn supports_multiple_non_unique_classes_tuple() {
let a = html! {
<div class=("class-1", "class-1 class-2")></div>
};
if let VNode::VTag(vtag) = a {
assert!(get_class_str(&vtag).contains("class-1"));
assert!(get_class_str(&vtag).contains("class-2"));
assert!(!get_class_str(&vtag).contains("class-3"));
} else {
panic!("vtag expected");
}
}
#[test]
fn supports_multiple_classes_string() {
let a = html! {
<div class="class-1 class-2 class-3"></div>
};
let b = html! {
<div class="class-2 class-3 class-1"></div>
};
assert_ne!(a, b);
if let VNode::VTag(vtag) = a {
assert!(get_class_str(&vtag).contains("class-1"));
assert!(get_class_str(&vtag).contains("class-2"));
assert!(get_class_str(&vtag).contains("class-3"));
} else {
panic!("vtag expected");
}
}
#[test]
fn supports_multiple_classes_slice() {
let classes = ["class-1", "class-2"];
let a = html! {
<div class=&classes[..]></div>
};
if let VNode::VTag(vtag) = a {
assert!(get_class_str(&vtag).contains("class-1"));
assert!(get_class_str(&vtag).contains("class-2"));
assert!(!get_class_str(&vtag).contains("class-3"));
} else {
panic!("vtag expected");
}
}
#[test]
fn supports_multiple_classes_one_value_slice() {
let classes = ["class-1 class-2", "class-1"];
let a = html! {
<div class=&classes[..]></div>
};
if let VNode::VTag(vtag) = a {
assert!(get_class_str(&vtag).contains("class-1"));
assert!(get_class_str(&vtag).contains("class-2"));
assert!(!get_class_str(&vtag).contains("class-3"));
} else {
panic!("vtag expected");
}
}
#[test]
fn supports_multiple_classes_vec() {
let mut classes = vec!["class-1"];
classes.push("class-2");
let a = html! {
<div class=classes></div>
};
if let VNode::VTag(vtag) = a {
assert!(get_class_str(&vtag).contains("class-1"));
assert!(get_class_str(&vtag).contains("class-2"));
assert!(!get_class_str(&vtag).contains("class-3"));
} else {
panic!("vtag expected");
}
}
#[test]
fn supports_multiple_classes_one_value_vec() {
let classes = vec!["class-1 class-2", "class-1"];
let a = html! {
<div class=classes></div>
};
if let VNode::VTag(vtag) = a {
assert!(get_class_str(&vtag).contains("class-1"));
assert!(get_class_str(&vtag).contains("class-2"));
assert!(!get_class_str(&vtag).contains("class-3"));
} else {
panic!("vtag expected");
}
}
#[test]
fn filter_empty_string_classes() {
let a = html! { <div class=vec![""]></div> };
let b = html! { <div class=("", "")></div> };
let c = html! { <div class=""></div> };
let d_arr = [""];
let d = html! { <div class=&d_arr[..]></div> };
macro_rules! has_class {
($vtag:expr) => {
$vtag.attributes.iter().any(|(k, _)| k == "class")
};
}
if let VNode::VTag(vtag) = a {
assert!(!has_class!(vtag));
} else {
panic!("vtag expected");
}
if let VNode::VTag(vtag) = b {
assert!(!has_class!(vtag));
} else {
panic!("vtag expected");
}
if let VNode::VTag(vtag) = c {
assert!(!has_class!(vtag));
} else {
panic!("vtag expected");
}
if let VNode::VTag(vtag) = d {
assert!(!vtag.attributes.iter().any(|(k, _)| k == "class"));
} else {
panic!("vtag expected");
}
}
fn assert_vtag(node: &mut VNode) -> &mut VTag {
if let VNode::VTag(vtag) = node {
return vtag;
@ -905,17 +727,6 @@ mod tests {
assert_namespace(g_tag, SVG_NAMESPACE);
}
#[test]
fn keeps_order_of_classes() {
let a = html! {
<div class=vec!["class-1", "class-2", "class-3"]></div>
};
if let VNode::VTag(vtag) = a {
assert_eq!(get_class_str(&vtag), "class-1 class-2 class-3");
}
}
#[test]
fn it_compares_values() {
let a = html! {
@ -1022,23 +833,6 @@ mod tests {
html! { <div><a data-val=Box::<u32>::default() /></div> };
}
#[test]
fn it_does_not_set_empty_class_name() {
let scope = test_scope();
let parent = document().create_element("div").unwrap();
#[cfg(feature = "std_web")]
document().body().unwrap().append_child(&parent);
#[cfg(feature = "web_sys")]
document().body().unwrap().append_child(&parent).unwrap();
let mut elem = html! { <div class=""></div> };
elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = assert_vtag(&mut elem);
// test if the className has not been set
assert!(!vtag.reference.as_ref().unwrap().has_attribute("class"));
}
#[test]
fn it_does_not_set_missing_class_name() {
let scope = test_scope();
@ -1073,157 +867,6 @@ mod tests {
assert!(vtag.reference.as_ref().unwrap().has_attribute("class"));
}
#[test]
fn tuple_different_types() {
// check if tuples containing different types are compiling
assert_class(
html! { <div class=("class-1", "class-2".to_string(), vec!["class-3", "class-4"])></div> },
"class-1 class-2 class-3 class-4",
);
assert_class(
html! { <div class=("class-1", Some("class-2"), "class-3", Some("class-4".to_string()))></div> },
"class-1 class-2 class-3 class-4",
);
// check different string references
let str = "some-class";
let string = str.to_string();
let string_ref = &string;
assert_class(html! { <p class=str /> }, "some-class");
assert_class(html! { <p class=string.clone() /> }, "some-class");
assert_class(html! { <p class=&Some(str) /> }, "some-class");
assert_class(html! { <p class=string_ref /> }, "some-class");
assert_class(html! { <p class=Some(str) /> }, "some-class");
assert_class(html! { <p class=Some(string.clone()) /> }, "some-class");
assert_class(html! { <p class=Some(string_ref) /> }, "some-class");
assert_class(html! { <p class=&Some(string.clone()) /> }, "some-class");
assert_class(html! { <p class=&Some(string_ref) /> }, "some-class");
// check with None
assert_class(html! { <p class=&Option::<&str>::None /> }, "");
assert_class(html! { <p class=Option::<String>::None /> }, "");
// check with variables
let some: Option<&'static str> = Some("some");
let none: Option<&'static str> = None;
assert_class(html! { <p class=some /> }, "some");
assert_class(html! { <p class=none /> }, "");
// check with variables of different type
let some: Option<bool> = Some(false);
let none: Option<bool> = None;
assert_class(html! { <p class=some.map(|i| i.to_string()) /> }, "false");
assert_class(html! { <p class=none.map(|i| i.to_string()) /> }, "");
}
#[test]
fn swap_order_of_classes() {
let scope = test_scope();
let parent = document().create_element("div").unwrap();
#[cfg(feature = "std_web")]
document().body().unwrap().append_child(&parent);
#[cfg(feature = "web_sys")]
document().body().unwrap().append_child(&parent).unwrap();
let mut elem = html! { <div class=("class-1", "class-2", "class-3")></div> };
elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = if let VNode::VTag(vtag) = elem {
vtag
} else {
panic!("should be vtag")
};
let expected = "class-1 class-2 class-3";
assert_eq!(get_class_str(&vtag), expected);
assert_eq!(
vtag.reference
.as_ref()
.unwrap()
.get_attribute("class")
.unwrap(),
expected
);
let ancestor = vtag;
let elem = html! { <div class=("class-3", "class-2", "class-1")></div> };
let mut vtag = if let VNode::VTag(vtag) = elem {
vtag
} else {
panic!("should be vtag")
};
vtag.apply(
&scope,
&parent,
NodeRef::default(),
Some(VNode::VTag(ancestor)),
);
let expected = "class-3 class-2 class-1";
assert_eq!(get_class_str(&vtag), expected);
assert_eq!(
vtag.reference
.as_ref()
.unwrap()
.get_attribute("class")
.unwrap(),
expected
);
}
#[test]
fn add_class_to_the_middle() {
let scope = test_scope();
let parent = document().create_element("div").unwrap();
#[cfg(feature = "std_web")]
document().body().unwrap().append_child(&parent);
#[cfg(feature = "web_sys")]
document().body().unwrap().append_child(&parent).unwrap();
let mut elem = html! { <div class=("class-1", "class-3")></div> };
elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = if let VNode::VTag(vtag) = elem {
vtag
} else {
panic!("should be vtag")
};
let expected = "class-1 class-3";
assert_eq!(get_class_str(&vtag), expected);
assert_eq!(
vtag.reference
.as_ref()
.unwrap()
.get_attribute("class")
.unwrap(),
expected
);
let ancestor = vtag;
let elem = html! { <div class=("class-1", "class-2", "class-3")></div> };
let mut vtag = if let VNode::VTag(vtag) = elem {
vtag
} else {
panic!("should be vtag")
};
vtag.apply(
&scope,
&parent,
NodeRef::default(),
Some(VNode::VTag(ancestor)),
);
let expected = "class-1 class-2 class-3";
assert_eq!(get_class_str(&vtag), expected);
assert_eq!(
vtag.reference
.as_ref()
.unwrap()
.get_attribute("class")
.unwrap(),
expected
);
}
#[test]
fn controlled_input_synced() {
let scope = test_scope();
@ -1423,6 +1066,32 @@ mod tests {
elem.detach(&parent);
assert!(node_ref.get().is_none());
}
/// Returns the class attribute as str reference, or "" if the attribute is not set.
fn get_class_str(vtag: &VTag) -> &str {
vtag.attributes
.iter()
.find(|(k, _)| k == &"class")
.map(|(_, v)| AsRef::as_ref(v))
.unwrap_or("")
}
#[test]
fn old_class_syntax_is_still_supported() {
let a_classes = "class-1 class-2".to_string();
#[allow(deprecated)]
let a = html! {
<div class=("class-1", a_classes)></div>
};
if let VNode::VTag(vtag) = a {
assert!(get_class_str(&vtag).contains("class-1"));
assert!(get_class_str(&vtag).contains("class-2"));
assert!(!get_class_str(&vtag).contains("class-3"));
} else {
panic!("vtag expected");
}
}
}
#[cfg(all(test, feature = "web_sys"))]