Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement imperative refs, capturing scopes to child components #2551

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
562e879
implement imperative refs, capturing a scope
WorldSEnder Mar 27, 2022
d1dd4bc
documentation for ComponentRef
WorldSEnder Mar 27, 2022
32dfe0d
fix doc test
WorldSEnder Mar 27, 2022
ff99444
Fix a bunch of issues with docs
WorldSEnder Mar 27, 2022
f8b917e
address review:
WorldSEnder Mar 29, 2022
1f3de5d
restrict new and get to struct components for now
WorldSEnder Mar 29, 2022
560e56e
shift ComponentRef into inner state
WorldSEnder Mar 31, 2022
7ecb58f
rename ComponentAnyRef
WorldSEnder Mar 31, 2022
fc563ad
fixup linting
WorldSEnder Apr 22, 2022
7070025
implement HtmlRef
WorldSEnder Apr 24, 2022
08fb45e
commit to changed macro errors
WorldSEnder Apr 24, 2022
f1908f4
commit the component ref immdiately after view
WorldSEnder Apr 24, 2022
39492bf
make the reference bindable
WorldSEnder Apr 24, 2022
3e3c674
some code size optimizations
WorldSEnder Apr 24, 2022
00a2e0b
Merge remote-tracking branch 'upstream/master' into imperative-ref
WorldSEnder Apr 25, 2022
0a4a2ee
fix doc test
WorldSEnder Apr 25, 2022
d5684bc
fix website tests
WorldSEnder Apr 25, 2022
0a05ffc
the ref is now bound in BaseComponent::create
WorldSEnder Apr 26, 2022
58087d5
remove dyn Debug of user binds, saves space
WorldSEnder Apr 26, 2022
c2785c7
fixup website tests
WorldSEnder Apr 26, 2022
fc753ba
feature soundness, apparently
WorldSEnder Apr 26, 2022
9eb2edf
re-expose event log for debugging
WorldSEnder May 4, 2022
b857074
add default fn to BaseComponent
WorldSEnder May 4, 2022
56fa37d
Merge remote-tracking branch 'upstream/master' into imperative-ref
WorldSEnder May 4, 2022
cbf0dd7
introduce intermediate trait ComponentWithRef
WorldSEnder May 6, 2022
da0c131
pass BindableRef to FunctionComponent
WorldSEnder May 6, 2022
4bf5e23
introduce explicit ErasedHtmlRef::unbound
WorldSEnder May 6, 2022
7698970
convert light_switch to example
WorldSEnder May 14, 2022
cbad47b
assert that ref is set in debug mode
WorldSEnder May 14, 2022
6c3d655
Merge remote-tracking branch 'upstream/master' into imperative-ref
WorldSEnder May 14, 2022
f229c62
inch towards imperative ref on function comps
WorldSEnder May 14, 2022
b104b58
remove ErasedStorage
WorldSEnder May 14, 2022
895ce20
NoReference is now an empty enum
WorldSEnder May 14, 2022
8e77558
fix website link
WorldSEnder May 14, 2022
22e6db5
Rc<Setter> -> Setter
WorldSEnder May 14, 2022
6f9590d
readability, use CompRef
WorldSEnder May 14, 2022
afcbeb2
move erased_ref to ComponentState
WorldSEnder May 14, 2022
5dd862a
fix feature linting
WorldSEnder May 15, 2022
9deac03
omit a drop impl
WorldSEnder May 15, 2022
cbabe59
adjust documentation
WorldSEnder May 15, 2022
258cc3a
move node erasure to construction
WorldSEnder May 15, 2022
42e1a0c
fixup of previous 2 commits
WorldSEnder May 15, 2022
3eeae19
fix doc test in website-docs
WorldSEnder May 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -22,6 +22,7 @@ members = [
"examples/inner_html",
"examples/js_callback",
"examples/keyed_list",
"examples/light_switch",
"examples/mount_point",
"examples/nested_list",
"examples/node_refs",
Expand Down
9 changes: 9 additions & 0 deletions examples/light_switch/Cargo.toml
@@ -0,0 +1,9 @@
[package]
name = "light_switch"
version = "0.1.0"
authors = ["WorldSEnder <WorldSEnder@users.noreply.github.com>"]
edition = "2021"
license = "MIT OR Apache-2.0"

[dependencies]
yew = { path = "../../packages/yew", features = ["csr"] }
23 changes: 23 additions & 0 deletions examples/light_switch/README.md
@@ -0,0 +1,23 @@
# Counter Example

[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Flight_switch)](https://examples.yew.rs/light_switch)

A simple example of a light switch controlling the light setting in neighboring component

## Running

Run a debug version of this application:

```bash
trunk serve
```

Run a release version of this application:

```bash
trunk serve --release
```

## Concepts

Demonstrates the use of ComponentRef to send messages to children and sibling components.
12 changes: 12 additions & 0 deletions examples/light_switch/index.html
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Yew • Light Switch</title>

<link data-trunk rel="rust" />
<link data-trunk rel="sass" href="index.scss" />
</head>

<body></body>
</html>
64 changes: 64 additions & 0 deletions examples/light_switch/index.scss
@@ -0,0 +1,64 @@
.switchlabel {
display: flex;
align-items: center;
justify-content: center;
}

.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
margin: 2px;
}

