Skip to content

Commit

Permalink
Add a macro for building properties outside of html! (#1599)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
siku2 committed Oct 21, 2020
1 parent 724ac1d commit fa2ab5a
Show file tree
Hide file tree
Showing 48 changed files with 2,094 additions and 1,270 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
60 changes: 31 additions & 29 deletions ci/dictionary.txt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
8 changes: 4 additions & 4 deletions ci/spellcheck.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
37 changes: 19 additions & 18 deletions docs/concepts/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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.

Expand All @@ -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
Expand Down
74 changes: 59 additions & 15 deletions docs/concepts/components/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

### Field attributes

:::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.
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]`

Initialize the prop value with the default value of the field's type using the `Default` trait.

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`.
#### `#[prop_or(value)]`

### PartialEq
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)
:::

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\).
## 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

Expand All @@ -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)]
Expand All @@ -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]
Expand All @@ -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"),
});
```
Loading

0 comments on commit fa2ab5a

Please sign in to comment.