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

Updated to support keyed attributes #1076

Merged
merged 20 commits into from
Apr 13, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions crates/macro/src/html_tree/html_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,20 @@ impl ToTokens for HtmlComponent {
quote! { ::yew::html::NodeRef::default() }
};

let key = if let Some(key) = props.key() {
quote_spanned! { key.span() => Some(#key) }
} else {
quote! {None }
};

tokens.extend(quote! {{
// These validation checks show a nice error message to the user.
// They do not execute at runtime
if false {
#validate_props
}

::yew::virtual_dom::VChild::<#ty>::new(#init_props, #node_ref)
::yew::virtual_dom::VChild::<#ty>::new(#init_props, #node_ref, #key)
}});
}
}
Expand Down Expand Up @@ -343,11 +349,13 @@ enum Props {
struct ListProps {
props: Vec<HtmlProp>,
node_ref: Option<Expr>,
key: Option<Expr>,
}

struct WithProps {
props: Ident,
node_ref: Option<Expr>,
key: Option<Expr>,
}

impl Props {
Expand All @@ -359,6 +367,14 @@ impl Props {
}
}

fn key(&self) -> Option<&Expr> {
match self {
Props::List(list_props) => list_props.key.as_ref(),
Props::With(with_props) => with_props.key.as_ref(),
Props::None => None,
}
}

fn collision_message() -> &'static str {
"Using special syntax `with props` along with named prop is not allowed. This rule does not apply to special `ref` prop"
}
Expand All @@ -368,6 +384,7 @@ impl Parse for Props {
fn parse(input: ParseStream) -> ParseResult<Self> {
let mut props = Props::None;
let mut node_ref: Option<Expr> = None;
let mut key: Option<Expr> = None;

while let Some((token, _)) = input.cursor().ident() {
if token == "with" {
Expand All @@ -383,6 +400,7 @@ impl Parse for Props {
props = Props::With(Box::new(WithProps {
props: input.parse::<Ident>()?,
node_ref: None,
key: None,
}));

// Handle optional comma
Expand All @@ -404,6 +422,15 @@ impl Parse for Props {
node_ref = Some(prop.value);
continue;
}
if prop.label.to_string() == "key" {
match key {
None => Ok(()),
Some(_) => Err(syn::Error::new_spanned(&prop.label, "too many keys set")),
}?;

key = Some(prop.value);
continue;
}

if prop.label.to_string() == "type" {
return Err(syn::Error::new_spanned(&prop.label, "expected identifier"));
Expand All @@ -418,6 +445,7 @@ impl Parse for Props {
*props = Props::List(Box::new(ListProps {
props: vec![prop],
node_ref: None,
key: None,
}));
}
Props::With(_) => {
Expand All @@ -431,9 +459,13 @@ impl Parse for Props {

match props {
Props::None => {}
Props::With(ref mut p) => p.node_ref = node_ref,
Props::With(ref mut p) => {
p.node_ref = node_ref;
p.key = key
}
Props::List(ref mut p) => {
p.node_ref = node_ref;
p.key = key;

// alphabetize
p.props.sort_by(|a, b| {
Expand Down
7 changes: 7 additions & 0 deletions crates/macro/src/html_tree/html_tag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ impl ToTokens for HtmlTag {
value,
checked,
node_ref,
key,
href,
listeners,
} = &attributes;
Expand Down Expand Up @@ -141,6 +142,11 @@ impl ToTokens for HtmlTag {
#vtag.node_ref = #node_ref;
}
});
let set_key = key.iter().map(|key| {
quote! {
#vtag.key = Some(#key);
}
});
let listeners = listeners.iter().map(|listener| {
let name = &listener.label.name;
let callback = &listener.value;
Expand All @@ -163,6 +169,7 @@ impl ToTokens for HtmlTag {
#(#set_booleans)*
#(#set_classes)*
#(#set_node_ref)*
#(#set_key)*
#vtag.add_attributes(vec![#(#attr_pairs),*]);
#vtag.add_listeners(vec![#(::std::rc::Rc::new(#listeners)),*]);
#vtag.add_children(vec![#(#children),*]);
Expand Down
4 changes: 4 additions & 0 deletions crates/macro/src/html_tree/html_tag/tag_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct TagAttributes {
pub kind: Option<Expr>,
pub checked: Option<Expr>,
pub node_ref: Option<Expr>,
pub key: Option<Expr>,
pub href: Option<Expr>,
}

Expand Down Expand Up @@ -189,6 +190,8 @@ impl Parse for TagAttributes {
let kind = TagAttributes::remove_attr(&mut attributes, "type");
let checked = TagAttributes::remove_attr(&mut attributes, "checked");
let node_ref = TagAttributes::remove_attr(&mut attributes, "ref");
let key = TagAttributes::remove_attr(&mut attributes, "key");

let href = TagAttributes::remove_attr(&mut attributes, "href");

Ok(TagAttributes {
Expand All @@ -201,6 +204,7 @@ impl Parse for TagAttributes {
kind,
node_ref,
href,
key,
})
}
}
4 changes: 2 additions & 2 deletions crates/macro/tests/macro/html-component-pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,8 @@ fn compile_pass() {

let variants = || -> Vec<ChildrenVariants> {
vec![
ChildrenVariants::Child(VChild::new(ChildProperties::default(), NodeRef::default())),
ChildrenVariants::AltChild(VChild::new((), NodeRef::default())),
ChildrenVariants::Child(VChild::new(ChildProperties::default(), NodeRef::default(), None)),
ChildrenVariants::AltChild(VChild::new((), NodeRef::default(), None)),
]
};

Expand Down
16 changes: 12 additions & 4 deletions src/virtual_dom/vcomp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub struct VComp {
type_id: TypeId,
state: MountState,
pub(crate) node_ref: NodeRef,
pub(crate) key: Option<String>,
}

/// A virtual child component.
Expand All @@ -39,13 +40,15 @@ pub struct VChild<COMP: Component> {
pub props: COMP::Properties,
/// Reference to the mounted node
node_ref: NodeRef,
key: Option<String>,
}

impl<COMP: Component> Clone for VChild<COMP> {
fn clone(&self) -> Self {
VChild {
props: self.props.clone(),
node_ref: self.node_ref.clone(),
key: self.key.clone(),
}
}
}
Expand All @@ -55,8 +58,12 @@ where
COMP: Component,
{
/// Creates a child component that can be accessed and modified by its parent.
pub fn new(props: COMP::Properties, node_ref: NodeRef) -> Self {
Self { props, node_ref }
pub fn new(props: COMP::Properties, node_ref: NodeRef, key: Option<String>) -> Self {
Self {
props,
node_ref,
key,
}
}
}

Expand All @@ -65,7 +72,7 @@ where
COMP: Component,
{
fn from(vchild: VChild<COMP>) -> Self {
VComp::new::<COMP>(vchild.props, vchild.node_ref)
VComp::new::<COMP>(vchild.props, vchild.node_ref, vchild.key)
}
}

Expand Down Expand Up @@ -97,7 +104,7 @@ impl Clone for Mounted {

impl VComp {
/// This method prepares a generator to make a new instance of the `Component`.
pub fn new<COMP>(props: COMP::Properties, node_ref: NodeRef) -> Self
pub fn new<COMP>(props: COMP::Properties, node_ref: NodeRef, key: Option<String>) -> Self
where
COMP: Component,
{
Expand Down Expand Up @@ -142,6 +149,7 @@ impl VComp {
generator: Rc::new(generator),
}),
node_ref,
key,
}
}
}
Expand Down
78 changes: 67 additions & 11 deletions src/virtual_dom/vlist.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! This module contains fragments implementation.
use super::{VDiff, VNode, VText};
use cfg_if::cfg_if;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
cfg_if! {
if #[cfg(feature = "std_web")] {
Expand Down Expand Up @@ -59,6 +60,22 @@ impl VList {
pub fn add_child(&mut self, child: VNode) {
self.children.push(child);
}

pub fn key(&self) -> Option<String> {
let mut key = String::with_capacity(150);
jstarry marked this conversation as resolved.
Show resolved Hide resolved
const START_STRING: &str = "#vlist_";
key.push_str(START_STRING);
for n in &self.children{
if let Some(child_key) = &n.key() {
key = key + child_key;
}
}
if START_STRING == &key {
None
} else {
Some(key)
}
}
}

impl VDiff for VList {
Expand Down Expand Up @@ -104,21 +121,60 @@ impl VDiff for VList {

// Process children
let mut lefts = self.children.iter_mut();
let mut rights = rights.drain(..);
loop {
match (lefts.next(), rights.next()) {
(Some(left), Some(right)) => {
previous_sibling = left.apply(parent, previous_sibling.as_ref(), Some(right));
}
(Some(left), None) => {
previous_sibling = left.apply(parent, previous_sibling.as_ref(), None);
let mut rights_nokeys = Vec::with_capacity(rights.len());
let mut rights_lookup = HashMap::with_capacity(rights.len());
jstarry marked this conversation as resolved.
Show resolved Hide resolved
for r in rights.drain(..) {
if let Some(key) = r.key() {
if rights_lookup.contains_key(&key) {
jstarry marked this conversation as resolved.
Show resolved Hide resolved
log::error!("Duplicate key of {}", &key);
jstarry marked this conversation as resolved.
Show resolved Hide resolved
} else {
rights_lookup.insert(key.clone(), r);
}
(None, Some(ref mut right)) => {
right.detach(parent);
} else {
rights_nokeys.push(r);
}
}
let mut rights = rights_nokeys.drain(..);
jstarry marked this conversation as resolved.
Show resolved Hide resolved
loop {
match lefts.next() {
jstarry marked this conversation as resolved.
Show resolved Hide resolved
Some(left) => {
if let Some(key) = &left.key() {
let right = rights_lookup.remove(key);
match right {
Some(right) => {
previous_sibling =
left.apply(parent, previous_sibling.as_ref(), Some(right));
}
None => {
jstarry marked this conversation as resolved.
Show resolved Hide resolved
previous_sibling =
left.apply(parent, previous_sibling.as_ref(), None);
}
}
} else {
match rights.next() {
Some(right) => {
previous_sibling =
left.apply(parent, previous_sibling.as_ref(), Some(right));
}
None => {
previous_sibling = left.apply(parent, previous_sibling.as_ref(), None);
}
}
}
}
(None, None) => break,
None => break,
}
}
loop {
if let Some(ref mut right) = rights.next() {
jstarry marked this conversation as resolved.
Show resolved Hide resolved
right.detach(parent);
} else {
break;
}
}
for right in rights_lookup.values_mut() {
right.detach(parent);
}
previous_sibling
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/virtual_dom/vnode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ pub enum VNode {
VRef(Node),
}

impl VNode {
pub fn key(&self) -> Option<String> {
match self {
VNode::VTag(vtag) => vtag.key.clone(),
jstarry marked this conversation as resolved.
Show resolved Hide resolved
VNode::VText(_) => None,
VNode::VComp(vcomp) => vcomp.key.clone(),
VNode::VList(vnode) => vnode.key().clone(),
VNode::VRef(_) => None,
}
}
}

impl VDiff for VNode {
/// Remove VNode from parent.
fn detach(&mut self, parent: &Element) -> Option<Node> {
Expand Down
6 changes: 5 additions & 1 deletion src/virtual_dom/vtag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ pub struct VTag {
pub node_ref: NodeRef,
/// Keeps handler for attached listeners to have an opportunity to drop them later.
captured: Vec<EventListener>,

pub key: Option<String>,
}

impl Clone for VTag {
Expand All @@ -84,6 +86,7 @@ impl Clone for VTag {
kind: self.kind.clone(),
checked: self.checked,
node_ref: self.node_ref.clone(),
key: self.key.clone(),
captured: Vec::new(),
}
}
Expand All @@ -101,6 +104,7 @@ impl VTag {
captured: Vec::new(),
children: VList::new_without_placeholder(),
node_ref: NodeRef::default(),
key: None,
value: None,
kind: None,
// In HTML node `checked` attribute sets `defaultChecked` parameter,
Expand Down Expand Up @@ -406,7 +410,7 @@ impl VDiff for VTag {
let (reform, mut ancestor) = {
match ancestor {
Some(VNode::VTag(mut vtag)) => {
if self.tag == vtag.tag {
if self.tag == vtag.tag && self.key == vtag.key {
jstarry marked this conversation as resolved.
Show resolved Hide resolved
// If tags are equal, preserve the reference that already exists.
self.reference = vtag.reference.take();
(Reform::Keep, Some(vtag))
Expand Down