mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
website: make tutorial testable (#3879)
This commit is contained in:
parent
7ff45d3198
commit
4f3b85e31d
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -3904,7 +3904,9 @@ dependencies = [
|
||||
"derive_more",
|
||||
"glob",
|
||||
"gloo",
|
||||
"gloo-net 0.6.0",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"tokio",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
|
||||
@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
publish = false
|
||||
rust-version = "1.62"
|
||||
rust-version = "1.81"
|
||||
|
||||
[dependencies]
|
||||
yew-agent = { path = "../../packages/yew-agent/" }
|
||||
@ -12,7 +12,9 @@ yew-agent = { path = "../../packages/yew-agent/" }
|
||||
[dev-dependencies]
|
||||
derive_more = { version = "2.0", features = ["from"] }
|
||||
gloo = "0.11"
|
||||
gloo-net = "0.6"
|
||||
js-sys = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
weblog = "0.3.0"
|
||||
|
||||
@ -1,46 +1,141 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Write};
|
||||
use std::error::Error;
|
||||
use std::fmt::Write;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use std::{env, fs};
|
||||
|
||||
use glob::glob;
|
||||
|
||||
type Result<T = ()> = core::result::Result<T, Box<dyn Error + 'static>>;
|
||||
|
||||
macro_rules! e {
|
||||
($($fmt:tt),* $(,)?) => {
|
||||
return Err(format!($($fmt),*).into())
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert {
|
||||
($condition:expr, $($fmt:tt),* $(,)?) => {
|
||||
if !$condition { e!($($fmt),*) }
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Level {
|
||||
nested: HashMap<String, Level>,
|
||||
files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let home = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let pattern = format!("{home}/../../website/docs/**/*.md*");
|
||||
let base = format!("{home}/../../website");
|
||||
let base = Path::new(&base).canonicalize().unwrap();
|
||||
let dir_pattern = format!("{home}/../../website/docs/**");
|
||||
for dir in glob(&dir_pattern).unwrap() {
|
||||
println!("cargo:rerun-if-changed={}", dir.unwrap().display());
|
||||
fn should_combine_code_blocks(path: &Path) -> io::Result<bool> {
|
||||
const FLAG: &[u8] = b"<!-- COMBINE CODE BLOCKS -->";
|
||||
|
||||
let mut file = File::open(path)?;
|
||||
match file.seek(SeekFrom::End(-32)) {
|
||||
Ok(_) => (),
|
||||
Err(e) if e.kind() == ErrorKind::InvalidInput => return Ok(false),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
let mut buf = [0u8; 32];
|
||||
file.read_exact(&mut buf)?;
|
||||
Ok(buf.trim_ascii_end().ends_with(FLAG))
|
||||
}
|
||||
|
||||
let mut level = Level::default();
|
||||
fn apply_diff(src: &mut String, preamble: &str, added: &str, removed: &str) -> Result {
|
||||
assert!(
|
||||
!preamble.is_empty() || !removed.is_empty(),
|
||||
"Failure on applying a diff: \nNo preamble or text to remove provided, unable to find \
|
||||
location to insert:\n{added}\nIn the following text:\n{src}",
|
||||
);
|
||||
|
||||
for entry in glob(&pattern).unwrap() {
|
||||
let path = entry.unwrap();
|
||||
let path = Path::new(&path).canonicalize().unwrap();
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
let rel = path.strip_prefix(&base).unwrap();
|
||||
let mut matches = src.match_indices(if preamble.is_empty() {
|
||||
removed
|
||||
} else {
|
||||
preamble
|
||||
});
|
||||
let Some((preamble_start, _)) = matches.next() else {
|
||||
e!(
|
||||
"Failure on applying a diff: \ncouldn't find the following text:\n{preamble}\n\nIn \
|
||||
the following text:\n{src}"
|
||||
)
|
||||
};
|
||||
|
||||
let mut parts = vec![];
|
||||
assert!(
|
||||
matches.next().is_none(),
|
||||
"Failure on applying a diff: \nAmbiguous preamble:\n{preamble}\nIn the following \
|
||||
text:\n{src}\nWhile trying to remove the following text:\n{removed}\nAnd add the \
|
||||
following:\n{added}\n"
|
||||
);
|
||||
|
||||
for part in rel {
|
||||
parts.push(part.to_str().unwrap());
|
||||
let preamble_end = preamble_start + preamble.len();
|
||||
assert!(
|
||||
src.get(preamble_end..preamble_end + removed.len()) == Some(removed),
|
||||
"Failure on applying a diff: \nText to remove not found:\n{removed}\n\nIn the following \
|
||||
text:\n{src}",
|
||||
);
|
||||
|
||||
src.replace_range(preamble_end..preamble_end + removed.len(), added);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn combined_code_blocks(path: &Path) -> Result<String> {
|
||||
let file = BufReader::new(File::open(path)?);
|
||||
let mut res = String::new();
|
||||
|
||||
let mut err = Ok(());
|
||||
let mut lines = file
|
||||
.lines()
|
||||
.filter_map(|i| i.map_err(|e| err = Err(e)).ok());
|
||||
while let Some(line) = lines.next() {
|
||||
if !line.starts_with("```rust") {
|
||||
continue;
|
||||
}
|
||||
|
||||
level.insert(path.clone(), &parts[..]);
|
||||
let mut preamble = String::new();
|
||||
let mut added = String::new();
|
||||
let mut removed = String::new();
|
||||
let mut diff_applied = false;
|
||||
for line in &mut lines {
|
||||
if line.starts_with("```") {
|
||||
if !added.is_empty() || !removed.is_empty() {
|
||||
apply_diff(&mut res, &preamble, &added, &removed)?;
|
||||
} else if !diff_applied {
|
||||
// if no diff markers were found, just add the contents
|
||||
res += &preamble;
|
||||
}
|
||||
break;
|
||||
} else if let Some(line) = line.strip_prefix('+') {
|
||||
if line.starts_with(char::is_whitespace) {
|
||||
added += " ";
|
||||
}
|
||||
added += line;
|
||||
added += "\n";
|
||||
} else if let Some(line) = line.strip_prefix('-') {
|
||||
if line.starts_with(char::is_whitespace) {
|
||||
removed += " ";
|
||||
}
|
||||
removed += line;
|
||||
removed += "\n";
|
||||
} else if line.trim_ascii() == "// ..." {
|
||||
// disregard the preamble
|
||||
preamble.clear();
|
||||
} else {
|
||||
if !added.is_empty() || !removed.is_empty() {
|
||||
diff_applied = true;
|
||||
apply_diff(&mut res, &preamble, &added, &removed)?;
|
||||
preamble += &added;
|
||||
added.clear();
|
||||
removed.clear();
|
||||
}
|
||||
preamble += &line;
|
||||
preamble += "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let out = format!("{}/website_tests.rs", env::var("OUT_DIR").unwrap());
|
||||
|
||||
fs::write(out, level.to_contents()).unwrap();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
impl Level {
|
||||
@ -53,14 +148,14 @@ impl Level {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_contents(&self) -> String {
|
||||
fn to_contents(&self) -> Result<String> {
|
||||
let mut dst = String::new();
|
||||
|
||||
self.write_inner(&mut dst, 0).unwrap();
|
||||
dst
|
||||
self.write_inner(&mut dst, 0)?;
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
fn write_into(&self, dst: &mut String, name: &str, level: usize) -> fmt::Result {
|
||||
fn write_into(&self, dst: &mut String, name: &str, level: usize) -> Result {
|
||||
self.write_space(dst, level);
|
||||
let name = name.replace(['-', '.'], "_");
|
||||
writeln!(dst, "pub mod {name} {{")?;
|
||||
@ -73,24 +168,33 @@ impl Level {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_inner(&self, dst: &mut String, level: usize) -> fmt::Result {
|
||||
fn write_inner(&self, dst: &mut String, level: usize) -> Result {
|
||||
for (name, nested) in &self.nested {
|
||||
nested.write_into(dst, name, level)?;
|
||||
}
|
||||
|
||||
self.write_space(dst, level);
|
||||
|
||||
for file in &self.files {
|
||||
let stem = Path::new(file)
|
||||
let stem = file
|
||||
.file_stem()
|
||||
.unwrap()
|
||||
.ok_or_else(|| format!("no filename in path {file:?}"))?
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.ok_or_else(|| format!("non-UTF8 path: {file:?}"))?
|
||||
.replace('-', "_");
|
||||
|
||||
self.write_space(dst, level);
|
||||
|
||||
writeln!(dst, "#[doc = include_str!(r\"{}\")]", file.display())?;
|
||||
if should_combine_code_blocks(file)? {
|
||||
let res = combined_code_blocks(file)?;
|
||||
self.write_space(dst, level);
|
||||
writeln!(dst, "/// ```rust, no_run")?;
|
||||
for line in res.lines() {
|
||||
self.write_space(dst, level);
|
||||
writeln!(dst, "/// {line}")?;
|
||||
}
|
||||
self.write_space(dst, level);
|
||||
writeln!(dst, "/// ```")?;
|
||||
} else {
|
||||
self.write_space(dst, level);
|
||||
writeln!(dst, "#[doc = include_str!(r\"{}\")]", file.display())?;
|
||||
}
|
||||
self.write_space(dst, level);
|
||||
writeln!(dst, "pub fn {stem}_md() {{}}")?;
|
||||
}
|
||||
@ -104,3 +208,48 @@ impl Level {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_main() -> Result {
|
||||
let home = env::var("CARGO_MANIFEST_DIR")?;
|
||||
let pattern = format!("{home}/../../website/docs/**/*.md*");
|
||||
let base = format!("{home}/../../website");
|
||||
let base = Path::new(&base).canonicalize()?;
|
||||
let dir_pattern = format!("{home}/../../website/docs/**");
|
||||
for dir in glob(&dir_pattern)? {
|
||||
println!("cargo:rerun-if-changed={}", dir?.display());
|
||||
}
|
||||
|
||||
let mut level = Level::default();
|
||||
|
||||
for entry in glob(&pattern)? {
|
||||
let path = entry?.canonicalize()?;
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
let rel = path.strip_prefix(&base)?;
|
||||
|
||||
let mut parts = vec![];
|
||||
|
||||
for part in rel {
|
||||
parts.push(
|
||||
part.to_str()
|
||||
.ok_or_else(|| format!("Non-UTF8 path: {rel:?}"))?,
|
||||
);
|
||||
}
|
||||
|
||||
level.insert(path.clone(), &parts[..]);
|
||||
}
|
||||
|
||||
let out = format!("{}/website_tests.rs", env::var("OUT_DIR")?);
|
||||
|
||||
fs::write(out, level.to_contents()?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match inner_main() {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1 @@
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
pub mod tutorial;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/website_tests.rs"));
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Video {
|
||||
pub id: usize,
|
||||
pub title: String,
|
||||
pub speaker: String,
|
||||
pub url: String,
|
||||
}
|
||||
@ -24,6 +24,55 @@ Note this only builds for English locale unlike a production build.
|
||||
> Documentation is written in `mdx`, a superset of markdown empowered with jsx.
|
||||
> JetBrains and VSCode both provide MDX plugins.
|
||||
|
||||
## Testing
|
||||
|
||||
```console
|
||||
cargo make website-test
|
||||
```
|
||||
|
||||
[`website-test`](../tools/website-test) is a tool to test all code blocks in the docs as Rust doctests.
|
||||
It gathers the Rust code blocks automatically, but by default they're all tested separate. In case of a
|
||||
walkthrough, it makes more sense to combine the changes described in the blocks & test the code as one.
|
||||
For this end `website-test` scans all doc files for a special flag:
|
||||
|
||||
```html
|
||||
<!-- COMBINE CODE BLOCKS -->
|
||||
```
|
||||
If a file ends with this specific comment (and an optional newline after it), all code blocks will be
|
||||
sown together, with respect to the diff markers in them. For example:
|
||||
|
||||
```md
|
||||
\`\`\`rust
|
||||
fn main() {
|
||||
println!("Hello, World");
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
\`\`\`rust
|
||||
fn main() {
|
||||
- println!("Hello, World");
|
||||
+ println!("Goodbye, World");
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
<!-- COMBINE CODE BLOCKS -->
|
||||
```
|
||||
|
||||
Will be tested as:
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Goodbye, World");
|
||||
}
|
||||
```
|
||||
|
||||
:::warning
|
||||
The current implementation only uses the code before the diff or the code to remove as context,
|
||||
so make sure there's enough of it. The test assembler will tell you if there isn't.
|
||||
:::
|
||||
|
||||
While assembling the code blocks, the test assembler will put special meaning into a code
|
||||
line `// ...`. This line tells the test assembler to disregard any previous context for applying a diff
|
||||
|
||||
## Production Build
|
||||
|
||||
```console
|
||||
|
||||
@ -318,14 +318,12 @@ Props are evaluated in the order they're specified, as shown by the following ex
|
||||
#[derive(yew::Properties, PartialEq)]
|
||||
struct Props { first: usize, second: usize, last: usize }
|
||||
|
||||
fn main() {
|
||||
let mut g = 1..=3;
|
||||
let props = yew::props!(Props { first: g.next().unwrap(), second: g.next().unwrap(), last: g.next().unwrap() });
|
||||
let mut g = 1..=3;
|
||||
let props = yew::props!(Props { first: g.next().unwrap(), second: g.next().unwrap(), last: g.next().unwrap() });
|
||||
|
||||
assert_eq!(props.first, 1);
|
||||
assert_eq!(props.second, 2);
|
||||
assert_eq!(props.last, 3);
|
||||
}
|
||||
assert_eq!(props.first, 1);
|
||||
assert_eq!(props.second, 2);
|
||||
assert_eq!(props.last, 3);
|
||||
```
|
||||
|
||||
## Anti Patterns
|
||||
|
||||
@ -164,7 +164,7 @@ html! { <div attribute={value} /> };
|
||||
|
||||
Properties are specified with `~` before the element name:
|
||||
|
||||
```rust
|
||||
```rust , ignore
|
||||
use yew::prelude::*;
|
||||
|
||||
html! { <my-element ~property="abc" /> };
|
||||
|
||||
@ -185,22 +185,28 @@ We want to build a layout that looks something like this in raw HTML:
|
||||
Now, let's convert this HTML into `html!`. Type (or copy/paste) the following snippet into the body of `app` function
|
||||
such that the value of `html!` is returned by the function
|
||||
|
||||
```rust ,ignore
|
||||
html! {
|
||||
<>
|
||||
<h1>{ "RustConf Explorer" }</h1>
|
||||
<div>
|
||||
<h3>{"Videos to watch"}</h3>
|
||||
<p>{ "John Doe: Building and breaking things" }</p>
|
||||
<p>{ "Jane Smith: The development process" }</p>
|
||||
<p>{ "Matt Miller: The Web 7.0" }</p>
|
||||
<p>{ "Tom Jerry: Mouseless development" }</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{ "John Doe: Building and breaking things" }</h3>
|
||||
<img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
|
||||
</div>
|
||||
</>
|
||||
```rust {3-21}
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
- html! {
|
||||
- <h1>{ "Hello World" }</h1>
|
||||
- }
|
||||
+ html! {
|
||||
+ <>
|
||||
+ <h1>{ "RustConf Explorer" }</h1>
|
||||
+ <div>
|
||||
+ <h3>{ "Videos to watch" }</h3>
|
||||
+ <p>{ "John Doe: Building and breaking things" }</p>
|
||||
+ <p>{ "Jane Smith: The development process" }</p>
|
||||
+ <p>{ "Matt Miller: The Web 7.0" }</p>
|
||||
+ <p>{ "Tom Jerry: Mouseless development" }</p>
|
||||
+ </div>
|
||||
+ <div>
|
||||
+ <h3>{ "John Doe: Building and breaking things" }</h3>
|
||||
+ <img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
|
||||
+ </div>
|
||||
+ </>
|
||||
+ }
|
||||
}
|
||||
```
|
||||
|
||||
@ -215,6 +221,7 @@ Now, instead of hardcoding the list of videos in the HTML, let's define them as
|
||||
We create a simple `struct` (in `main.rs` or any file of our choice) that will hold our data.
|
||||
|
||||
```rust
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Video {
|
||||
id: usize,
|
||||
title: String,
|
||||
@ -225,44 +232,49 @@ struct Video {
|
||||
|
||||
Next, we will create instances of this struct in our `app` function and use those instead of hardcoding the data:
|
||||
|
||||
```rust
|
||||
use website_test::tutorial::Video; // replace with your own path
|
||||
|
||||
let videos = vec![
|
||||
Video {
|
||||
id: 1,
|
||||
title: "Building and breaking things".to_string(),
|
||||
speaker: "John Doe".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
Video {
|
||||
id: 2,
|
||||
title: "The development process".to_string(),
|
||||
speaker: "Jane Smith".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
Video {
|
||||
id: 3,
|
||||
title: "The Web 7.0".to_string(),
|
||||
speaker: "Matt Miller".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
Video {
|
||||
id: 4,
|
||||
title: "Mouseless development".to_string(),
|
||||
speaker: "Tom Jerry".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
];
|
||||
```rust {3-29}
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
+ let videos = vec![
|
||||
+ Video {
|
||||
+ id: 1,
|
||||
+ title: "Building and breaking things".to_string(),
|
||||
+ speaker: "John Doe".to_string(),
|
||||
+ url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
+ },
|
||||
+ Video {
|
||||
+ id: 2,
|
||||
+ title: "The development process".to_string(),
|
||||
+ speaker: "Jane Smith".to_string(),
|
||||
+ url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
+ },
|
||||
+ Video {
|
||||
+ id: 3,
|
||||
+ title: "The Web 7.0".to_string(),
|
||||
+ speaker: "Matt Miller".to_string(),
|
||||
+ url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
+ },
|
||||
+ Video {
|
||||
+ id: 4,
|
||||
+ title: "Mouseless development".to_string(),
|
||||
+ speaker: "Tom Jerry".to_string(),
|
||||
+ url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
+ },
|
||||
+ ];
|
||||
+
|
||||
```
|
||||
|
||||
To display them, we need to convert the `Vec` into `Html`. We can do that by creating an iterator,
|
||||
mapping it to `html!` and collecting it as `Html`:
|
||||
|
||||
```rust ,ignore
|
||||
let videos = videos.iter().map(|video| html! {
|
||||
<p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
|
||||
}).collect::<Html>();
|
||||
```rust {4-7}
|
||||
},
|
||||
];
|
||||
|
||||
+ let videos = videos.iter().map(|video| html! {
|
||||
+ <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
|
||||
+ }).collect::<Html>();
|
||||
+
|
||||
```
|
||||
|
||||
:::tip
|
||||
@ -271,21 +283,21 @@ Keys on list items help Yew keep track of which items have changed in the list,
|
||||
|
||||
And finally, we need to replace the hardcoded list of videos with the `Html` we created from the data:
|
||||
|
||||
```rust ,ignore {6-10}
|
||||
html! {
|
||||
<>
|
||||
<h1>{ "RustConf Explorer" }</h1>
|
||||
<div>
|
||||
<h3>{ "Videos to watch" }</h3>
|
||||
- <p>{ "John Doe: Building and breaking things" }</p>
|
||||
- <p>{ "Jane Smith: The development process" }</p>
|
||||
- <p>{ "Matt Miller: The Web 7.0" }</p>
|
||||
- <p>{ "Tom Jerry: Mouseless development" }</p>
|
||||
+ { videos }
|
||||
</div>
|
||||
// ...
|
||||
</>
|
||||
}
|
||||
```rust {6-10}
|
||||
html! {
|
||||
<>
|
||||
<h1>{ "RustConf Explorer" }</h1>
|
||||
<div>
|
||||
<h3>{ "Videos to watch" }</h3>
|
||||
- <p>{ "John Doe: Building and breaking things" }</p>
|
||||
- <p>{ "Jane Smith: The development process" }</p>
|
||||
- <p>{ "Matt Miller: The Web 7.0" }</p>
|
||||
- <p>{ "Tom Jerry: Mouseless development" }</p>
|
||||
+ { videos }
|
||||
</div>
|
||||
// ...
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
## Components
|
||||
@ -305,16 +317,7 @@ In this tutorial, we will be using function components.
|
||||
Now, let's split up our `App` component into smaller components. We begin by extracting the videos list into
|
||||
its own component.
|
||||
|
||||
```rust ,compile_fail
|
||||
use yew::prelude::*;
|
||||
|
||||
struct Video {
|
||||
id: usize,
|
||||
title: String,
|
||||
speaker: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
```rust
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct VideosListProps {
|
||||
videos: Vec<Video>,
|
||||
@ -336,33 +339,21 @@ In this case, `VideosListProps` is a struct that defines the props.
|
||||
The struct used for props must implement `Properties` by deriving it.
|
||||
:::
|
||||
|
||||
For the above code to compile, we need to modify the `Video` struct like this:
|
||||
|
||||
```rust {1}
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Video {
|
||||
id: usize,
|
||||
title: String,
|
||||
speaker: String,
|
||||
url: String,
|
||||
}
|
||||
```
|
||||
|
||||
Now, we can update our `App` component to make use of `VideosList` component.
|
||||
|
||||
```rust ,ignore {4-7,13-14}
|
||||
```rust {4-7,13-14}
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
// ...
|
||||
- let videos = videos.iter().map(|video| html! {
|
||||
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
|
||||
- }).collect::<Html>();
|
||||
- let videos = videos.iter().map(|video| html! {
|
||||
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
|
||||
- }).collect::<Html>();
|
||||
-
|
||||
html! {
|
||||
<>
|
||||
<h1>{ "RustConf Explorer" }</h1>
|
||||
<div>
|
||||
<h3>{"Videos to watch"}</h3>
|
||||
<h3>{ "Videos to watch" }</h3>
|
||||
- { videos }
|
||||
+ <VideosList videos={videos} />
|
||||
</div>
|
||||
@ -382,34 +373,35 @@ The final goal here is to display the selected video. To do that, `VideosList` c
|
||||
parent when a video is selected, which is done via a `Callback`. This concept is called "passing handlers".
|
||||
We modify its props to take an `on_click` callback:
|
||||
|
||||
```rust ,ignore {4}
|
||||
```rust {4}
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct VideosListProps {
|
||||
videos: Vec<Video>,
|
||||
+ on_click: Callback<Video>
|
||||
+ on_click: Callback<Video>,
|
||||
}
|
||||
```
|
||||
|
||||
Then we modify the `VideosList` component to "emit" the selected video to the callback.
|
||||
|
||||
```rust ,ignore {2-4,6-12,15-16}
|
||||
```rust {2-18}
|
||||
#[function_component(VideosList)]
|
||||
-fn videos_list(VideosListProps { videos }: &VideosListProps) -> Html {
|
||||
+fn videos_list(VideosListProps { videos, on_click }: &VideosListProps) -> Html {
|
||||
+ let on_click = on_click.clone();
|
||||
videos.iter().map(|video| {
|
||||
+ let on_video_select = {
|
||||
+ let on_click = on_click.clone();
|
||||
+ let video = video.clone();
|
||||
+ Callback::from(move |_| {
|
||||
+ on_click.emit(video.clone())
|
||||
+ })
|
||||
+ };
|
||||
|
||||
html! {
|
||||
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
|
||||
- videos.iter().map(|video| html! {
|
||||
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
|
||||
+ let on_click = on_click.clone();
|
||||
+ videos.iter().map(|video| {
|
||||
+ let on_video_select = {
|
||||
+ let on_click = on_click.clone();
|
||||
+ let video = video.clone();
|
||||
+ Callback::from(move |_| {
|
||||
+ on_click.emit(video.clone())
|
||||
+ })
|
||||
+ };
|
||||
+
|
||||
+ html! {
|
||||
+ <p key={video.id} onclick={on_video_select}>{format!("{}: {}", video.speaker, video.title)}</p>
|
||||
}
|
||||
+ }
|
||||
}).collect()
|
||||
}
|
||||
```
|
||||
@ -418,9 +410,6 @@ Next, we need to modify the usage of `VideosList` to pass that callback. But bef
|
||||
a new component, `VideoDetails`, that is displayed when a video is clicked.
|
||||
|
||||
```rust
|
||||
use website_test::tutorial::Video;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct VideosDetailsProps {
|
||||
video: Video,
|
||||
@ -439,36 +428,36 @@ fn video_details(VideosDetailsProps { video }: &VideosDetailsProps) -> Html {
|
||||
|
||||
Now, modify the `App` component to display `VideoDetails` component whenever a video is selected.
|
||||
|
||||
```rust ,ignore {4,6-11,13-15,22-23,25-29}
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
// ...
|
||||
+ let selected_video = use_state(|| None);
|
||||
|
||||
+ let on_video_select = {
|
||||
+ let selected_video = selected_video.clone();
|
||||
+ Callback::from(move |video: Video| {
|
||||
+ selected_video.set(Some(video))
|
||||
+ })
|
||||
+ };
|
||||
|
||||
+ let details = selected_video.as_ref().map(|video| html! {
|
||||
+ <VideoDetails video={video.clone()} />
|
||||
+ });
|
||||
```rust {3-15,22-23,25-29}
|
||||
},
|
||||
];
|
||||
+
|
||||
+ let selected_video = use_state(|| None);
|
||||
+
|
||||
+ let on_video_select = {
|
||||
+ let selected_video = selected_video.clone();
|
||||
+ Callback::from(move |video: Video| {
|
||||
+ selected_video.set(Some(video))
|
||||
+ })
|
||||
+ };
|
||||
+
|
||||
+ let details = selected_video.as_ref().map(|video| html! {
|
||||
+ <VideoDetails video={video.clone()} />
|
||||
+ });
|
||||
|
||||
html! {
|
||||
<>
|
||||
<h1>{ "RustConf Explorer" }</h1>
|
||||
<div>
|
||||
<h3>{"Videos to watch"}</h3>
|
||||
<h3>{ "Videos to watch" }</h3>
|
||||
- <VideosList videos={videos} />
|
||||
+ <VideosList videos={videos} on_click={on_video_select.clone()} />
|
||||
</div>
|
||||
+ { for details }
|
||||
- <div>
|
||||
- <h3>{ "John Doe: Building and breaking things" }</h3>
|
||||
- <img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
|
||||
- </div>
|
||||
+ { for details }
|
||||
- <div>
|
||||
- <h3>{ "John Doe: Building and breaking things" }</h3>
|
||||
- <img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
|
||||
- </div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
@ -517,11 +506,12 @@ Otherwise you won't be able to run your application.
|
||||
|
||||
Update the `Video` struct to derive the `Deserialize` trait:
|
||||
|
||||
```rust ,ignore {1, 3-4}
|
||||
+ use serde::Deserialize;
|
||||
|
||||
- #[derive(Clone, PartialEq)]
|
||||
+ #[derive(Clone, PartialEq, Deserialize)]
|
||||
```rust {2,4-5}
|
||||
use yew::prelude::*;
|
||||
+use serde::Deserialize;
|
||||
// ...
|
||||
-#[derive(Clone, PartialEq)]
|
||||
+#[derive(Clone, PartialEq, Deserialize)]
|
||||
struct Video {
|
||||
id: usize,
|
||||
title: String,
|
||||
@ -532,32 +522,57 @@ struct Video {
|
||||
|
||||
Now as the last step, we need to update our `App` component to make the fetch request instead of using hardcoded data
|
||||
|
||||
```rust ,ignore {1,5-25,34-35}
|
||||
+ use gloo_net::http::Request;
|
||||
```rust {2,6-50,59-60}
|
||||
use yew::prelude::*;
|
||||
+use gloo_net::http::Request;
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
- let videos = vec![
|
||||
- // ...
|
||||
- ]
|
||||
+ let videos = use_state(|| vec![]);
|
||||
+ {
|
||||
+ let videos = videos.clone();
|
||||
+ use_effect_with((), move |_| {
|
||||
+ let videos = videos.clone();
|
||||
+ wasm_bindgen_futures::spawn_local(async move {
|
||||
+ let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
|
||||
+ .send()
|
||||
+ .await
|
||||
+ .unwrap()
|
||||
+ .json()
|
||||
+ .await
|
||||
+ .unwrap();
|
||||
+ videos.set(fetched_videos);
|
||||
+ });
|
||||
+ || ()
|
||||
+ });
|
||||
+ }
|
||||
- let videos = vec![
|
||||
- Video {
|
||||
- id: 1,
|
||||
- title: "Building and breaking things".to_string(),
|
||||
- speaker: "John Doe".to_string(),
|
||||
- url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
- },
|
||||
- Video {
|
||||
- id: 2,
|
||||
- title: "The development process".to_string(),
|
||||
- speaker: "Jane Smith".to_string(),
|
||||
- url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
- },
|
||||
- Video {
|
||||
- id: 3,
|
||||
- title: "The Web 7.0".to_string(),
|
||||
- speaker: "Matt Miller".to_string(),
|
||||
- url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
- },
|
||||
- Video {
|
||||
- id: 4,
|
||||
- title: "Mouseless development".to_string(),
|
||||
- speaker: "Tom Jerry".to_string(),
|
||||
- url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
- },
|
||||
- ];
|
||||
-
|
||||
+ let videos = use_state(|| vec![]);
|
||||
+ {
|
||||
+ let videos = videos.clone();
|
||||
+ use_effect_with((), move |_| {
|
||||
+ let videos = videos.clone();
|
||||
+ wasm_bindgen_futures::spawn_local(async move {
|
||||
+ let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
|
||||
+ .send()
|
||||
+ .await
|
||||
+ .unwrap()
|
||||
+ .json()
|
||||
+ .await
|
||||
+ .unwrap();
|
||||
+ videos.set(fetched_videos);
|
||||
+ });
|
||||
+ || ()
|
||||
+ });
|
||||
+ }
|
||||
|
||||
// ...
|
||||
|
||||
@ -565,9 +580,9 @@ fn app() -> Html {
|
||||
<>
|
||||
<h1>{ "RustConf Explorer" }</h1>
|
||||
<div>
|
||||
<h3>{"Videos to watch"}</h3>
|
||||
- <VideosList videos={videos} on_click={on_video_select.clone()} />
|
||||
+ <VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
|
||||
<h3>{ "Videos to watch" }</h3>
|
||||
- <VideosList videos={videos} on_click={on_video_select.clone()} />
|
||||
+ <VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
|
||||
</div>
|
||||
{ for details }
|
||||
</>
|
||||
@ -585,11 +600,9 @@ To fix that, we need a proxy server. Luckily trunk provides that.
|
||||
|
||||
Update the following line:
|
||||
|
||||
```rust ,ignore {2-3}
|
||||
// ...
|
||||
- let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
|
||||
+ let fetched_videos: Vec<Video> = Request::get("/tutorial/data.json")
|
||||
// ...
|
||||
```rust {2-3}
|
||||
- let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
|
||||
+ let fetched_videos: Vec<Video> = Request::get("/tutorial/data.json")
|
||||
```
|
||||
|
||||
Now, rerun the server with the following command:
|
||||
@ -624,3 +637,5 @@ See [external libraries](/community/external-libs) for more details.
|
||||
|
||||
Read our [official documentation](../getting-started/introduction.mdx). It explains a lot of concepts in much more detail.
|
||||
To learn more about the Yew API, see our [API docs](https://docs.rs/yew).
|
||||
|
||||
<!-- COMBINE CODE BLOCKS -->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user