Skip to content

Commit b7242c3

Browse files
committed
Add ref attributes to enabling DOM access in Components
1 parent 3a72766 commit b7242c3

22 files changed

+454
-177
lines changed

crates/macro/src/html_tree/html_component.rs

+73-34
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ use syn::parse;
1111
use syn::parse::{Parse, ParseStream, Result as ParseResult};
1212
use syn::punctuated::Punctuated;
1313
use syn::spanned::Spanned;
14-
use syn::{Ident, Path, PathArguments, PathSegment, Token, Type, TypePath};
14+
use syn::{Expr, Ident, Path, PathArguments, PathSegment, Token, Type, TypePath};
1515

1616
pub struct HtmlComponent {
1717
ty: Type,
18-
props: Option<Props>,
18+
props: Props,
1919
children: Vec<HtmlTreeNested>,
2020
}
2121

@@ -85,9 +85,9 @@ impl ToTokens for HtmlComponent {
8585
} = self;
8686
let vcomp_scope = Ident::new("__yew_vcomp_scope", Span::call_site());
8787

88-
let validate_props = if let Some(Props::List(ListProps(vec_props))) = props {
88+
let validate_props = if let Props::List(ListProps { props, .. }) = props {
8989
let prop_ref = Ident::new("__yew_prop_ref", Span::call_site());
90-
let check_props = vec_props.iter().map(|HtmlProp { label, .. }| {
90+
let check_props = props.iter().map(|HtmlProp { label, .. }| {
9191
quote! { #prop_ref.#label; }
9292
});
9393

@@ -136,30 +136,27 @@ impl ToTokens for HtmlComponent {
136136
quote! {}
137137
};
138138

139-
let init_props = if let Some(props) = props {
140-
match props {
141-
Props::List(ListProps(vec_props)) => {
142-
let set_props = vec_props.iter().map(|HtmlProp { label, value }| {
143-
quote_spanned! { value.span()=>
144-
.#label(<::yew::virtual_dom::vcomp::VComp<_> as ::yew::virtual_dom::vcomp::Transformer<_, _, _>>::transform(#vcomp_scope.clone(), #value))
145-
}
146-
});
147-
148-
quote! {
149-
<<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder()
150-
#(#set_props)*
151-
#set_children
152-
.build()
139+
let init_props = match props {
140+
Props::List(ListProps { props, .. }) => {
141+
let set_props = props.iter().map(|HtmlProp { label, value }| {
142+
quote_spanned! { value.span()=>
143+
.#label(<::yew::virtual_dom::vcomp::VComp<_> as ::yew::virtual_dom::vcomp::Transformer<_, _, _>>::transform(#vcomp_scope.clone(), #value))
153144
}
145+
});
146+
147+
quote! {
148+
<<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder()
149+
#(#set_props)*
150+
#set_children
151+
.build()
154152
}
155-
Props::With(WithProps(props)) => quote! { #props },
156153
}
157-
} else {
158-
quote! {
154+
Props::With(WithProps { props, .. }) => quote! { #props },
155+
Props::None => quote! {
159156
<<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder()
160157
#set_children
161158
.build()
162-
}
159+
},
163160
};
164161

165162
let validate_comp = quote_spanned! { ty.span()=>
@@ -171,6 +168,12 @@ impl ToTokens for HtmlComponent {
171168
}
172169
};
173170

171+
let node_ref = if let Some(node_ref) = props.node_ref() {
172+
quote_spanned! { node_ref.span()=> #node_ref }
173+
} else {
174+
quote! { ::yew::html::NodeRef::default() }
175+
};
176+
174177
tokens.extend(quote! {{
175178
// Validation nevers executes at runtime
176179
if false {
@@ -179,7 +182,8 @@ impl ToTokens for HtmlComponent {
179182
}
180183

181184
let #vcomp_scope: ::yew::virtual_dom::vcomp::ScopeHolder<_> = ::std::default::Default::default();
182-
::yew::virtual_dom::VChild::<#ty, _>::new(#init_props, #vcomp_scope)
185+
let __yew_node_ref: ::yew::html::NodeRef = #node_ref;
186+
::yew::virtual_dom::VChild::<#ty, _>::new(#init_props, #vcomp_scope, __yew_node_ref)
183187
}});
184188
}
185189
}
@@ -244,7 +248,7 @@ impl HtmlComponent {
244248
struct HtmlComponentOpen {
245249
lt: Token![<],
246250
ty: Type,
247-
props: Option<Props>,
251+
props: Props,
248252
div: Option<Token![/]>,
249253
gt: Token![>],
250254
}
@@ -264,7 +268,7 @@ impl Parse for HtmlComponentOpen {
264268
// backwards compat
265269
let _ = input.parse::<Token![:]>();
266270
let HtmlPropSuffix { stream, div, gt } = input.parse()?;
267-
let props: Option<Props> = parse(stream).ok();
271+
let props = parse(stream)?;
268272

269273
Ok(HtmlComponentOpen {
270274
lt,
@@ -327,6 +331,17 @@ enum PropType {
327331
enum Props {
328332
List(ListProps),
329333
With(WithProps),
334+
None,
335+
}
336+
337+
impl Props {
338+
fn node_ref(&self) -> Option<&Expr> {
339+
match self {
340+
Props::List(ListProps { node_ref, .. }) => node_ref.as_ref(),
341+
Props::With(WithProps { node_ref, .. }) => node_ref.as_ref(),
342+
Props::None => None,
343+
}
344+
}
330345
}
331346

332347
impl PeekValue<PropType> for Props {
@@ -344,24 +359,32 @@ impl PeekValue<PropType> for Props {
344359

345360
impl Parse for Props {
346361
fn parse(input: ParseStream) -> ParseResult<Self> {
347-
let prop_type = Props::peek(input.cursor())
348-
.ok_or_else(|| syn::Error::new(Span::call_site(), "ignore - no props found"))?;
349-
match prop_type {
350-
PropType::List => input.parse().map(Props::List),
351-
PropType::With => input.parse().map(Props::With),
362+
match Props::peek(input.cursor()) {
363+
Some(PropType::List) => input.parse().map(Props::List),
364+
Some(PropType::With) => input.parse().map(Props::With),
365+
None => Ok(Props::None),
352366
}
353367
}
354368
}
355369

356-
struct ListProps(Vec<HtmlProp>);
370+
struct ListProps {
371+
props: Vec<HtmlProp>,
372+
node_ref: Option<Expr>,
373+
}
374+
357375
impl Parse for ListProps {
358376
fn parse(input: ParseStream) -> ParseResult<Self> {
359377
let mut props: Vec<HtmlProp> = Vec::new();
360378
while HtmlProp::peek(input.cursor()).is_some() {
361379
props.push(input.parse::<HtmlProp>()?);
362380
}
363381

382+
let ref_position = props.iter().position(|p| p.label.to_string() == "ref");
383+
let node_ref = ref_position.and_then(|i| Some(props.remove(i).value));
364384
for prop in &props {
385+
if prop.label.to_string() == "ref" {
386+
return Err(syn::Error::new_spanned(&prop.label, "too many refs set"));
387+
}
365388
if prop.label.to_string() == "type" {
366389
return Err(syn::Error::new_spanned(&prop.label, "expected identifier"));
367390
}
@@ -386,11 +409,15 @@ impl Parse for ListProps {
386409
}
387410
});
388411

389-
Ok(ListProps(props))
412+
Ok(ListProps { props, node_ref })
390413
}
391414
}
392415

393-
struct WithProps(Ident);
416+
struct WithProps {
417+
props: Ident,
418+
node_ref: Option<Expr>,
419+
}
420+
394421
impl Parse for WithProps {
395422
fn parse(input: ParseStream) -> ParseResult<Self> {
396423
let with = input.parse::<Ident>()?;
@@ -399,6 +426,18 @@ impl Parse for WithProps {
399426
}
400427
let props = input.parse::<Ident>()?;
401428
let _ = input.parse::<Token![,]>();
402-
Ok(WithProps(props))
429+
430+
// Check for the ref tag after `with`
431+
let mut node_ref = None;
432+
if let Some(ident) = input.cursor().ident() {
433+
let prop = input.parse::<HtmlProp>()?;
434+
if ident.0 == "ref" {
435+
node_ref = Some(prop.value);
436+
} else {
437+
return Err(syn::Error::new_spanned(&prop.label, "unexpected token"));
438+
}
439+
}
440+
441+
Ok(WithProps { props, node_ref })
403442
}
404443
}

crates/macro/src/html_tree/html_tag/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ impl ToTokens for HtmlTag {
9696
checked,
9797
disabled,
9898
selected,
99+
node_ref,
99100
href,
100101
listeners,
101102
} = &attributes;
@@ -142,6 +143,11 @@ impl ToTokens for HtmlTag {
142143
#vtag.set_classes(#classes);
143144
},
144145
});
146+
let set_node_ref = node_ref.iter().map(|node_ref| {
147+
quote! {
148+
#vtag.node_ref = #node_ref;
149+
}
150+
});
145151

146152
tokens.extend(quote! {{
147153
let mut #vtag = ::yew::virtual_dom::vtag::VTag::new(#name);
@@ -152,6 +158,7 @@ impl ToTokens for HtmlTag {
152158
#(#add_disabled)*
153159
#(#add_selected)*
154160
#(#set_classes)*
161+
#(#set_node_ref)*
155162
#vtag.add_attributes(vec![#(#attr_pairs),*]);
156163
#vtag.add_listeners(vec![#(::std::boxed::Box::new(#listeners)),*]);
157164
#vtag.add_children(vec![#(#children),*]);

crates/macro/src/html_tree/html_tag/tag_attributes.rs

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub struct TagAttributes {
1616
pub checked: Option<Expr>,
1717
pub disabled: Option<Expr>,
1818
pub selected: Option<Expr>,
19+
pub node_ref: Option<Expr>,
1920
pub href: Option<Expr>,
2021
}
2122

@@ -213,6 +214,7 @@ impl Parse for TagAttributes {
213214
let checked = TagAttributes::remove_attr(&mut attributes, "checked");
214215
let disabled = TagAttributes::remove_attr(&mut attributes, "disabled");
215216
let selected = TagAttributes::remove_attr(&mut attributes, "selected");
217+
let node_ref = TagAttributes::remove_attr(&mut attributes, "ref");
216218
let href = TagAttributes::remove_attr(&mut attributes, "href");
217219

218220
Ok(TagAttributes {
@@ -224,6 +226,7 @@ impl Parse for TagAttributes {
224226
checked,
225227
disabled,
226228
selected,
229+
node_ref,
227230
href,
228231
})
229232
}

examples/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ members = [
44
"crm",
55
"custom_components",
66
"dashboard",
7+
"node_refs",
78
"file_upload",
89
"fragments",
910
"game_of_life",

examples/node_refs/Cargo.toml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "node_refs"
3+
version = "0.1.0"
4+
authors = ["Justin Starry <[email protected]>"]
5+
edition = "2018"
6+
7+
[dependencies]
8+
yew = { path = "../.." }
9+
stdweb = "0.4.20"

examples/node_refs/src/input.rs

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use yew::prelude::*;
2+
3+
pub struct InputComponent {
4+
props: Props,
5+
}
6+
7+
#[derive(Properties)]
8+
pub struct Props {
9+
#[props(required)]
10+
pub on_hover: Callback<()>,
11+
}
12+
13+
pub enum Msg {
14+
Hover,
15+
}
16+
17+
impl Component for InputComponent {
18+
type Message = Msg;
19+
type Properties = Props;
20+
21+
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
22+
InputComponent { props }
23+
}
24+
25+
fn update(&mut self, msg: Self::Message) -> ShouldRender {
26+
match msg {
27+
Msg::Hover => {
28+
self.props.on_hover.emit(());
29+
}
30+
}
31+
false
32+
}
33+
34+
fn view(&self) -> Html<Self> {
35+
html! {
36+
<input class="input-component" type="text" onmouseover=|_| Msg::Hover />
37+
}
38+
}
39+
}

examples/node_refs/src/lib.rs

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#![recursion_limit = "256"]
2+
3+
mod input;
4+
5+
use input::InputComponent;
6+
use stdweb::web::html_element::InputElement;
7+
use stdweb::web::IHtmlElement;
8+
use yew::prelude::*;
9+
10+
pub struct Model {
11+
refs: Vec<NodeRef>,
12+
focus_index: usize,
13+
}
14+
15+
pub enum Msg {
16+
HoverIndex(usize),
17+
}
18+
19+
impl Component for Model {
20+
type Message = Msg;
21+
type Properties = ();
22+
23+
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
24+
Model {
25+
focus_index: 0,
26+
refs: vec![NodeRef::default(), NodeRef::default()],
27+
}
28+
}
29+
30+
fn mounted(&mut self) -> ShouldRender {
31+
if let Some(input) = self.refs[self.focus_index].try_into::<InputElement>() {
32+
input.focus();
33+
}
34+
false
35+
}
36+
37+
fn update(&mut self, msg: Self::Message) -> ShouldRender {
38+
match msg {
39+
Msg::HoverIndex(index) => self.focus_index = index,
40+
}
41+
if let Some(input) = self.refs[self.focus_index].try_into::<InputElement>() {
42+
input.focus();
43+
}
44+
true
45+
}
46+
47+
fn view(&self) -> Html<Self> {
48+
html! {
49+
<div class="main">
50+
<h1>{ "Node Refs Demo" }</h1>
51+
<p>{ "Refs can be used to access and manipulate DOM elements directly" }</p>
52+
<ul>
53+
<li>{ "First input will focus on mount" }</li>
54+
<li>{ "Each input will focus on hover" }</li>
55+
</ul>
56+
<div>
57+
<label>{ "Using tag ref: " }</label>
58+
<input ref=self.refs[0].clone() class="input-element" type="text" onmouseover=|_| Msg::HoverIndex(0) />
59+
</div>
60+
<div>
61+
<label>{ "Using component ref: " }</label>
62+
<InputComponent ref=self.refs[1].clone() on_hover=|_| Msg::HoverIndex(1) />
63+
</div>
64+
</div>
65+
}
66+
}
67+
}

examples/node_refs/src/main.rs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
yew::start_app::<node_refs::Model>();
3+
}

examples/showcase/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ counter = { path = "../counter" }
1414
crm = { path = "../crm" }
1515
custom_components = { path = "../custom_components" }
1616
dashboard = { path = "../dashboard" }
17+
node_refs = { path = "../node_refs" }
1718
fragments = { path = "../fragments" }
1819
game_of_life = { path = "../game_of_life" }
1920
inner_html = { path = "../inner_html" }

0 commit comments

Comments
 (0)