Feat: docs, code frm percy
This commit is contained in:
commit
2b9c8d09d9
72 files changed
+5130
No files matched your search
@@ -0,0 +1,647 @@
|
||||
//! The virtual_node module exposes the `VirtualNode` struct and methods that power our
|
||||
//! virtual dom.
|
||||
|
||||
// TODO: A few of these dependencies (including js_sys) are used to power events.. yet events
|
||||
// only work on wasm32 targest. So we should start sprinkling some
|
||||
//
|
||||
// #[cfg(target_arch = "wasm32")]
|
||||
// #[cfg(not(target_arch = "wasm32"))]
|
||||
//
|
||||
// Around in order to get rid of dependencies that we don't need in non wasm32 targets
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub mod virtual_node_test_utils;
|
||||
|
||||
use web_sys::{self, Element, EventTarget, Node, Text};
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Used to uniquely identify elements that contain closures so that the DomUpdater can
|
||||
// look them up by their unique id.
|
||||
// When the DomUpdater sees that the element no longer exists it will drop all of it's
|
||||
// Rc'd Closures for those events.
|
||||
use lazy_static::lazy_static;
|
||||
lazy_static! {
|
||||
static ref ELEM_UNIQUE_ID: Mutex<u32> = Mutex::new(0);
|
||||
}
|
||||
|
||||
/// When building your views you'll typically use the `html!` macro to generate
|
||||
/// `VirtualNode`'s.
|
||||
///
|
||||
/// `html! { <div> <span></span> </div> }` really generates a `VirtualNode` with
|
||||
/// one child (span).
|
||||
///
|
||||
/// Later, on the client side, you'll use the `diff` and `patch` modules to
|
||||
/// update the real DOM with your latest tree of virtual nodes (virtual dom).
|
||||
///
|
||||
/// Or on the server side you'll just call `.to_string()` on your root virtual node
|
||||
/// in order to recursively render the node and all of its children.
|
||||
///
|
||||
/// TODO: Make all of these fields private and create accessor methods
|
||||
/// TODO: Create a builder to create instances of VirtualNode::Element with
|
||||
/// attrs and children without having to explicitly create a VElement
|
||||
#[derive(PartialEq)]
|
||||
pub enum VirtualNode {
|
||||
/// An element node (node type `ELEMENT_NODE`).
|
||||
Element(VElement),
|
||||
/// A text node (node type `TEXT_NODE`).
|
||||
///
|
||||
/// Note: This wraps a `VText` instead of a plain `String` in
|
||||
/// order to enable custom methods like `create_text_node()` on the
|
||||
/// wrapped type.
|
||||
Text(VText),
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct VElement {
|
||||
/// The HTML tag, such as "div"
|
||||
pub tag: String,
|
||||
/// HTML attributes such as id, class, style, etc
|
||||
pub attrs: HashMap<String, String>,
|
||||
/// Events that will get added to your real DOM element via `.addEventListener`
|
||||
pub events: Events,
|
||||
/// The children of this `VirtualNode`. So a <div> <em></em> </div> structure would
|
||||
/// have a parent div and one child, em.
|
||||
pub children: Vec<VirtualNode>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct VText {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl VirtualNode {
|
||||
/// Create a new virtual element node with a given tag.
|
||||
///
|
||||
/// These get patched into the DOM using `document.createElement`
|
||||
///
|
||||
/// ```ignore
|
||||
/// use virtual_dom_rs::VirtualNode;
|
||||
///
|
||||
/// let div = VirtualNode::element("div");
|
||||
/// ```
|
||||
pub fn element<S>(tag: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VirtualNode::Element(VElement::new(tag))
|
||||
}
|
||||
|
||||
/// Create a new virtual text node with the given text.
|
||||
///
|
||||
/// These get patched into the DOM using `document.createTextNode`
|
||||
///
|
||||
/// ```ignore
|
||||
/// use virtual_dom_rs::VirtualNode;
|
||||
///
|
||||
/// let div = VirtualNode::text("div");
|
||||
/// ```
|
||||
pub fn text<S>(text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VirtualNode::Text(VText::new(text.into()))
|
||||
}
|
||||
|
||||
/// Return a [`VElement`] reference, if this is an [`Element`] variant.
|
||||
///
|
||||
/// [`VElement`]: struct.VElement.html
|
||||
/// [`Element`]: enum.VirtualNode.html#variant.Element
|
||||
pub fn as_velement_ref(&self) -> Option<&VElement> {
|
||||
match self {
|
||||
VirtualNode::Element(ref element_node) => Some(element_node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a mutable [`VElement`] reference, if this is an [`Element`] variant.
|
||||
///
|
||||
/// [`VElement`]: struct.VElement.html
|
||||
/// [`Element`]: enum.VirtualNode.html#variant.Element
|
||||
pub fn as_velement_mut(&mut self) -> Option<&mut VElement> {
|
||||
match self {
|
||||
VirtualNode::Element(ref mut element_node) => Some(element_node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a [`VText`] reference, if this is an [`Text`] variant.
|
||||
///
|
||||
/// [`VText`]: struct.VText.html
|
||||
/// [`Text`]: enum.VirtualNode.html#variant.Text
|
||||
pub fn as_vtext_ref(&self) -> Option<&VText> {
|
||||
match self {
|
||||
VirtualNode::Text(ref text_node) => Some(text_node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a mutable [`VText`] reference, if this is an [`Text`] variant.
|
||||
///
|
||||
/// [`VText`]: struct.VText.html
|
||||
/// [`Text`]: enum.VirtualNode.html#variant.Text
|
||||
pub fn as_vtext_mut(&mut self) -> Option<&mut VText> {
|
||||
match self {
|
||||
VirtualNode::Text(ref mut text_node) => Some(text_node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create and return a `CreatedNode` instance (containing a DOM `Node`
|
||||
/// together with potentially related closures) for this virtual node.
|
||||
pub fn create_dom_node(&self) -> CreatedNode<Node> {
|
||||
match self {
|
||||
VirtualNode::Text(text_node) => {
|
||||
CreatedNode::without_closures(text_node.create_text_node())
|
||||
}
|
||||
VirtualNode::Element(element_node) => element_node.create_element_node().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by html-macro to insert space before text that is inside of a block that came after
|
||||
/// an open tag.
|
||||
///
|
||||
/// html! { <div> {world}</div> }
|
||||
///
|
||||
/// So that we end up with <div> world</div> when we're finished parsing.
|
||||
pub fn insert_space_before_text(&mut self) {
|
||||
match self {
|
||||
VirtualNode::Text(text_node) => {
|
||||
text_node.text = " ".to_string() + &text_node.text;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by html-macro to insert space after braced text if we know that the next block is
|
||||
/// another block or a closing tag.
|
||||
///
|
||||
/// html! { <div>{Hello} {world}</div> } -> <div>Hello world</div>
|
||||
/// html! { <div>{Hello} </div> } -> <div>Hello </div>
|
||||
///
|
||||
/// So that we end up with <div>Hello world</div> when we're finished parsing.
|
||||
pub fn insert_space_after_text(&mut self) {
|
||||
match self {
|
||||
VirtualNode::Text(text_node) => {
|
||||
text_node.text += " ";
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VElement {
|
||||
pub fn new<S>(tag: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VElement {
|
||||
tag: tag.into(),
|
||||
attrs: HashMap::new(),
|
||||
events: Events(HashMap::new()),
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a DOM element by recursively creating DOM nodes for this element and it's
|
||||
/// children, it's children's children, etc.
|
||||
pub fn create_element_node(&self) -> CreatedNode<Element> {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
let element = if html_validation::is_svg_namespace(&self.tag) {
|
||||
document
|
||||
.create_element_ns(Some("http://www.w3.org/2000/svg"), &self.tag)
|
||||
.unwrap()
|
||||
} else {
|
||||
document.create_element(&self.tag).unwrap()
|
||||
};
|
||||
|
||||
let mut closures = HashMap::new();
|
||||
|
||||
self.attrs.iter().for_each(|(name, value)| {
|
||||
if name == "unsafe_inner_html" {
|
||||
element.set_inner_html(value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
element
|
||||
.set_attribute(name, value)
|
||||
.expect("Set element attribute in create element");
|
||||
});
|
||||
|
||||
if self.events.0.len() > 0 {
|
||||
let unique_id = create_unique_identifier();
|
||||
|
||||
element
|
||||
.set_attribute("data-vdom-id".into(), &unique_id.to_string())
|
||||
.expect("Could not set attribute on element");
|
||||
|
||||
closures.insert(unique_id, vec![]);
|
||||
|
||||
self.events.0.iter().for_each(|(onevent, callback)| {
|
||||
// onclick -> click
|
||||
let event = &onevent[2..];
|
||||
|
||||
let current_elem: &EventTarget = element.dyn_ref().unwrap();
|
||||
|
||||
current_elem
|
||||
.add_event_listener_with_callback(
|
||||
event,
|
||||
callback.as_ref().as_ref().unchecked_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
closures
|
||||
.get_mut(&unique_id)
|
||||
.unwrap()
|
||||
.push(Rc::clone(callback));
|
||||
});
|
||||
}
|
||||
|
||||
let mut previous_node_was_text = false;
|
||||
|
||||
self.children.iter().for_each(|child| {
|
||||
match child {
|
||||
VirtualNode::Text(text_node) => {
|
||||
let current_node = element.as_ref() as &web_sys::Node;
|
||||
|
||||
// We ensure that the text siblings are patched by preventing the browser from merging
|
||||
// neighboring text nodes. Originally inspired by some of React's work from 2016.
|
||||
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
|
||||
// -> https://github.com/facebook/react/pull/5753
|
||||
//
|
||||
// `ptns` = Percy text node separator
|
||||
if previous_node_was_text {
|
||||
let separator = document.create_comment("ptns");
|
||||
current_node
|
||||
.append_child(separator.as_ref() as &web_sys::Node)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
current_node
|
||||
.append_child(&text_node.create_text_node())
|
||||
.unwrap();
|
||||
|
||||
previous_node_was_text = true;
|
||||
}
|
||||
VirtualNode::Element(element_node) => {
|
||||
previous_node_was_text = false;
|
||||
|
||||
let child = element_node.create_element_node();
|
||||
let child_elem: Element = child.node;
|
||||
|
||||
closures.extend(child.closures);
|
||||
|
||||
element.append_child(&child_elem).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(on_create_elem) = self.events.0.get("on_create_elem") {
|
||||
let on_create_elem: &js_sys::Function =
|
||||
on_create_elem.as_ref().as_ref().unchecked_ref();
|
||||
on_create_elem
|
||||
.call1(&wasm_bindgen::JsValue::NULL, &element)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
CreatedNode {
|
||||
node: element,
|
||||
closures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VText {
|
||||
/// Create an new `VText` instance with the specified text.
|
||||
pub fn new<S>(text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VText { text: text.into() }
|
||||
}
|
||||
|
||||
/// Return a `Text` element from a `VirtualNode`, typically right before adding it
|
||||
/// into the DOM.
|
||||
pub fn create_text_node(&self) -> Text {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
document.create_text_node(&self.text)
|
||||
}
|
||||
}
|
||||
|
||||
/// A node along with all of the closures that were created for that
|
||||
/// node's events and all of it's child node's events.
|
||||
pub struct CreatedNode<T> {
|
||||
/// A `Node` or `Element` that was created from a `VirtualNode`
|
||||
pub node: T,
|
||||
/// A map of a node's unique identifier along with all of the Closures for that node.
|
||||
///
|
||||
/// The DomUpdater uses this to look up nodes and see if they're still in the page. If not
|
||||
/// the reference that we maintain to their closure will be dropped, thus freeing the Closure's
|
||||
/// memory.
|
||||
pub closures: HashMap<u32, Vec<DynClosure>>,
|
||||
}
|
||||
|
||||
impl<T> CreatedNode<T> {
|
||||
pub fn without_closures<N: Into<T>>(node: N) -> Self {
|
||||
CreatedNode {
|
||||
node: node.into(),
|
||||
closures: HashMap::with_capacity(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for CreatedNode<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.node
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreatedNode<Element>> for CreatedNode<Node> {
|
||||
fn from(other: CreatedNode<Element>) -> CreatedNode<Node> {
|
||||
CreatedNode {
|
||||
node: other.node.into(),
|
||||
closures: other.closures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_unique_identifier() -> u32 {
|
||||
let mut elem_unique_id = ELEM_UNIQUE_ID.lock().unwrap();
|
||||
|
||||
*elem_unique_id += 1;
|
||||
|
||||
*elem_unique_id
|
||||
}
|
||||
|
||||
/// A trait with common functionality for rendering front-end views.
|
||||
pub trait View {
|
||||
/// Render a VirtualNode, or any IntoIter<VirtualNode>
|
||||
fn render(&self) -> VirtualNode;
|
||||
}
|
||||
|
||||
impl<V> From<&V> for VirtualNode
|
||||
where
|
||||
V: View,
|
||||
{
|
||||
fn from(v: &V) -> Self {
|
||||
v.render()
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by the html! macro for all braced child nodes so that we can use any type
|
||||
/// that implements Into<IterableNodes>
|
||||
///
|
||||
/// html! { <div> { nodes } </div> }
|
||||
///
|
||||
/// nodes can be a String .. VirtualNode .. Vec<VirtualNode> ... etc
|
||||
pub struct IterableNodes(Vec<VirtualNode>);
|
||||
|
||||
impl IterableNodes {
|
||||
/// Retrieve the first node mutably
|
||||
pub fn first(&mut self) -> &mut VirtualNode {
|
||||
self.0.first_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Retrieve the last node mutably
|
||||
pub fn last(&mut self) -> &mut VirtualNode {
|
||||
self.0.last_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for IterableNodes {
|
||||
type Item = VirtualNode;
|
||||
// TODO: Is this possible with an array [VirtualNode] instead of a vec?
|
||||
type IntoIter = ::std::vec::IntoIter<VirtualNode>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VirtualNode> for IterableNodes {
|
||||
fn from(other: VirtualNode) -> Self {
|
||||
IterableNodes(vec![other])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for IterableNodes {
|
||||
fn from(other: &str) -> Self {
|
||||
IterableNodes(vec![VirtualNode::text(other)])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for IterableNodes {
|
||||
fn from(other: String) -> Self {
|
||||
IterableNodes(vec![VirtualNode::text(other.as_str())])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<VirtualNode>> for IterableNodes {
|
||||
fn from(other: Vec<VirtualNode>) -> Self {
|
||||
IterableNodes(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> From<Vec<V>> for IterableNodes {
|
||||
fn from(other: Vec<V>) -> Self {
|
||||
IterableNodes(other.into_iter().map(|it| it.render()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> From<&Vec<V>> for IterableNodes {
|
||||
fn from(other: &Vec<V>) -> Self {
|
||||
IterableNodes(other.iter().map(|it| it.render()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> From<&[V]> for IterableNodes {
|
||||
fn from(other: &[V]) -> Self {
|
||||
IterableNodes(other.iter().map(|it| it.render()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VText> for VirtualNode {
|
||||
fn from(other: VText) -> Self {
|
||||
VirtualNode::Text(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VElement> for VirtualNode {
|
||||
fn from(other: VElement) -> Self {
|
||||
VirtualNode::Element(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for VirtualNode {
|
||||
fn from(other: &str) -> Self {
|
||||
VirtualNode::text(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for VirtualNode {
|
||||
fn from(other: String) -> Self {
|
||||
VirtualNode::text(other.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for VText {
|
||||
fn from(text: &str) -> Self {
|
||||
VText {
|
||||
text: text.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for VText {
|
||||
fn from(text: String) -> Self {
|
||||
VText { text }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for VirtualNode {
|
||||
type Item = VirtualNode;
|
||||
// TODO: Is this possible with an array [VirtualNode] instead of a vec?
|
||||
type IntoIter = ::std::vec::IntoIter<VirtualNode>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
vec![self].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<::std::vec::IntoIter<VirtualNode>> for VirtualNode {
|
||||
fn into(self) -> ::std::vec::IntoIter<VirtualNode> {
|
||||
self.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VirtualNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
VirtualNode::Element(e) => write!(f, "Node::{:?}", e),
|
||||
VirtualNode::Text(t) => write!(f, "Node::{:?}", t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VElement {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Element(<{}>, attrs: {:?}, children: {:?})",
|
||||
self.tag, self.attrs, self.children,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VText {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Text({})", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VElement {
|
||||
// Turn a VElement and all of it's children (recursively) into an HTML string
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "<{}", self.tag).unwrap();
|
||||
|
||||
for (attr, value) in self.attrs.iter() {
|
||||
write!(f, r#" {}="{}""#, attr, value)?;
|
||||
}
|
||||
|
||||
write!(f, ">")?;
|
||||
|
||||
for child in self.children.iter() {
|
||||
write!(f, "{}", child.to_string())?;
|
||||
}
|
||||
|
||||
if !html_validation::is_self_closing(&self.tag) {
|
||||
write!(f, "</{}>", self.tag)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a VText into an HTML string
|
||||
impl fmt::Display for VText {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a VirtualNode into an HTML string (delegate impl to variants)
|
||||
impl fmt::Display for VirtualNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
VirtualNode::Element(element) => write!(f, "{}", element),
|
||||
VirtualNode::Text(text) => write!(f, "{}", text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Box<dyn AsRef<JsValue>>> is our js_sys::Closure. Stored this way to allow us to store
|
||||
/// any Closure regardless of the arguments.
|
||||
pub type DynClosure = Rc<dyn AsRef<JsValue>>;
|
||||
|
||||
/// We need a custom implementation of fmt::Debug since JsValue doesn't
|
||||
/// implement debug.
|
||||
pub struct Events(pub HashMap<String, DynClosure>);
|
||||
|
||||
impl PartialEq for Events {
|
||||
// TODO: What should happen here..? And why?
|
||||
fn eq(&self, _rhs: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Events {
|
||||
// Print out all of the event names for this VirtualNode
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let events: String = self.0.keys().map(|key| " ".to_string() + key).collect();
|
||||
write!(f, "{}", events)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn self_closing_tag_to_string() {
|
||||
let node = VirtualNode::element("br");
|
||||
|
||||
// No </br> since self closing tag
|
||||
assert_eq!(&node.to_string(), "<br>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let mut node = VirtualNode::Element(VElement::new("div"));
|
||||
node.as_velement_mut()
|
||||
.unwrap()
|
||||
.attrs
|
||||
.insert("id".into(), "some-id".into());
|
||||
|
||||
let mut child = VirtualNode::Element(VElement::new("span"));
|
||||
|
||||
let mut text = VirtualNode::Text(VText::new("Hello world"));
|
||||
|
||||
child.as_velement_mut().unwrap().children.push(text);
|
||||
|
||||
node.as_velement_mut().unwrap().children.push(child);
|
||||
|
||||
let expected = r#"<div id="some-id"><span>Hello world</span></div>"#;
|
||||
|
||||
assert_eq!(node.to_string(), expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
//! A collection of functions that are useful for unit testing your html! views.
|
||||
|
||||
use crate::VirtualNode;
|
||||
|
||||
impl VirtualNode {
|
||||
/// Get a vector of all of the VirtualNode children / grandchildren / etc of
|
||||
/// your virtual_node that have a label that matches your filter.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # #[macro_use] extern crate virtual_dom_rs; fn main() {
|
||||
///
|
||||
/// let component = html! {<div>
|
||||
/// <span label="hello",> {"Hi!"} </span>
|
||||
/// <em label="world",> {"There!!"} </em>
|
||||
/// <em label="hello",></em>
|
||||
/// </div> };
|
||||
///
|
||||
/// let hello_nodes = component.filter_label(|label| {
|
||||
/// label.contains("hello")
|
||||
/// });
|
||||
///
|
||||
/// assert_eq!(hello_nodes.len(), 2);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn filter_label<'a, F>(&'a self, filter: F) -> Vec<&'a VirtualNode>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
// Get descendants recursively
|
||||
let mut descendants: Vec<&'a VirtualNode> = vec![];
|
||||
match self {
|
||||
VirtualNode::Text(_) => { /* nothing to do */ }
|
||||
VirtualNode::Element(element_node) => {
|
||||
for child in element_node.children.iter() {
|
||||
get_descendants(&mut descendants, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter descendants
|
||||
descendants
|
||||
.into_iter()
|
||||
.filter(|vn: &&'a VirtualNode| match vn {
|
||||
VirtualNode::Text(_) => false,
|
||||
VirtualNode::Element(element_node) => match element_node.attrs.get("label") {
|
||||
Some(label) => filter(label),
|
||||
None => false,
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a vector of all of the descendants of this VirtualNode
|
||||
/// that have the provided `filter`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # #[macro_use] extern crate virtual_dom_rs; fn main() {
|
||||
///
|
||||
/// let component = html! {<div>
|
||||
/// <span label="hello",> {"Hi!"} </span>
|
||||
/// <em label="world",> {"There!!"} </em>
|
||||
/// <em label="hello",></em>
|
||||
/// </div> };
|
||||
///
|
||||
/// let hello_nodes = component.filter_label_equals("hello");
|
||||
///
|
||||
/// assert_eq!(hello_nodes.len(), 2);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn filter_label_equals<'a>(&'a self, label: &str) -> Vec<&'a VirtualNode> {
|
||||
self.filter_label(|node_label| node_label == label)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_descendants<'a>(descendants: &mut Vec<&'a VirtualNode>, node: &'a VirtualNode) {
|
||||
descendants.push(node);
|
||||
match node {
|
||||
VirtualNode::Text(_) => { /* nothing to do */ }
|
||||
VirtualNode::Element(element_node) => {
|
||||
for child in element_node.children.iter() {
|
||||
get_descendants(descendants, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::VElement;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// TODO: Move this test somewhere that we can use the `html!` macro
|
||||
// #[test]
|
||||
// fn filter_label() {
|
||||
// let html = html! {
|
||||
// // Should not pick up labels on the root node
|
||||
// <div label="hello0",>
|
||||
// // This node gets picked up
|
||||
// <span label="hello1",>
|
||||
// </span>
|
||||
// // This node gets picked up
|
||||
// <em label="hello2",>
|
||||
// { "hello there :)!" }
|
||||
// </em>
|
||||
// <div label="world",></div>
|
||||
// </div>
|
||||
// };
|
||||
//
|
||||
// let hello_nodes = html.filter_label(|label| label.contains("hello"));
|
||||
//
|
||||
// assert_eq!(
|
||||
// hello_nodes.len(),
|
||||
// 2,
|
||||
// "2 elements with label containing 'hello'"
|
||||
// );
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn label_equals() {
|
||||
let span = VirtualNode::element("span");
|
||||
|
||||
let mut attrs = HashMap::new();
|
||||
attrs.insert("label".to_string(), "hello".to_string());
|
||||
let mut em = VElement::new("em");
|
||||
em.attrs = attrs;
|
||||
|
||||
let mut html = VElement::new("div");
|
||||
html.children.push(span);
|
||||
html.children.push(em.into());
|
||||
|
||||
let html_node = VirtualNode::Element(html);
|
||||
let hello_nodes = html_node.filter_label_equals("hello");
|
||||
|
||||
assert_eq!(hello_nodes.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user