Since I took 12 hours to figure this out i decided to write up this article so you won't have to.
The Problem
Sometimes we want to control a child component from within a parent component. One of those instances might be an event handler that acts on the parent but triggers something in the child. Imagine you want to change the #properties of a child based on cursor position for example.
A concrete example would be to snap an component to the pointer, but within the confines of a parent component. You can't place the event listener on the child component, because you're not interacting with the child, so you have to put the listener on the parent, pass the event to the child and then execute some logic for that child.
Now this might sound quite trivial and if you'd be willing to leave the confines of yew (or rust effectiely for that matter) and just use the web_sys and js_sys bindings, you could. It's still kind of hard to do and feels quite hacky. It's the kind of hacky solution you'd be absolutely convinced that theres a better way to solve the problem, if not a native one.
Well, good news is it exists. Bad news is that it's overcomplicated and kind of annoying to set up. Once it's set up however, it is quite nice to interact with.
so, how do we solve this problem?
The Approach
We know that we have to send a message to a child component. However there is no native way to receive the context of said child object. Likely because it was instantiated after the parent component. You can access children if you go the children prop route, but you're limited to whatever the VChild type implements, which is not alot and useless for our case.
So... that won't work.
Now, communication with children might be difficult, but communication with the parent is not... supposed to be. It still is but at least it's possible.
In other words:
- in order to communicate with a child from a parent we need to get the child component.
- the child component can communicate with the parent.
Why don't we link the child component to the parent from within the child.
Prerequisites
In order to link the child we need to have a field in our parent struct that represents that childs link. The context.link() method returns a Scope of whatever component it contains, which means we set up our parent like so:
struct Parent {
selected_child: Scope<Child>,
}
Now immediately theres a problem because when it comes to instantiating the struct we'll need a Scope of type Child, which, if we had that, we wouldn't have to create a workaround in the first place. Luckily we can turn that bad boy into an Option, which means that the value of selected_child is either a Scope or None.
struct Parent {
selected_child: Option<Scope<Child>>,
}
fn create -> Self {
Self {
selected_child: None,
}
}
We also know that we can get the context of a parent with the .get_parent() method of the context.link(), which means we can also send a message to that parent. So we might aswell just handle the setup with that.
enum Msg {
SetSelectedChild(Scope<EditableCssBoxShape>),
// won't need to wrap that as an option yet,
// since we have the element when we send the message
}
struct Parent {
selected_child: Option<Scope<Child>>,
}
impl Component for Parent {
type Message: Msg;
fn create -> Self {
Self {
selected_child: None,
}
}
fn update -> bool {
match msg {
Msg::SetSelectedChild(child_scope) => {
self.selected_child = Some( child_scope );
// no need to rerender
false
}
}
}
}
and that's basically all the setup the parent component needs. Annoyed yet? It gets worse :)
Now for the setup for the child component.
In the childs component we now have to send that message to the parent in order to set up a link for the parent component to use. We could use the rendered method of the component, but that only makes sense if we only have one child component. Instead we use an event listener and the update method.
enum Msg {
StartMovingWithCursor(web_sys::DragEvent)
}
fn update -> bool {
match msg {
Msg::StartMovingWithCursor(e) => {
e.prevent_default();
}
}
}
Now we just want to send a message to the parent. Sounds easy enough. Warning: this code doesn't work.
enum Msg {
StartMovingWithCursor(web_sys::DragEvent)
}
fn update -> bool {
match msg {
Msg::StartMovingWithCursor(e) => {
e.prevent_default();
let parent = ctx.link().get_parent();
parent.send_message(parent.Message::SetSelectedChild( ctx.link() ));
}
}
}
...is how you'd expect it to work. Maybe throw in some references here and there so we don't copy the values out of scope, but other than that seems good enough, right? Wrong! Instead of the simple intuitiveness of that code we have to do something like:
enum Msg {
StartMovingWithCursor(web_sys::DragEvent)
}
fn update -> bool {
match msg {
Msg::StartMovingWithCursor(e) => {
e.prevent_default();
let child_link = ctx.link().clone();
let parent_link = ctx.link().get_parent().expect("No Parent found").clone();
parent_link.downcast::<super::super::Parent>().send_message( super::super::Msg::SetSelectedChild( child_link ));
}
}
}
Now this does work. A few of the subsequent function calls try to move the values it turns out. some consume the struct entirely. Some of it makes sense, some of it is just weird.
Anyway here are the reasons for what we have to do:
- clone the context link so we dont move the context out of the scope (fair)
- clone the unwraped parent link because downcast consumes the parent_link (weird, just give me a reference)
- it can't infer the type of the parent component so we have to manually grab that from wherever that component sits (what, why?)
- the same goes for the Message we have to send (i mean, at least it's consistent on that part)
But, at long last, we did it. we can finally communicate with a child component, given the child and the parent component sit in the same crate.
All that so we can do stuff like this:
if let Some(link) = self.selected_child.as_ref() {
link.send_message(Child::Msg::SendMessageToAChild);
}
All because we can't just go
ctx.props()
.children
.iter()
.for_each( |child| child.link().send_nessage())
or whatever.
Summary
You'll need this mess in your Child
enum Msg {
StartMovingWithCursor(web_sys::DragEvent)
}
fn update -> bool {
match msg {
Msg::StartMovingWithCursor(e) => {
e.prevent_default();
let child_link = ctx.link().clone();
let parent_link = ctx.link().get_parent().expect("No Parent found").clone();
parent_link.downcast::<super::super::Parent>().send_message( super::super::Msg::SetSelectedChild( child_link ));
}
}
}
And this mess in your Parent
enum Msg {
SetSelectedChild(Scope<EditableCssBoxShape>),
}
struct Parent {
selected_child: Option<Scope<Child>>,
}
impl Component for Parent {
type Message: Msg;
fn create -> Self {
Self {
selected_child: None,
}
}
fn update -> bool {
match msg {
Msg::SetSelectedChild(child_scope) => {
self.selected_child = Some( child_scope );
// no need to rerender
false
}
}
}
}
I get why most of it works that way (i think). It's just a bad developer experience.
Afterword
In all seriousness I do love rust and I think yew is pretty awesome. There is just something about the explicity that feels like you're done when you've done something. Unlike having to handle edge-cases it feels like you only handle the cases you need to handle because anything outside of that will either throw an actually helpful error or not occur in the first place. I'm exagerating quite a bit and I've run into a few runtime errors myself but it's no where near as bad as working with javascript on the frontend.
Thanks for the great work to the yew / web_sys / rust / wasm guys and gals and non-binary pals out there.
Top comments (0)