.switch input {
display: none;
}

.switch .drive {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
border-radius: 34px;
display: flex;
}

.switch .pin {
height: 26px;
width: 26px;
background-color: white;
border-radius: 50%;
margin: 4px;
}

.drive .buffer {
transition: flex .1s;
}

.switch input:checked + .drive {
background-color: #2196F3;
}

.switch input:checked + .drive .buffer {
flex: 1;
}

.switch input:focus + .drive {
box-shadow: 0 0 1px #2196F3;
}

.light {
min-height: 400px;
background-color: #404040;
}

.light.on {
background-color: rgb(242, 245, 53);
}
77 changes: 77 additions & 0 deletions examples/light_switch/src/main.rs
@@ -0,0 +1,77 @@
use yew::html::{BindableRef, Scope};
use yew::prelude::*;

#[derive(PartialEq, Properties)]
struct SwitchProps {
on_toggle: Callback<MouseEvent>,
}

#[function_component]
fn Switch(SwitchProps { on_toggle }: &SwitchProps) -> Html {
html! {
<div class="switchlabel">
<label class="switch">
<input type="checkbox" onclick={on_toggle} />
<span class="drive">
<span class="buffer" />
<span class="pin" />
</span>
</label>
{"Light switch"}
</div>
}
}

enum LightMessage {
Toggle,
}

struct Light {
is_on: bool,
}

impl ComponentWithRef for Light {
type Message = LightMessage;
type Properties = ();
type Reference = Scope<Self>;

fn create(ctx: &Context<Self>, bindable_ref: BindableRef<'_, Self::Reference>) -> Self {
bindable_ref.bind(ctx.link().clone());
Light { is_on: false }
}

fn update(&mut self, _ctx: &Context<Self>, LightMessage::Toggle: LightMessage) -> bool {
self.is_on = !self.is_on;
true
}

fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<div class={classes!["light", self.is_on.then(|| "on")]} />
}
}
}

#[function_component]
fn Room() -> Html {
let light_ref: ComponentRef<Light> = use_html_ref();
let on_toggle = {
let light_ref = light_ref.clone();
Callback::from(move |_| {
light_ref
.get()
.expect("a light to have rendered")
.send_message(LightMessage::Toggle)
})
};
html! {
<>
<Light ref={&light_ref} />
<Switch {on_toggle} />
</>
}
}

fn main() {
yew::Renderer::<Room>::new().render();
}
2 changes: 1 addition & 1 deletion examples/nested_list/README.md
Expand Up @@ -7,7 +7,7 @@ This example shows a nested list and displays which item was last hovered.
## Concepts

- Creating components which only accepts specific child elements
- Communicating with a component that isn't a parent. See `WeakComponentLink` in [main.rs](src/main.rs).
- Communicating with a component that isn't a parent.

## Improvements

Expand Down
14 changes: 7 additions & 7 deletions examples/nested_list/src/app.rs
Expand Up @@ -3,16 +3,16 @@ use yew::prelude::*;
use super::header::ListHeader;
use super::item::ListItem;
use super::list::List;
use super::{Hovered, WeakComponentLink};
use super::Hovered;

pub enum Msg {
Hover(Hovered),
}

pub struct App {
hovered: Hovered,
list_link: WeakComponentLink<List>,
sub_list_link: WeakComponentLink<List>,
list_link: ComponentRef<List>,
sub_list_link: ComponentRef<List>,
}

