mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
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:
parent
6ed5365be5
commit
9737fe77db
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: html!
|
||||
title: HTML
|
||||
sidebar_label: Introduction
|
||||
description: The procedural macro for generating HTML and SVG
|
||||
---
|
||||
|
||||
122
docs/concepts/html/classes.md
Normal file
122
docs/concepts/html/classes.md
Normal 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.
|
||||
@ -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:
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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() }
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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"
|
||||
]
|
||||
|
||||
71
yew-macro/src/classes/mod.rs
Normal file
71
yew-macro/src/classes/mod.rs
Normal 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))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
21
yew-macro/tests/classes_macro/classes-fail.rs
Normal file
21
yew-macro/tests/classes_macro/classes-fail.rs
Normal 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() {}
|
||||
101
yew-macro/tests/classes_macro/classes-fail.stderr
Normal file
101
yew-macro/tests/classes_macro/classes-fail.stderr
Normal 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}`
|
||||
29
yew-macro/tests/classes_macro/classes-pass.rs
Normal file
29
yew-macro/tests/classes_macro/classes-pass.rs
Normal 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() {}
|
||||
7
yew-macro/tests/classes_macro_test.rs
Normal file
7
yew-macro/tests/classes_macro_test.rs
Normal 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");
|
||||
}
|
||||
@ -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");
|
||||
|
||||
@ -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`
|
||||
@ -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() {}
|
||||
@ -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`
|
||||
@ -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" />
|
||||
30
yew-macro/tests/html_macro_test.rs
Normal file
30
yew-macro/tests/html_macro_test.rs
Normal 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! {
|
||||
<@{"❤"}/>
|
||||
};
|
||||
}
|
||||
@ -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! {
|
||||
<@{"❤"}/>
|
||||
};
|
||||
}
|
||||
@ -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
252
yew/src/html/classes.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
@ -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};
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"))]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user