mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Add a macro for building properties outside of html! (#1599)
* remove renamed imports from yew-macro They add a lot of cognitive overhead and don't provide much benefit in this case. * just a prototype * cleanup * add prop type resolver * use new props for tags * silence clippy * simplify tag parsing * clean up * improve names * fix list span * new component props parsing * fix rogue lint * update tag attribute parsing * unify prop handling * add new tests * integrate prop validation * improve error span regression * add docstring * update tests * add test for specifying `children` twice * move properties derive macro * component transformer documentation * update properties documentation * document special properties * let's try to fix the spellcheck * let's just use a newer image then * document `with props` children * clean up a tad * is boolean the missing word? Starting to question the use of this spell checker... * add the note for the recursion limit back in * code review * improve error for duplicate children * clippyfying * revert Task: Drop * HtmlTag -> HtmlElement * link the issue for prop_or_else * PropList -> SortedPropList * use struct syntax * use html! in transformer demonstration
This commit is contained in:
parent
724ac1d481
commit
fa2ab5abe7
7
.github/workflows/pull-request.yml
vendored
7
.github/workflows/pull-request.yml
vendored
@ -82,7 +82,8 @@ jobs:
|
||||
|
||||
doc_tests:
|
||||
name: Documentation Tests
|
||||
runs-on: ubuntu-latest
|
||||
# Using 20.04 because 18.04 (latest) only has aspell 0.60.7 and we need 0.60.8
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
@ -103,7 +104,9 @@ jobs:
|
||||
cargo-${{ runner.os }}-
|
||||
|
||||
- name: Check spelling
|
||||
run: bash ci/spellcheck.sh list
|
||||
run: |
|
||||
sudo apt-get install aspell
|
||||
ci/spellcheck.sh list
|
||||
|
||||
- name: Run doctest
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
@ -1,20 +1,11 @@
|
||||
personal_ws-1.1 en 88 utf-8
|
||||
Angular's
|
||||
Config
|
||||
Deref
|
||||
Github
|
||||
Json
|
||||
Lifecycle
|
||||
React's
|
||||
Todo
|
||||
VecDeque
|
||||
Webpack
|
||||
personal_ws-1.1 en 0 utf-8
|
||||
alloc
|
||||
ALLOC
|
||||
allocator
|
||||
Angular's
|
||||
asmjs
|
||||
backends
|
||||
barebones
|
||||
binaryen
|
||||
Binaryen
|
||||
bindgen
|
||||
bool
|
||||
boolean
|
||||
@ -23,47 +14,58 @@ charset
|
||||
codebase
|
||||
codegen
|
||||
composable
|
||||
Config
|
||||
declaratively
|
||||
doctype
|
||||
defs
|
||||
DerefMut
|
||||
DOCTYPE
|
||||
DOCUSAURUS
|
||||
emscripten
|
||||
Emscripten
|
||||
enum
|
||||
enums
|
||||
Github
|
||||
href
|
||||
html
|
||||
Html
|
||||
IHtmlElement
|
||||
impl
|
||||
init
|
||||
INIT
|
||||
interop
|
||||
interoperability
|
||||
interoperable
|
||||
lang
|
||||
libs
|
||||
lifecycle
|
||||
Lifecycle
|
||||
linecap
|
||||
linejoin
|
||||
memoized
|
||||
metaprogramming
|
||||
minimize
|
||||
miniserve
|
||||
mkdir
|
||||
natively
|
||||
onclick
|
||||
proc
|
||||
React's
|
||||
README
|
||||
rlib
|
||||
roadmap
|
||||
Roadmap
|
||||
rollup
|
||||
rustc
|
||||
rustfmt
|
||||
rustwasm
|
||||
stacktraces
|
||||
rustup
|
||||
stdweb
|
||||
struct
|
||||
structs
|
||||
tbody
|
||||
textarea
|
||||
thead
|
||||
TODO
|
||||
toml
|
||||
Unselected
|
||||
usize
|
||||
vdom
|
||||
vtag
|
||||
VecDeque
|
||||
Vuetify
|
||||
wasm
|
||||
Wasm
|
||||
webpack
|
||||
Webpack
|
||||
WeeAlloc
|
||||
workspaces
|
||||
vuetify
|
||||
xmlns
|
||||
yewtil
|
||||
Yewtil
|
||||
|
||||
@ -60,8 +60,8 @@ if [[ ! -f "$dict_filename" ]]; then
|
||||
echo "Scanning files to generate dictionary file '$dict_filename'."
|
||||
echo "Please check that it doesn't contain any misspellings."
|
||||
|
||||
echo "personal_ws-1.1 en 0 utf-8" > "$dict_filename"
|
||||
cat "${markdown_sources[@]}" | aspell --ignore 3 list | sort -u >> "$dict_filename"
|
||||
echo "personal_ws-1.1 en 0 utf-8" >"$dict_filename"
|
||||
cat "${markdown_sources[@]}" | aspell --ignore 3 --camel-case list | sort -u >>"$dict_filename"
|
||||
elif [[ "$mode" == "list" ]]; then
|
||||
# List (default) mode: scan all files, report errors.
|
||||
declare -i retval=0
|
||||
@ -74,7 +74,7 @@ elif [[ "$mode" == "list" ]]; then
|
||||
fi
|
||||
|
||||
for fname in "${markdown_sources[@]}"; do
|
||||
command=$(aspell --ignore 3 --camel-case --personal="$dict_path" "$mode" < "$fname")
|
||||
command=$(aspell --ignore 3 --camel-case --personal="$dict_path" "$mode" <"$fname")
|
||||
if [[ -n "$command" ]]; then
|
||||
for error in $command; do
|
||||
# FIXME: find more correct way to get line number
|
||||
@ -96,6 +96,6 @@ elif [[ "$mode" == "check" ]]; then
|
||||
fi
|
||||
|
||||
for fname in "${markdown_sources[@]}"; do
|
||||
aspell --ignore 3 --dont-backup --personal="$dict_path" "$mode" "$fname"
|
||||
aspell --ignore 3 --camel-case --dont-backup --personal="$dict_path" "$mode" "$fname"
|
||||
done
|
||||
fi
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
title: Introduction
|
||||
description: Components and their lifecycle hooks
|
||||
---
|
||||
|
||||
## What are Components?
|
||||
|
||||
Components are the building blocks of Yew. They manage their own state and can render themselves to the DOM. Components are created by implementing the `Component` trait for a type. The `Component`
|
||||
@ -20,7 +21,7 @@ in the lifecycle of a component.
|
||||
|
||||
When a component is created, it receives properties from its parent component as well as a `ComponentLink`. The properties can be used to initialize the component's state and the "link" can be used to register callbacks or send messages to the component.
|
||||
|
||||
It is common to store the props (data which can be passed from parent to child components) and the
|
||||
It is common to store the props (data which can be passed from parent to child components) and the
|
||||
`ComponentLink` in your component struct, like so:
|
||||
|
||||
```rust
|
||||
@ -43,10 +44,10 @@ impl Component for MyComponent {
|
||||
|
||||
### View
|
||||
|
||||
The `view` method allows you to describe how a component should be rendered to the DOM. Writing
|
||||
HTML-like code using Rust functions can become quite messy, so Yew provides a macro called `html!`
|
||||
for declaring HTML and SVG nodes (as well as attaching attributes and event listeners to them) and a
|
||||
convenient way to render child components. The macro is somewhat similar to React's JSX (the
|
||||
The `view` method allows you to describe how a component should be rendered to the DOM. Writing
|
||||
HTML-like code using Rust functions can become quite messy, so Yew provides a macro called `html!`
|
||||
for declaring HTML and SVG nodes (as well as attaching attributes and event listeners to them) and a
|
||||
convenient way to render child components. The macro is somewhat similar to React's JSX (the
|
||||
differences in programming language aside).
|
||||
|
||||
```rust
|
||||
@ -66,9 +67,9 @@ For usage details, check out [the `html!` guide](html.md).
|
||||
|
||||
### Rendered
|
||||
|
||||
The `rendered` component lifecycle method is called once `view` has been called and Yew has rendered
|
||||
The `rendered` component lifecycle method is called once `view` has been called and Yew has rendered
|
||||
the results to the DOM, but before the browser refreshes the page. This method is useful when you
|
||||
want to perform actions that can only be completed after the component has rendered elements. There
|
||||
want to perform actions that can only be completed after the component has rendered elements. There
|
||||
is also a parameter called `first_render` which can be used to determine whether this function is
|
||||
being called on the first render, or instead a subsequent one.
|
||||
|
||||
@ -107,8 +108,8 @@ Note that this lifecycle method does not require an implementation and will do n
|
||||
### Update
|
||||
|
||||
Communication with components happens primarily through messages which are handled by the
|
||||
`update` lifecycle method. This allows the component to update itself
|
||||
based on what the message was, and determine if it needs to re-render itself. Messages can be sent
|
||||
`update` lifecycle method. This allows the component to update itself
|
||||
based on what the message was, and determine if it needs to re-render itself. Messages can be sent
|
||||
by event listeners, child components, Agents, Services, or Futures.
|
||||
|
||||
Here's an example of what an implementation of `update` could look like:
|
||||
@ -140,8 +141,8 @@ impl Component for MyComponent {
|
||||
|
||||
### Change
|
||||
|
||||
Components may be re-rendered by their parents. When this happens, they could receive new properties
|
||||
and need to re-render. This design facilitates parent to child component communication by just
|
||||
Components may be re-rendered by their parents. When this happens, they could receive new properties
|
||||
and need to re-render. This design facilitates parent to child component communication by just
|
||||
changing the values of a property.
|
||||
|
||||
A typical implementation would look something like:
|
||||
@ -163,7 +164,7 @@ impl Component for MyComponent {
|
||||
|
||||
### Destroy
|
||||
|
||||
After Components are unmounted from the DOM, Yew calls the `destroy` lifecycle method; this is
|
||||
After Components are unmounted from the DOM, Yew calls the `destroy` lifecycle method; this is
|
||||
necessary if you need to undertake operations to clean up after earlier actions of a component
|
||||
before it is destroyed. This method is optional and does nothing by default.
|
||||
|
||||
@ -180,15 +181,15 @@ impl Component for MyComponent {
|
||||
}
|
||||
```
|
||||
|
||||
The `Message` type is used to send messages to a component after an event has taken place; for
|
||||
example you might want to undertake some action when a user clicks a button or scrolls down the
|
||||
The `Message` type is used to send messages to a component after an event has taken place; for
|
||||
example you might want to undertake some action when a user clicks a button or scrolls down the
|
||||
page. Because components tend to have to respond to more than one event, the `Message` type will
|
||||
normally be an enum, where each variant is an event to be handled.
|
||||
|
||||
When organising your codebase, it is sensible to include the definition of the `Message` type in the
|
||||
same module in which your component is defined. You may find it helpful to adopt a consistent naming
|
||||
convention for message types. One option (though not the only one) is to name the types
|
||||
`ComponentNameMsg`, e.g. if your component was called `Homepage` then you might call the type
|
||||
When organizing your codebase, it is sensible to include the definition of the `Message` type in the
|
||||
same module in which your component is defined. You may find it helpful to adopt a consistent naming
|
||||
convention for message types. One option (though not the only one) is to name the types
|
||||
`ComponentNameMsg`, e.g. if your component was called `Homepage` then you might call the type
|
||||
`HomepageMsg`.
|
||||
|
||||
```rust
|
||||
|
||||
@ -2,23 +2,50 @@
|
||||
title: Properties
|
||||
description: Parent to child communication
|
||||
---
|
||||
|
||||
Properties enable child and parent components to communicate with each other.
|
||||
Every component has an associated properties type which describes what is passed down from the parent.
|
||||
In theory this can be any type that implements the `Properties` trait, but in practice there's no
|
||||
reason for it to be anything but a struct where each field represents a property.
|
||||
|
||||
## Derive macro
|
||||
|
||||
Don't try to implement `Properties` yourself, derive it by using `#[derive(Properties)]` instead.
|
||||
Instead of implementing the `Properties` trait yourself, you should use `#[derive(Properties)]` to
|
||||
automatically generate the implementation instead.
|
||||
Types for which you derive `Properties` must also implement `Clone`.
|
||||
|
||||
:::note
|
||||
Types for which you derive `Properties` must also implement `Clone`. This can be done by either using `#[derive(Properties, Clone)` or manually implementing `Clone` for your type.
|
||||
### Field attributes
|
||||
|
||||
When deriving `Properties`, all fields are required by default.
|
||||
The following attributes allow you to give your props initial values which will be used unless they're set to another value.
|
||||
|
||||
:::tip
|
||||
Attributes aren't visible in Rustdoc generated documentation.
|
||||
The docstrings of your properties should mention whether a prop is optional and if it has a special default value.
|
||||
:::
|
||||
|
||||
### Required attributes
|
||||
#### `#[prop_or_default]`
|
||||
|
||||
The fields within a struct that derives `Properties` are required by default. When a field is missing and the component is created in the `html!` macro, a compiler error is returned. For fields with optional properties, use the `#[prop_or_default]` attribute to use the default value for that type when the prop is not specified. To specify a value, use the `#[prop_or(value)]` attribute where value is the default value for the property or alternatively use `#[prop_or_else(function)]` where `function` returns the default value. For example, to default a boolean value as `true`, use the attribute `#[prop_or(true)]`. It is common for optional properties to use the `Option` enum which has the default value `None`.
|
||||
Initialize the prop value with the default value of the field's type using the `Default` trait.
|
||||
|
||||
### PartialEq
|
||||
#### `#[prop_or(value)]`
|
||||
|
||||
It is likely to make sense to derive `PartialEq` on your props if you can do this. Using `PartialEq` makes it much easier to avoid unnecessary rerendering \(this is explained in the **Optimizations & Best Practices** section\).
|
||||
Use `value` to initialize the prop value. `value` can be any expression that returns the field's type.
|
||||
For example, to default a boolean prop to `true`, use the attribute `#[prop_or(true)]`.
|
||||
|
||||
#### `#[prop_or_else(function)]`
|
||||
|
||||
Call `function` to initialize the prop value. `function` should have the signature `FnMut() -> T` where `T` is the field type.
|
||||
|
||||
:::warning
|
||||
The function is currently called even if the prop is explicitly set. If your function is performance intensive, consider using `Option` where `None` values are initialized in the `create` method.
|
||||
See [#1623](https://github.com/yewstack/yew/issues/1623)
|
||||
:::
|
||||
|
||||
## PartialEq
|
||||
|
||||
It makes sense to derive `PartialEq` on your props if you can do so.
|
||||
Using `PartialEq` makes it much easier to avoid unnecessary rendering \(this is explained in the **Optimizations & Best Practices** section\).
|
||||
|
||||
## Memory/speed overhead of using Properties
|
||||
|
||||
@ -45,11 +72,8 @@ pub enum LinkColor {
|
||||
Purple,
|
||||
}
|
||||
|
||||
impl Default for LinkColor {
|
||||
fn default() -> Self {
|
||||
// The link color will be blue unless otherwise specified.
|
||||
LinkColor::Blue
|
||||
}
|
||||
fn create_default_link_color() -> LinkColor {
|
||||
LinkColor::Blue
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
@ -58,9 +82,9 @@ pub struct LinkProps {
|
||||
href: String,
|
||||
/// If the link text is huge, this will make copying the string much cheaper.
|
||||
/// This isn't usually recommended unless performance is known to be a problem.
|
||||
text: Rc<String>,
|
||||
/// Color of the link.
|
||||
#[prop_or_default]
|
||||
text: Rc<str>,
|
||||
/// Color of the link. Defaults to `Blue`.
|
||||
#[prop_or_else(create_default_link_color)]
|
||||
color: LinkColor,
|
||||
/// The view function will not specify a size if this is None.
|
||||
#[prop_or_default]
|
||||
@ -71,3 +95,23 @@ pub struct LinkProps {
|
||||
}
|
||||
```
|
||||
|
||||
## Props macro
|
||||
|
||||
The `yew::props!` macro allows you to build properties the same way the `html!` macro does it.
|
||||
|
||||
The macro uses the same syntax as a struct expression except that you can't use attributes or a base expression (`Foo { ..base }`).
|
||||
The type path can either point to the props directly (`path::to::Props`) or the associated properties of a component (`MyComp::Properties`).
|
||||
|
||||
```rust
|
||||
let props = yew::props!(LinkProps {
|
||||
href: "/",
|
||||
text: Rc::from("imagine this text being really long"),
|
||||
size: 64,
|
||||
});
|
||||
|
||||
// build the associated properties of a component
|
||||
let props = yew::props!(Model::Properties {
|
||||
href: "/book",
|
||||
text: Rc::from("my bestselling novel"),
|
||||
});
|
||||
```
|
||||
|
||||
@ -4,21 +4,126 @@ sidebar_label: Introduction
|
||||
description: The procedural macro for generating HTML and SVG
|
||||
---
|
||||
|
||||
The `html!` macro allows you to write HTML and SVG code declaratively. It is similar to JSX
|
||||
The `html!` macro allows you to write HTML and SVG code declaratively. It is similar to JSX
|
||||
\(an extension to JavaScript which allows you to write HTML-like code inside of JavaScript\).
|
||||
|
||||
**Important notes**
|
||||
|
||||
1. The `html!` macro only accepts one root html node \(you can counteract this by
|
||||
[using fragments or iterators](html/lists.md)\)
|
||||
1. The `html!` macro only accepts one root html node \(you can counteract this by
|
||||
[using fragments or iterators](html/lists.md)\)
|
||||
2. An empty `html! {}` invocation is valid and will not render anything
|
||||
3. Literals must always be quoted and wrapped in braces: `html! { "Hello, World" }`
|
||||
|
||||
:::note
|
||||
The `html!` macro can reach easily the default recursion limit of the compiler. It is advised to
|
||||
bump its value if you encounter compilation errors. Use an attribute like
|
||||
`#![recursion_limit="1024"]` in the crate root \(i.e. either `lib.rs` or `main.rs`\) to overcome the
|
||||
problem.
|
||||
The `html!` macro can reach the default recursion limit of the compiler. If you encounter compilation errors, add an attribute like `#![recursion_limit="1024"]` in the crate root to overcome the problem.
|
||||
:::
|
||||
|
||||
See the [official documentation](https://doc.rust-lang.org/reference/attributes/limits.html#the-recursion_limit-attribute) and [this Stack Overflow question](https://stackoverflow.com/questions/27454761/what-is-a-crate-attribute-and-where-do-i-add-it) for details.
|
||||
:::
|
||||
## Tag Structure
|
||||
|
||||
Tags are based on HTML tags. Components, Elements, and Lists are all based on this tag syntax.
|
||||
|
||||
Tags must either self-close `<... />` or have a corresponding end tag for each start tag.
|
||||
|
||||
<!--DOCUSAURUS_CODE_TABS-->
|
||||
<!--Open - Close-->
|
||||
|
||||
```rust
|
||||
html! {
|
||||
<div id="my_div"></div>
|
||||
}
|
||||
```
|
||||
|
||||
<!--Invalid-->
|
||||
|
||||
```rust
|
||||
html! {
|
||||
<div id="my_div"> // <- MISSING CLOSE TAG
|
||||
}
|
||||
```
|
||||
|
||||
<!--Self-closing-->
|
||||
|
||||
```rust
|
||||
html! {
|
||||
<input id="my_input" />
|
||||
}
|
||||
```
|
||||
|
||||
<!--Invalid-->
|
||||
|
||||
```rust
|
||||
html! {
|
||||
<input id="my_input"> // <- MISSING SELF-CLOSE
|
||||
}
|
||||
```
|
||||
|
||||
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||
|
||||
:::tip
|
||||
For convenience, elements which _usually_ require a closing tag are **allowed** to self-close. For example, writing `html! { <div class="placeholder" /> }` is valid.
|
||||
:::
|
||||
|
||||
## Children
|
||||
|
||||
Create complex nested HTML and SVG layouts with ease:
|
||||
|
||||
<!--DOCUSAURUS_CODE_TABS-->
|
||||
<!--HTML-->
|
||||
|
||||
```rust
|
||||
html! {
|
||||
<div>
|
||||
<div data-key="abc"></div>
|
||||
<div class="parent">
|
||||
<span class="child" value="anything"></span>
|
||||
<label for="first-name">{ "First Name" }</label>
|
||||
<input type="text" id="first-name" value="placeholder" />
|
||||
<input type="checkbox" checked=true />
|
||||
<textarea value="write a story" />
|
||||
<select name="status">
|
||||
<option selected=true disabled=false value="">{ "Selected" }</option>
|
||||
<option selected=false disabled=true value="">{ "Unselected" }</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
<!--SVG-->
|
||||
|
||||
```rust
|
||||
html! {
|
||||
<svg width="149" height="147" viewBox="0 0 149 147" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M60.5776 13.8268L51.8673 42.6431L77.7475 37.331L60.5776 13.8268Z" fill="#DEB819"/>
|
||||
<path d="M108.361 94.9937L138.708 90.686L115.342 69.8642" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<circle cx="75.3326" cy="73.4918" r="55" fill="#FDD630"/>
|
||||
<circle cx="75.3326" cy="73.4918" r="52.5" stroke="black" stroke-width="5"/>
|
||||
</g>
|
||||
<circle cx="71" cy="99" r="5" fill="white" fill-opacity="0.75" stroke="black" stroke-width="3"/>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="16.3326" y="18.4918" width="118" height="118" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
```
|
||||
|
||||
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||
|
||||
## Special properties
|
||||
|
||||
There are special properties which don't directly influence the DOM but instead act as instructions to Yew's virtual DOM.
|
||||
Currently, there are two such special props: `ref` and `key`.
|
||||
|
||||
`ref` allows you to access and manipulate the underlying DOM node directly. See [Refs](components/refs) for more details.
|
||||
|
||||
`key` on the other hand gives an element a unique identifier which Yew can use for optimization purposes.
|
||||
|
||||
:::important
|
||||
The documentation for keys is yet to be written. See [#1263](https://github.com/yewstack/yew/issues/1263).
|
||||
|
||||
For now, use keys when you have a list where the order of elements changes. This includes inserting or removing elements from anywhere but the end of the list.
|
||||
:::
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
title: Components
|
||||
description: Create complex layouts with component hierarchies
|
||||
---
|
||||
|
||||
## Basic
|
||||
|
||||
Any type that implements `Component` can be used in the `html!` macro:
|
||||
@ -27,21 +28,38 @@ Components can be passed children if they have a `children` field in their `Prop
|
||||
|
||||
```rust title="parent.rs"
|
||||
html! {
|
||||
<Container>
|
||||
<Container id="container">
|
||||
<h4>{ "Hi" }</h4>
|
||||
<div>{ "Hello" }</div>
|
||||
</Container>
|
||||
}
|
||||
```
|
||||
|
||||
```rust title="container.rs"
|
||||
pub struct Container(Props);
|
||||
When using the `with props` syntax, the children passed in the `html!` macro overwrite the ones already present in the props.
|
||||
|
||||
```rust
|
||||
let props = yew::props!(Container::Properties {
|
||||
id: "container-2",
|
||||
children: Children::default(),
|
||||
});
|
||||
html! {
|
||||
<Container with props>
|
||||
// props.children will be overwritten with this
|
||||
<span>{ "I am a child, as you can see" }</span>
|
||||
</Container>
|
||||
}
|
||||
```
|
||||
|
||||
Here's the implementation of `Container`:
|
||||
|
||||
```rust
|
||||
#[derive(Properties, Clone)]
|
||||
pub struct Props {
|
||||
pub id: String,
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
pub struct Container(Props);
|
||||
impl Component for Container {
|
||||
type Properties = Props;
|
||||
|
||||
@ -49,7 +67,7 @@ impl Component for Container {
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<div id="container">
|
||||
<div id=&self.0.id>
|
||||
{ self.0.children.clone() }
|
||||
</div>
|
||||
}
|
||||
@ -57,15 +75,11 @@ impl Component for Container {
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
Types for which you derive `Properties` must also implement `Clone`. This can be done by either using `#[derive(Properties, Clone)]` or manually implementing `Clone` for your type.
|
||||
:::
|
||||
|
||||
## Nested Children with Props
|
||||
|
||||
Nested component properties can be accessed and mutated if the containing component types its children. In the following example, the `List` component can wrap `ListItem` components. For a real world example of this pattern, check out the `yew-router` source code. For a more advanced example, check out the `nested-list` example in the main yew repository.
|
||||
|
||||
```rust title="parent.rs"
|
||||
```rust
|
||||
html! {
|
||||
<List>
|
||||
<ListItem value="a" />
|
||||
@ -75,14 +89,13 @@ html! {
|
||||
}
|
||||
```
|
||||
|
||||
```rust title="list.rs"
|
||||
pub struct List(Props);
|
||||
|
||||
```rust
|
||||
#[derive(Properties, Clone)]
|
||||
pub struct Props {
|
||||
pub children: ChildrenWithProps<ListItem>,
|
||||
}
|
||||
|
||||
pub struct List(Props);
|
||||
impl Component for List {
|
||||
type Properties = Props;
|
||||
|
||||
@ -99,3 +112,41 @@ impl Component for List {
|
||||
}
|
||||
```
|
||||
|
||||
## Transformers
|
||||
|
||||
Whenever you set a prop its value goes through a transformation step first.
|
||||
If the value already has the correct type, this step doesn't do anything.
|
||||
However, transformers can be useful to reduce code repetition.
|
||||
|
||||
The following is a list of transformers you should know about:
|
||||
|
||||
- `&T` -> `T`
|
||||
|
||||
Clones the reference to get an owned value.
|
||||
|
||||
- `&str` -> `String`
|
||||
|
||||
Allows you to use string literals without adding `.to_owned()` at the end.
|
||||
|
||||
- `T` -> `Option<T>`
|
||||
|
||||
Wraps the value in `Some`.
|
||||
|
||||
```rust
|
||||
struct Props {
|
||||
unique_id: Option<usize>,
|
||||
text: String,
|
||||
}
|
||||
|
||||
struct Model;
|
||||
impl Component for Model {
|
||||
type Properties = Props;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
// transformers allow you to write this:
|
||||
html! { <Model unique_id=5 text="literals are fun" /> };
|
||||
// instead of:
|
||||
html! { <Model unique_id=Some(5) text="literals are fun".to_owned() /> };
|
||||
```
|
||||
|
||||
@ -2,92 +2,6 @@
|
||||
title: Elements
|
||||
description: Both HTML and SVG elements are supported
|
||||
---
|
||||
## Tag Structure
|
||||
|
||||
Element tags must either self-close `<... />` or have a corresponding close tag for each open tag
|
||||
|
||||
<!--DOCUSAURUS_CODE_TABS-->
|
||||
<!--Open - Close-->
|
||||
```rust
|
||||
html! {
|
||||
<div id="my_div"></div>
|
||||
}
|
||||
```
|
||||
|
||||
<!--Invalid-->
|
||||
```rust
|
||||
html! {
|
||||
<div id="my_div"> // <- MISSING CLOSE TAG
|
||||
}
|
||||
```
|
||||
|
||||
<!--Self-closing-->
|
||||
```rust
|
||||
html! {
|
||||
<input id="my_input" />
|
||||
}
|
||||
```
|
||||
|
||||
<!--Invalid-->
|
||||
```rust
|
||||
html! {
|
||||
<input id="my_input"> // <- MISSING SELF-CLOSE
|
||||
}
|
||||
```
|
||||
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||
|
||||
:::note
|
||||
For convenience, elements which _usually_ require a closing tag are **allowed** to self-close. For example, writing `html! { <div class="placeholder" /> }` is valid.
|
||||
:::
|
||||
|
||||
## Children
|
||||
|
||||
Create complex nested HTML and SVG layouts with ease:
|
||||
|
||||
<!--DOCUSAURUS_CODE_TABS-->
|
||||
<!--HTML-->
|
||||
|
||||
```rust
|
||||
html! {
|
||||
<div>
|
||||
<div data-key="abc"></div>
|
||||
<div class="parent">
|
||||
<span class="child" value="anything"></span>
|
||||
<label for="first-name">{ "First Name" }</label>
|
||||
<input type="text" id="first-name" value="placeholder" />
|
||||
<input type="checkbox" checked=true />
|
||||
<textarea value="write a story" />
|
||||
<select name="status">
|
||||
<option selected=true disabled=false value="">{ "Selected" }</option>
|
||||
<option selected=false disabled=true value="">{ "Unselected" }</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
<!--SVG-->
|
||||
|
||||
```rust
|
||||
html! {
|
||||
<svg width="149" height="147" viewBox="0 0 149 147" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M60.5776 13.8268L51.8673 42.6431L77.7475 37.331L60.5776 13.8268Z" fill="#DEB819"/>
|
||||
<path d="M108.361 94.9937L138.708 90.686L115.342 69.8642" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<circle cx="75.3326" cy="73.4918" r="55" fill="#FDD630"/>
|
||||
<circle cx="75.3326" cy="73.4918" r="52.5" stroke="black" stroke-width="5"/>
|
||||
</g>
|
||||
<circle cx="71" cy="99" r="5" fill="white" fill-opacity="0.75" stroke="black" stroke-width="3"/>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="16.3326" y="18.4918" width="118" height="118" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
```
|
||||
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||
|
||||
## Optional attributes for HTML elements
|
||||
|
||||
@ -111,6 +25,7 @@ There are a number of convenient ways to specify classes for an element:
|
||||
|
||||
<!--DOCUSAURUS_CODE_TABS-->
|
||||
<!--Literal-->
|
||||
|
||||
```rust
|
||||
html! {
|
||||
<div class="container"></div>
|
||||
@ -118,39 +33,37 @@ html! {
|
||||
```
|
||||
|
||||
<!--Multiple-->
|
||||
```rust
|
||||
html! {
|
||||
<div class="container center-align"></div>
|
||||
}
|
||||
```
|
||||
|
||||
<!--Interpolated-->
|
||||
```rust
|
||||
html! {
|
||||
<div class=format!("{}-container", size)></div>
|
||||
}
|
||||
```
|
||||
|
||||
<!--Expression-->
|
||||
```rust
|
||||
html! {
|
||||
<div class=self.classes()></div>
|
||||
}
|
||||
```
|
||||
|
||||
<!--Tuple-->
|
||||
```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
|
||||
@ -159,6 +72,7 @@ Listener attributes need to be passed a `Callback` which is a wrapper around a c
|
||||
|
||||
<!--DOCUSAURUS_CODE_TABS-->
|
||||
<!--Component handler-->
|
||||
|
||||
```rust
|
||||
struct MyComponent {
|
||||
link: ComponentLink<Self>,
|
||||
@ -230,6 +144,7 @@ impl Component for MyComponent {
|
||||
```
|
||||
|
||||
<!--Other Cases-->
|
||||
|
||||
```rust
|
||||
struct MyComponent;
|
||||
|
||||
@ -259,116 +174,113 @@ impl Component for MyComponent {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||
|
||||
## Event Types
|
||||
|
||||
:::note
|
||||
In the following table `web-sys`'s event types should only be used if you're using `yew` with `web-sys`
|
||||
(this is enabled by default). Use `stdweb`'s event types if you're using the `yew-stdweb` crate. See
|
||||
(this is enabled by default). Use `stdweb`'s event types if you're using the `yew-stdweb` crate. See
|
||||
[the documentation page about whether to choose `web-sys` or `stdweb`](https://yew.rs/docs/getting-started/choose-web-library) for more information.
|
||||
|
||||
:::tip
|
||||
All the event types mentioned in the following table are re-exported under `yew::events`.
|
||||
Using the types from `yew::events` makes it easier to ensure version compatibility than if you were to manually include `web-sys` or `stdweb` as dependencies in your crate because you won't end up using a version which conflicts with the version Yew specifies.
|
||||
:::
|
||||
|
||||
:::note
|
||||
All the event types mentioned in the following table are re-exported under `yew::events`. Using the types from
|
||||
`yew::events` makes it easier to ensure version compatibility than if you were to manually include `web-sys`
|
||||
or `stdweb` as dependencies in your crate because you won't end up using a version which conflicts with
|
||||
the version Yew specifies.
|
||||
:::
|
||||
|
||||
| Event name | `web_sys` Event Type | `stdweb` Event Type |
|
||||
| ----------- | -------------------- | ------------------ |
|
||||
| `onabort` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ResourceAbortEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ResourceAbortEvent.html) |
|
||||
| `onauxclick` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [AuxClickEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.AuxClickEvent.html) |
|
||||
| `onblur` | [FocusEvent](https://docs.rs/web-sys/latest/web_sys/struct.FocusEvent.html) | [BlurEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.BlurEvent.html) |
|
||||
| `oncancel` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oncanplay` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oncanplaythrough` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onchange` | [ChangeData](https://docs.rs/yew/latest/yew/events/enum.ChangeData.html) | [ChangeData](https://docs.rs/yew-stdweb/latest/yew_stdweb/events/enum.ChangeData.html) |
|
||||
| `onclick` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [ClickEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ClickEvent.html) |
|
||||
| `onclose` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oncontextmenu` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [ContextMenuEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ContextMenuEvent.html) |
|
||||
| `oncuechange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `ondblclick` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [DoubleClickEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DoubleClickEvent.html) |
|
||||
| `ondrag` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragEvent.html) |
|
||||
| `ondragend` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragEndEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragEndEvent.html) |
|
||||
| `ondragenter` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragEnterEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragEnterEvent.html) |
|
||||
| `ondragexit` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragExitEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragExitEvent.html) |
|
||||
| `ondragleave` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.htmk) | [DragLeaveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragLeaveEvent.html) |
|
||||
| `ondragover` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragOverEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragOverEvent.html) |
|
||||
| `ondragstart` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragStartEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragStartEvent.html) |
|
||||
| `ondrop` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragDropEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragDropEvent.html) |
|
||||
| `ondurationchange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onemptied` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onended` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onerror` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ResourceErrorEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ResourceErrorEvent.html) |
|
||||
| `onfocus` | [FocusEvent](https://docs.rs/web-sys/latest/web_sys/struct.FocusEvent.html) | [FocusEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.FocusEvent.html) |
|
||||
| `onformdata` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oninput` | [InputData](https://docs.rs/yew/latest/yew/events/struct.InputData.html) | [InputData](https://docs.rs/yew-stdweb/latest/yew_stdweb/events/struct.InputData.html) |
|
||||
| `oninvalid` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onkeydown` | [KeyboardEvent](https://docs.rs/web-sys/latest/web_sys/struct.KeyboardEvent.html) | [KeyDownEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.KeyDownEvent.html) |
|
||||
| `onkeypress` | [KeyboardEvent](https://docs.rs/web-sys/latest/web_sys/struct.KeyboardEvent.html) | [KeyPressEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.KeyPressEvent.html) |
|
||||
| `onkeyup` | [KeyboardEvent](https://docs.rs/web-sys/latest/web_sys/struct.KeyboardEvent.html) | [KeyUpEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.KeyUpEvent.html) |
|
||||
| `onload` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ResourceLoadEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ResourceLoadEvent.html) |
|
||||
| `onloadeddata` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onloadedmetadata` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onloadstart` | [ProgressEvent](https://docs.rs/web-sys/latest/web_sys/struct.ProgressEvent.html) | [LoadStartEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.LoadStartEvent.html) |
|
||||
| `onmousedown` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseDownEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseDownEvent.html) |
|
||||
| `onmouseenter` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseEnterEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseEnterEvent.html) |
|
||||
| `onmouseleave` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseLeaveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseLeaveEvent.html) |
|
||||
| `onmousemove` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseMoveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseMoveEvent.html) |
|
||||
| `onmouseout` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseOutEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseOutEvent.html) |
|
||||
| `onmouseover` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseOverEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseOverEvent.html) |
|
||||
| `onmouseup` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseUpEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseUpEvent.html) |
|
||||
| `onpause` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onplay` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onplaying` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onprogress` | [ProgressEvent](https://docs.rs/web-sys/latest/web_sys/struct.ProgressEvent.html) | [ProgressEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ProgressEvent.html) |
|
||||
| `onratechange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onreset` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onresize` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ResizeEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ResizeEvent.html) |
|
||||
| `onscroll` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ScrollEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ScrollEvent.html) |
|
||||
| `onsecuritypolicyviolation` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onseeked` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onseeking` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onselect` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onslotchange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [SlotChangeEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.SlotChangeEvent.html) |
|
||||
| `onstalled` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onsubmit` | [FocusEvent](https://docs.rs/web-sys/latest/web_sys/struct.FocusEvent.html) | [SubmitEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.SubmitEvent.html) |
|
||||
| `onsuspend` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `ontimeupdate` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `ontoggle` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onvolumechange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onwaiting` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onwheel` | [WheelEvent](https://docs.rs/web-sys/latest/web_sys/struct.WheelEvent.html) | [MouseWheelEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseWheelEvent.html) |
|
||||
| `oncopy` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oncut` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onpaste` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onanimationcancel` | [AnimationEvent](https://docs.rs/web-sys/latest/web_sys/struct.AnimationEvent.html) | Unsupported |
|
||||
| `onanimationend` | [AnimationEvent](https://docs.rs/web-sys/latest/web_sys/struct.AnimationEvent.html) | Unsupported |
|
||||
| `onanimationiteration` | [AnimationEvent](https://docs.rs/web-sys/latest/web_sys/struct.AnimationEvent.html) | Unsupported |
|
||||
| `onanimationstart` | [AnimationEvent](https://docs.rs/web-sys/latest/web_sys/struct.AnimationEvent.html) | Unsupported |
|
||||
| `ongotpointercapture` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [GotPointerCaptureEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.GotPointerCaptureEvent.html) |
|
||||
| `onloadend` | [ProgressEvent](https://docs.rs/web-sys/latest/web_sys/struct.ProgressEvent.html) | [LoadEndEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.LoadEndEvent.html) |
|
||||
| `onlostpointercapture` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [LostPointerCaptureEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.LostPointerCaptureEvent.html) |
|
||||
| `onpointercancel` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerCancelEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerCancelEvent.html) |
|
||||
| `onpointerdown` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerDownEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerDownEvent.html) |
|
||||
| `onpointerenter` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerEnterEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerEnterEvent.html) |
|
||||
| `onpointerleave` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerLeaveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerLeaveEvent.html) |
|
||||
| `onpointerlockchange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [PointerLockChangeEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerLockChangeEvent.html) |
|
||||
| `onpointerlockerror` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [PointerLockErrorEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerLockErrorEvent.html) |
|
||||
| `onpointermove` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerMoveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerMoveEvent.html) |
|
||||
| `onpointerout` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerOutEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerOutEvent.html) |
|
||||
| `onpointerover` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerOverEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerOverEvent.html) |
|
||||
| `onpointerup` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerUpEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerUpEvent.html) |
|
||||
| `onselectionchange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [SelectionChangeEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.SelectionChangeEvent.html) |
|
||||
| `onselectstart` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onshow` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `ontouchcancel` | [TouchEvent](https://docs.rs/web-sys/latest/web_sys/struct.TouchEvent.html) | [TouchCancel](https://docs.rs/stdweb/latest/stdweb/web/event/struct.TouchCancel.html) |
|
||||
| `ontouchend` | [TouchEvent](https://docs.rs/web-sys/latest/web_sys/struct.TouchEvent.html) | [TouchEnd](https://docs.rs/stdweb/latest/stdweb/web/event/struct.TouchEnd.html) |
|
||||
| `ontouchmove` | [TouchEvent](https://docs.rs/web-sys/latest/web_sys/struct.TouchEvent.html) | [TouchMove](https://docs.rs/stdweb/latest/stdweb/web/event/struct.TouchMove.html) |
|
||||
| `ontouchstart` | [TouchEvent](https://docs.rs/web-sys/latest/web_sys/struct.TouchEvent.html) | [TouchStart](https://docs.rs/stdweb/latest/stdweb/web/event/struct.TouchStart.html) |
|
||||
| `ontransitioncancel` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | Unsupported |
|
||||
| `ontransitionend` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | Unsupported |
|
||||
| `ontransitionrun` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | Unsupported |
|
||||
| `ontransitionstart` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | Unsupported |
|
||||
| Event name | `web_sys` Event Type | `stdweb` Event Type |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `onabort` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ResourceAbortEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ResourceAbortEvent.html) |
|
||||
| `onauxclick` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [AuxClickEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.AuxClickEvent.html) |
|
||||
| `onblur` | [FocusEvent](https://docs.rs/web-sys/latest/web_sys/struct.FocusEvent.html) | [BlurEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.BlurEvent.html) |
|
||||
| `oncancel` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oncanplay` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oncanplaythrough` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onchange` | [ChangeData](https://docs.rs/yew/latest/yew/events/enum.ChangeData.html) | [ChangeData](https://docs.rs/yew-stdweb/latest/yew_stdweb/events/enum.ChangeData.html) |
|
||||
| `onclick` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [ClickEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ClickEvent.html) |
|
||||
| `onclose` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oncontextmenu` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [ContextMenuEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ContextMenuEvent.html) |
|
||||
| `oncuechange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `ondblclick` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [DoubleClickEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DoubleClickEvent.html) |
|
||||
| `ondrag` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragEvent.html) |
|
||||
| `ondragend` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragEndEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragEndEvent.html) |
|
||||
| `ondragenter` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragEnterEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragEnterEvent.html) |
|
||||
| `ondragexit` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragExitEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragExitEvent.html) |
|
||||
| `ondragleave` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.htmk) | [DragLeaveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragLeaveEvent.html) |
|
||||
| `ondragover` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragOverEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragOverEvent.html) |
|
||||
| `ondragstart` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragStartEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragStartEvent.html) |
|
||||
| `ondrop` | [DragEvent](https://docs.rs/web-sys/latest/web_sys/struct.DragEvent.html) | [DragDropEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.DragDropEvent.html) |
|
||||
| `ondurationchange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onemptied` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onended` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onerror` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ResourceErrorEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ResourceErrorEvent.html) |
|
||||
| `onfocus` | [FocusEvent](https://docs.rs/web-sys/latest/web_sys/struct.FocusEvent.html) | [FocusEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.FocusEvent.html) |
|
||||
| `onformdata` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oninput` | [InputData](https://docs.rs/yew/latest/yew/events/struct.InputData.html) | [InputData](https://docs.rs/yew-stdweb/latest/yew_stdweb/events/struct.InputData.html) |
|
||||
| `oninvalid` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onkeydown` | [KeyboardEvent](https://docs.rs/web-sys/latest/web_sys/struct.KeyboardEvent.html) | [KeyDownEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.KeyDownEvent.html) |
|
||||
| `onkeypress` | [KeyboardEvent](https://docs.rs/web-sys/latest/web_sys/struct.KeyboardEvent.html) | [KeyPressEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.KeyPressEvent.html) |
|
||||
| `onkeyup` | [KeyboardEvent](https://docs.rs/web-sys/latest/web_sys/struct.KeyboardEvent.html) | [KeyUpEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.KeyUpEvent.html) |
|
||||
| `onload` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ResourceLoadEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ResourceLoadEvent.html) |
|
||||
| `onloadeddata` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onloadedmetadata` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onloadstart` | [ProgressEvent](https://docs.rs/web-sys/latest/web_sys/struct.ProgressEvent.html) | [LoadStartEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.LoadStartEvent.html) |
|
||||
| `onmousedown` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseDownEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseDownEvent.html) |
|
||||
| `onmouseenter` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseEnterEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseEnterEvent.html) |
|
||||
| `onmouseleave` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseLeaveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseLeaveEvent.html) |
|
||||
| `onmousemove` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseMoveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseMoveEvent.html) |
|
||||
| `onmouseout` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseOutEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseOutEvent.html) |
|
||||
| `onmouseover` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseOverEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseOverEvent.html) |
|
||||
| `onmouseup` | [MouseEvent](https://docs.rs/web-sys/latest/web_sys/struct.MouseEvent.html) | [MouseUpEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseUpEvent.html) |
|
||||
| `onpause` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onplay` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onplaying` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onprogress` | [ProgressEvent](https://docs.rs/web-sys/latest/web_sys/struct.ProgressEvent.html) | [ProgressEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ProgressEvent.html) |
|
||||
| `onratechange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onreset` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onresize` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ResizeEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ResizeEvent.html) |
|
||||
| `onscroll` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [ScrollEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.ScrollEvent.html) |
|
||||
| `onsecuritypolicyviolation` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onseeked` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onseeking` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onselect` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onslotchange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [SlotChangeEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.SlotChangeEvent.html) |
|
||||
| `onstalled` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onsubmit` | [FocusEvent](https://docs.rs/web-sys/latest/web_sys/struct.FocusEvent.html) | [SubmitEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.SubmitEvent.html) |
|
||||
| `onsuspend` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `ontimeupdate` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `ontoggle` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onvolumechange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onwaiting` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onwheel` | [WheelEvent](https://docs.rs/web-sys/latest/web_sys/struct.WheelEvent.html) | [MouseWheelEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.MouseWheelEvent.html) |
|
||||
| `oncopy` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `oncut` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onpaste` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onanimationcancel` | [AnimationEvent](https://docs.rs/web-sys/latest/web_sys/struct.AnimationEvent.html) | Unsupported |
|
||||
| `onanimationend` | [AnimationEvent](https://docs.rs/web-sys/latest/web_sys/struct.AnimationEvent.html) | Unsupported |
|
||||
| `onanimationiteration` | [AnimationEvent](https://docs.rs/web-sys/latest/web_sys/struct.AnimationEvent.html) | Unsupported |
|
||||
| `onanimationstart` | [AnimationEvent](https://docs.rs/web-sys/latest/web_sys/struct.AnimationEvent.html) | Unsupported |
|
||||
| `ongotpointercapture` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [GotPointerCaptureEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.GotPointerCaptureEvent.html) |
|
||||
| `onloadend` | [ProgressEvent](https://docs.rs/web-sys/latest/web_sys/struct.ProgressEvent.html) | [LoadEndEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.LoadEndEvent.html) |
|
||||
| `onlostpointercapture` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [LostPointerCaptureEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.LostPointerCaptureEvent.html) |
|
||||
| `onpointercancel` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerCancelEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerCancelEvent.html) |
|
||||
| `onpointerdown` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerDownEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerDownEvent.html) |
|
||||
| `onpointerenter` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerEnterEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerEnterEvent.html) |
|
||||
| `onpointerleave` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerLeaveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerLeaveEvent.html) |
|
||||
| `onpointerlockchange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [PointerLockChangeEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerLockChangeEvent.html) |
|
||||
| `onpointerlockerror` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [PointerLockErrorEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerLockErrorEvent.html) |
|
||||
| `onpointermove` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerMoveEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerMoveEvent.html) |
|
||||
| `onpointerout` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerOutEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerOutEvent.html) |
|
||||
| `onpointerover` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerOverEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerOverEvent.html) |
|
||||
| `onpointerup` | [PointerEvent](https://docs.rs/web-sys/latest/web_sys/struct.PointerEvent.html) | [PointerUpEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.PointerUpEvent.html) |
|
||||
| `onselectionchange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | [SelectionChangeEvent](https://docs.rs/stdweb/latest/stdweb/web/event/struct.SelectionChangeEvent.html) |
|
||||
| `onselectstart` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `onshow` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | Unsupported |
|
||||
| `ontouchcancel` | [TouchEvent](https://docs.rs/web-sys/latest/web_sys/struct.TouchEvent.html) | [TouchCancel](https://docs.rs/stdweb/latest/stdweb/web/event/struct.TouchCancel.html) |
|
||||
| `ontouchend` | [TouchEvent](https://docs.rs/web-sys/latest/web_sys/struct.TouchEvent.html) | [TouchEnd](https://docs.rs/stdweb/latest/stdweb/web/event/struct.TouchEnd.html) |
|
||||
| `ontouchmove` | [TouchEvent](https://docs.rs/web-sys/latest/web_sys/struct.TouchEvent.html) | [TouchMove](https://docs.rs/stdweb/latest/stdweb/web/event/struct.TouchMove.html) |
|
||||
| `ontouchstart` | [TouchEvent](https://docs.rs/web-sys/latest/web_sys/struct.TouchEvent.html) | [TouchStart](https://docs.rs/stdweb/latest/stdweb/web/event/struct.TouchStart.html) |
|
||||
| `ontransitioncancel` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | Unsupported |
|
||||
| `ontransitionend` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | Unsupported |
|
||||
| `ontransitionrun` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | Unsupported |
|
||||
| `ontransitionstart` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | Unsupported |
|
||||
|
||||
@ -49,7 +49,7 @@ This was the best preferred tool to use before the creation of `wasm-bindgen`.
|
||||
| | `trunk` | `wasm-pack` | `cargo-web` |
|
||||
| ----------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Project Status | Actively maintained | Actively maintained by the [Rust / Wasm Working Group](https://rustwasm.github.io) | No Github activity for over 6 months |
|
||||
| Dev Experience | Just works! Batteries included, no external dependencies needed. | Barebones. You'll need to write some scripts to streamline the experience or use the webpack plugin. | Works great for code but needs separate asset pipeline. |
|
||||
| Dev Experience | Just works! Batteries included, no external dependencies needed. | Bare-bones. You'll need to write some scripts to streamline the experience or use the webpack plugin. | Works great for code but needs separate asset pipeline. |
|
||||
| Local Server | Supported | Only with webpack plugin | Supported |
|
||||
| Auto rebuild on local changes | Supported | Only with webpack plugin | Supported |
|
||||
| Asset handling | Supported | Only with webpack plugin | Static assets only |
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
use web_sys::HtmlSelectElement;
|
||||
use yew::callback::Callback;
|
||||
use yew::html::{ChangeData, Component, ComponentLink, Html, NodeRef, ShouldRender};
|
||||
use yew::macros::{html, Properties};
|
||||
use yew::{html, Properties};
|
||||
|
||||
/// An alternative to the HTML `<select>` tag.
|
||||
///
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
use super::html_iterable::HtmlIterable;
|
||||
use super::html_node::HtmlNode;
|
||||
use super::ToNodeIterator;
|
||||
use super::{HtmlIterable, HtmlNode, ToNodeIterator};
|
||||
use crate::PeekValue;
|
||||
use proc_macro2::Delimiter;
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::braced;
|
||||
use syn::buffer::Cursor;
|
||||
use syn::parse::{Parse, ParseStream, Result as ParseResult};
|
||||
use syn::token;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::{braced, token};
|
||||
|
||||
pub struct HtmlBlock {
|
||||
content: BlockContent,
|
||||
@ -26,7 +23,7 @@ impl PeekValue<()> for HtmlBlock {
|
||||
}
|
||||
|
||||
impl Parse for HtmlBlock {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let content;
|
||||
let brace = braced!(content in input);
|
||||
let content = if HtmlIterable::peek(content.cursor()).is_some() {
|
||||
|
||||
@ -1,23 +1,20 @@
|
||||
use super::HtmlChildrenTree;
|
||||
use super::HtmlProp;
|
||||
use super::HtmlPropSuffix;
|
||||
use crate::PeekValue;
|
||||
use super::{HtmlChildrenTree, TagTokens};
|
||||
use crate::{props::ComponentProps, PeekValue};
|
||||
use boolinator::Boolinator;
|
||||
use proc_macro2::Span;
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use std::cmp::Ordering;
|
||||
use syn::buffer::Cursor;
|
||||
use syn::parse::{Parse, ParseStream, Result as ParseResult};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{
|
||||
AngleBracketedGenericArguments, Expr, GenericArgument, Ident, Path, PathArguments, PathSegment,
|
||||
Token, Type, TypePath,
|
||||
AngleBracketedGenericArguments, GenericArgument, Path, PathArguments, PathSegment, Token, Type,
|
||||
TypePath,
|
||||
};
|
||||
|
||||
pub struct HtmlComponent {
|
||||
ty: Type,
|
||||
props: Props,
|
||||
props: ComponentProps,
|
||||
children: HtmlChildrenTree,
|
||||
}
|
||||
|
||||
@ -30,11 +27,11 @@ impl PeekValue<()> for HtmlComponent {
|
||||
}
|
||||
|
||||
impl Parse for HtmlComponent {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
if HtmlComponentClose::peek(input.cursor()).is_some() {
|
||||
return match input.parse::<HtmlComponentClose>() {
|
||||
Ok(close) => Err(syn::Error::new_spanned(
|
||||
close,
|
||||
close.to_spanned(),
|
||||
"this closing tag has no corresponding opening tag",
|
||||
)),
|
||||
Err(err) => Err(err),
|
||||
@ -43,7 +40,7 @@ impl Parse for HtmlComponent {
|
||||
|
||||
let open = input.parse::<HtmlComponentOpen>()?;
|
||||
// Return early if it's a self-closing tag
|
||||
if open.div.is_some() {
|
||||
if open.is_self_closing() {
|
||||
return Ok(HtmlComponent {
|
||||
ty: open.ty,
|
||||
props: open.props,
|
||||
@ -55,7 +52,7 @@ impl Parse for HtmlComponent {
|
||||
loop {
|
||||
if input.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
open,
|
||||
open.to_spanned(),
|
||||
"this opening tag has no corresponding closing tag",
|
||||
));
|
||||
}
|
||||
@ -70,6 +67,18 @@ impl Parse for HtmlComponent {
|
||||
|
||||
input.parse::<HtmlComponentClose>()?;
|
||||
|
||||
if !children.is_empty() {
|
||||
// check if the `children` prop is given explicitly
|
||||
if let ComponentProps::List(props) = &open.props {
|
||||
if let Some(children_prop) = props.get_by_label("children") {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&children_prop.label,
|
||||
"cannot specify the `children` prop when the component already has children",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HtmlComponent {
|
||||
ty: open.ty,
|
||||
props: open.props,
|
||||
@ -86,90 +95,36 @@ impl ToTokens for HtmlComponent {
|
||||
children,
|
||||
} = self;
|
||||
|
||||
let validate_props = if let PropType::List(list_props) = &props.prop_type {
|
||||
let check_props = list_props.iter().map(|HtmlProp { label, .. }| {
|
||||
quote! { props.#label; }
|
||||
});
|
||||
|
||||
let check_children = if !children.is_empty() {
|
||||
quote! { props.children; }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
quote! {
|
||||
let _ = |props: <#ty as ::yew::html::Component>::Properties| {
|
||||
#check_children
|
||||
#(#check_props)*
|
||||
};
|
||||
}
|
||||
let props_ty = quote_spanned!(ty.span()=> <#ty as ::yew::html::Component>::Properties);
|
||||
let children_renderer = if children.is_empty() {
|
||||
None
|
||||
} else {
|
||||
quote! {}
|
||||
Some(quote! { ::yew::html::ChildrenRenderer::new(#children) })
|
||||
};
|
||||
let build_props = props.build_properties_tokens(&props_ty, children_renderer);
|
||||
|
||||
let set_children = if !children.is_empty() {
|
||||
// using span of type because the error message goes something like "children method not found".
|
||||
// If we could control the message, it should say "component X doesn't accept children" and point at the children.
|
||||
quote_spanned! {ty.span()=>
|
||||
.children(::yew::html::ChildrenRenderer::new(#children))
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let init_props = match &props.prop_type {
|
||||
PropType::List(list_props) => {
|
||||
let set_props = list_props.iter().map(|HtmlProp { label, value, .. }| {
|
||||
quote_spanned! {value.span()=> .#label(
|
||||
#[allow(unused_braces)]
|
||||
<::yew::virtual_dom::VComp as ::yew::virtual_dom::Transformer<_, _>>::transform(
|
||||
#value
|
||||
)
|
||||
)}
|
||||
});
|
||||
|
||||
quote_spanned! {ty.span()=>
|
||||
<<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder()
|
||||
#(#set_props)*
|
||||
#set_children
|
||||
.build()
|
||||
}
|
||||
}
|
||||
PropType::With(props) => {
|
||||
quote! { #props }
|
||||
}
|
||||
PropType::None => quote_spanned! {ty.span()=>
|
||||
<<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder()
|
||||
#set_children
|
||||
.build()
|
||||
},
|
||||
};
|
||||
|
||||
let node_ref = if let Some(node_ref) = &props.node_ref {
|
||||
quote_spanned! {node_ref.span()=> #node_ref }
|
||||
let special_props = props.special();
|
||||
let node_ref = if let Some(node_ref) = &special_props.node_ref {
|
||||
let value = &node_ref.value;
|
||||
quote_spanned! {value.span()=> #value }
|
||||
} else {
|
||||
quote! { ::yew::html::NodeRef::default() }
|
||||
};
|
||||
|
||||
let key = if let Some(key) = &props.key {
|
||||
quote_spanned! {key.span()=>
|
||||
let key = if let Some(key) = &special_props.key {
|
||||
let value = &key.value;
|
||||
quote_spanned! {value.span()=>
|
||||
#[allow(clippy::useless_conversion)]
|
||||
Some(::std::convert::Into::<::yew::virtual_dom::Key>::into(#key))
|
||||
Some(::std::convert::Into::<::yew::virtual_dom::Key>::into(#value))
|
||||
}
|
||||
} else {
|
||||
quote! {None}
|
||||
quote! { None }
|
||||
};
|
||||
|
||||
tokens.extend(quote_spanned! {ty.span()=>
|
||||
{
|
||||
// These validation checks show a nice error message to the user.
|
||||
// They do not execute at runtime
|
||||
if false {
|
||||
#validate_props
|
||||
}
|
||||
|
||||
#[allow(clippy::unit_arg)]
|
||||
::yew::virtual_dom::VChild::<#ty>::new(#init_props, #node_ref, #key)
|
||||
::yew::virtual_dom::VChild::<#ty>::new(#build_props, #node_ref, #key)
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -260,11 +215,18 @@ impl HtmlComponent {
|
||||
}
|
||||
|
||||
struct HtmlComponentOpen {
|
||||
lt: Token![<],
|
||||
tag: TagTokens,
|
||||
ty: Type,
|
||||
props: Props,
|
||||
div: Option<Token![/]>,
|
||||
gt: Token![>],
|
||||
props: ComponentProps,
|
||||
}
|
||||
impl HtmlComponentOpen {
|
||||
fn is_self_closing(&self) -> bool {
|
||||
self.tag.div.is_some()
|
||||
}
|
||||
|
||||
fn to_spanned(&self) -> impl ToTokens {
|
||||
self.tag.to_spanned()
|
||||
}
|
||||
}
|
||||
|
||||
impl PeekValue<Type> for HtmlComponentOpen {
|
||||
@ -277,36 +239,24 @@ impl PeekValue<Type> for HtmlComponentOpen {
|
||||
}
|
||||
|
||||
impl Parse for HtmlComponentOpen {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
let lt = input.parse::<Token![<]>()?;
|
||||
let ty = input.parse()?;
|
||||
// backwards compat
|
||||
let _ = input.parse::<Token![:]>();
|
||||
let HtmlPropSuffix { stream, div, gt } = input.parse()?;
|
||||
let props = syn::parse2(stream)?;
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
TagTokens::parse_start_content(input, |input, tag| {
|
||||
let ty = input.parse()?;
|
||||
let props = input.parse()?;
|
||||
|
||||
Ok(HtmlComponentOpen {
|
||||
lt,
|
||||
ty,
|
||||
props,
|
||||
div,
|
||||
gt,
|
||||
Ok(Self { tag, ty, props })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlComponentOpen {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let HtmlComponentOpen { lt, gt, .. } = self;
|
||||
tokens.extend(quote! {#lt#gt});
|
||||
}
|
||||
}
|
||||
|
||||
struct HtmlComponentClose {
|
||||
lt: Token![<],
|
||||
div: Token![/],
|
||||
ty: Type,
|
||||
gt: Token![>],
|
||||
tag: TagTokens,
|
||||
_ty: Type,
|
||||
}
|
||||
impl HtmlComponentClose {
|
||||
fn to_spanned(&self) -> impl ToTokens {
|
||||
self.tag.to_spanned()
|
||||
}
|
||||
}
|
||||
|
||||
impl PeekValue<Type> for HtmlComponentClose {
|
||||
@ -326,130 +276,10 @@ impl PeekValue<Type> for HtmlComponentClose {
|
||||
}
|
||||
}
|
||||
impl Parse for HtmlComponentClose {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
Ok(HtmlComponentClose {
|
||||
lt: input.parse()?,
|
||||
div: input.parse()?,
|
||||
ty: input.parse()?,
|
||||
gt: input.parse()?,
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
TagTokens::parse_end_content(input, |input, tag| {
|
||||
let ty = input.parse()?;
|
||||
Ok(Self { tag, _ty: ty })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlComponentClose {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let HtmlComponentClose { lt, div, ty, gt } = self;
|
||||
tokens.extend(quote! {#lt#div#ty#gt});
|
||||
}
|
||||
}
|
||||
|
||||
enum PropType {
|
||||
List(Vec<HtmlProp>),
|
||||
With(Ident),
|
||||
None,
|
||||
}
|
||||
|
||||
struct Props {
|
||||
node_ref: Option<Expr>,
|
||||
key: Option<Expr>,
|
||||
prop_type: PropType,
|
||||
}
|
||||
|
||||
const COLLISION_MSG: &str = "Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to the `ref` prop).";
|
||||
|
||||
impl Parse for Props {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
let mut props = Props {
|
||||
node_ref: None,
|
||||
key: None,
|
||||
prop_type: PropType::None,
|
||||
};
|
||||
while let Some((token, _)) = input.cursor().ident() {
|
||||
if token == "with" {
|
||||
match props.prop_type {
|
||||
PropType::None => Ok(()),
|
||||
PropType::With(_) => Err(input.error("too many `with` tokens used")),
|
||||
PropType::List(_) => Err(syn::Error::new_spanned(&token, COLLISION_MSG)),
|
||||
}?;
|
||||
|
||||
input.parse::<Ident>()?;
|
||||
props.prop_type = PropType::With(input.parse::<Ident>().map_err(|_| {
|
||||
syn::Error::new_spanned(&token, "`with` must be followed by an identifier")
|
||||
})?);
|
||||
|
||||
// Handle optional comma
|
||||
let _ = input.parse::<Token![,]>();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HtmlProp::peek(input.cursor())).is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
let prop = input.parse::<HtmlProp>()?;
|
||||
if prop.label.to_string() == "ref" {
|
||||
match props.node_ref {
|
||||
None => Ok(()),
|
||||
Some(_) => Err(syn::Error::new_spanned(&prop.label, "too many refs set")),
|
||||
}?;
|
||||
|
||||
props.node_ref = Some(prop.value);
|
||||
continue;
|
||||
}
|
||||
if prop.label.to_string() == "key" {
|
||||
match props.key {
|
||||
None => Ok(()),
|
||||
Some(_) => Err(syn::Error::new_spanned(&prop.label, "too many keys set")),
|
||||
}?;
|
||||
|
||||
props.key = Some(prop.value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if prop.label.to_string() == "type" {
|
||||
return Err(syn::Error::new_spanned(&prop.label, "expected identifier"));
|
||||
}
|
||||
|
||||
if !prop.label.extended.is_empty() {
|
||||
return Err(syn::Error::new_spanned(&prop.label, "expected identifier"));
|
||||
}
|
||||
|
||||
if prop.question_mark.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&prop.label,
|
||||
"optional attributes are only supported on HTML tags. Yew components can use `Option<T>` properties to accomplish the same thing.",
|
||||
));
|
||||
}
|
||||
|
||||
match props.prop_type {
|
||||
ref mut prop_type @ PropType::None => {
|
||||
*prop_type = PropType::List(vec![prop]);
|
||||
}
|
||||
PropType::With(_) => return Err(syn::Error::new_spanned(&token, COLLISION_MSG)),
|
||||
PropType::List(ref mut list) => {
|
||||
list.push(prop);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if let PropType::List(list) = &mut props.prop_type {
|
||||
// sort alphabetically
|
||||
list.sort_by(|a, b| {
|
||||
if a.label == b.label {
|
||||
Ordering::Equal
|
||||
} else if a.label.to_string() == "children" {
|
||||
Ordering::Greater
|
||||
} else if b.label.to_string() == "children" {
|
||||
Ordering::Less
|
||||
} else {
|
||||
a.label
|
||||
.to_string()
|
||||
.partial_cmp(&b.label.to_string())
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(props)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ use quote::{quote, ToTokens};
|
||||
use std::fmt;
|
||||
use syn::buffer::Cursor;
|
||||
use syn::ext::IdentExt;
|
||||
use syn::parse::{Parse, ParseStream, Result as ParseResult};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::{spanned::Spanned, LitStr, Token};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
@ -16,13 +16,6 @@ pub struct HtmlDashedName {
|
||||
}
|
||||
|
||||
impl HtmlDashedName {
|
||||
pub fn new(name: Ident) -> Self {
|
||||
HtmlDashedName {
|
||||
name,
|
||||
extended: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_ascii_lowercase_string(&self) -> String {
|
||||
let mut s = self.to_string();
|
||||
s.make_ascii_lowercase();
|
||||
@ -68,7 +61,7 @@ impl Peek<'_, Self> for HtmlDashedName {
|
||||
}
|
||||
|
||||
impl Parse for HtmlDashedName {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let name = input.call(Ident::parse_any)?;
|
||||
let mut extended = Vec::new();
|
||||
while input.peek(Token![-]) {
|
||||
|
||||
@ -1,56 +1,52 @@
|
||||
mod tag_attributes;
|
||||
|
||||
use super::{
|
||||
HtmlChildrenTree, HtmlDashedName, HtmlProp as TagAttribute, HtmlPropSuffix as TagSuffix,
|
||||
};
|
||||
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
|
||||
use crate::props::{ClassesForm, ElementProps, Prop};
|
||||
use crate::stringify::Stringify;
|
||||
use crate::{non_capitalized_ascii, stringify, Peek, PeekValue};
|
||||
use boolinator::Boolinator;
|
||||
use proc_macro2::{Delimiter, Span, TokenStream};
|
||||
use proc_macro2::{Delimiter, TokenStream};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::buffer::Cursor;
|
||||
use syn::parse::{Parse, ParseStream, Result as ParseResult};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Block, Ident, Token};
|
||||
use tag_attributes::{ClassesForm, TagAttributes};
|
||||
|
||||
pub struct HtmlTag {
|
||||
tag_name: TagName,
|
||||
attributes: TagAttributes,
|
||||
pub struct HtmlElement {
|
||||
name: TagName,
|
||||
props: ElementProps,
|
||||
children: HtmlChildrenTree,
|
||||
}
|
||||
|
||||
impl PeekValue<()> for HtmlTag {
|
||||
impl PeekValue<()> for HtmlElement {
|
||||
fn peek(cursor: Cursor) -> Option<()> {
|
||||
HtmlTagOpen::peek(cursor)
|
||||
.or_else(|| HtmlTagClose::peek(cursor))
|
||||
HtmlElementOpen::peek(cursor)
|
||||
.or_else(|| HtmlElementClose::peek(cursor))
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for HtmlTag {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
if HtmlTagClose::peek(input.cursor()).is_some() {
|
||||
return match input.parse::<HtmlTagClose>() {
|
||||
impl Parse for HtmlElement {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
if HtmlElementClose::peek(input.cursor()).is_some() {
|
||||
return match input.parse::<HtmlElementClose>() {
|
||||
Ok(close) => Err(syn::Error::new_spanned(
|
||||
close,
|
||||
close.to_spanned(),
|
||||
"this closing tag has no corresponding opening tag",
|
||||
)),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
}
|
||||
|
||||
let open = input.parse::<HtmlTagOpen>()?;
|
||||
let open = input.parse::<HtmlElementOpen>()?;
|
||||
// Return early if it's a self-closing tag
|
||||
if open.div.is_some() {
|
||||
return Ok(HtmlTag {
|
||||
tag_name: open.tag_name,
|
||||
attributes: open.attributes,
|
||||
if open.is_self_closing() {
|
||||
return Ok(HtmlElement {
|
||||
name: open.name,
|
||||
props: open.props,
|
||||
children: HtmlChildrenTree::new(),
|
||||
});
|
||||
}
|
||||
|
||||
if let TagName::Lit(name) = &open.tag_name {
|
||||
if let TagName::Lit(name) = &open.name {
|
||||
// Void elements should not have children.
|
||||
// See https://html.spec.whatwg.org/multipage/syntax.html#void-elements
|
||||
//
|
||||
@ -58,22 +54,22 @@ impl Parse for HtmlTag {
|
||||
match name.to_ascii_lowercase_string().as_str() {
|
||||
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link"
|
||||
| "meta" | "param" | "source" | "track" | "wbr" => {
|
||||
return Err(syn::Error::new_spanned(&open, format!("the tag `<{}>` is a void element and cannot have children (hint: rewrite this as `<{0}/>`)", name)));
|
||||
return Err(syn::Error::new_spanned(open.to_spanned(), format!("the tag `<{}>` is a void element and cannot have children (hint: rewrite this as `<{0}/>`)", name)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let open_key = open.tag_name.get_key();
|
||||
let open_key = open.name.get_key();
|
||||
let mut children = HtmlChildrenTree::new();
|
||||
loop {
|
||||
if input.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
open,
|
||||
open.to_spanned(),
|
||||
"this opening tag has no corresponding closing tag",
|
||||
));
|
||||
}
|
||||
if let Some(close_key) = HtmlTagClose::peek(input.cursor()) {
|
||||
if let Some(close_key) = HtmlElementClose::peek(input.cursor()) {
|
||||
if open_key == close_key {
|
||||
break;
|
||||
}
|
||||
@ -82,26 +78,26 @@ impl Parse for HtmlTag {
|
||||
children.parse_child(input)?;
|
||||
}
|
||||
|
||||
input.parse::<HtmlTagClose>()?;
|
||||
input.parse::<HtmlElementClose>()?;
|
||||
|
||||
Ok(HtmlTag {
|
||||
tag_name: open.tag_name,
|
||||
attributes: open.attributes,
|
||||
Ok(Self {
|
||||
name: open.name,
|
||||
props: open.props,
|
||||
children,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlTag {
|
||||
impl ToTokens for HtmlElement {
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self {
|
||||
tag_name,
|
||||
attributes,
|
||||
name,
|
||||
props,
|
||||
children,
|
||||
} = self;
|
||||
|
||||
let name = match &tag_name {
|
||||
let name_sr = match &name {
|
||||
TagName::Lit(name) => name.stringify(),
|
||||
TagName::Expr(name) => {
|
||||
let expr = &name.expr;
|
||||
@ -120,7 +116,7 @@ impl ToTokens for HtmlTag {
|
||||
}
|
||||
};
|
||||
|
||||
let TagAttributes {
|
||||
let ElementProps {
|
||||
classes,
|
||||
attributes,
|
||||
booleans,
|
||||
@ -130,20 +126,22 @@ impl ToTokens for HtmlTag {
|
||||
node_ref,
|
||||
key,
|
||||
listeners,
|
||||
} = &attributes;
|
||||
} = &props;
|
||||
|
||||
let vtag = Ident::new("__yew_vtag", tag_name.span());
|
||||
let vtag = Ident::new("__yew_vtag", name.span());
|
||||
|
||||
// attributes with special treatment
|
||||
|
||||
let set_node_ref = node_ref.as_ref().map(|node_ref| {
|
||||
let set_node_ref = node_ref.as_ref().map(|attr| {
|
||||
let value = &attr.value;
|
||||
quote! {
|
||||
#vtag.node_ref = #node_ref;
|
||||
#vtag.node_ref = #value;
|
||||
}
|
||||
});
|
||||
let set_key = key.as_ref().map(|key| {
|
||||
let set_key = key.as_ref().map(|attr| {
|
||||
let value = &attr.value;
|
||||
quote! {
|
||||
#vtag.key = ::std::option::Option::Some(::std::convert::Into::<::yew::virtual_dom::Key>::into(#key));
|
||||
#vtag.key = ::std::option::Option::Some(::std::convert::Into::<::yew::virtual_dom::Key>::into(#value));
|
||||
}
|
||||
});
|
||||
let set_value = value.as_ref().map(|attr| {
|
||||
@ -176,7 +174,8 @@ impl ToTokens for HtmlTag {
|
||||
}
|
||||
}
|
||||
});
|
||||
let set_checked = checked.as_ref().map(|value| {
|
||||
let set_checked = checked.as_ref().map(|attr| {
|
||||
let value = &attr.value;
|
||||
quote_spanned! {value.span()=>
|
||||
#vtag.set_checked(#value);
|
||||
}
|
||||
@ -188,10 +187,11 @@ impl ToTokens for HtmlTag {
|
||||
None
|
||||
} else {
|
||||
let attrs = attributes.iter().map(
|
||||
|TagAttribute {
|
||||
|Prop {
|
||||
label,
|
||||
question_mark,
|
||||
value,
|
||||
..
|
||||
}| {
|
||||
let key = label.to_lit_str();
|
||||
if question_mark.is_some() {
|
||||
@ -217,7 +217,7 @@ impl ToTokens for HtmlTag {
|
||||
} else {
|
||||
let tokens = booleans
|
||||
.iter()
|
||||
.map(|TagAttribute { label, value, .. }| {
|
||||
.map(|Prop { label, value, .. }| {
|
||||
let label_str = label.to_lit_str();
|
||||
let sr = label.stringify();
|
||||
quote_spanned! {value.span()=> {
|
||||
@ -279,10 +279,11 @@ impl ToTokens for HtmlTag {
|
||||
let add_listeners = listeners
|
||||
.iter()
|
||||
.map(
|
||||
|TagAttribute {
|
||||
|Prop {
|
||||
label,
|
||||
question_mark,
|
||||
value,
|
||||
..
|
||||
}| {
|
||||
let name = &label.name;
|
||||
|
||||
@ -311,7 +312,7 @@ impl ToTokens for HtmlTag {
|
||||
} else {
|
||||
let listeners_it = listeners
|
||||
.iter()
|
||||
.map(|TagAttribute { label, value, .. }| to_wrapped_listener(&label.name, value));
|
||||
.map(|Prop { label, value, .. }| to_wrapped_listener(&label.name, value));
|
||||
|
||||
Some(quote! {
|
||||
#vtag.add_listeners(::std::vec![#(#listeners_it),*]);
|
||||
@ -329,7 +330,7 @@ impl ToTokens for HtmlTag {
|
||||
|
||||
// These are the runtime-checks exclusive to dynamic tags.
|
||||
// For literal tags this is already done at compile-time.
|
||||
let dyn_tag_runtime_checks = if matches!(&tag_name, TagName::Expr(_)) {
|
||||
let dyn_tag_runtime_checks = if matches!(&name, TagName::Expr(_)) {
|
||||
// when Span::source_file Span::start get stabilised or yew-macro introduces a nightly feature flag
|
||||
// we should expand the panic message to contain the exact location of the dynamic tag.
|
||||
let sr = stringify::stringify_at_runtime(quote! { __yew_v });
|
||||
@ -361,10 +362,10 @@ impl ToTokens for HtmlTag {
|
||||
None
|
||||
};
|
||||
|
||||
tokens.extend(quote_spanned! {tag_name.span()=>
|
||||
tokens.extend(quote_spanned! {name.span()=>
|
||||
{
|
||||
#[allow(unused_braces)]
|
||||
let mut #vtag = ::yew::virtual_dom::VTag::new(#name);
|
||||
let mut #vtag = ::yew::virtual_dom::VTag::new(#name_sr);
|
||||
|
||||
#set_node_ref
|
||||
#set_key
|
||||
@ -416,7 +417,7 @@ impl Peek<'_, ()> for DynamicName {
|
||||
}
|
||||
|
||||
impl Parse for DynamicName {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let at = input.parse()?;
|
||||
// the expression block is optional, closing tags don't have it.
|
||||
let expr = if input.cursor().group(Delimiter::Brace).is_some() {
|
||||
@ -467,7 +468,7 @@ impl Peek<'_, TagKey> for TagName {
|
||||
}
|
||||
|
||||
impl Parse for TagName {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
if DynamicName::peek(input.cursor()).is_some() {
|
||||
DynamicName::parse(input).map(Self::Expr)
|
||||
} else {
|
||||
@ -485,25 +486,32 @@ impl ToTokens for TagName {
|
||||
}
|
||||
}
|
||||
|
||||
struct HtmlTagOpen {
|
||||
lt: Token![<],
|
||||
tag_name: TagName,
|
||||
attributes: TagAttributes,
|
||||
div: Option<Token![/]>,
|
||||
gt: Token![>],
|
||||
struct HtmlElementOpen {
|
||||
tag: TagTokens,
|
||||
name: TagName,
|
||||
props: ElementProps,
|
||||
}
|
||||
impl HtmlElementOpen {
|
||||
fn is_self_closing(&self) -> bool {
|
||||
self.tag.div.is_some()
|
||||
}
|
||||
|
||||
fn to_spanned(&self) -> impl ToTokens {
|
||||
self.tag.to_spanned()
|
||||
}
|
||||
}
|
||||
|
||||
impl PeekValue<TagKey> for HtmlTagOpen {
|
||||
impl PeekValue<TagKey> for HtmlElementOpen {
|
||||
fn peek(cursor: Cursor) -> Option<TagKey> {
|
||||
let (punct, cursor) = cursor.punct()?;
|
||||
(punct.as_char() == '<').as_option()?;
|
||||
|
||||
let (tag_key, cursor) = TagName::peek(cursor)?;
|
||||
if let TagKey::Lit(name) = &tag_key {
|
||||
// Avoid parsing `<key=[...]>` as an HtmlTag. It needs to be parsed as an HtmlList.
|
||||
// Avoid parsing `<key=[...]>` as an element. It needs to be parsed as an `HtmlList`.
|
||||
if name.to_string() == "key" {
|
||||
let (punct, _) = cursor.punct()?;
|
||||
// ... unless it isn't followed by a '='. `<key></key>` is a valid HtmlTag!
|
||||
// ... unless it isn't followed by a '='. `<key></key>` is a valid element!
|
||||
(punct.as_char() != '=').as_option()?;
|
||||
} else {
|
||||
non_capitalized_ascii(&name.to_string()).as_option()?;
|
||||
@ -514,65 +522,51 @@ impl PeekValue<TagKey> for HtmlTagOpen {
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for HtmlTagOpen {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
let lt = input.parse::<Token![<]>()?;
|
||||
let tag_name = input.parse::<TagName>()?;
|
||||
let TagSuffix { stream, div, gt } = input.parse()?;
|
||||
let mut attributes: TagAttributes = syn::parse2(stream)?;
|
||||
impl Parse for HtmlElementOpen {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
TagTokens::parse_start_content(input, |input, tag| {
|
||||
let name = input.parse::<TagName>()?;
|
||||
let mut props = input.parse::<ElementProps>()?;
|
||||
|
||||
match &tag_name {
|
||||
TagName::Lit(name) => {
|
||||
// Don't treat value as special for non input / textarea fields
|
||||
// For dynamic tags this is done at runtime!
|
||||
match name.to_ascii_lowercase_string().as_str() {
|
||||
"input" | "textarea" => {}
|
||||
_ => {
|
||||
if let Some(attr) = attributes.value.take() {
|
||||
attributes.attributes.push(TagAttribute {
|
||||
label: HtmlDashedName::new(Ident::new("value", Span::call_site())),
|
||||
question_mark: attr.question_mark,
|
||||
value: attr.value,
|
||||
});
|
||||
match &name {
|
||||
TagName::Lit(name) => {
|
||||
// Don't treat value as special for non input / textarea fields
|
||||
// For dynamic tags this is done at runtime!
|
||||
match name.to_ascii_lowercase_string().as_str() {
|
||||
"input" | "textarea" => {}
|
||||
_ => {
|
||||
if let Some(attr) = props.value.take() {
|
||||
props.attributes.push(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TagName::Expr(name) => {
|
||||
if name.expr.is_none() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
tag_name,
|
||||
"this dynamic tag is missing an expression block defining its value",
|
||||
));
|
||||
TagName::Expr(name) => {
|
||||
if name.expr.is_none() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
name,
|
||||
"this dynamic tag is missing an expression block defining its value",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HtmlTagOpen {
|
||||
lt,
|
||||
tag_name,
|
||||
attributes,
|
||||
div,
|
||||
gt,
|
||||
Ok(Self { tag, name, props })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlTagOpen {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let HtmlTagOpen { lt, gt, .. } = self;
|
||||
tokens.extend(quote! {#lt#gt});
|
||||
struct HtmlElementClose {
|
||||
tag: TagTokens,
|
||||
_name: TagName,
|
||||
}
|
||||
impl HtmlElementClose {
|
||||
fn to_spanned(&self) -> impl ToTokens {
|
||||
self.tag.to_spanned()
|
||||
}
|
||||
}
|
||||
|
||||
struct HtmlTagClose {
|
||||
lt: Token![<],
|
||||
div: Option<Token![/]>,
|
||||
tag_name: TagName,
|
||||
gt: Token![>],
|
||||
}
|
||||
|
||||
impl PeekValue<TagKey> for HtmlTagClose {
|
||||
impl PeekValue<TagKey> for HtmlElementClose {
|
||||
fn peek(cursor: Cursor) -> Option<TagKey> {
|
||||
let (punct, cursor) = cursor.punct()?;
|
||||
(punct.as_char() == '<').as_option()?;
|
||||
@ -592,39 +586,21 @@ impl PeekValue<TagKey> for HtmlTagClose {
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for HtmlTagClose {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
let lt = input.parse()?;
|
||||
let div = input.parse()?;
|
||||
let tag_name = input.parse()?;
|
||||
let gt = input.parse()?;
|
||||
impl Parse for HtmlElementClose {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
TagTokens::parse_end_content(input, |input, tag| {
|
||||
let name = input.parse()?;
|
||||
|
||||
if let TagName::Expr(name) = &tag_name {
|
||||
if let Some(expr) = &name.expr {
|
||||
return Err(syn::Error::new_spanned(
|
||||
if let TagName::Expr(name) = &name {
|
||||
if let Some(expr) = &name.expr {
|
||||
return Err(syn::Error::new_spanned(
|
||||
expr,
|
||||
"dynamic closing tags must not have a body (hint: replace it with just `</@>`)",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HtmlTagClose {
|
||||
lt,
|
||||
div,
|
||||
tag_name,
|
||||
gt,
|
||||
Ok(Self { tag, _name: name })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlTagClose {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let HtmlTagClose {
|
||||
lt,
|
||||
div,
|
||||
tag_name,
|
||||
gt,
|
||||
} = self;
|
||||
tokens.extend(quote! {#lt#div#tag_name#gt});
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ use boolinator::Boolinator;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote_spanned, ToTokens};
|
||||
use syn::buffer::Cursor;
|
||||
use syn::parse::{Parse, ParseStream, Result as ParseResult};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Expr, Token};
|
||||
|
||||
@ -18,7 +18,7 @@ impl PeekValue<()> for HtmlIterable {
|
||||
}
|
||||
|
||||
impl Parse for HtmlIterable {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let for_token = input.parse::<Token![for]>()?;
|
||||
|
||||
match input.parse() {
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
use super::{html_dashed_name::HtmlDashedName, HtmlChildrenTree};
|
||||
use crate::html_tree::{HtmlProp, HtmlPropSuffix};
|
||||
use crate::{Peek, PeekValue};
|
||||
use super::{html_dashed_name::HtmlDashedName, HtmlChildrenTree, TagTokens};
|
||||
use crate::{props::Prop, Peek, PeekValue};
|
||||
use boolinator::Boolinator;
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::buffer::Cursor;
|
||||
use syn::parse::{Parse, ParseStream, Result as ParseResult};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Expr, Token};
|
||||
use syn::Expr;
|
||||
|
||||
pub struct HtmlList {
|
||||
open: HtmlListOpen,
|
||||
@ -23,11 +22,11 @@ impl PeekValue<()> for HtmlList {
|
||||
}
|
||||
|
||||
impl Parse for HtmlList {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
if HtmlListClose::peek(input.cursor()).is_some() {
|
||||
return match input.parse::<HtmlListClose>() {
|
||||
Ok(close) => Err(syn::Error::new_spanned(
|
||||
close,
|
||||
close.to_spanned(),
|
||||
"this closing fragment has no corresponding opening fragment",
|
||||
)),
|
||||
Err(err) => Err(err),
|
||||
@ -40,7 +39,7 @@ impl Parse for HtmlList {
|
||||
children.parse_child(input)?;
|
||||
if input.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
open,
|
||||
open.to_spanned(),
|
||||
"this opening fragment has no corresponding closing fragment",
|
||||
));
|
||||
}
|
||||
@ -48,7 +47,7 @@ impl Parse for HtmlList {
|
||||
|
||||
let close = input.parse::<HtmlListClose>()?;
|
||||
|
||||
Ok(HtmlList {
|
||||
Ok(Self {
|
||||
open,
|
||||
children,
|
||||
close,
|
||||
@ -70,8 +69,13 @@ impl ToTokens for HtmlList {
|
||||
quote! {None}
|
||||
};
|
||||
|
||||
let open_close_tokens = quote! {#open#close};
|
||||
tokens.extend(quote_spanned! {open_close_tokens.span()=>
|
||||
let spanned = {
|
||||
let open = open.to_spanned();
|
||||
let close = close.to_spanned();
|
||||
quote! { #open#close }
|
||||
};
|
||||
|
||||
tokens.extend(quote_spanned! {spanned.span()=>
|
||||
::yew::virtual_dom::VNode::VList(
|
||||
::yew::virtual_dom::VList::new_with_children(#children, #key)
|
||||
)
|
||||
@ -80,9 +84,13 @@ impl ToTokens for HtmlList {
|
||||
}
|
||||
|
||||
struct HtmlListOpen {
|
||||
lt: Token![<],
|
||||
tag: TagTokens,
|
||||
props: HtmlListProps,
|
||||
gt: Token![>],
|
||||
}
|
||||
impl HtmlListOpen {
|
||||
fn to_spanned(&self) -> impl ToTokens {
|
||||
self.tag.to_spanned()
|
||||
}
|
||||
}
|
||||
|
||||
impl PeekValue<()> for HtmlListOpen {
|
||||
@ -101,18 +109,11 @@ impl PeekValue<()> for HtmlListOpen {
|
||||
}
|
||||
|
||||
impl Parse for HtmlListOpen {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
let lt = input.parse()?;
|
||||
let HtmlPropSuffix { stream, gt, .. } = input.parse()?;
|
||||
let props = syn::parse2(stream)?;
|
||||
Ok(Self { lt, props, gt })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlListOpen {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let HtmlListOpen { lt, gt, .. } = self;
|
||||
tokens.extend(quote! {#lt#gt});
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
TagTokens::parse_start_content(input, |input, tag| {
|
||||
let props = input.parse()?;
|
||||
Ok(Self { tag, props })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,11 +121,11 @@ struct HtmlListProps {
|
||||
key: Option<Expr>,
|
||||
}
|
||||
impl Parse for HtmlListProps {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let key = if input.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let prop: HtmlProp = input.parse()?;
|
||||
let prop: Prop = input.parse()?;
|
||||
if !input.is_empty() {
|
||||
return Err(input.error("only a single `key` prop is allowed on a fragment"));
|
||||
}
|
||||
@ -145,12 +146,12 @@ impl Parse for HtmlListProps {
|
||||
}
|
||||
}
|
||||
|
||||
struct HtmlListClose {
|
||||
lt: Token![<],
|
||||
div: Token![/],
|
||||
gt: Token![>],
|
||||
struct HtmlListClose(TagTokens);
|
||||
impl HtmlListClose {
|
||||
fn to_spanned(&self) -> impl ToTokens {
|
||||
self.0.to_spanned()
|
||||
}
|
||||
}
|
||||
|
||||
impl PeekValue<()> for HtmlListClose {
|
||||
fn peek(cursor: Cursor) -> Option<()> {
|
||||
let (punct, cursor) = cursor.punct()?;
|
||||
@ -162,20 +163,14 @@ impl PeekValue<()> for HtmlListClose {
|
||||
(punct.as_char() == '>').as_option()
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for HtmlListClose {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
Ok(HtmlListClose {
|
||||
lt: input.parse()?,
|
||||
div: input.parse()?,
|
||||
gt: input.parse()?,
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
TagTokens::parse_end_content(input, |input, tag| {
|
||||
if !input.is_empty() {
|
||||
Err(input.error("unexpected content in list close"))
|
||||
} else {
|
||||
Ok(Self(tag))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlListClose {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let HtmlListClose { lt, div, gt } = self;
|
||||
tokens.extend(quote! {#lt#div#gt});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,106 +0,0 @@
|
||||
use crate::html_tree::HtmlDashedName as HtmlPropLabel;
|
||||
use crate::{Peek, PeekValue};
|
||||
use proc_macro2::{TokenStream, TokenTree};
|
||||
use syn::buffer::Cursor;
|
||||
use syn::parse::{Parse, ParseStream, Result as ParseResult};
|
||||
use syn::{Expr, Token};
|
||||
|
||||
pub struct HtmlProp {
|
||||
pub label: HtmlPropLabel,
|
||||
pub question_mark: Option<Token![?]>,
|
||||
pub value: Expr,
|
||||
}
|
||||
impl HtmlProp {
|
||||
/// Checks if the prop uses the optional attribute syntax.
|
||||
/// If it does, an error is returned.
|
||||
pub fn ensure_not_optional(&self) -> syn::Result<()> {
|
||||
if self.question_mark.is_some() {
|
||||
let msg = format!(
|
||||
"the `{}` attribute does not support being used as an optional attribute",
|
||||
self.label
|
||||
);
|
||||
Err(syn::Error::new_spanned(&self.label, msg))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PeekValue<()> for HtmlProp {
|
||||
fn peek(cursor: Cursor) -> Option<()> {
|
||||
HtmlPropLabel::peek(cursor).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for HtmlProp {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
let label = input.parse::<HtmlPropLabel>()?;
|
||||
let question_mark = input.parse::<Token![?]>().ok();
|
||||
let equals = input
|
||||
.parse::<Token![=]>()
|
||||
.map_err(|_| syn::Error::new_spanned(&label, "this prop doesn't have a value"))?;
|
||||
if input.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
equals,
|
||||
"expected an expression following this equals sign",
|
||||
));
|
||||
}
|
||||
let value = input.parse::<Expr>()?;
|
||||
// backwards compat
|
||||
let _ = input.parse::<Token![,]>();
|
||||
Ok(Self {
|
||||
label,
|
||||
question_mark,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HtmlPropSuffix {
|
||||
pub stream: TokenStream,
|
||||
pub div: Option<Token![/]>,
|
||||
pub gt: Token![>],
|
||||
}
|
||||
|
||||
impl Parse for HtmlPropSuffix {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
let mut trees: Vec<TokenTree> = vec![];
|
||||
let mut div: Option<Token![/]> = None;
|
||||
let mut angle_count = 1;
|
||||
let gt: Option<Token![>]>;
|
||||
|
||||
loop {
|
||||
let next = input.parse()?;
|
||||
if let TokenTree::Punct(punct) = &next {
|
||||
match punct.as_char() {
|
||||
'>' => {
|
||||
angle_count -= 1;
|
||||
if angle_count == 0 {
|
||||
gt = Some(syn::token::Gt {
|
||||
spans: [punct.span()],
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
'<' => angle_count += 1,
|
||||
'/' => {
|
||||
if angle_count == 1 && input.peek(Token![>]) {
|
||||
div = Some(syn::token::Div {
|
||||
spans: [punct.span()],
|
||||
});
|
||||
gt = Some(input.parse()?);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
trees.push(next);
|
||||
}
|
||||
|
||||
let gt: Token![>] = gt.ok_or_else(|| input.error("missing tag close"))?;
|
||||
let stream: TokenStream = trees.into_iter().collect();
|
||||
|
||||
Ok(HtmlPropSuffix { stream, div, gt })
|
||||
}
|
||||
}
|
||||
@ -1,33 +1,33 @@
|
||||
mod html_block;
|
||||
mod html_component;
|
||||
mod html_dashed_name;
|
||||
mod html_iterable;
|
||||
mod html_list;
|
||||
mod html_node;
|
||||
mod html_prop;
|
||||
mod html_tag;
|
||||
|
||||
use crate::PeekValue;
|
||||
use html_block::HtmlBlock;
|
||||
use html_component::HtmlComponent;
|
||||
use html_dashed_name::HtmlDashedName;
|
||||
use html_iterable::HtmlIterable;
|
||||
use html_list::HtmlList;
|
||||
use html_node::HtmlNode;
|
||||
use html_prop::HtmlProp;
|
||||
use html_prop::HtmlPropSuffix;
|
||||
use html_tag::HtmlTag;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::buffer::Cursor;
|
||||
use syn::parse::{Parse, ParseStream, Result};
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
mod html_block;
|
||||
mod html_component;
|
||||
mod html_dashed_name;
|
||||
mod html_element;
|
||||
mod html_iterable;
|
||||
mod html_list;
|
||||
mod html_node;
|
||||
mod tag;
|
||||
|
||||
use html_block::HtmlBlock;
|
||||
use html_component::HtmlComponent;
|
||||
pub use html_dashed_name::HtmlDashedName;
|
||||
use html_element::HtmlElement;
|
||||
use html_iterable::HtmlIterable;
|
||||
use html_list::HtmlList;
|
||||
use html_node::HtmlNode;
|
||||
use tag::TagTokens;
|
||||
|
||||
pub enum HtmlType {
|
||||
Block,
|
||||
Component,
|
||||
List,
|
||||
Tag,
|
||||
Element,
|
||||
Empty,
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ pub enum HtmlTree {
|
||||
Block(Box<HtmlBlock>),
|
||||
Component(Box<HtmlComponent>),
|
||||
List(Box<HtmlList>),
|
||||
Tag(Box<HtmlTag>),
|
||||
Element(Box<HtmlElement>),
|
||||
Empty,
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ impl Parse for HtmlTree {
|
||||
let html_tree = match html_type {
|
||||
HtmlType::Empty => HtmlTree::Empty,
|
||||
HtmlType::Component => HtmlTree::Component(Box::new(input.parse()?)),
|
||||
HtmlType::Tag => HtmlTree::Tag(Box::new(input.parse()?)),
|
||||
HtmlType::Element => HtmlTree::Element(Box::new(input.parse()?)),
|
||||
HtmlType::Block => HtmlTree::Block(Box::new(input.parse()?)),
|
||||
HtmlType::List => HtmlTree::List(Box::new(input.parse()?)),
|
||||
};
|
||||
@ -62,8 +62,8 @@ impl PeekValue<HtmlType> for HtmlTree {
|
||||
Some(HtmlType::List)
|
||||
} else if HtmlComponent::peek(cursor).is_some() {
|
||||
Some(HtmlType::Component)
|
||||
} else if HtmlTag::peek(cursor).is_some() {
|
||||
Some(HtmlType::Tag)
|
||||
} else if HtmlElement::peek(cursor).is_some() {
|
||||
Some(HtmlType::Element)
|
||||
} else if HtmlBlock::peek(cursor).is_some() {
|
||||
Some(HtmlType::Block)
|
||||
} else {
|
||||
@ -79,7 +79,7 @@ impl ToTokens for HtmlTree {
|
||||
::yew::virtual_dom::VNode::VList(::yew::virtual_dom::VList::new())
|
||||
}),
|
||||
HtmlTree::Component(comp) => comp.to_tokens(tokens),
|
||||
HtmlTree::Tag(tag) => tag.to_tokens(tokens),
|
||||
HtmlTree::Element(tag) => tag.to_tokens(tokens),
|
||||
HtmlTree::List(list) => list.to_tokens(tokens),
|
||||
HtmlTree::Block(block) => block.to_tokens(tokens),
|
||||
}
|
||||
|
||||
150
yew-macro/src/html_tree/tag.rs
Normal file
150
yew-macro/src/html_tree/tag.rs
Normal file
@ -0,0 +1,150 @@
|
||||
use proc_macro2::{Span, TokenStream, TokenTree};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{
|
||||
parse::{ParseStream, Parser},
|
||||
Token,
|
||||
};
|
||||
|
||||
/// Check whether two spans are equal.
|
||||
/// The implementation is really silly but I couldn't find another way to do it on stable.
|
||||
/// This check isn't required to be fully accurate so it's not the end of the world if it breaks.
|
||||
fn span_eq_hack(a: &Span, b: &Span) -> bool {
|
||||
format!("{:?}", a) == format!("{:?}", b)
|
||||
}
|
||||
|
||||
/// Change all occurrences of span `from` to `to` in the given error.
|
||||
fn error_replace_span(err: syn::Error, from: Span, to: impl ToTokens) -> syn::Error {
|
||||
let err_it = err.into_iter().map(|err| {
|
||||
if span_eq_hack(&err.span(), &from) {
|
||||
syn::Error::new_spanned(&to, err.to_string())
|
||||
} else {
|
||||
err
|
||||
}
|
||||
});
|
||||
|
||||
// SAFETY: all errors have at least one message
|
||||
crate::join_errors(err_it).unwrap_err()
|
||||
}
|
||||
|
||||
/// Helper type for parsing HTML tags.
|
||||
/// The struct only stores the associated tokens, not the content of the tag.
|
||||
/// This is meant to mirror the design of delimiters in `syn`.
|
||||
pub struct TagTokens {
|
||||
pub lt: Token![<],
|
||||
pub div: Option<Token![/]>,
|
||||
pub gt: Token![>],
|
||||
}
|
||||
impl TagTokens {
|
||||
/// Parse the content of a start tag.
|
||||
/// The given parse function is called with a `ParseStream`
|
||||
/// containing only the contents of the tag and surrounding `TagTokens`.
|
||||
pub fn parse_start_content<T>(
|
||||
input: ParseStream,
|
||||
parse: impl FnOnce(ParseStream, Self) -> syn::Result<T>,
|
||||
) -> syn::Result<T> {
|
||||
Self::parse_content(Self::parse_start(input)?, parse)
|
||||
}
|
||||
|
||||
/// Same as `parse_start_content` but for end tags.
|
||||
pub fn parse_end_content<T>(
|
||||
input: ParseStream,
|
||||
parse: impl FnOnce(ParseStream, Self) -> syn::Result<T>,
|
||||
) -> syn::Result<T> {
|
||||
Self::parse_content(Self::parse_end(input)?, parse)
|
||||
}
|
||||
|
||||
fn parse_content<T>(
|
||||
(tag, content): (Self, TokenStream),
|
||||
parse: impl FnOnce(ParseStream, Self) -> syn::Result<T>,
|
||||
) -> syn::Result<T> {
|
||||
let scope_spanned = tag.to_spanned();
|
||||
let content_parser = |input: ParseStream| {
|
||||
parse(input, tag).map_err(|err| {
|
||||
// we can't modify the scope span used by `ParseStream`. It just uses the call site by default.
|
||||
// The scope span is used when an error can't be attributed to a token tree (ex. when the input is empty).
|
||||
// We rewrite all spans to point at the tag which at least narrows down the correct location.
|
||||
// It's not ideal, but it'll have to do until `syn` gives us more access.
|
||||
error_replace_span(err, Span::call_site(), &scope_spanned)
|
||||
})
|
||||
};
|
||||
content_parser.parse2(content)
|
||||
}
|
||||
|
||||
/// Parse a start tag
|
||||
fn parse_start(input: ParseStream) -> syn::Result<(Self, TokenStream)> {
|
||||
let lt = input.parse()?;
|
||||
let (content, div, gt) = Self::parse_until_end(input)?;
|
||||
|
||||
Ok((Self { lt, div, gt }, content))
|
||||
}
|
||||
|
||||
/// Parse an end tag.
|
||||
/// `div` will always be `Some` for end tags.
|
||||
fn parse_end(input: ParseStream) -> syn::Result<(Self, TokenStream)> {
|
||||
let lt = input.parse()?;
|
||||
let div = Some(input.parse()?);
|
||||
|
||||
let (content, end_div, gt) = Self::parse_until_end(input)?;
|
||||
if end_div.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
end_div,
|
||||
"unexpected `/` in this end tag",
|
||||
));
|
||||
}
|
||||
|
||||
Ok((Self { lt, div, gt }, content))
|
||||
}
|
||||
|
||||
fn parse_until_end(
|
||||
input: ParseStream,
|
||||
) -> syn::Result<(TokenStream, Option<Token![/]>, Token![>])> {
|
||||
let mut inner_trees = Vec::new();
|
||||
let mut angle_count: usize = 1;
|
||||
let mut div: Option<Token![/]> = None;
|
||||
let gt: Token![>];
|
||||
|
||||
loop {
|
||||
let next = input.parse()?;
|
||||
if let TokenTree::Punct(punct) = &next {
|
||||
match punct.as_char() {
|
||||
'/' => {
|
||||
if angle_count == 1 && input.peek(Token![>]) {
|
||||
div = Some(syn::token::Div {
|
||||
spans: [punct.span()],
|
||||
});
|
||||
gt = input.parse()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
'>' => {
|
||||
angle_count = angle_count.checked_sub(1).ok_or_else(|| {
|
||||
syn::Error::new_spanned(
|
||||
punct,
|
||||
"this tag close has no corresponding tag open",
|
||||
)
|
||||
})?;
|
||||
if angle_count == 0 {
|
||||
gt = syn::token::Gt {
|
||||
spans: [punct.span()],
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
'<' => angle_count += 1,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
inner_trees.push(next);
|
||||
}
|
||||
|
||||
Ok((inner_trees.into_iter().collect(), div, gt))
|
||||
}
|
||||
|
||||
/// Generate tokens which can be used in `syn::Error::new_spanned` to span the entire tag.
|
||||
/// This is to work around the limitation of being unable to manually join spans on stable.
|
||||
pub fn to_spanned(&self) -> impl ToTokens {
|
||||
let Self { lt, gt, .. } = self;
|
||||
quote! {#lt#gt}
|
||||
}
|
||||
}
|
||||
@ -55,16 +55,15 @@
|
||||
//!
|
||||
//! Please refer to [https://github.com/yewstack/yew](https://github.com/yewstack/yew) for how to set this up.
|
||||
|
||||
#![recursion_limit = "128"]
|
||||
|
||||
mod derive_props;
|
||||
mod html_tree;
|
||||
mod props;
|
||||
mod stringify;
|
||||
|
||||
use derive_props::DerivePropsInput;
|
||||
use html_tree::{HtmlRoot, HtmlRootVNode};
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
use quote::ToTokens;
|
||||
use syn::buffer::Cursor;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
@ -86,6 +85,17 @@ fn non_capitalized_ascii(string: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Combine multiple `syn` errors into a single one.
|
||||
/// Returns `Result::Ok` if the given iterator is empty
|
||||
fn join_errors(mut it: impl Iterator<Item = syn::Error>) -> syn::Result<()> {
|
||||
it.next().map_or(Ok(()), |mut err| {
|
||||
for other in it {
|
||||
err.combine(other);
|
||||
}
|
||||
Err(err)
|
||||
})
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Properties, attributes(prop_or, prop_or_else, prop_or_default))]
|
||||
pub fn derive_props(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DerivePropsInput);
|
||||
@ -95,11 +105,17 @@ pub fn derive_props(input: TokenStream) -> TokenStream {
|
||||
#[proc_macro]
|
||||
pub fn html_nested(input: TokenStream) -> TokenStream {
|
||||
let root = parse_macro_input!(input as HtmlRoot);
|
||||
TokenStream::from(quote! {#root})
|
||||
TokenStream::from(root.into_token_stream())
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn html(input: TokenStream) -> TokenStream {
|
||||
let root = parse_macro_input!(input as HtmlRootVNode);
|
||||
TokenStream::from(quote! {#root})
|
||||
TokenStream::from(root.into_token_stream())
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn props(input: TokenStream) -> TokenStream {
|
||||
let props = parse_macro_input!(input as props::PropsMacroInput);
|
||||
TokenStream::from(props.into_token_stream())
|
||||
}
|
||||
|
||||
218
yew-macro/src/props/component.rs
Normal file
218
yew-macro/src/props/component.rs
Normal file
@ -0,0 +1,218 @@
|
||||
use super::{Prop, Props, SpecialProps};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use std::convert::TryFrom;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned,
|
||||
Expr, Token,
|
||||
};
|
||||
|
||||
mod kw {
|
||||
syn::custom_keyword!(with);
|
||||
}
|
||||
|
||||
pub struct WithProps {
|
||||
pub special: SpecialProps,
|
||||
pub with: kw::with,
|
||||
pub expr: Expr,
|
||||
}
|
||||
impl WithProps {
|
||||
/// Check if the `ParseStream` contains a `with expr` expression.
|
||||
/// This function advances the given `ParseStream`!
|
||||
fn contains_with_expr(input: ParseStream) -> bool {
|
||||
while !input.is_empty() {
|
||||
if input.peek(kw::with) && !input.peek2(Token![=]) {
|
||||
return true;
|
||||
}
|
||||
input.parse::<TokenTree>().ok();
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
impl Parse for WithProps {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut special = SpecialProps::default();
|
||||
let mut with_expr: Option<(kw::with, Expr)> = None;
|
||||
while !input.is_empty() {
|
||||
// no need to check if it's followed by `=` because `with` isn't a special prop
|
||||
if input.peek(kw::with) {
|
||||
let with = input.parse::<kw::with>()?;
|
||||
if input.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
with,
|
||||
"expected expression following this `with`",
|
||||
));
|
||||
}
|
||||
with_expr = Some((with, input.parse()?));
|
||||
} else {
|
||||
let prop = input.parse::<Prop>()?;
|
||||
|
||||
if let Some(slot) = special.get_slot_mut(&prop.label.to_string()) {
|
||||
if slot.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&prop.label,
|
||||
&format!("`{}` can only be set once", prop.label),
|
||||
));
|
||||
}
|
||||
slot.replace(prop);
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
prop.label,
|
||||
"Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`)",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (with, expr) =
|
||||
with_expr.ok_or_else(|| input.error("missing `with props` expression"))?;
|
||||
|
||||
Ok(Self {
|
||||
special,
|
||||
with,
|
||||
expr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ComponentProps {
|
||||
List(Props),
|
||||
With(Box<WithProps>),
|
||||
}
|
||||
impl ComponentProps {
|
||||
/// Get the special props supported by both variants
|
||||
pub fn special(&self) -> &SpecialProps {
|
||||
match self {
|
||||
Self::List(props) => &props.special,
|
||||
Self::With(props) => &props.special,
|
||||
}
|
||||
}
|
||||
|
||||
fn prop_validation_tokens(&self, props_ty: impl ToTokens, has_children: bool) -> TokenStream {
|
||||
let check_children = if has_children {
|
||||
Some(quote_spanned! {props_ty.span()=> __yew_props.children; })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let check_props = match self {
|
||||
Self::List(props) => props
|
||||
.iter()
|
||||
.map(|Prop { label, .. }| {
|
||||
quote_spanned! {label.span()=> __yew_props.#label; }
|
||||
})
|
||||
.collect(),
|
||||
Self::With(with_props) => {
|
||||
let expr = &with_props.expr;
|
||||
quote_spanned! {props_ty.span()=>
|
||||
let _: #props_ty = #expr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote_spanned! {props_ty.span()=>
|
||||
#[allow(clippy::no_effect)]
|
||||
if false {
|
||||
let _ = |__yew_props: #props_ty| {
|
||||
#check_children
|
||||
#check_props
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_properties_tokens<CR: ToTokens>(
|
||||
&self,
|
||||
props_ty: impl ToTokens,
|
||||
children_renderer: Option<CR>,
|
||||
) -> TokenStream {
|
||||
let validate_props = self.prop_validation_tokens(&props_ty, children_renderer.is_some());
|
||||
let build_props = match self {
|
||||
Self::List(props) => {
|
||||
let set_props = props.iter().map(|Prop { label, value, .. }| {
|
||||
quote_spanned! {value.span()=> .#label(
|
||||
#[allow(unused_braces)]
|
||||
<::yew::virtual_dom::VComp as ::yew::virtual_dom::Transformer<_, _>>::transform(
|
||||
#value
|
||||
)
|
||||
)}
|
||||
});
|
||||
|
||||
let set_children = if let Some(children) = children_renderer {
|
||||
Some(quote_spanned! {props_ty.span()=>
|
||||
.children(#children)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
quote_spanned! {props_ty.span()=>
|
||||
<#props_ty as ::yew::html::Properties>::builder()
|
||||
#(#set_props)*
|
||||
#set_children
|
||||
.build()
|
||||
}
|
||||
}
|
||||
Self::With(with_props) => {
|
||||
let ident = Ident::new("__yew_props", props_ty.span());
|
||||
let set_children = if let Some(children) = children_renderer {
|
||||
Some(quote_spanned! {props_ty.span()=>
|
||||
#ident.children = #children;
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let expr = &with_props.expr;
|
||||
quote! {
|
||||
let mut #ident = #expr;
|
||||
#set_children
|
||||
#ident
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
{
|
||||
#validate_props
|
||||
#build_props
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Parse for ComponentProps {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
if WithProps::contains_with_expr(&input.fork()) {
|
||||
input.parse().map(Self::With)
|
||||
} else {
|
||||
input.parse::<Props>().and_then(Self::try_from)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Props> for ComponentProps {
|
||||
type Error = syn::Error;
|
||||
|
||||
fn try_from(props: Props) -> Result<Self, Self::Error> {
|
||||
props.check_no_duplicates()?;
|
||||
props.check_all(|prop| {
|
||||
if prop.question_mark.is_some() {
|
||||
Err(syn::Error::new_spanned(
|
||||
&prop.label,
|
||||
"optional attributes are only supported on elements. Components can use `Option<T>` properties to accomplish the same thing.",
|
||||
))
|
||||
} else if !prop.label.extended.is_empty() {
|
||||
Err(syn::Error::new_spanned(
|
||||
&prop.label,
|
||||
"expected a valid Rust identifier",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Self::List(props))
|
||||
}
|
||||
}
|
||||
@ -1,27 +1,95 @@
|
||||
use crate::html_tree::HtmlProp as TagAttribute;
|
||||
use crate::PeekValue;
|
||||
use super::{Prop, Props, SpecialProps};
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashSet;
|
||||
use std::iter::FromIterator;
|
||||
use syn::parse::{Parse, ParseStream, Result as ParseResult};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::{Expr, ExprTuple};
|
||||
|
||||
pub struct TagAttributes {
|
||||
pub attributes: Vec<TagAttribute>,
|
||||
pub listeners: Vec<TagAttribute>,
|
||||
pub classes: Option<ClassesForm>,
|
||||
pub booleans: Vec<TagAttribute>,
|
||||
pub value: Option<TagAttribute>,
|
||||
pub kind: Option<TagAttribute>,
|
||||
pub checked: Option<Expr>,
|
||||
pub node_ref: Option<Expr>,
|
||||
pub key: Option<Expr>,
|
||||
}
|
||||
|
||||
pub enum ClassesForm {
|
||||
Tuple(Vec<Expr>),
|
||||
Single(Box<Expr>),
|
||||
}
|
||||
impl ClassesForm {
|
||||
fn from_expr(expr: Expr) -> Self {
|
||||
match expr {
|
||||
Expr::Tuple(ExprTuple { elems, .. }) => ClassesForm::Tuple(elems.into_iter().collect()),
|
||||
expr => ClassesForm::Single(Box::new(expr)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ElementProps {
|
||||
pub attributes: Vec<Prop>,
|
||||
pub listeners: Vec<Prop>,
|
||||
pub classes: Option<ClassesForm>,
|
||||
pub booleans: Vec<Prop>,
|
||||
pub value: Option<Prop>,
|
||||
pub kind: Option<Prop>,
|
||||
pub checked: Option<Prop>,
|
||||
pub node_ref: Option<Prop>,
|
||||
pub key: Option<Prop>,
|
||||
}
|
||||
|
||||
impl Parse for ElementProps {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut props = input.parse::<Props>()?;
|
||||
|
||||
let listeners =
|
||||
props.drain_filter(|prop| LISTENER_SET.contains(prop.label.to_string().as_str()));
|
||||
#[cfg(feature = "std_web")]
|
||||
listeners.check_all(|prop| {
|
||||
let label = &prop.label;
|
||||
if UNSUPPORTED_LISTENER_SET.contains(label.to_string().as_str()) {
|
||||
Err(syn::Error::new_spanned(
|
||||
&label,
|
||||
format!(
|
||||
"the listener `{}` is only available when using web-sys",
|
||||
&label
|
||||
),
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
|
||||
// Multiple listener attributes are allowed, but no others
|
||||
props.check_no_duplicates()?;
|
||||
|
||||
let booleans =
|
||||
props.drain_filter(|prop| BOOLEAN_SET.contains(prop.label.to_string().as_str()));
|
||||
booleans.check_all(|prop| {
|
||||
if prop.question_mark.is_some() {
|
||||
Err(syn::Error::new_spanned(
|
||||
&prop.label,
|
||||
"boolean attributes don't support being used as an optional attribute (hint: a value of false results in the attribute not being set)"
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
|
||||
let classes = props
|
||||
.pop_nonoptional("class")?
|
||||
.map(|prop| ClassesForm::from_expr(prop.value));
|
||||
let value = props.pop("value");
|
||||
let kind = props.pop("type");
|
||||
let checked = props.pop_nonoptional("checked")?;
|
||||
|
||||
let SpecialProps { node_ref, key } = props.special;
|
||||
|
||||
Ok(Self {
|
||||
attributes: props.prop_list.into_vec(),
|
||||
classes,
|
||||
listeners: listeners.into_vec(),
|
||||
checked,
|
||||
booleans: booleans.into_vec(),
|
||||
value,
|
||||
kind,
|
||||
node_ref,
|
||||
key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref BOOLEAN_SET: HashSet<&'static str> = {
|
||||
@ -211,138 +279,3 @@ lazy_static! {
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
impl TagAttributes {
|
||||
fn drain_listeners(attrs: &mut Vec<TagAttribute>) -> Vec<TagAttribute> {
|
||||
let mut i = 0;
|
||||
let mut drained = Vec::new();
|
||||
while i < attrs.len() {
|
||||
let name_str = attrs[i].label.to_string();
|
||||
if LISTENER_SET.contains(&name_str.as_str()) {
|
||||
drained.push(attrs.remove(i));
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
drained
|
||||
}
|
||||
|
||||
fn drain_boolean(attrs: &mut Vec<TagAttribute>) -> Vec<TagAttribute> {
|
||||
let mut i = 0;
|
||||
let mut drained = Vec::new();
|
||||
while i < attrs.len() {
|
||||
let name_str = attrs[i].label.to_string();
|
||||
if BOOLEAN_SET.contains(&name_str.as_str()) {
|
||||
drained.push(attrs.remove(i));
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
drained
|
||||
}
|
||||
|
||||
fn remove_attr(attrs: &mut Vec<TagAttribute>, name: &str) -> Option<TagAttribute> {
|
||||
let mut i = 0;
|
||||
while i < attrs.len() {
|
||||
if attrs[i].label.to_string() == name {
|
||||
return Some(attrs.remove(i));
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn remove_attr_nonoptional(
|
||||
attrs: &mut Vec<TagAttribute>,
|
||||
name: &str,
|
||||
) -> syn::Result<Option<TagAttribute>> {
|
||||
match Self::remove_attr(attrs, name) {
|
||||
Some(attr) => attr.ensure_not_optional().map(|_| Some(attr)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_classes(class_expr: Expr) -> ClassesForm {
|
||||
match class_expr {
|
||||
Expr::Tuple(ExprTuple { elems, .. }) => ClassesForm::Tuple(elems.into_iter().collect()),
|
||||
expr => ClassesForm::Single(Box::new(expr)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for TagAttributes {
|
||||
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||
let mut attributes: Vec<TagAttribute> = Vec::new();
|
||||
while TagAttribute::peek(input.cursor()).is_some() {
|
||||
attributes.push(input.parse::<TagAttribute>()?);
|
||||
}
|
||||
|
||||
let mut listeners = Vec::new();
|
||||
for listener in Self::drain_listeners(&mut attributes) {
|
||||
#[cfg(feature = "std_web")]
|
||||
{
|
||||
let label = &listener.label;
|
||||
if UNSUPPORTED_LISTENER_SET.contains(&label.to_string().as_str()) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&label,
|
||||
format!(
|
||||
"the listener `{}` is only available when using web-sys",
|
||||
&label
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
listeners.push(listener);
|
||||
}
|
||||
|
||||
// Multiple listener attributes are allowed, but no others
|
||||
attributes.sort_by(|a, b| {
|
||||
a.label
|
||||
.to_string()
|
||||
.partial_cmp(&b.label.to_string())
|
||||
.unwrap()
|
||||
});
|
||||
let mut i = 0;
|
||||
while i + 1 < attributes.len() {
|
||||
if attributes[i].label.to_string() == attributes[i + 1].label.to_string() {
|
||||
let label = &attributes[i + 1].label;
|
||||
return Err(syn::Error::new_spanned(
|
||||
label,
|
||||
format!("the attribute `{}` can only be specified once", label),
|
||||
));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let booleans = Self::drain_boolean(&mut attributes);
|
||||
for attr in &booleans {
|
||||
if attr.question_mark.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&attr.label,
|
||||
"boolean attributes don't support being used as an optional attribute (hint: a value of false results in the attribute not being set)"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let classes = Self::remove_attr_nonoptional(&mut attributes, "class")?
|
||||
.map(|a| Self::map_classes(a.value));
|
||||
let value = Self::remove_attr(&mut attributes, "value");
|
||||
let kind = Self::remove_attr(&mut attributes, "type");
|
||||
let checked = Self::remove_attr_nonoptional(&mut attributes, "checked")?.map(|v| v.value);
|
||||
let node_ref = Self::remove_attr_nonoptional(&mut attributes, "ref")?.map(|v| v.value);
|
||||
let key = Self::remove_attr_nonoptional(&mut attributes, "key")?.map(|v| v.value);
|
||||
|
||||
Ok(Self {
|
||||
attributes,
|
||||
classes,
|
||||
listeners,
|
||||
checked,
|
||||
booleans,
|
||||
value,
|
||||
kind,
|
||||
node_ref,
|
||||
key,
|
||||
})
|
||||
}
|
||||
}
|
||||
9
yew-macro/src/props/mod.rs
Normal file
9
yew-macro/src/props/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
mod component;
|
||||
mod element;
|
||||
mod prop;
|
||||
mod prop_macro;
|
||||
|
||||
pub use component::*;
|
||||
pub use element::*;
|
||||
pub use prop::*;
|
||||
pub use prop_macro::PropsMacroInput;
|
||||
280
yew-macro/src/props/prop.rs
Normal file
280
yew-macro/src/props/prop.rs
Normal file
@ -0,0 +1,280 @@
|
||||
use crate::html_tree::HtmlDashedName;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
convert::TryFrom,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Expr, Token,
|
||||
};
|
||||
|
||||
pub enum PropPunct {
|
||||
Eq(Token![=]),
|
||||
Colon(Token![:]),
|
||||
}
|
||||
impl ToTokens for PropPunct {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
Self::Eq(p) => p.to_tokens(tokens),
|
||||
Self::Colon(p) => p.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Prop {
|
||||
pub label: HtmlDashedName,
|
||||
pub question_mark: Option<Token![?]>,
|
||||
/// Punctuation between `label` and `value`.
|
||||
pub punct: Option<PropPunct>,
|
||||
pub value: Expr,
|
||||
}
|
||||
impl Prop {
|
||||
/// Checks if the prop uses the optional attribute syntax.
|
||||
/// If it does, an error is returned.
|
||||
pub fn ensure_not_optional(&self) -> syn::Result<()> {
|
||||
let Self {
|
||||
label,
|
||||
question_mark,
|
||||
punct,
|
||||
..
|
||||
} = self;
|
||||
if question_mark.is_some() {
|
||||
let msg = format!(
|
||||
"`{}` does not support being used as an optional attribute",
|
||||
label
|
||||
);
|
||||
// include `?=` in the span
|
||||
Err(syn::Error::new_spanned(
|
||||
quote! { #label#question_mark#punct },
|
||||
msg,
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Parse for Prop {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let label = input.parse::<HtmlDashedName>()?;
|
||||
let question_mark = input.parse::<Token![?]>().ok();
|
||||
let equals = input.parse::<Token![=]>().map_err(|_| {
|
||||
syn::Error::new_spanned(
|
||||
&label,
|
||||
format!("`{}` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes)", label),
|
||||
)
|
||||
})?;
|
||||
if input.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
equals,
|
||||
"expected an expression following this equals sign",
|
||||
));
|
||||
}
|
||||
let value = input.parse::<Expr>()?;
|
||||
Ok(Self {
|
||||
label,
|
||||
question_mark,
|
||||
punct: Some(PropPunct::Eq(equals)),
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// List of props sorted in alphabetical order*.
|
||||
///
|
||||
/// \*The "children" prop always comes last to match the behaviour of the `Properties` derive macro.
|
||||
///
|
||||
/// The list may contain multiple props with the same label.
|
||||
/// Use `check_no_duplicates` to ensure that there are no duplicates.
|
||||
pub struct SortedPropList(Vec<Prop>);
|
||||
impl SortedPropList {
|
||||
const CHILDREN_LABEL: &'static str = "children";
|
||||
|
||||
/// Create a new `SortedPropList` from a vector of props.
|
||||
/// The given `props` doesn't need to be sorted.
|
||||
pub fn new(mut props: Vec<Prop>) -> Self {
|
||||
props.sort_by(|a, b| Self::cmp_label(&a.label.to_string(), &b.label.to_string()));
|
||||
Self(props)
|
||||
}
|
||||
|
||||
fn cmp_label(a: &str, b: &str) -> Ordering {
|
||||
if a == b {
|
||||
Ordering::Equal
|
||||
} else if a == Self::CHILDREN_LABEL {
|
||||
Ordering::Greater
|
||||
} else if b == Self::CHILDREN_LABEL {
|
||||
Ordering::Less
|
||||
} else {
|
||||
a.cmp(b)
|
||||
}
|
||||
}
|
||||
|
||||
fn position(&self, key: &str) -> Option<usize> {
|
||||
self.0
|
||||
.binary_search_by(|prop| Self::cmp_label(prop.label.to_string().as_str(), key))
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Get the first prop with the given key.
|
||||
pub fn get_by_label(&self, key: &str) -> Option<&Prop> {
|
||||
self.position(key).and_then(|i| self.0.get(i))
|
||||
}
|
||||
|
||||
/// Pop the first prop with the given key.
|
||||
pub fn pop(&mut self, key: &str) -> Option<Prop> {
|
||||
self.position(key).map(|i| self.0.remove(i))
|
||||
}
|
||||
|
||||
/// Pop the prop with the given key and error if there are multiple ones.
|
||||
pub fn pop_unique(&mut self, key: &str) -> syn::Result<Option<Prop>> {
|
||||
let prop = self.pop(key);
|
||||
if prop.is_some() {
|
||||
if let Some(other_prop) = self.get_by_label(key) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&other_prop.label,
|
||||
format!("`{}` can only be specified once", key),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(prop)
|
||||
}
|
||||
/// Pop the prop with the given key and error if it uses the optional attribute syntax.
|
||||
pub fn pop_nonoptional(&mut self, key: &str) -> syn::Result<Option<Prop>> {
|
||||
match self.pop_unique(key) {
|
||||
Ok(Some(prop)) => {
|
||||
prop.ensure_not_optional()?;
|
||||
Ok(Some(prop))
|
||||
}
|
||||
res => res,
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn the props into a vector of `Prop`.
|
||||
pub fn into_vec(self) -> Vec<Prop> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Iterate over all duplicate props in order of appearance.
|
||||
fn iter_duplicates(&self) -> impl Iterator<Item = &Prop> {
|
||||
self.0.windows(2).filter_map(|pair| {
|
||||
let (a, b) = (&pair[0], &pair[1]);
|
||||
|
||||
if a.label == b.label {
|
||||
Some(b)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove and return all props for which `filter` returns `true`.
|
||||
pub fn drain_filter(&mut self, filter: impl FnMut(&Prop) -> bool) -> Self {
|
||||
let (drained, others) = self.0.drain(..).partition(filter);
|
||||
self.0 = others;
|
||||
Self(drained)
|
||||
}
|
||||
|
||||
/// Run the given function for all props and aggregate the errors.
|
||||
/// If there's at least one error, the result will be `Result::Err`.
|
||||
pub fn check_all(&self, f: impl FnMut(&Prop) -> syn::Result<()>) -> syn::Result<()> {
|
||||
crate::join_errors(self.0.iter().map(f).filter_map(Result::err))
|
||||
}
|
||||
|
||||
/// Return an error for all duplicate props.
|
||||
pub fn check_no_duplicates(&self) -> syn::Result<()> {
|
||||
crate::join_errors(self.iter_duplicates().map(|prop| {
|
||||
syn::Error::new_spanned(
|
||||
&prop.label,
|
||||
format!(
|
||||
"`{}` can only be specified once but is given here again",
|
||||
prop.label
|
||||
),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
impl Parse for SortedPropList {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut props: Vec<Prop> = Vec::new();
|
||||
while !input.is_empty() {
|
||||
props.push(input.parse()?);
|
||||
}
|
||||
|
||||
Ok(Self::new(props))
|
||||
}
|
||||
}
|
||||
impl Deref for SortedPropList {
|
||||
type Target = [Prop];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SpecialProps {
|
||||
pub node_ref: Option<Prop>,
|
||||
pub key: Option<Prop>,
|
||||
}
|
||||
impl SpecialProps {
|
||||
const REF_LABEL: &'static str = "ref";
|
||||
const KEY_LABEL: &'static str = "key";
|
||||
|
||||
fn pop_from(props: &mut SortedPropList) -> syn::Result<Self> {
|
||||
let node_ref = props.pop_nonoptional(Self::REF_LABEL)?;
|
||||
let key = props.pop_nonoptional(Self::KEY_LABEL)?;
|
||||
Ok(Self { node_ref, key })
|
||||
}
|
||||
|
||||
pub fn get_slot_mut(&mut self, key: &str) -> Option<&mut Option<Prop>> {
|
||||
match key {
|
||||
Self::REF_LABEL => Some(&mut self.node_ref),
|
||||
Self::KEY_LABEL => Some(&mut self.key),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = &Prop> {
|
||||
self.node_ref.as_ref().into_iter().chain(self.key.as_ref())
|
||||
}
|
||||
|
||||
/// Run the given function for all props and aggregate the errors.
|
||||
/// If there's at least one error, the result will be `Result::Err`.
|
||||
pub fn check_all(&self, f: impl FnMut(&Prop) -> syn::Result<()>) -> syn::Result<()> {
|
||||
crate::join_errors(self.iter().map(f).filter_map(Result::err))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Props {
|
||||
pub special: SpecialProps,
|
||||
pub prop_list: SortedPropList,
|
||||
}
|
||||
impl Parse for Props {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
Self::try_from(input.parse::<SortedPropList>()?)
|
||||
}
|
||||
}
|
||||
impl Deref for Props {
|
||||
type Target = SortedPropList;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.prop_list
|
||||
}
|
||||
}
|
||||
impl DerefMut for Props {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.prop_list
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<SortedPropList> for Props {
|
||||
type Error = syn::Error;
|
||||
|
||||
fn try_from(mut prop_list: SortedPropList) -> Result<Self, Self::Error> {
|
||||
let special = SpecialProps::pop_from(&mut prop_list)?;
|
||||
Ok(Self { special, prop_list })
|
||||
}
|
||||
}
|
||||
140
yew-macro/src/props/prop_macro.rs
Normal file
140
yew-macro/src/props/prop_macro.rs
Normal file
@ -0,0 +1,140 @@
|
||||
use super::{ComponentProps, Prop, PropPunct, Props, SortedPropList};
|
||||
use crate::html_tree::HtmlDashedName;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote_spanned, ToTokens};
|
||||
use std::convert::TryInto;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
token::Brace,
|
||||
Expr, Token, TypePath,
|
||||
};
|
||||
|
||||
/// Pop from `Punctuated` without leaving it in a state where it has trailing punctuation.
|
||||
fn pop_last_punctuated<T, P>(punctuated: &mut Punctuated<T, P>) -> Option<T> {
|
||||
let value = punctuated.pop().map(|pair| pair.into_value());
|
||||
// remove the 2nd last value and push it right back to remove the trailing punctuation
|
||||
if let Some(pair) = punctuated.pop() {
|
||||
punctuated.push_value(pair.into_value());
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
/// Check if the given type path looks like an associated `Properties` type.
|
||||
fn is_associated_properties(ty: &TypePath) -> bool {
|
||||
let mut segments_it = ty.path.segments.iter();
|
||||
if let Some(seg) = segments_it.next_back() {
|
||||
// if the last segment is `Properties` ...
|
||||
if seg.ident == "Properties" {
|
||||
if let Some(seg) = segments_it.next_back() {
|
||||
// ... and we can be reasonably sure that the previous segment is a component ...
|
||||
if !crate::non_capitalized_ascii(&seg.ident.to_string()) {
|
||||
// ... then we assume that this is an associated type like `Component::Properties`
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
struct PropValue {
|
||||
label: HtmlDashedName,
|
||||
colon_token: Option<Token![:]>,
|
||||
value: Expr,
|
||||
}
|
||||
impl Parse for PropValue {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let label = input.parse()?;
|
||||
let (colon_token, value) = if input.peek(Token![:]) {
|
||||
let colon_token = input.parse()?;
|
||||
let value = input.parse()?;
|
||||
(Some(colon_token), value)
|
||||
} else {
|
||||
let value = syn::parse_quote!(#label);
|
||||
(None, value)
|
||||
};
|
||||
Ok(Self {
|
||||
label,
|
||||
colon_token,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Prop> for PropValue {
|
||||
fn into(self) -> Prop {
|
||||
let Self {
|
||||
label,
|
||||
colon_token,
|
||||
value,
|
||||
} = self;
|
||||
Prop {
|
||||
label,
|
||||
question_mark: None,
|
||||
punct: colon_token.map(PropPunct::Colon),
|
||||
value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PropsExpr {
|
||||
ty: TypePath,
|
||||
_brace_token: Brace,
|
||||
fields: Punctuated<PropValue, Token![,]>,
|
||||
}
|
||||
impl Parse for PropsExpr {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut ty: TypePath = input.parse()?;
|
||||
|
||||
// if the type isn't already qualified (`<x as y>`) and it's an associated type (`MyComp::Properties`) ...
|
||||
if ty.qself.is_none() && is_associated_properties(&ty) {
|
||||
pop_last_punctuated(&mut ty.path.segments);
|
||||
// .. transform it into a "qualified-self" type
|
||||
ty = syn::parse2(quote_spanned! {ty.span()=>
|
||||
<#ty as ::yew::html::Component>::Properties
|
||||
})?;
|
||||
}
|
||||
|
||||
let content;
|
||||
let brace_token = syn::braced!(content in input);
|
||||
let fields = content.parse_terminated(PropValue::parse)?;
|
||||
Ok(Self {
|
||||
ty,
|
||||
_brace_token: brace_token,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PropsMacroInput {
|
||||
ty: TypePath,
|
||||
props: ComponentProps,
|
||||
}
|
||||
impl Parse for PropsMacroInput {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let PropsExpr { ty, fields, .. } = input.parse()?;
|
||||
let prop_list = SortedPropList::new(fields.into_iter().map(Into::into).collect());
|
||||
let props: Props = prop_list.try_into()?;
|
||||
props.special.check_all(|prop| {
|
||||
let label = &prop.label;
|
||||
Err(syn::Error::new_spanned(
|
||||
label,
|
||||
"special props cannot be specified in the `props!` macro",
|
||||
))
|
||||
})?;
|
||||
Ok(Self {
|
||||
ty,
|
||||
props: props.try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl ToTokens for PropsMacroInput {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self { ty, props } = self;
|
||||
|
||||
tokens.extend(props.build_properties_tokens(ty, None::<TokenStream>))
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
#![recursion_limit = "128"]
|
||||
|
||||
use std::marker::PhantomData;
|
||||
use yew::html::ChildrenRenderer;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Clone, Properties, PartialEq)]
|
||||
@ -89,13 +88,13 @@ fn compile_fail() {
|
||||
html! { <Child value=1 ref=() with props ref=() /> };
|
||||
html! { <Child ref=() ref=() value=1 with props /> };
|
||||
html! { <Child with blah /> };
|
||||
html! { <Child with props () /> };
|
||||
html! { <Child value=1 with props /> };
|
||||
html! { <Child with props value=1 /> };
|
||||
html! { <Child type=0 /> };
|
||||
html! { <Child invalid-prop-name=0 /> };
|
||||
html! { <Child unknown="unknown" /> };
|
||||
html! { <Child string= /> };
|
||||
html! { <Child int=1 int=2 int=3 /> };
|
||||
html! { <Child int=1 string={} /> };
|
||||
html! { <Child int=1 string=3 /> };
|
||||
html! { <Child int=1 string={3} /> };
|
||||
@ -108,12 +107,27 @@ fn compile_fail() {
|
||||
html! { <Child></Child><Child></Child> };
|
||||
html! { <Child>{ "Not allowed" }</Child> };
|
||||
|
||||
// trying to overwrite `children` on props which don't take any.
|
||||
html! {
|
||||
<Child with ChildProperties { string: "hello".to_owned(), int: 5 }>
|
||||
{ "please error" }
|
||||
</Child>
|
||||
};
|
||||
|
||||
html! { <ChildContainer /> };
|
||||
html! { <ChildContainer></ChildContainer> };
|
||||
html! { <ChildContainer>{ "Not allowed" }</ChildContainer> };
|
||||
html! { <ChildContainer><></></ChildContainer> };
|
||||
html! { <ChildContainer><other /></ChildContainer> };
|
||||
|
||||
// using `children` as a prop while simultaneously passing children using the syntactic sugar
|
||||
let children = ChildrenRenderer::new(vec![html_nested! { <Child int=0 /> }]);
|
||||
html! {
|
||||
<ChildContainer children=children>
|
||||
<Child int=1 />
|
||||
</ChildContainer>
|
||||
};
|
||||
|
||||
html! { <Generic<String>></Generic> };
|
||||
html! { <Generic<String>></Generic<Vec<String>>> };
|
||||
|
||||
|
||||
@ -1,187 +1,235 @@
|
||||
error: this opening tag has no corresponding closing tag
|
||||
--> $DIR/html-component-fail.rs:79:13
|
||||
--> $DIR/html-component-fail.rs:78:13
|
||||
|
|
||||
79 | html! { <Child> };
|
||||
78 | html! { <Child> };
|
||||
| ^^^^^^^
|
||||
|
||||
error: expected identifier
|
||||
--> $DIR/html-component-fail.rs:80:22
|
||||
error: unexpected end of input, expected identifier
|
||||
--> $DIR/html-component-fail.rs:79:13
|
||||
|
|
||||
80 | html! { <Child:: /> };
|
||||
| ^
|
||||
79 | html! { <Child:: /> };
|
||||
| ^^^^^^^^^^^
|
||||
|
||||
error: `with` must be followed by an identifier
|
||||
--> $DIR/html-component-fail.rs:81:20
|
||||
error: expected expression following this `with`
|
||||
--> $DIR/html-component-fail.rs:80:20
|
||||
|
|
||||
81 | html! { <Child with /> };
|
||||
80 | html! { <Child with /> };
|
||||
| ^^^^
|
||||
|
||||
error: this prop doesn't have a value
|
||||
--> $DIR/html-component-fail.rs:82:20
|
||||
error: `props` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes)
|
||||
--> $DIR/html-component-fail.rs:81:20
|
||||
|
|
||||
82 | html! { <Child props /> };
|
||||
81 | html! { <Child props /> };
|
||||
| ^^^^^
|
||||
|
||||
error: this opening tag has no corresponding closing tag
|
||||
--> $DIR/html-component-fail.rs:83:13
|
||||
--> $DIR/html-component-fail.rs:82:13
|
||||
|
|
||||
83 | html! { <Child with props > };
|
||||
82 | html! { <Child with props > };
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: too many refs set
|
||||
error: `ref` can only be set once
|
||||
--> $DIR/html-component-fail.rs:83:38
|
||||
|
|
||||
83 | html! { <Child with props ref=() ref=() /> };
|
||||
| ^^^
|
||||
|
||||
error: `ref` can only be set once
|
||||
--> $DIR/html-component-fail.rs:84:38
|
||||
|
|
||||
84 | html! { <Child with props ref=() ref=() /> };
|
||||
84 | html! { <Child with props ref=() ref=() value=1 /> };
|
||||
| ^^^
|
||||
|
||||
error: too many refs set
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`)
|
||||
--> $DIR/html-component-fail.rs:85:38
|
||||
|
|
||||
85 | html! { <Child with props ref=() ref=() value=1 /> };
|
||||
| ^^^
|
||||
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to the `ref` prop).
|
||||
--> $DIR/html-component-fail.rs:86:38
|
||||
|
|
||||
86 | html! { <Child with props ref=() value=1 ref=() /> };
|
||||
85 | html! { <Child with props ref=() value=1 ref=() /> };
|
||||
| ^^^^^
|
||||
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to the `ref` prop).
|
||||
--> $DIR/html-component-fail.rs:87:31
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`)
|
||||
--> $DIR/html-component-fail.rs:86:31
|
||||
|
|
||||
87 | html! { <Child with props value=1 ref=() ref=() /> };
|
||||
86 | html! { <Child with props value=1 ref=() ref=() /> };
|
||||
| ^^^^^
|
||||
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to the `ref` prop).
|
||||
--> $DIR/html-component-fail.rs:88:28
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`)
|
||||
--> $DIR/html-component-fail.rs:87:20
|
||||
|
|
||||
88 | html! { <Child value=1 with props ref=() ref=() /> };
|
||||
| ^^^^
|
||||
87 | html! { <Child value=1 with props ref=() ref=() /> };
|
||||
| ^^^^^
|
||||
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to the `ref` prop).
|
||||
--> $DIR/html-component-fail.rs:89:35
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`)
|
||||
--> $DIR/html-component-fail.rs:88:20
|
||||
|
|
||||
89 | html! { <Child value=1 ref=() with props ref=() /> };
|
||||
| ^^^^
|
||||
88 | html! { <Child value=1 ref=() with props ref=() /> };
|
||||
| ^^^^^
|
||||
|
||||
error: too many refs set
|
||||
--> $DIR/html-component-fail.rs:90:27
|
||||
error: `ref` can only be set once
|
||||
--> $DIR/html-component-fail.rs:89:27
|
||||
|
|
||||
90 | html! { <Child ref=() ref=() value=1 with props /> };
|
||||
89 | html! { <Child ref=() ref=() value=1 with props /> };
|
||||
| ^^^
|
||||
|
||||
error: unexpected token
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`)
|
||||
--> $DIR/html-component-fail.rs:91:20
|
||||
|
|
||||
91 | html! { <Child value=1 with props /> };
|
||||
| ^^^^^
|
||||
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`)
|
||||
--> $DIR/html-component-fail.rs:92:31
|
||||
|
|
||||
92 | html! { <Child with props () /> };
|
||||
| ^^
|
||||
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to the `ref` prop).
|
||||
--> $DIR/html-component-fail.rs:93:28
|
||||
|
|
||||
93 | html! { <Child value=1 with props /> };
|
||||
| ^^^^
|
||||
|
||||
error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to the `ref` prop).
|
||||
--> $DIR/html-component-fail.rs:94:31
|
||||
|
|
||||
94 | html! { <Child with props value=1 /> };
|
||||
92 | html! { <Child with props value=1 /> };
|
||||
| ^^^^^
|
||||
|
||||
error: expected identifier
|
||||
--> $DIR/html-component-fail.rs:95:20
|
||||
error: expected identifier, found keyword `type`
|
||||
--> $DIR/html-component-fail.rs:93:20
|
||||
|
|
||||
95 | html! { <Child type=0 /> };
|
||||
| ^^^^
|
||||
93 | html! { <Child type=0 /> };
|
||||
| ^^^^ expected identifier, found keyword
|
||||
|
|
||||
help: you can escape reserved keywords to use them as identifiers
|
||||
|
|
||||
93 | html! { <Child r#type=0 /> };
|
||||
| ^^^^^^
|
||||
|
||||
error: expected identifier
|
||||
--> $DIR/html-component-fail.rs:96:20
|
||||
error: expected a valid Rust identifier
|
||||
--> $DIR/html-component-fail.rs:94:20
|
||||
|
|
||||
96 | html! { <Child invalid-prop-name=0 /> };
|
||||
94 | html! { <Child invalid-prop-name=0 /> };
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: expected an expression following this equals sign
|
||||
--> $DIR/html-component-fail.rs:98:26
|
||||
--> $DIR/html-component-fail.rs:96:26
|
||||
|
|
||||
98 | html! { <Child string= /> };
|
||||
96 | html! { <Child string= /> };
|
||||
| ^
|
||||
|
||||
error: too many refs set
|
||||
--> $DIR/html-component-fail.rs:103:33
|
||||
error: `int` can only be specified once but is given here again
|
||||
--> $DIR/html-component-fail.rs:97:26
|
||||
|
|
||||
97 | html! { <Child int=1 int=2 int=3 /> };
|
||||
| ^^^
|
||||
|
||||
error: `int` can only be specified once but is given here again
|
||||
--> $DIR/html-component-fail.rs:97:32
|
||||
|
|
||||
97 | html! { <Child int=1 int=2 int=3 /> };
|
||||
| ^^^
|
||||
|
||||
error: `ref` can only be specified once
|
||||
--> $DIR/html-component-fail.rs:102:26
|
||||
|
|
||||
103 | html! { <Child int=1 ref=() ref=() /> };
|
||||
| ^^^
|
||||
102 | html! { <Child int=1 ref=() ref=() /> };
|
||||
| ^^^
|
||||
|
||||
error: this closing tag has no corresponding opening tag
|
||||
--> $DIR/html-component-fail.rs:106:13
|
||||
--> $DIR/html-component-fail.rs:105:13
|
||||
|
|
||||
106 | html! { </Child> };
|
||||
105 | html! { </Child> };
|
||||
| ^^^^^^^^
|
||||
|
||||
error: this opening tag has no corresponding closing tag
|
||||
--> $DIR/html-component-fail.rs:107:13
|
||||
--> $DIR/html-component-fail.rs:106:13
|
||||
|
|
||||
107 | html! { <Child><Child></Child> };
|
||||
106 | html! { <Child><Child></Child> };
|
||||
| ^^^^^^^
|
||||
|
||||
error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<></>`)
|
||||
--> $DIR/html-component-fail.rs:108:28
|
||||
--> $DIR/html-component-fail.rs:107:28
|
||||
|
|
||||
108 | html! { <Child></Child><Child></Child> };
|
||||
107 | html! { <Child></Child><Child></Child> };
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
||||
error: this closing tag has no corresponding opening tag
|
||||
--> $DIR/html-component-fail.rs:117:30
|
||||
error: cannot specify the `children` prop when the component already has children
|
||||
--> $DIR/html-component-fail.rs:126:25
|
||||
|
|
||||
117 | html! { <Generic<String>></Generic> };
|
||||
126 | <ChildContainer children=children>
|
||||
| ^^^^^^^^
|
||||
|
||||
error: this closing tag has no corresponding opening tag
|
||||
--> $DIR/html-component-fail.rs:131:30
|
||||
|
|
||||
131 | html! { <Generic<String>></Generic> };
|
||||
| ^^^^^^^^^^
|
||||
|
||||
error: this closing tag has no corresponding opening tag
|
||||
--> $DIR/html-component-fail.rs:118:30
|
||||
--> $DIR/html-component-fail.rs:132:30
|
||||
|
|
||||
118 | html! { <Generic<String>></Generic<Vec<String>>> };
|
||||
132 | html! { <Generic<String>></Generic<Vec<String>>> };
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<></>`)
|
||||
--> $DIR/html-component-fail.rs:122:9
|
||||
--> $DIR/html-component-fail.rs:136:9
|
||||
|
|
||||
122 | <span>{ 2 }</span>
|
||||
136 | <span>{ 2 }</span>
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: optional attributes are only supported on HTML tags. Yew components can use `Option<T>` properties to accomplish the same thing.
|
||||
--> $DIR/html-component-fail.rs:125:28
|
||||
error: optional attributes are only supported on elements. Components can use `Option<T>` properties to accomplish the same thing.
|
||||
--> $DIR/html-component-fail.rs:139:28
|
||||
|
|
||||
125 | html! { <TestComponent value?="not_supported" /> };
|
||||
139 | html! { <TestComponent value?="not_supported" /> };
|
||||
| ^^^^^
|
||||
|
||||
error[E0425]: cannot find value `blah` in this scope
|
||||
--> $DIR/html-component-fail.rs:91:25
|
||||
--> $DIR/html-component-fail.rs:90:25
|
||||
|
|
||||
91 | html! { <Child with blah /> };
|
||||
90 | html! { <Child with blah /> };
|
||||
| ^^^^ not found in this scope
|
||||
|
||||
error[E0609]: no field `unknown` on type `ChildProperties`
|
||||
--> $DIR/html-component-fail.rs:97:20
|
||||
error[E0609]: no field `r#type` on type `ChildProperties`
|
||||
--> $DIR/html-component-fail.rs:93:20
|
||||
|
|
||||
97 | html! { <Child unknown="unknown" /> };
|
||||
93 | html! { <Child type=0 /> };
|
||||
| ^^^^ unknown field
|
||||
|
|
||||
= note: available fields are: `string`, `int`
|
||||
|
||||
error[E0599]: no method named `r#type` found for struct `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` in the current scope
|
||||
--> $DIR/html-component-fail.rs:93:20
|
||||
|
|
||||
5 | #[derive(Clone, Properties, PartialEq)]
|
||||
| ---------- method `r#type` not found for this
|
||||
...
|
||||
93 | html! { <Child type=0 /> };
|
||||
| ^^^^ method not found in `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>`
|
||||
|
||||
error[E0609]: no field `unknown` on type `ChildProperties`
|
||||
--> $DIR/html-component-fail.rs:95:20
|
||||
|
|
||||
95 | html! { <Child unknown="unknown" /> };
|
||||
| ^^^^^^^ unknown field
|
||||
|
|
||||
= note: available fields are: `string`, `int`
|
||||
|
||||
error[E0599]: no method named `unknown` found for struct `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` in the current scope
|
||||
--> $DIR/html-component-fail.rs:97:20
|
||||
--> $DIR/html-component-fail.rs:95:20
|
||||
|
|
||||
6 | #[derive(Clone, Properties, PartialEq)]
|
||||
5 | #[derive(Clone, Properties, PartialEq)]
|
||||
| ---------- method `unknown` not found for this
|
||||
...
|
||||
97 | html! { <Child unknown="unknown" /> };
|
||||
95 | html! { <Child unknown="unknown" /> };
|
||||
| ^^^^^^^ method not found in `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>`
|
||||
|
||||
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer<(), std::string::String>` is not satisfied
|
||||
--> $DIR/html-component-fail.rs:98:33
|
||||
|
|
||||
98 | html! { <Child int=1 string={} /> };
|
||||
| ^^ the trait `yew::virtual_dom::Transformer<(), std::string::String>` is not implemented for `yew::virtual_dom::vcomp::VComp`
|
||||
|
|
||||
= help: the following implementations were found:
|
||||
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, T>>
|
||||
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, std::option::Option<T>>>
|
||||
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::option::Option<std::string::String>>>
|
||||
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::string::String>>
|
||||
and 3 others
|
||||
= note: required by `yew::virtual_dom::Transformer::transform`
|
||||
|
||||
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer<{integer}, std::string::String>` is not satisfied
|
||||
--> $DIR/html-component-fail.rs:99:33
|
||||
|
|
||||
99 | html! { <Child int=1 string={} /> };
|
||||
| ^^ the trait `yew::virtual_dom::Transformer<(), std::string::String>` is not implemented for `yew::virtual_dom::vcomp::VComp`
|
||||
99 | html! { <Child int=1 string=3 /> };
|
||||
| ^ the trait `yew::virtual_dom::Transformer<{integer}, std::string::String>` is not implemented for `yew::virtual_dom::vcomp::VComp`
|
||||
|
|
||||
= help: the following implementations were found:
|
||||
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, T>>
|
||||
@ -194,21 +242,7 @@ error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom:
|
||||
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer<{integer}, std::string::String>` is not satisfied
|
||||
--> $DIR/html-component-fail.rs:100:33
|
||||
|
|
||||
100 | html! { <Child int=1 string=3 /> };
|
||||
| ^ the trait `yew::virtual_dom::Transformer<{integer}, std::string::String>` is not implemented for `yew::virtual_dom::vcomp::VComp`
|
||||
|
|
||||
= help: the following implementations were found:
|
||||
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, T>>
|
||||
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a T, std::option::Option<T>>>
|
||||
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::option::Option<std::string::String>>>
|
||||
<yew::virtual_dom::vcomp::VComp as yew::virtual_dom::Transformer<&'a str, std::string::String>>
|
||||
and 3 others
|
||||
= note: required by `yew::virtual_dom::Transformer::transform`
|
||||
|
||||
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer<{integer}, std::string::String>` is not satisfied
|
||||
--> $DIR/html-component-fail.rs:101:33
|
||||
|
|
||||
101 | html! { <Child int=1 string={3} /> };
|
||||
100 | html! { <Child int=1 string={3} /> };
|
||||
| ^^^ the trait `yew::virtual_dom::Transformer<{integer}, std::string::String>` is not implemented for `yew::virtual_dom::vcomp::VComp`
|
||||
|
|
||||
= help: the following implementations were found:
|
||||
@ -220,15 +254,15 @@ error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom:
|
||||
= note: required by `yew::virtual_dom::Transformer::transform`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-component-fail.rs:102:30
|
||||
--> $DIR/html-component-fail.rs:101:30
|
||||
|
|
||||
102 | html! { <Child int=1 ref=() /> };
|
||||
101 | html! { <Child int=1 ref=() /> };
|
||||
| ^^ expected struct `yew::html::NodeRef`, found `()`
|
||||
|
||||
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom::Transformer<u32, i32>` is not satisfied
|
||||
--> $DIR/html-component-fail.rs:104:24
|
||||
--> $DIR/html-component-fail.rs:103:24
|
||||
|
|
||||
104 | html! { <Child int=0u32 /> };
|
||||
103 | html! { <Child int=0u32 /> };
|
||||
| ^^^^ the trait `yew::virtual_dom::Transformer<u32, i32>` is not implemented for `yew::virtual_dom::vcomp::VComp`
|
||||
|
|
||||
= help: the following implementations were found:
|
||||
@ -240,34 +274,50 @@ error[E0277]: the trait bound `yew::virtual_dom::vcomp::VComp: yew::virtual_dom:
|
||||
= note: required by `yew::virtual_dom::Transformer::transform`
|
||||
|
||||
error[E0599]: no method named `string` found for struct `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` in the current scope
|
||||
--> $DIR/html-component-fail.rs:105:20
|
||||
--> $DIR/html-component-fail.rs:104:20
|
||||
|
|
||||
6 | #[derive(Clone, Properties, PartialEq)]
|
||||
5 | #[derive(Clone, Properties, PartialEq)]
|
||||
| ---------- method `string` not found for this
|
||||
...
|
||||
105 | html! { <Child string="abc" /> };
|
||||
104 | html! { <Child string="abc" /> };
|
||||
| ^^^^^^ method not found in `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>`
|
||||
|
|
||||
= help: items from traits can only be used if the trait is implemented and in scope
|
||||
= note: the following trait defines an item `string`, perhaps you need to implement it:
|
||||
candidate #1: `proc_macro::bridge::server::Literal`
|
||||
|
||||
error[E0599]: no method named `children` found for struct `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` in the current scope
|
||||
--> $DIR/html-component-fail.rs:109:14
|
||||
error[E0609]: no field `children` on type `ChildProperties`
|
||||
--> $DIR/html-component-fail.rs:108:14
|
||||
|
|
||||
6 | #[derive(Clone, Properties, PartialEq)]
|
||||
108 | html! { <Child>{ "Not allowed" }</Child> };
|
||||
| ^^^^^ unknown field
|
||||
|
|
||||
= note: available fields are: `string`, `int`
|
||||
|
||||
error[E0599]: no method named `children` found for struct `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` in the current scope
|
||||
--> $DIR/html-component-fail.rs:108:14
|
||||
|
|
||||
5 | #[derive(Clone, Properties, PartialEq)]
|
||||
| ---------- method `children` not found for this
|
||||
...
|
||||
109 | html! { <Child>{ "Not allowed" }</Child> };
|
||||
108 | html! { <Child>{ "Not allowed" }</Child> };
|
||||
| ^^^^^ method not found in `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>`
|
||||
|
||||
error[E0599]: no method named `build` found for struct `ChildContainerPropertiesBuilder<ChildContainerPropertiesBuilderStep_missing_required_prop_children>` in the current scope
|
||||
--> $DIR/html-component-fail.rs:111:14
|
||||
error[E0609]: no field `children` on type `ChildProperties`
|
||||
--> $DIR/html-component-fail.rs:112:10
|
||||
|
|
||||
32 | #[derive(Clone, Properties)]
|
||||
112 | <Child with ChildProperties { string: "hello".to_owned(), int: 5 }>
|
||||
| ^^^^^ unknown field
|
||||
|
|
||||
= note: available fields are: `string`, `int`
|
||||
|
||||
error[E0599]: no method named `build` found for struct `ChildContainerPropertiesBuilder<ChildContainerPropertiesBuilderStep_missing_required_prop_children>` in the current scope
|
||||
--> $DIR/html-component-fail.rs:117:14
|
||||
|
|
||||
31 | #[derive(Clone, Properties)]
|
||||
| ---------- method `build` not found for this
|
||||
...
|
||||
111 | html! { <ChildContainer /> };
|
||||
117 | html! { <ChildContainer /> };
|
||||
| ^^^^^^^^^^^^^^ method not found in `ChildContainerPropertiesBuilder<ChildContainerPropertiesBuilderStep_missing_required_prop_children>`
|
||||
|
|
||||
= help: items from traits can only be used if the trait is implemented and in scope
|
||||
@ -275,12 +325,12 @@ error[E0599]: no method named `build` found for struct `ChildContainerProperties
|
||||
candidate #1: `proc_macro::bridge::server::TokenStreamBuilder`
|
||||
|
||||
error[E0599]: no method named `build` found for struct `ChildContainerPropertiesBuilder<ChildContainerPropertiesBuilderStep_missing_required_prop_children>` in the current scope
|
||||
--> $DIR/html-component-fail.rs:112:14
|
||||
--> $DIR/html-component-fail.rs:118:14
|
||||
|
|
||||
32 | #[derive(Clone, Properties)]
|
||||
31 | #[derive(Clone, Properties)]
|
||||
| ---------- method `build` not found for this
|
||||
...
|
||||
112 | html! { <ChildContainer></ChildContainer> };
|
||||
118 | html! { <ChildContainer></ChildContainer> };
|
||||
| ^^^^^^^^^^^^^^ method not found in `ChildContainerPropertiesBuilder<ChildContainerPropertiesBuilderStep_missing_required_prop_children>`
|
||||
|
|
||||
= help: items from traits can only be used if the trait is implemented and in scope
|
||||
@ -288,27 +338,27 @@ error[E0599]: no method named `build` found for struct `ChildContainerProperties
|
||||
candidate #1: `proc_macro::bridge::server::TokenStreamBuilder`
|
||||
|
||||
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vtext::VText>` is not satisfied
|
||||
--> $DIR/html-component-fail.rs:113:31
|
||||
--> $DIR/html-component-fail.rs:119:31
|
||||
|
|
||||
113 | html! { <ChildContainer>{ "Not allowed" }</ChildContainer> };
|
||||
119 | html! { <ChildContainer>{ "Not allowed" }</ChildContainer> };
|
||||
| ^^^^^^^^^^^^^ the trait `std::convert::From<yew::virtual_dom::vtext::VText>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
|
||||
|
|
||||
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `yew::virtual_dom::vtext::VText`
|
||||
= note: required by `std::convert::Into::into`
|
||||
|
||||
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied
|
||||
--> $DIR/html-component-fail.rs:114:29
|
||||
--> $DIR/html-component-fail.rs:120:29
|
||||
|
|
||||
114 | html! { <ChildContainer><></></ChildContainer> };
|
||||
120 | html! { <ChildContainer><></></ChildContainer> };
|
||||
| ^ the trait `std::convert::From<yew::virtual_dom::vnode::VNode>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
|
||||
|
|
||||
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `yew::virtual_dom::vnode::VNode`
|
||||
= note: required by `std::convert::Into::into`
|
||||
|
||||
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied
|
||||
--> $DIR/html-component-fail.rs:115:30
|
||||
--> $DIR/html-component-fail.rs:121:30
|
||||
|
|
||||
115 | html! { <ChildContainer><other /></ChildContainer> };
|
||||
121 | html! { <ChildContainer><other /></ChildContainer> };
|
||||
| ^^^^^ the trait `std::convert::From<yew::virtual_dom::vnode::VNode>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
|
||||
|
|
||||
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `yew::virtual_dom::vnode::VNode`
|
||||
|
||||
@ -183,31 +183,20 @@ mod scoped {
|
||||
fn compile_pass() {
|
||||
html! { <Child int=1 /> };
|
||||
|
||||
// backwards compat
|
||||
html! { <Child: int=1 /> };
|
||||
|
||||
html! {
|
||||
<>
|
||||
<Child int=1 />
|
||||
<scoped::Child int=1 />
|
||||
|
||||
// backwards compat
|
||||
<Child: int=1 />
|
||||
<scoped::Child: int=1 />
|
||||
</>
|
||||
};
|
||||
|
||||
let props = <Child as Component>::Properties::default();
|
||||
let props2 = <Child as Component>::Properties::default();
|
||||
let props3 = <Child as Component>::Properties::default();
|
||||
let props4 = <Child as Component>::Properties::default();
|
||||
let node_ref = NodeRef::default();
|
||||
html! {
|
||||
<>
|
||||
<Child with props />
|
||||
<Child: with props2, /> // backwards compat
|
||||
<Child ref=node_ref.clone() with props3 />
|
||||
<Child with props4 ref=node_ref />
|
||||
<Child ref=node_ref.clone() with yew::props!(Child::Properties { int: 5 }) />
|
||||
<Child with <Child as Component>::Properties::default() ref=node_ref />
|
||||
</>
|
||||
};
|
||||
|
||||
@ -223,9 +212,6 @@ fn compile_pass() {
|
||||
<Child opt_str=String::from("child") int=1 />
|
||||
<Child opt_str=Some("child") int=1 />
|
||||
<Child opt_str=Some(String::from("child")) int=1 />
|
||||
|
||||
// backwards compat
|
||||
<Child: string="child", int=3, />
|
||||
</>
|
||||
};
|
||||
|
||||
@ -257,7 +243,7 @@ fn compile_pass() {
|
||||
<Container int=1></Container>
|
||||
|
||||
<Container with props>
|
||||
<></>
|
||||
<div>{ "hello world" }</div>
|
||||
</Container>
|
||||
|
||||
<Container int=1>
|
||||
|
||||
@ -1,185 +1,185 @@
|
||||
error: this opening tag has no corresponding closing tag
|
||||
--> $DIR/html-tag-fail.rs:6:13
|
||||
--> $DIR/html-element-fail.rs:6:13
|
||||
|
|
||||
6 | html! { <div> };
|
||||
| ^^^^^
|
||||
|
||||
error: this opening tag has no corresponding closing tag
|
||||
--> $DIR/html-tag-fail.rs:7:18
|
||||
--> $DIR/html-element-fail.rs:7:18
|
||||
|
|
||||
7 | html! { <div><div> };
|
||||
| ^^^^^
|
||||
|
||||
error: this closing tag has no corresponding opening tag
|
||||
--> $DIR/html-tag-fail.rs:8:13
|
||||
--> $DIR/html-element-fail.rs:8:13
|
||||
|
|
||||
8 | html! { </div> };
|
||||
| ^^^^^^
|
||||
|
||||
error: this opening tag has no corresponding closing tag
|
||||
--> $DIR/html-tag-fail.rs:9:13
|
||||
--> $DIR/html-element-fail.rs:9:13
|
||||
|
|
||||
9 | html! { <div><div></div> };
|
||||
| ^^^^^
|
||||
|
||||
error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<></>`)
|
||||
--> $DIR/html-tag-fail.rs:10:24
|
||||
--> $DIR/html-element-fail.rs:10:24
|
||||
|
|
||||
10 | html! { <div></div><div></div> };
|
||||
| ^^^^^^^^^^^
|
||||
|
||||
error: this closing tag has no corresponding opening tag
|
||||
--> $DIR/html-tag-fail.rs:11:18
|
||||
--> $DIR/html-element-fail.rs:11:18
|
||||
|
|
||||
11 | html! { <div></span> };
|
||||
| ^^^^^^^
|
||||
|
||||
error: this closing tag has no corresponding opening tag
|
||||
--> $DIR/html-tag-fail.rs:12:20
|
||||
--> $DIR/html-element-fail.rs:12:20
|
||||
|
|
||||
12 | html! { <tag-a></tag-b> };
|
||||
| ^^^^^^^^
|
||||
|
||||
error: this closing tag has no corresponding opening tag
|
||||
--> $DIR/html-tag-fail.rs:13:18
|
||||
--> $DIR/html-element-fail.rs:13:18
|
||||
|
|
||||
13 | html! { <div></span></div> };
|
||||
| ^^^^^^^
|
||||
|
||||
error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<></>`)
|
||||
--> $DIR/html-tag-fail.rs:14:20
|
||||
--> $DIR/html-element-fail.rs:14:20
|
||||
|
|
||||
14 | html! { <img /></img> };
|
||||
| ^^^^^^
|
||||
|
||||
error: expected a valid html element
|
||||
--> $DIR/html-tag-fail.rs:15:18
|
||||
--> $DIR/html-element-fail.rs:15:18
|
||||
|
|
||||
15 | html! { <div>Invalid</div> };
|
||||
| ^^^^^^^
|
||||
|
||||
error: the attribute `attr` can only be specified once
|
||||
--> $DIR/html-tag-fail.rs:17:27
|
||||
error: `attr` can only be specified once but is given here again
|
||||
--> $DIR/html-element-fail.rs:17:27
|
||||
|
|
||||
17 | html! { <input attr=1 attr=2 /> };
|
||||
| ^^^^
|
||||
|
||||
error: the attribute `value` can only be specified once
|
||||
--> $DIR/html-tag-fail.rs:18:32
|
||||
error: `value` can only be specified once but is given here again
|
||||
--> $DIR/html-element-fail.rs:18:32
|
||||
|
|
||||
18 | html! { <input value="123" value="456" /> };
|
||||
| ^^^^^
|
||||
|
||||
error: the attribute `kind` can only be specified once
|
||||
--> $DIR/html-tag-fail.rs:19:36
|
||||
error: `kind` can only be specified once but is given here again
|
||||
--> $DIR/html-element-fail.rs:19:36
|
||||
|
|
||||
19 | html! { <input kind="checkbox" kind="submit" /> };
|
||||
| ^^^^
|
||||
|
||||
error: the attribute `checked` can only be specified once
|
||||
--> $DIR/html-tag-fail.rs:20:33
|
||||
error: `checked` can only be specified once but is given here again
|
||||
--> $DIR/html-element-fail.rs:20:33
|
||||
|
|
||||
20 | html! { <input checked=true checked=false /> };
|
||||
| ^^^^^^^
|
||||
|
||||
error: the attribute `disabled` can only be specified once
|
||||
--> $DIR/html-tag-fail.rs:21:34
|
||||
error: `disabled` can only be specified once but is given here again
|
||||
--> $DIR/html-element-fail.rs:21:34
|
||||
|
|
||||
21 | html! { <input disabled=true disabled=false /> };
|
||||
| ^^^^^^^^
|
||||
|
||||
error: the attribute `selected` can only be specified once
|
||||
--> $DIR/html-tag-fail.rs:22:35
|
||||
error: `selected` can only be specified once but is given here again
|
||||
--> $DIR/html-element-fail.rs:22:35
|
||||
|
|
||||
22 | html! { <option selected=true selected=false /> };
|
||||
| ^^^^^^^^
|
||||
|
||||
error: the attribute `class` can only be specified once
|
||||
--> $DIR/html-tag-fail.rs:23:32
|
||||
error: `class` can only be specified once but is given here again
|
||||
--> $DIR/html-element-fail.rs:23:32
|
||||
|
|
||||
23 | html! { <div class="first" class="second" /> };
|
||||
| ^^^^^
|
||||
|
||||
error: the attribute `ref` can only be specified once
|
||||
--> $DIR/html-tag-fail.rs:38:27
|
||||
error: `ref` can only be specified once
|
||||
--> $DIR/html-element-fail.rs:38:20
|
||||
|
|
||||
38 | html! { <input ref=() ref=() /> };
|
||||
| ^^^
|
||||
| ^^^
|
||||
|
||||
error: the tag `<input>` is a void element and cannot have children (hint: rewrite this as `<input/>`)
|
||||
--> $DIR/html-tag-fail.rs:40:13
|
||||
--> $DIR/html-element-fail.rs:40:13
|
||||
|
|
||||
40 | html! { <input type="text"></input> };
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: the tag `<iNpUt>` is a void element and cannot have children (hint: rewrite this as `<iNpUt/>`)
|
||||
--> $DIR/html-tag-fail.rs:41:13
|
||||
--> $DIR/html-element-fail.rs:41:13
|
||||
|
|
||||
41 | html! { <iNpUt type="text"></iNpUt> };
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: this dynamic tag is missing an expression block defining its value
|
||||
--> $DIR/html-tag-fail.rs:43:14
|
||||
--> $DIR/html-element-fail.rs:43:14
|
||||
|
|
||||
43 | html! { <@></@> };
|
||||
| ^
|
||||
|
||||
error: dynamic closing tags must not have a body (hint: replace it with just `</@>`)
|
||||
--> $DIR/html-tag-fail.rs:44:27
|
||||
--> $DIR/html-element-fail.rs:44:27
|
||||
|
|
||||
44 | html! { <@{"test"}></@{"test"}> };
|
||||
| ^^^^^^^^
|
||||
|
||||
error: this dynamic tag is missing an expression block defining its value
|
||||
--> $DIR/html-tag-fail.rs:46:14
|
||||
--> $DIR/html-element-fail.rs:46:14
|
||||
|
|
||||
46 | html! { <@/> };
|
||||
| ^
|
||||
|
||||
error: boolean attributes don't support being used as an optional attribute (hint: a value of false results in the attribute not being set)
|
||||
--> $DIR/html-tag-fail.rs:50:20
|
||||
--> $DIR/html-element-fail.rs:50:20
|
||||
|
|
||||
50 | html! { <input disabled?=Some(true) /> };
|
||||
| ^^^^^^^^
|
||||
|
||||
error: the `checked` attribute does not support being used as an optional attribute
|
||||
--> $DIR/html-tag-fail.rs:58:20
|
||||
error: `checked` does not support being used as an optional attribute
|
||||
--> $DIR/html-element-fail.rs:58:20
|
||||
|
|
||||
58 | html! { <input checked?=Some(false) /> };
|
||||
| ^^^^^^^
|
||||
| ^^^^^^^^^
|
||||
|
||||
error: the `class` attribute does not support being used as an optional attribute
|
||||
--> $DIR/html-tag-fail.rs:59:20
|
||||
error: `class` does not support being used as an optional attribute
|
||||
--> $DIR/html-element-fail.rs:59:20
|
||||
|
|
||||
59 | html! { <input class?=() /> };
|
||||
| ^^^^^
|
||||
| ^^^^^^^
|
||||
|
||||
error: the `ref` attribute does not support being used as an optional attribute
|
||||
--> $DIR/html-tag-fail.rs:60:20
|
||||
error: `ref` does not support being used as an optional attribute
|
||||
--> $DIR/html-element-fail.rs:60:20
|
||||
|
|
||||
60 | html! { <input ref?=() /> };
|
||||
| ^^^
|
||||
| ^^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:25:28
|
||||
--> $DIR/html-element-fail.rs:25:28
|
||||
|
|
||||
25 | html! { <input checked=1 /> };
|
||||
| ^ expected `bool`, found integer
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:26:29
|
||||
--> $DIR/html-element-fail.rs:26:29
|
||||
|
|
||||
26 | html! { <input disabled=1 /> };
|
||||
| ^ expected `bool`, found integer
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:27:30
|
||||
--> $DIR/html-element-fail.rs:27:30
|
||||
|
|
||||
27 | html! { <option selected=1 /> };
|
||||
| ^ expected `bool`, found integer
|
||||
|
||||
error[E0277]: `()` doesn't implement `std::fmt::Display`
|
||||
--> $DIR/html-tag-fail.rs:28:25
|
||||
--> $DIR/html-element-fail.rs:28:25
|
||||
|
|
||||
28 | html! { <input type=() /> };
|
||||
| ^^ `()` cannot be formatted with the default formatter
|
||||
@ -190,7 +190,7 @@ error[E0277]: `()` doesn't implement `std::fmt::Display`
|
||||
= note: required by `std::string::ToString::to_string`
|
||||
|
||||
error[E0277]: `()` doesn't implement `std::fmt::Display`
|
||||
--> $DIR/html-tag-fail.rs:29:26
|
||||
--> $DIR/html-element-fail.rs:29:26
|
||||
|
|
||||
29 | html! { <input value=() /> };
|
||||
| ^^ `()` cannot be formatted with the default formatter
|
||||
@ -200,7 +200,7 @@ error[E0277]: `()` doesn't implement `std::fmt::Display`
|
||||
= note: required because of the requirements on the impl of `std::string::ToString` for `()`
|
||||
|
||||
error[E0277]: `()` doesn't implement `std::fmt::Display`
|
||||
--> $DIR/html-tag-fail.rs:30:21
|
||||
--> $DIR/html-element-fail.rs:30:21
|
||||
|
|
||||
30 | html! { <a href=() /> };
|
||||
| ^^ `()` cannot be formatted with the default formatter
|
||||
@ -211,7 +211,7 @@ error[E0277]: `()` doesn't implement `std::fmt::Display`
|
||||
= note: required by `std::string::ToString::to_string`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:32:28
|
||||
--> $DIR/html-element-fail.rs:32:28
|
||||
|
|
||||
32 | html! { <input onclick=1 /> };
|
||||
| ^ expected enum `yew::callback::Callback`, found integer
|
||||
@ -220,7 +220,7 @@ error[E0308]: mismatched types
|
||||
found type `{integer}`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:33:28
|
||||
--> $DIR/html-element-fail.rs:33:28
|
||||
|
|
||||
33 | html! { <input onclick=Callback::from(|a: String| ()) /> };
|
||||
| ^^^^^^^^ expected struct `web_sys::features::gen_MouseEvent::MouseEvent`, found struct `std::string::String`
|
||||
@ -229,7 +229,7 @@ error[E0308]: mismatched types
|
||||
found enum `yew::callback::Callback<std::string::String>`
|
||||
|
||||
error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
--> $DIR/html-tag-fail.rs:35:27
|
||||
--> $DIR/html-element-fail.rs:35:27
|
||||
|
|
||||
35 | html! { <input string=NotToString /> };
|
||||
| ^^^^^^^^^^^ `NotToString` cannot be formatted with the default formatter
|
||||
@ -240,13 +240,13 @@ error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
= note: required by `std::string::ToString::to_string`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:37:24
|
||||
--> $DIR/html-element-fail.rs:37:24
|
||||
|
|
||||
37 | html! { <input ref=() /> };
|
||||
| ^^ expected struct `yew::html::NodeRef`, found `()`
|
||||
|
||||
error[E0277]: the trait bound `std::borrow::Cow<'static, str>: std::convert::From<{integer}>` is not satisfied
|
||||
--> $DIR/html-tag-fail.rs:45:15
|
||||
--> $DIR/html-element-fail.rs:45:15
|
||||
|
|
||||
45 | html! { <@{55}></@> };
|
||||
| ^^^^ the trait `std::convert::From<{integer}>` is not implemented for `std::borrow::Cow<'static, str>`
|
||||
@ -261,7 +261,7 @@ error[E0277]: the trait bound `std::borrow::Cow<'static, str>: std::convert::Fro
|
||||
= note: required by `std::convert::Into::into`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:48:23
|
||||
--> $DIR/html-element-fail.rs:48:23
|
||||
|
|
||||
48 | html! { <a media?="media" /> };
|
||||
| ^^^^^^^
|
||||
@ -273,7 +273,7 @@ error[E0308]: mismatched types
|
||||
found reference `&'static str`
|
||||
|
||||
error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
--> $DIR/html-tag-fail.rs:49:23
|
||||
--> $DIR/html-element-fail.rs:49:23
|
||||
|
|
||||
49 | html! { <a media?=Some(NotToString) /> };
|
||||
| ^^^^ `NotToString` cannot be formatted with the default formatter
|
||||
@ -284,7 +284,7 @@ error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
= note: required by `std::string::ToString::to_string`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:51:26
|
||||
--> $DIR/html-element-fail.rs:51:26
|
||||
|
|
||||
51 | html! { <input type?="kind" /> };
|
||||
| ^^^^^^
|
||||
@ -296,7 +296,7 @@ error[E0308]: mismatched types
|
||||
found reference `&'static str`
|
||||
|
||||
error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
--> $DIR/html-tag-fail.rs:52:26
|
||||
--> $DIR/html-element-fail.rs:52:26
|
||||
|
|
||||
52 | html! { <input type?=Some(NotToString) /> };
|
||||
| ^^^^ `NotToString` cannot be formatted with the default formatter
|
||||
@ -307,7 +307,7 @@ error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
= note: required by `std::string::ToString::to_string`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:53:24
|
||||
--> $DIR/html-element-fail.rs:53:24
|
||||
|
|
||||
53 | html! { <li value?="value" /> };
|
||||
| ^^^^^^^
|
||||
@ -319,7 +319,7 @@ error[E0308]: mismatched types
|
||||
found reference `&'static str`
|
||||
|
||||
error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
--> $DIR/html-tag-fail.rs:54:24
|
||||
--> $DIR/html-element-fail.rs:54:24
|
||||
|
|
||||
54 | html! { <li value?=Some(NotToString) /> };
|
||||
| ^^^^ `NotToString` cannot be formatted with the default formatter
|
||||
@ -330,7 +330,7 @@ error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
= note: required by `std::string::ToString::to_string`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:56:22
|
||||
--> $DIR/html-element-fail.rs:56:22
|
||||
|
|
||||
56 | html! { <a href?="href" /> };
|
||||
| ^^^^^^
|
||||
@ -342,7 +342,7 @@ error[E0308]: mismatched types
|
||||
found reference `&'static str`
|
||||
|
||||
error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
--> $DIR/html-tag-fail.rs:57:22
|
||||
--> $DIR/html-element-fail.rs:57:22
|
||||
|
|
||||
57 | html! { <a href?=Some(NotToString) /> };
|
||||
| ^^^^ `NotToString` cannot be formatted with the default formatter
|
||||
@ -353,7 +353,7 @@ error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
|
||||
= note: required by `std::string::ToString::to_string`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:61:20
|
||||
--> $DIR/html-element-fail.rs:61:20
|
||||
|
|
||||
61 | html! { <input onfocus?=Some(5) /> };
|
||||
| ^^^^^^^ expected enum `yew::callback::Callback`, found integer
|
||||
@ -362,7 +362,7 @@ error[E0308]: mismatched types
|
||||
found type `{integer}`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/html-tag-fail.rs:62:29
|
||||
--> $DIR/html-element-fail.rs:62:29
|
||||
|
|
||||
62 | html! { <input onfocus?=Callback::from(|_| ()) /> };
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -12,7 +12,7 @@ fn compile_pass() {
|
||||
<div>
|
||||
<div data-key="abc"></div>
|
||||
<div ref=parent_ref class="parent">
|
||||
<span class="child", value="anything",></span>
|
||||
<span class="child" value="anything"></span>
|
||||
<label for="first-name">{"First Name"}</label>
|
||||
<input type="text" id="first-name" value="placeholder" />
|
||||
<input type="checkbox" checked=true />
|
||||
@ -38,7 +38,7 @@ fn compile_pass() {
|
||||
</defs>
|
||||
</svg>
|
||||
<img class=("avatar", "hidden") src="http://pic.com" />
|
||||
<img class="avatar hidden", />
|
||||
<img class="avatar hidden" />
|
||||
<button onclick=&onclick onclick=onclick />
|
||||
<a href="http://google.com" />
|
||||
<custom-tag-a>
|
||||
@ -64,8 +64,8 @@ error: fragments only accept the `key` prop
|
||||
15 | html! { <some_attr="test"></> };
|
||||
| ^^^^^^^^^
|
||||
|
||||
error: the `key` attribute does not support being used as an optional attribute
|
||||
error: `key` does not support being used as an optional attribute
|
||||
--> $DIR/html-list-fail.rs:17:14
|
||||
|
|
||||
17 | html! { <key?=None></> };
|
||||
| ^^^
|
||||
| ^^^^^
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Clone, Properties, PartialEq)]
|
||||
pub struct TestProperties {
|
||||
pub string: String,
|
||||
pub int: i32,
|
||||
}
|
||||
|
||||
pub struct TestComponent;
|
||||
impl Component for TestComponent {
|
||||
type Message = ();
|
||||
type Properties = TestProperties;
|
||||
|
||||
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
|
||||
TestComponent
|
||||
}
|
||||
|
||||
fn update(&mut self, _: Self::Message) -> ShouldRender {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn change(&mut self, _: Self::Properties) -> ShouldRender {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@ -21,8 +21,8 @@ fn tests() {
|
||||
t.pass("tests/macro/html-node-pass.rs");
|
||||
t.compile_fail("tests/macro/html-node-fail.rs");
|
||||
|
||||
t.pass("tests/macro/html-tag-pass.rs");
|
||||
t.compile_fail("tests/macro/html-tag-fail.rs");
|
||||
t.pass("tests/macro/html-element-pass.rs");
|
||||
t.compile_fail("tests/macro/html-element-fail.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
18
yew-macro/tests/props_macro/props-fail.rs
Normal file
18
yew-macro/tests/props_macro/props-fail.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Clone, Properties)]
|
||||
struct Props {
|
||||
a: usize,
|
||||
}
|
||||
|
||||
fn compile_fail() {
|
||||
yew::props!(Props { ref: NodeRef::default(), key: "key" });
|
||||
yew::props!(Props { a: 5, fail: 10 });
|
||||
|
||||
let props = yew::props!(Props { a: 1 });
|
||||
yew::props!(Props { a: 1, ..props });
|
||||
|
||||
yew::props!(Props { does_not_exist });
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
57
yew-macro/tests/props_macro/props-fail.stderr
Normal file
57
yew-macro/tests/props_macro/props-fail.stderr
Normal file
@ -0,0 +1,57 @@
|
||||
error: special props cannot be specified in the `props!` macro
|
||||
--> $DIR/props-fail.rs:9:25
|
||||
|
|
||||
9 | yew::props!(Props { ref: NodeRef::default(), key: "key" });
|
||||
| ^^^
|
||||
|
||||
error: special props cannot be specified in the `props!` macro
|
||||
--> $DIR/props-fail.rs:9:50
|
||||
|
|
||||
9 | yew::props!(Props { ref: NodeRef::default(), key: "key" });
|
||||
| ^^^
|
||||
|
||||
error: expected ident
|
||||
--> $DIR/props-fail.rs:13:31
|
||||
|
|
||||
13 | yew::props!(Props { a: 1, ..props });
|
||||
| ^^
|
||||
|
||||
error[E0425]: cannot find value `does_not_exist` in this scope
|
||||
--> $DIR/props-fail.rs:15:25
|
||||
|
|
||||
15 | yew::props!(Props { does_not_exist });
|
||||
| ^^^^^^^^^^^^^^ not found in this scope
|
||||
|
||||
error[E0609]: no field `fail` on type `Props`
|
||||
--> $DIR/props-fail.rs:10:31
|
||||
|
|
||||
10 | yew::props!(Props { a: 5, fail: 10 });
|
||||
| ^^^^ unknown field
|
||||
|
|
||||
= note: available fields are: `a`
|
||||
|
||||
error[E0599]: no method named `fail` found for struct `PropsBuilder<PropsBuilderStep_build>` in the current scope
|
||||
--> $DIR/props-fail.rs:10:31
|
||||
|
|
||||
3 | #[derive(Clone, Properties)]
|
||||
| ---------- method `fail` not found for this
|
||||
...
|
||||
10 | yew::props!(Props { a: 5, fail: 10 });
|
||||
| ^^^^ method not found in `PropsBuilder<PropsBuilderStep_build>`
|
||||
|
||||
error[E0609]: no field `does_not_exist` on type `Props`
|
||||
--> $DIR/props-fail.rs:15:25
|
||||
|
|
||||
15 | yew::props!(Props { does_not_exist });
|
||||
| ^^^^^^^^^^^^^^ unknown field
|
||||
|
|
||||
= note: available fields are: `a`
|
||||
|
||||
error[E0599]: no method named `does_not_exist` found for struct `PropsBuilder<PropsBuilderStep_missing_required_prop_a>` in the current scope
|
||||
--> $DIR/props-fail.rs:15:25
|
||||
|
|
||||
3 | #[derive(Clone, Properties)]
|
||||
| ---------- method `does_not_exist` not found for this
|
||||
...
|
||||
15 | yew::props!(Props { does_not_exist });
|
||||
| ^^^^^^^^^^^^^^ method not found in `PropsBuilder<PropsBuilderStep_missing_required_prop_a>`
|
||||
16
yew-macro/tests/props_macro/props-pass.rs
Normal file
16
yew-macro/tests/props_macro/props-pass.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Clone, Properties)]
|
||||
struct Props {
|
||||
a: usize,
|
||||
#[prop_or_default]
|
||||
b: usize,
|
||||
}
|
||||
|
||||
fn compile_pass() {
|
||||
yew::props!(Props { a: 5 });
|
||||
let (a, b) = (3, 5);
|
||||
yew::props!(Props { a, b });
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
43
yew-macro/tests/props_macro/resolve-prop-fail.rs
Normal file
43
yew-macro/tests/props_macro/resolve-prop-fail.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Clone, Properties)]
|
||||
struct Props {}
|
||||
|
||||
struct MyComp;
|
||||
impl Component for MyComp {
|
||||
type Message = ();
|
||||
type Properties = Props;
|
||||
|
||||
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
trait NotAComponent {
|
||||
type Properties;
|
||||
}
|
||||
|
||||
struct MyNotAComponent;
|
||||
impl NotAComponent for MyNotAComponent {
|
||||
type Properties = ();
|
||||
}
|
||||
|
||||
fn compile_fail() {
|
||||
yew::props!(Vec<_> {});
|
||||
yew::props!(MyComp {});
|
||||
yew::props!(MyNotAComponent::Properties {});
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
17
yew-macro/tests/props_macro/resolve-prop-fail.stderr
Normal file
17
yew-macro/tests/props_macro/resolve-prop-fail.stderr
Normal file
@ -0,0 +1,17 @@
|
||||
error[E0277]: the trait bound `std::vec::Vec<_>: yew::html::Properties` is not satisfied
|
||||
--> $DIR/resolve-prop-fail.rs:38:17
|
||||
|
|
||||
38 | yew::props!(Vec<_> {});
|
||||
| ^^^ the trait `yew::html::Properties` is not implemented for `std::vec::Vec<_>`
|
||||
|
||||
error[E0277]: the trait bound `MyComp: yew::html::Properties` is not satisfied
|
||||
--> $DIR/resolve-prop-fail.rs:39:17
|
||||
|
|
||||
39 | yew::props!(MyComp {});
|
||||
| ^^^^^^ the trait `yew::html::Properties` is not implemented for `MyComp`
|
||||
|
||||
error[E0277]: the trait bound `MyNotAComponent: yew::html::Component` is not satisfied
|
||||
--> $DIR/resolve-prop-fail.rs:40:17
|
||||
|
|
||||
40 | yew::props!(MyNotAComponent::Properties {});
|
||||
| ^^^^^^^^^^^^^^^ the trait `yew::html::Component` is not implemented for `MyNotAComponent`
|
||||
38
yew-macro/tests/props_macro/resolve-prop-pass.rs
Normal file
38
yew-macro/tests/props_macro/resolve-prop-pass.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Clone, Properties)]
|
||||
struct Props {
|
||||
n: i32,
|
||||
}
|
||||
|
||||
struct MyComp;
|
||||
impl Component for MyComp {
|
||||
type Message = ();
|
||||
type Properties = Props;
|
||||
|
||||
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_pass() {
|
||||
yew::props!(Props { n: 1 });
|
||||
yew::props!(self::Props { n: 1 });
|
||||
yew::props!(MyComp::Properties { n: 2 });
|
||||
yew::props!(self::MyComp::Properties { n: 3 });
|
||||
yew::props!(<MyComp as Component>::Properties { n: 5 });
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
7
yew-macro/tests/props_macro_test.rs
Normal file
7
yew-macro/tests/props_macro_test.rs
Normal file
@ -0,0 +1,7 @@
|
||||
#[allow(dead_code)]
|
||||
#[rustversion::attr(stable(1.45), test)]
|
||||
fn tests() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.pass("tests/props_macro/*-pass.rs");
|
||||
t.compile_fail("tests/props_macro/*-fail.rs");
|
||||
}
|
||||
@ -59,9 +59,9 @@ impl<SW: Switch + Clone + 'static, STATE: RouterState> Component for RouterButto
|
||||
});
|
||||
html! {
|
||||
<button
|
||||
class=self.props.classes.clone(),
|
||||
onclick=cb,
|
||||
disabled=self.props.disabled,
|
||||
class=self.props.classes.clone()
|
||||
onclick=cb
|
||||
disabled=self.props.disabled
|
||||
>
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
|
||||
@ -72,10 +72,10 @@ impl<SW: Switch + Clone + 'static, STATE: RouterState> Component for RouterAncho
|
||||
|
||||
html! {
|
||||
<a
|
||||
class=self.props.classes.clone(),
|
||||
onclick=cb,
|
||||
disabled=self.props.disabled,
|
||||
href=target,
|
||||
class=self.props.classes.clone()
|
||||
onclick=cb
|
||||
disabled=self.props.disabled
|
||||
href=target
|
||||
>
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
|
||||
@ -9,6 +9,7 @@ mod scope;
|
||||
pub use listener::*;
|
||||
pub use scope::{AnyScope, Scope, SendAsMessage};
|
||||
pub(crate) use scope::{ComponentUpdate, Scoped};
|
||||
pub use yew_macro::Properties;
|
||||
|
||||
use crate::callback::Callback;
|
||||
use crate::virtual_dom::{VChild, VNode};
|
||||
@ -318,7 +319,7 @@ where
|
||||
}
|
||||
|
||||
/// Render children components and return `Iterator`
|
||||
pub fn iter<'a>(&'a self) -> impl Iterator<Item = T> + 'a {
|
||||
pub fn iter(&self) -> impl Iterator<Item = T> + '_ {
|
||||
// clone each child lazily.
|
||||
// This way `self.iter().next()` only has to clone a single node.
|
||||
self.children.iter().cloned()
|
||||
|
||||
@ -36,19 +36,20 @@
|
||||
//! ```rust
|
||||
//! use yew::prelude::*;
|
||||
//!
|
||||
//! enum Msg {
|
||||
//! AddOne,
|
||||
//! }
|
||||
//!
|
||||
//! struct Model {
|
||||
//! link: ComponentLink<Self>,
|
||||
//! value: i64,
|
||||
//! }
|
||||
//!
|
||||
//! enum Msg {
|
||||
//! AddOne,
|
||||
//! }
|
||||
//!
|
||||
//! impl Component for Model {
|
||||
//! type Message = Msg;
|
||||
//! type Properties = ();
|
||||
//! fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
//!
|
||||
//! fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
//! Self {
|
||||
//! link,
|
||||
//! value: 0,
|
||||
@ -57,12 +58,14 @@
|
||||
//!
|
||||
//! fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
//! match msg {
|
||||
//! Msg::AddOne => self.value += 1
|
||||
//! Msg::AddOne => {
|
||||
//! self.value += 1;
|
||||
//! true
|
||||
//! }
|
||||
//! }
|
||||
//! true
|
||||
//! }
|
||||
//!
|
||||
//! fn change(&mut self, _: Self::Properties) -> ShouldRender {
|
||||
//! fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||
//! false
|
||||
//! }
|
||||
//!
|
||||
@ -78,8 +81,7 @@
|
||||
//!
|
||||
//!# fn dont_execute() {
|
||||
//! fn main() {
|
||||
//! yew::initialize();
|
||||
//! App::<Model>::new().mount_to_body();
|
||||
//! yew::start_app::<Model>();
|
||||
//! }
|
||||
//!# }
|
||||
//! ```
|
||||
@ -194,11 +196,65 @@ pub use yew_macro::html;
|
||||
/// [`ChildrenRenderer<ListItem>`]: ./html/struct.ChildrenRenderer.html
|
||||
pub use yew_macro::html_nested;
|
||||
|
||||
/// Build [`Properties`] outside of the [`html!`] macro.
|
||||
///
|
||||
/// It's already possible to create properties like normal Rust structs
|
||||
/// but if there are lots of optional props the end result is often needlessly verbose.
|
||||
/// This macro allows you to build properties the same way the [`html!`] macro does.
|
||||
///
|
||||
/// The macro doesn't support special props like `ref` and `key`, they need to be set in the [`html!`] macro.
|
||||
///
|
||||
/// You can read more about `Properties` in the [Yew Docs].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use yew::prelude::*;
|
||||
/// use std::borrow::Cow;
|
||||
///
|
||||
/// #[derive(Clone, Properties)]
|
||||
/// struct Props {
|
||||
/// #[prop_or_default]
|
||||
/// id: usize,
|
||||
/// name: Cow<'static, str>,
|
||||
/// }
|
||||
///
|
||||
/// struct Model(Props);
|
||||
/// impl Component for Model {
|
||||
/// # type Message = ();
|
||||
/// type Properties = Props;
|
||||
/// // ...
|
||||
/// # fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self { unimplemented!() }
|
||||
/// # fn update(&mut self, _msg: Self::Message) -> ShouldRender { unimplemented!() }
|
||||
/// # fn change(&mut self, _props: Self::Properties) -> ShouldRender { unimplemented!() }
|
||||
/// # fn view(&self) -> Html { unimplemented!() }
|
||||
/// }
|
||||
///
|
||||
/// # fn foo() -> Html {
|
||||
/// // You can build props directly ...
|
||||
/// let props = yew::props!(Props { name: Cow::from("Minka") });
|
||||
/// # assert_eq!(props.name, "Minka");
|
||||
/// // ... or build the associated properties of a component
|
||||
/// let props = yew::props!(Model::Properties { id: 2, name: Cow::from("Lemmy") });
|
||||
/// # assert_eq!(props.id, 2);
|
||||
///
|
||||
/// // Use the `with props` syntax to create a component with the props.
|
||||
/// html! {
|
||||
/// <Model key=1 with props />
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`html!`]: ./macro.html.html
|
||||
/// [`Properties`]: ./html/trait.Properties.html
|
||||
/// [yew docs]: https://yew.rs/docs/en/concepts/components/properties
|
||||
pub use yew_macro::props;
|
||||
|
||||
/// This module contains macros which implements html! macro and JSX-like templates
|
||||
pub mod macros {
|
||||
pub use crate::html;
|
||||
pub use crate::html_nested;
|
||||
pub use yew_macro::Properties;
|
||||
pub use crate::props;
|
||||
}
|
||||
|
||||
pub mod app;
|
||||
@ -301,7 +357,7 @@ pub mod prelude {
|
||||
Children, ChildrenWithProps, Component, ComponentLink, Html, NodeRef, Properties,
|
||||
ShouldRender,
|
||||
};
|
||||
pub use crate::macros::*;
|
||||
pub use crate::macros::{html, html_nested};
|
||||
pub use crate::virtual_dom::Classes;
|
||||
|
||||
/// Prelude module for creating worker.
|
||||
|
||||
@ -284,8 +284,9 @@ impl<COMP: Component> fmt::Debug for VChild<COMP> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::macros::Properties;
|
||||
use crate::{html, Children, Component, ComponentLink, Html, NodeRef, ShouldRender};
|
||||
use crate::{
|
||||
html, Children, Component, ComponentLink, Html, NodeRef, Properties, ShouldRender,
|
||||
};
|
||||
use cfg_match::cfg_match;
|
||||
|
||||
#[cfg(feature = "std_web")]
|
||||
|
||||
@ -908,7 +908,7 @@ mod tests {
|
||||
#[test]
|
||||
fn keeps_order_of_classes() {
|
||||
let a = html! {
|
||||
<div class=vec!["class-1", "class-2", "class-3"],></div>
|
||||
<div class=vec!["class-1", "class-2", "class-3"]></div>
|
||||
};
|
||||
|
||||
if let VNode::VTag(vtag) = a {
|
||||
|
||||
@ -68,7 +68,7 @@ pub struct ReadOnly<S> {
|
||||
|
||||
impl<S> ReadOnly<S> {
|
||||
/// Allow only immutable borrows to the underlying data
|
||||
pub fn borrow<'a>(&'a self) -> impl Deref<Target = S> + 'a {
|
||||
pub fn borrow(&self) -> impl Deref<Target = S> + '_ {
|
||||
self.state.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user