impl Component for App {
Expand All @@ -22,8 +22,8 @@ impl Component for App {
fn create(_ctx: &Context<Self>) -> Self {
Self {
hovered: Hovered::None,
list_link: WeakComponentLink::default(),
sub_list_link: WeakComponentLink::default(),
list_link: HtmlRef::new(),
sub_list_link: HtmlRef::new(),
}
}

Expand Down Expand Up @@ -53,13 +53,13 @@ impl Component for App {
html! {
<div class="main" {onmouseover}>
<h1>{ "Nested List Demo" }</h1>
<List {on_hover} weak_link={list_link}>
<List {on_hover} ref={list_link}>
<ListHeader text="Calling all Rusties!" {on_hover} {list_link} />
<ListItem name="Rustin" {on_hover} />
<ListItem hide=true name="Rustaroo" {on_hover} />
<ListItem name="Rustifer" {on_hover}>
<div class="sublist" onmouseover={onmouseoversublist}>{ "Sublist!" }</div>
<List {on_hover} weak_link={sub_list_link}>
<List {on_hover} ref={sub_list_link}>
<ListHeader text="Sub Rusties!" {on_hover} list_link={sub_list_link}/>
<ListItem hide=true name="Hidden Sub" {on_hover} />
{ for letters }
Expand Down
6 changes: 3 additions & 3 deletions examples/nested_list/src/header.rs
@@ -1,13 +1,13 @@
use yew::prelude::*;

use super::list::{List, Msg as ListMsg};
use super::{Hovered, WeakComponentLink};
use super::Hovered;

#[derive(Clone, PartialEq, Properties)]
pub struct Props {
pub on_hover: Callback<Hovered>,
pub text: String,
pub list_link: WeakComponentLink<List>,
pub list_link: ComponentRef<List>,
}

pub struct ListHeader;
Expand All @@ -21,7 +21,7 @@ impl Component for ListHeader {
}

fn view(&self, ctx: &Context<Self>) -> Html {
let list_link = ctx.props().list_link.borrow().clone().unwrap();
let list_link = ctx.props().list_link.get().unwrap();
let onmouseover = ctx.props().on_hover.reform(|e: MouseEvent| {
e.stop_propagation();
Hovered::Header
Expand Down
21 changes: 8 additions & 13 deletions examples/nested_list/src/list.rs
@@ -1,12 +1,12 @@
use std::rc::Rc;

use yew::html::{ChildrenRenderer, NodeRef};
use yew::html::{BindableRef, ChildrenRenderer, ComponentWithRef, Scope};
use yew::prelude::*;
use yew::virtual_dom::{VChild, VComp};

use crate::header::{ListHeader, Props as HeaderProps};
use crate::item::{ListItem, Props as ItemProps};
use crate::{Hovered, WeakComponentLink};
use crate::Hovered;

#[derive(Clone, PartialEq)]
pub enum Variants {
Expand Down Expand Up @@ -46,10 +46,8 @@ where
impl From<ListVariant> for Html {
fn from(variant: ListVariant) -> Html {
match variant.props {
Variants::Header(props) => {
VComp::new::<ListHeader>(props, NodeRef::default(), None).into()
}
Variants::Item(props) => VComp::new::<ListItem>(props, NodeRef::default(), None).into(),
Variants::Header(props) => VComp::new::<ListHeader>(props, None, None).into(),
Variants::Item(props) => VComp::new::<ListItem>(props, None, None).into(),
}
}
}
Expand All @@ -62,22 +60,19 @@ pub enum Msg {
pub struct Props {
pub children: ChildrenRenderer<ListVariant>,
pub on_hover: Callback<Hovered>,
pub weak_link: WeakComponentLink<List>,
}

pub struct List {
inactive: bool,
}

impl Component for List {
impl ComponentWithRef for List {
type Message = Msg;
type Properties = Props;
type Reference = Scope<Self>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whilst I was originally suggesting to not classify ref as a normal prop, however, after @hamza1311 brought up the idea of making the ref as a normal prop, I actually think this is a better approach and leads to a much simpler design.

Suppose that if there are 2 refs (e.g.: ref and inputRef) for a single component, how to handle these 2 at the same time?
(It's not always the case to simply forward the inputRef to the inner input element, as it can be enhanced by adding additional methods to it.)
In addition, I don't think it actually provides any benefit of actually separating the handling of these 2 refs especially if you have to set it manually.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a semantic difference: ref means that this component is responsible for binding the referenced value. Normally, you get a currently referenced value from a HtmlRef. Hence why crate accepts a special BindableRef type that allows you to bind (and maybe rebind, I'm not sure at this point, but it could make sense) a value. BindableRef also could have a method to decay it to a ref the component binds to one of its children, i.e. to forward the ref.

But being the HtmlRef that this and only this component is in charge of binding, is imo special enough to deserve separate treatment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you wrap a third party component library that is linked with VRef and you have 2 props (ref and input_ref) with ref linked to the container div.

How do you expose input_ref to the inner input element created by VRef without creating another component?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added BindableRef::forward so the component can capture a HtmlRef it can itself bind on children returned from view.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not quite solve the question I mentioned.

Suppose there is a component with 2 refs, input_ref and ref.

#[derive(Properties, PartialEq)]
pub struct MyInputProps {
    input_ref: HtmlRef<Element>,
}

// Suppose there is a third-party UI library
// Using custom component as an example, 
// but applies to any UI library that cannot be created by directly rendering layout.
// e.g.: React, etc.
let el = document().create_element("my-input");
let input_el = el.query_selector("input");

let html = VNode::VRef(el);

bindable_ref.set(el);
// how to set input_ref to input_el?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose VRef should accept a NodeRef, too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose VRef should accept a NodeRef, too?

I don't see how this solves the example in previous comment.

VRef is registered on el but input_ref is supposed to be registered on input_el which is queried from the el by query_selector.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I didn't quite get what you were asking: The best strategy for this case, imo, would be to expose a struct containing all the referenced elements. Such as

struct InputRefs {
    el: Element,
    input_el: Element,
}
impl ComponentWithRef for MyInput {
    type Reference = InputRefs;

    fn create(ctx: Context<Self>, bindable_ref: BindableRef<Self>) {
        let el = document().create_element("my-input");
        let input_el = el.query_selector("input");
        bindable_ref.set(InputRefs { el: el.clone(), input_el: input_el.clone() });
        // store el in Self
    }
    fn view(...) {
        VNode::VRef(self.el);
    }
}

Copy link
Member

@futursolo futursolo Jun 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to mention that usually a custom element usually will not render its content until its connectedCallback is invoked. Which means that el and input_el are actually made available in different component lifecycle, the input_el element will not become available until rendered() cycle when as el must be passed as VRef for it to become rendered.


fn create(ctx: &Context<Self>) -> Self {
ctx.props()
.weak_link
.borrow_mut()
.replace(ctx.link().clone());
fn create(ctx: &Context<Self>, bindable_ref: BindableRef<'_, Self::Reference>) -> Self {
bindable_ref.bind(ctx.link().clone());
Self { inactive: false }
}

Expand Down
34 changes: 0 additions & 34 deletions examples/nested_list/src/main.rs
Expand Up @@ -3,41 +3,7 @@ mod header;
mod item;
mod list;

use std::cell::RefCell;
use std::fmt;
use std::ops::Deref;
use std::rc::Rc;

use yew::html::{Component, ImplicitClone, Scope};

pub struct WeakComponentLink<COMP: Component>(Rc<RefCell<Option<Scope<COMP>>>>);

impl<COMP: Component> Clone for WeakComponentLink<COMP> {
fn clone(&self) -> Self {
Self(Rc::clone(&self.0))
}
}
impl<COMP: Component> ImplicitClone for WeakComponentLink<COMP> {}

impl<COMP: Component> Default for WeakComponentLink<COMP> {
fn default() -> Self {
Self(Rc::default())
}
}

impl<COMP: Component> Deref for WeakComponentLink<COMP> {
type Target = Rc<RefCell<Option<Scope<COMP>>>>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<COMP: Component> PartialEq for WeakComponentLink<COMP> {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}

#[derive(Debug)]
pub enum Hovered {
Expand Down
2 changes: 2 additions & 0 deletions examples/node_refs/src/input.rs
Expand Up @@ -7,6 +7,7 @@ pub enum Msg {
#[derive(Properties, PartialEq)]
pub struct Props {
pub on_hover: Callback<()>,
pub input_ref: NodeRef,
}

pub struct InputComponent;
Expand All @@ -33,6 +34,7 @@ impl Component for InputComponent {
<input
type="text"
class="input-component"
ref={&ctx.props().input_ref}
onmouseover={ctx.link().callback(|_| Msg::Hover)}
/>
}
Expand Down