Merge branch 'DioxusLabs:master' into router-typesafe
This commit is contained in:
26 files changed
+273
-179
No files matched your search
@@ -29,7 +29,7 @@ jobs:
|
|||||||
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
uses: JamesIves/github-pages-deploy-action@v4.4.2
|
||||||
with:
|
with:
|
||||||
branch: gh-pages # The branch the action should deploy to.
|
branch: gh-pages # The branch the action should deploy to.
|
||||||
folder: docs/nightly # The folder the action should deploy.
|
folder: docs/nightly # The folder the action should deploy.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
uses: JamesIves/github-pages-deploy-action@v4.4.2
|
||||||
with:
|
with:
|
||||||
branch: gh-pages # The branch the action should deploy to.
|
branch: gh-pages # The branch the action should deploy to.
|
||||||
folder: docs/nightly # The folder the action should deploy.
|
folder: docs/nightly # The folder the action should deploy.
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ Example: suppose you have a list of comments you want to render. Then, you can r
|
|||||||
|
|
||||||
### Inline for loops
|
### Inline for loops
|
||||||
|
|
||||||
Because of how common it is to render a list of items, Dioxus provides a shorthand for this. Instead of using `.iter, `.map`, and `rsx`, you can use a `for` loop with a body of rsx code:
|
Because of how common it is to render a list of items, Dioxus provides a shorthand for this. Instead of using `.iter`, `.map`, and `rsx`, you can use a `for` loop with a body of rsx code:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/rendering_lists.rs:render_list_for_loop}}
|
{{#include ../../../examples/rendering_lists.rs:render_list_for_loop}}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use std::fmt::{Result, Write};
|
|||||||
|
|
||||||
use dioxus_rsx::IfmtInput;
|
use dioxus_rsx::IfmtInput;
|
||||||
|
|
||||||
|
use crate::write_ifmt;
|
||||||
|
|
||||||
/// The output buffer that tracks indent and string
|
/// The output buffer that tracks indent and string
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
@@ -48,7 +50,7 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
|
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
|
||||||
write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value())
|
write_ifmt(text, &mut self.buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{writer::Location, Writer};
|
use crate::{ifmt_to_string, writer::Location, Writer};
|
||||||
use dioxus_rsx::*;
|
use dioxus_rsx::*;
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use std::fmt::{Result, Write};
|
use std::fmt::{Result, Write};
|
||||||
@@ -170,9 +170,9 @@ impl Writer<'_> {
|
|||||||
ContentField::Formatted(s) => {
|
ContentField::Formatted(s) => {
|
||||||
write!(
|
write!(
|
||||||
self.out,
|
self.out,
|
||||||
"{}: \"{}\"",
|
"{}: {}",
|
||||||
name,
|
name,
|
||||||
s.source.as_ref().unwrap().value()
|
s.source.as_ref().unwrap().to_token_stream()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
ContentField::OnHandlerRaw(exp) => {
|
ContentField::OnHandlerRaw(exp) => {
|
||||||
@@ -215,7 +215,7 @@ impl Writer<'_> {
|
|||||||
let attr_len = fields
|
let attr_len = fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|field| match &field.content {
|
.map(|field| match &field.content {
|
||||||
ContentField::Formatted(s) => s.source.as_ref().unwrap().value().len() ,
|
ContentField::Formatted(s) => ifmt_to_string(s).len() ,
|
||||||
ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => {
|
ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => {
|
||||||
let formatted = prettyplease::unparse_expr(exp);
|
let formatted = prettyplease::unparse_expr(exp);
|
||||||
let len = if formatted.contains('\n') {
|
let len = if formatted.contains('\n') {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::Writer;
|
use crate::{ifmt_to_string, Writer};
|
||||||
use dioxus_rsx::*;
|
use dioxus_rsx::*;
|
||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
|
use quote::ToTokens;
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Result,
|
fmt::Result,
|
||||||
fmt::{self, Write},
|
fmt::{self, Write},
|
||||||
@@ -175,11 +176,7 @@ impl Writer<'_> {
|
|||||||
if !sameline {
|
if !sameline {
|
||||||
self.out.indented_tabbed_line()?;
|
self.out.indented_tabbed_line()?;
|
||||||
}
|
}
|
||||||
write!(
|
write!(self.out, "key: {}", ifmt_to_string(key))?;
|
||||||
self.out,
|
|
||||||
"key: \"{}\"",
|
|
||||||
key.source.as_ref().unwrap().value()
|
|
||||||
)?;
|
|
||||||
if !attributes.is_empty() {
|
if !attributes.is_empty() {
|
||||||
write!(self.out, ",")?;
|
write!(self.out, ",")?;
|
||||||
if sameline {
|
if sameline {
|
||||||
@@ -216,11 +213,7 @@ impl Writer<'_> {
|
|||||||
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
|
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
|
||||||
match &attr.attr {
|
match &attr.attr {
|
||||||
ElementAttr::AttrText { name, value } => {
|
ElementAttr::AttrText { name, value } => {
|
||||||
write!(
|
write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?;
|
||||||
self.out,
|
|
||||||
"{name}: \"{value}\"",
|
|
||||||
value = value.source.as_ref().unwrap().value()
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
ElementAttr::AttrExpression { name, value } => {
|
ElementAttr::AttrExpression { name, value } => {
|
||||||
let out = prettyplease::unparse_expr(value);
|
let out = prettyplease::unparse_expr(value);
|
||||||
@@ -230,15 +223,15 @@ impl Writer<'_> {
|
|||||||
ElementAttr::CustomAttrText { name, value } => {
|
ElementAttr::CustomAttrText { name, value } => {
|
||||||
write!(
|
write!(
|
||||||
self.out,
|
self.out,
|
||||||
"\"{name}\": \"{value}\"",
|
"{name}: {value}",
|
||||||
name = name.value(),
|
name = name.to_token_stream(),
|
||||||
value = value.source.as_ref().unwrap().value()
|
value = ifmt_to_string(value)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ElementAttr::CustomAttrExpression { name, value } => {
|
ElementAttr::CustomAttrExpression { name, value } => {
|
||||||
let out = prettyplease::unparse_expr(value);
|
let out = prettyplease::unparse_expr(value);
|
||||||
write!(self.out, "\"{}\": {}", name.value(), out)?;
|
write!(self.out, "{}: {}", name.to_token_stream(), out)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ElementAttr::EventTokens { name, tokens } => {
|
ElementAttr::EventTokens { name, tokens } => {
|
||||||
@@ -315,7 +308,7 @@ impl Writer<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match children {
|
match children {
|
||||||
[BodyNode::Text(ref text)] => Some(text.source.as_ref().unwrap().value().len()),
|
[BodyNode::Text(ref text)] => Some(ifmt_to_string(text).len()),
|
||||||
[BodyNode::Component(ref comp)] => {
|
[BodyNode::Component(ref comp)] => {
|
||||||
let attr_len = self.field_len(&comp.fields, &comp.manual_props);
|
let attr_len = self.field_len(&comp.fields, &comp.manual_props);
|
||||||
|
|
||||||
@@ -338,7 +331,7 @@ impl Writer<'_> {
|
|||||||
|
|
||||||
if el.children.len() == 1 {
|
if el.children.len() == 1 {
|
||||||
if let BodyNode::Text(ref text) = el.children[0] {
|
if let BodyNode::Text(ref text) = el.children[0] {
|
||||||
let value = text.source.as_ref().unwrap().value();
|
let value = ifmt_to_string(text);
|
||||||
|
|
||||||
if value.len() + el.name.to_string().len() + attr_len < 80 {
|
if value.len() + el.name.to_string().len() + attr_len < 80 {
|
||||||
return Some(value.len() + el.name.to_string().len() + attr_len);
|
return Some(value.len() + el.name.to_string().len() + attr_len);
|
||||||
@@ -356,7 +349,7 @@ impl Writer<'_> {
|
|||||||
match item {
|
match item {
|
||||||
BodyNode::Component(_) | BodyNode::Element(_) => return None,
|
BodyNode::Component(_) | BodyNode::Element(_) => return None,
|
||||||
BodyNode::Text(text) => {
|
BodyNode::Text(text) => {
|
||||||
total_count += text.source.as_ref().unwrap().value().len()
|
total_count += ifmt_to_string(text).len();
|
||||||
}
|
}
|
||||||
BodyNode::RawExpr(expr) => match get_expr_length(expr) {
|
BodyNode::RawExpr(expr) => match get_expr_length(expr) {
|
||||||
Some(len) => total_count += len,
|
Some(len) => total_count += len,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
use std::fmt::{Display, Write};
|
||||||
|
|
||||||
use crate::writer::*;
|
use crate::writer::*;
|
||||||
use collect_macros::byte_offset;
|
use collect_macros::byte_offset;
|
||||||
use dioxus_rsx::{BodyNode, CallBody};
|
use dioxus_rsx::{BodyNode, CallBody, IfmtInput};
|
||||||
use proc_macro2::LineColumn;
|
use proc_macro2::LineColumn;
|
||||||
|
use quote::ToTokens;
|
||||||
use syn::{ExprMacro, MacroDelimiter};
|
use syn::{ExprMacro, MacroDelimiter};
|
||||||
|
|
||||||
mod buffer;
|
mod buffer;
|
||||||
@@ -130,8 +133,6 @@ pub fn write_block_out(body: CallBody) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_body(buf: &mut Writer, body: &CallBody) {
|
fn write_body(buf: &mut Writer, body: &CallBody) {
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
if buf.is_short_children(&body.roots).is_some() {
|
if buf.is_short_children(&body.roots).is_some() {
|
||||||
// write all the indents with spaces and commas between
|
// write all the indents with spaces and commas between
|
||||||
for idx in 0..body.roots.len() - 1 {
|
for idx in 0..body.roots.len() - 1 {
|
||||||
@@ -208,3 +209,24 @@ pub fn apply_formats(input: &str, blocks: Vec<FormattedBlock>) -> String {
|
|||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DisplayIfmt<'a>(&'a IfmtInput);
|
||||||
|
|
||||||
|
impl Display for DisplayIfmt<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let inner_tokens = self.0.source.as_ref().unwrap().to_token_stream();
|
||||||
|
inner_tokens.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ifmt_to_string(input: &IfmtInput) -> String {
|
||||||
|
let mut buf = String::new();
|
||||||
|
let display = DisplayIfmt(input);
|
||||||
|
write!(&mut buf, "{}", display).unwrap();
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::fmt::Result {
|
||||||
|
let display = DisplayIfmt(input);
|
||||||
|
write!(writable, "{}", display)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use std::{
|
|||||||
use syn::{spanned::Spanned, Expr, ExprIf};
|
use syn::{spanned::Spanned, Expr, ExprIf};
|
||||||
|
|
||||||
use crate::buffer::Buffer;
|
use crate::buffer::Buffer;
|
||||||
|
use crate::ifmt_to_string;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Writer<'a> {
|
pub struct Writer<'a> {
|
||||||
@@ -147,16 +148,16 @@ impl<'a> Writer<'a> {
|
|||||||
|
|
||||||
total += match &attr.attr {
|
total += match &attr.attr {
|
||||||
ElementAttr::AttrText { value, name } => {
|
ElementAttr::AttrText { value, name } => {
|
||||||
value.source.as_ref().unwrap().value().len() + name.span().line_length() + 6
|
ifmt_to_string(value).len() + name.span().line_length() + 6
|
||||||
}
|
}
|
||||||
ElementAttr::AttrExpression { name, value } => {
|
ElementAttr::AttrExpression { name, value } => {
|
||||||
value.span().line_length() + name.span().line_length() + 6
|
value.span().line_length() + name.span().line_length() + 6
|
||||||
}
|
}
|
||||||
ElementAttr::CustomAttrText { value, name } => {
|
ElementAttr::CustomAttrText { value, name } => {
|
||||||
value.source.as_ref().unwrap().value().len() + name.value().len() + 6
|
ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6
|
||||||
}
|
}
|
||||||
ElementAttr::CustomAttrExpression { name, value } => {
|
ElementAttr::CustomAttrExpression { name, value } => {
|
||||||
name.value().len() + value.span().line_length() + 6
|
name.to_token_stream().to_string().len() + value.span().line_length() + 6
|
||||||
}
|
}
|
||||||
ElementAttr::EventTokens { tokens, name } => {
|
ElementAttr::EventTokens { tokens, name } => {
|
||||||
let location = Location::new(tokens.span().start());
|
let location = Location::new(tokens.span().start());
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ macro_rules! twoway {
|
|||||||
|
|
||||||
// doc attrs
|
// doc attrs
|
||||||
$( #[doc = $doc:expr] )*
|
$( #[doc = $doc:expr] )*
|
||||||
$name:ident
|
$name:ident,
|
||||||
),*
|
)*
|
||||||
) => {
|
) => {
|
||||||
$(
|
$(
|
||||||
$( #[doc = $doc] )*
|
$( #[doc = $doc] )*
|
||||||
@@ -14,6 +14,9 @@ macro_rules! twoway {
|
|||||||
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
|
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
|
||||||
let formatted = dioxus_autofmt::fmt_file(src);
|
let formatted = dioxus_autofmt::fmt_file(src);
|
||||||
let out = dioxus_autofmt::apply_formats(src, formatted);
|
let out = dioxus_autofmt::apply_formats(src, formatted);
|
||||||
|
// normalize line endings
|
||||||
|
let out = out.replace("\r", "");
|
||||||
|
let src = src.replace("\r", "");
|
||||||
pretty_assertions::assert_eq!(&src, &out);
|
pretty_assertions::assert_eq!(&src, &out);
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
@@ -21,24 +24,25 @@ macro_rules! twoway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
twoway![
|
twoway![
|
||||||
simple,
|
|
||||||
comments,
|
|
||||||
attributes,
|
attributes,
|
||||||
manual_props,
|
collapse_expr,
|
||||||
|
comments,
|
||||||
|
commentshard,
|
||||||
complex,
|
complex,
|
||||||
|
emoji,
|
||||||
|
ifchain_forloop,
|
||||||
|
immediate_expr,
|
||||||
|
key,
|
||||||
|
long_exprs,
|
||||||
|
long,
|
||||||
|
manual_props,
|
||||||
|
messy_indent,
|
||||||
|
multirsx,
|
||||||
|
raw_strings,
|
||||||
|
reallylong,
|
||||||
|
simple,
|
||||||
|
t2,
|
||||||
tiny,
|
tiny,
|
||||||
tinynoopt,
|
tinynoopt,
|
||||||
long,
|
trailing_expr,
|
||||||
key,
|
|
||||||
multirsx,
|
|
||||||
commentshard,
|
|
||||||
emoji,
|
|
||||||
messy_indent,
|
|
||||||
long_exprs,
|
|
||||||
ifchain_forloop,
|
|
||||||
t2,
|
|
||||||
reallylong,
|
|
||||||
immediate_expr,
|
|
||||||
collapse_expr,
|
|
||||||
trailing_expr
|
|
||||||
];
|
];
|
||||||
@@ -13,7 +13,9 @@ rsx! {
|
|||||||
|
|
||||||
// Complex nesting with handlers
|
// Complex nesting with handlers
|
||||||
li {
|
li {
|
||||||
Link { class: "flex items-center pl-3 py-3 pr-4 {active_class} rounded", to: "{to}",
|
Link {
|
||||||
|
class: "flex items-center pl-3 py-3 pr-4 {active_class} rounded",
|
||||||
|
to: "{to}",
|
||||||
span { class: "inline-block mr-3", icons::icon_0 {} }
|
span { class: "inline-block mr-3", icons::icon_0 {} }
|
||||||
span { "{name}" }
|
span { "{name}" }
|
||||||
children.is_some().then(|| rsx! {
|
children.is_some().then(|| rsx! {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
rsx! {
|
||||||
|
// Raw text strings
|
||||||
|
button { r#"Click me"#, r##"Click me"##, r######"Click me"######, r#"dynamic {1}"# }
|
||||||
|
|
||||||
|
// Raw attribute strings
|
||||||
|
div {
|
||||||
|
width: r#"10px"#,
|
||||||
|
height: r##"{10}px"##,
|
||||||
|
"raw-attr": r###"raw-attr"###,
|
||||||
|
"raw-attr2": r###"{100}"###
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,11 @@ macro_rules! twoway {
|
|||||||
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
|
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
|
||||||
let formatted = dioxus_autofmt::fmt_file(src_wrong);
|
let formatted = dioxus_autofmt::fmt_file(src_wrong);
|
||||||
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
|
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
|
||||||
|
|
||||||
|
// normalize line endings
|
||||||
|
let out = out.replace("\r", "");
|
||||||
|
let src_right = src_right.replace("\r", "");
|
||||||
|
|
||||||
pretty_assertions::assert_eq!(&src_right, &out);
|
pretty_assertions::assert_eq!(&src_right, &out);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::{
|
|||||||
nodes::{DynamicNode, VNode},
|
nodes::{DynamicNode, VNode},
|
||||||
scopes::ScopeId,
|
scopes::ScopeId,
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
Attribute, AttributeValue, TemplateNode,
|
Attribute, TemplateNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
@@ -125,10 +125,8 @@ impl<'b> VirtualDom {
|
|||||||
.mounted_element
|
.mounted_element
|
||||||
.set(left_attr.mounted_element.get());
|
.set(left_attr.mounted_element.get());
|
||||||
|
|
||||||
// We want to make sure anything listener that gets pulled is valid
|
// We want to make sure anything that gets pulled is valid
|
||||||
if let AttributeValue::Listener(_) = right_attr.value {
|
self.update_template(left_attr.mounted_element.get(), right_template);
|
||||||
self.update_template(left_attr.mounted_element.get(), right_template);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the attributes are different (or volatile), we need to update them
|
// If the attributes are different (or volatile), we need to update them
|
||||||
if left_attr.value != right_attr.value || left_attr.volatile {
|
if left_attr.value != right_attr.value || left_attr.volatile {
|
||||||
|
|||||||
@@ -284,6 +284,17 @@ where
|
|||||||
Ok(&*Box::leak(deserialized))
|
Ok(&*Box::leak(deserialized))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
fn deserialize_option_leaky<'a, 'de, D>(deserializer: D) -> Result<Option<&'static str>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
let deserialized = Option::<String>::deserialize(deserializer)?;
|
||||||
|
Ok(deserialized.map(|deserialized| &*Box::leak(deserialized.into_boxed_str())))
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> Template<'a> {
|
impl<'a> Template<'a> {
|
||||||
/// Is this template worth caching at all, since it's completely runtime?
|
/// Is this template worth caching at all, since it's completely runtime?
|
||||||
///
|
///
|
||||||
@@ -319,6 +330,10 @@ pub enum TemplateNode<'a> {
|
|||||||
///
|
///
|
||||||
/// In HTML, this would be a valid URI that defines a namespace for all elements below it
|
/// In HTML, this would be a valid URI that defines a namespace for all elements below it
|
||||||
/// SVG is an example of this namespace
|
/// SVG is an example of this namespace
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serialize",
|
||||||
|
serde(deserialize_with = "deserialize_option_leaky")
|
||||||
|
)]
|
||||||
namespace: Option<&'a str>,
|
namespace: Option<&'a str>,
|
||||||
|
|
||||||
/// A list of possibly dynamic attribues for this element
|
/// A list of possibly dynamic attribues for this element
|
||||||
|
|||||||
@@ -389,64 +389,70 @@ impl VirtualDom {
|
|||||||
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
|
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
|
||||||
while let Some(el_ref) = parent_path {
|
while let Some(el_ref) = parent_path {
|
||||||
// safety: we maintain references of all vnodes in the element slab
|
// safety: we maintain references of all vnodes in the element slab
|
||||||
let template = unsafe { el_ref.template.unwrap().as_ref() };
|
if let Some(template) = el_ref.template {
|
||||||
let node_template = template.template.get();
|
let template = unsafe { template.as_ref() };
|
||||||
let target_path = el_ref.path;
|
let node_template = template.template.get();
|
||||||
|
let target_path = el_ref.path;
|
||||||
|
|
||||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||||
let this_path = node_template.attr_paths[idx];
|
let this_path = node_template.attr_paths[idx];
|
||||||
|
|
||||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||||
if attr.name.trim_start_matches("on") == name
|
if attr.name.trim_start_matches("on") == name
|
||||||
&& target_path.is_decendant(&this_path)
|
&& target_path.is_decendant(&this_path)
|
||||||
{
|
{
|
||||||
listeners.push(&attr.value);
|
listeners.push(&attr.value);
|
||||||
|
|
||||||
// Break if this is the exact target element.
|
// Break if this is the exact target element.
|
||||||
// This means we won't call two listeners with the same name on the same element. This should be
|
// This means we won't call two listeners with the same name on the same element. This should be
|
||||||
// documented, or be rejected from the rsx! macro outright
|
// documented, or be rejected from the rsx! macro outright
|
||||||
if target_path == this_path {
|
if target_path == this_path {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
||||||
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
||||||
for listener in listeners.drain(..).rev() {
|
for listener in listeners.drain(..).rev() {
|
||||||
if let AttributeValue::Listener(listener) = listener {
|
if let AttributeValue::Listener(listener) = listener {
|
||||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||||
cb(uievent.clone());
|
cb(uievent.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !uievent.propagates.get() {
|
if !uievent.propagates.get() {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
parent_path = dbg!(template.parent).and_then(|id| self.elements.get(id.0));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, we just call the listener on the target element
|
// Otherwise, we just call the listener on the target element
|
||||||
if let Some(el_ref) = parent_path {
|
if let Some(el_ref) = parent_path {
|
||||||
// safety: we maintain references of all vnodes in the element slab
|
// safety: we maintain references of all vnodes in the element slab
|
||||||
let template = unsafe { el_ref.template.unwrap().as_ref() };
|
if let Some(template) = el_ref.template {
|
||||||
let node_template = template.template.get();
|
let template = unsafe { template.as_ref() };
|
||||||
let target_path = el_ref.path;
|
let node_template = template.template.get();
|
||||||
|
let target_path = el_ref.path;
|
||||||
|
|
||||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||||
let this_path = node_template.attr_paths[idx];
|
let this_path = node_template.attr_paths[idx];
|
||||||
|
|
||||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||||
// Only call the listener if this is the exact target element.
|
// Only call the listener if this is the exact target element.
|
||||||
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
||||||
if let AttributeValue::Listener(listener) = &attr.value {
|
if let AttributeValue::Listener(listener) = &attr.value {
|
||||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||||
cb(uievent.clone());
|
cb(uievent.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use dioxus_interpreter_js::INTERPRETER_JS;
|
use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@@ -37,7 +37,7 @@ fn module_loader(root_name: &str) -> String {
|
|||||||
);
|
);
|
||||||
format!(
|
format!(
|
||||||
r#"
|
r#"
|
||||||
<script>
|
<script type="module">
|
||||||
{js}
|
{js}
|
||||||
|
|
||||||
let rootname = "{root_name}";
|
let rootname = "{root_name}";
|
||||||
@@ -84,6 +84,11 @@ pub(super) fn desktop_handler(
|
|||||||
.header("Content-Type", "text/html")
|
.header("Content-Type", "text/html")
|
||||||
.body(Cow::from(body))
|
.body(Cow::from(body))
|
||||||
.map_err(From::from);
|
.map_err(From::from);
|
||||||
|
} else if request.uri().path() == "/common.js" {
|
||||||
|
return Response::builder()
|
||||||
|
.header("Content-Type", "text/javascript")
|
||||||
|
.body(Cow::from(COMMON_JS.as_bytes()))
|
||||||
|
.map_err(From::from);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Else, try to serve a file from the filesystem.
|
// Else, try to serve a file from the filesystem.
|
||||||
|
|||||||
@@ -1154,6 +1154,7 @@ builder_constructors! {
|
|||||||
r#type: InputType "type",
|
r#type: InputType "type",
|
||||||
// value: String,
|
// value: String,
|
||||||
value: String volatile,
|
value: String volatile,
|
||||||
|
initial_value: String DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Build a
|
/// Build a
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dioxus-interpreter-js"
|
name = "dioxus-interpreter-js"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
authors = ["Jonathan Kelley"]
|
authors = ["Jonathan Kelley"]
|
||||||
description = "JS Intepreter for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
description = "JS Intepreter for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||||
@@ -25,3 +25,4 @@ default = []
|
|||||||
serialize = ["serde"]
|
serialize = ["serde"]
|
||||||
web = ["wasm-bindgen", "js-sys", "web-sys"]
|
web = ["wasm-bindgen", "js-sys", "web-sys"]
|
||||||
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
|
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
|
||||||
|
minimal_bindings = []
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const bool_attrs = {
|
||||||
|
allowfullscreen: true,
|
||||||
|
allowpaymentrequest: true,
|
||||||
|
async: true,
|
||||||
|
autofocus: true,
|
||||||
|
autoplay: true,
|
||||||
|
checked: true,
|
||||||
|
controls: true,
|
||||||
|
default: true,
|
||||||
|
defer: true,
|
||||||
|
disabled: true,
|
||||||
|
formnovalidate: true,
|
||||||
|
hidden: true,
|
||||||
|
ismap: true,
|
||||||
|
itemscope: true,
|
||||||
|
loop: true,
|
||||||
|
multiple: true,
|
||||||
|
muted: true,
|
||||||
|
nomodule: true,
|
||||||
|
novalidate: true,
|
||||||
|
open: true,
|
||||||
|
playsinline: true,
|
||||||
|
readonly: true,
|
||||||
|
required: true,
|
||||||
|
reversed: true,
|
||||||
|
selected: true,
|
||||||
|
truespeed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function setAttributeInner(node, field, value, ns) {
|
||||||
|
const name = field;
|
||||||
|
if (ns === "style") {
|
||||||
|
// ????? why do we need to do this
|
||||||
|
if (node.style === undefined) {
|
||||||
|
node.style = {};
|
||||||
|
}
|
||||||
|
node.style[name] = value;
|
||||||
|
} else if (ns != null && ns != undefined) {
|
||||||
|
node.setAttributeNS(ns, name, value);
|
||||||
|
} else {
|
||||||
|
switch (name) {
|
||||||
|
case "value":
|
||||||
|
if (value !== node.value) {
|
||||||
|
node.value = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "initial_value":
|
||||||
|
node.defaultValue = value;
|
||||||
|
break;
|
||||||
|
case "checked":
|
||||||
|
node.checked = value === "true" || value === true;
|
||||||
|
break;
|
||||||
|
case "selected":
|
||||||
|
node.selected = value === "true" || value === true;
|
||||||
|
break;
|
||||||
|
case "dangerous_inner_html":
|
||||||
|
node.innerHTML = value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
|
||||||
|
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
|
||||||
|
node.removeAttribute(name);
|
||||||
|
} else {
|
||||||
|
node.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { setAttributeInner } from "./common.js";
|
||||||
|
|
||||||
class ListenerMap {
|
class ListenerMap {
|
||||||
constructor(root) {
|
constructor(root) {
|
||||||
// bubbling events can listen at the root element
|
// bubbling events can listen at the root element
|
||||||
@@ -153,43 +155,7 @@ class Interpreter {
|
|||||||
this.RemoveAttribute(id, field, ns);
|
this.RemoveAttribute(id, field, ns);
|
||||||
} else {
|
} else {
|
||||||
const node = this.nodes[id];
|
const node = this.nodes[id];
|
||||||
this.SetAttributeInner(node, field, value, ns);
|
setAttributeInner(node, field, value, ns);
|
||||||
}
|
|
||||||
}
|
|
||||||
SetAttributeInner(node, field, value, ns) {
|
|
||||||
const name = field;
|
|
||||||
if (ns === "style") {
|
|
||||||
// ????? why do we need to do this
|
|
||||||
if (node.style === undefined) {
|
|
||||||
node.style = {};
|
|
||||||
}
|
|
||||||
node.style[name] = value;
|
|
||||||
} else if (ns != null && ns != undefined) {
|
|
||||||
node.setAttributeNS(ns, name, value);
|
|
||||||
} else {
|
|
||||||
switch (name) {
|
|
||||||
case "value":
|
|
||||||
if (value !== node.value) {
|
|
||||||
node.value = value;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "checked":
|
|
||||||
node.checked = value === "true" || value === true;
|
|
||||||
break;
|
|
||||||
case "selected":
|
|
||||||
node.selected = value === "true" || value === true;
|
|
||||||
break;
|
|
||||||
case "dangerous_inner_html":
|
|
||||||
node.innerHTML = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
|
|
||||||
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
|
|
||||||
node.removeAttribute(name);
|
|
||||||
} else {
|
|
||||||
node.setAttribute(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RemoveAttribute(root, field, ns) {
|
RemoveAttribute(root, field, ns) {
|
||||||
@@ -291,7 +257,7 @@ class Interpreter {
|
|||||||
|
|
||||||
for (let attr of node.attrs) {
|
for (let attr of node.attrs) {
|
||||||
if (attr.type == "Static") {
|
if (attr.type == "Static") {
|
||||||
this.SetAttributeInner(el, attr.name, attr.value, attr.namespace);
|
setAttributeInner(el, attr.name, attr.value, attr.namespace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,34 +760,6 @@ function serialize_event(event) {
|
|||||||
function serializeIpcMessage(method, params = {}) {
|
function serializeIpcMessage(method, params = {}) {
|
||||||
return JSON.stringify({ method, params });
|
return JSON.stringify({ method, params });
|
||||||
}
|
}
|
||||||
const bool_attrs = {
|
|
||||||
allowfullscreen: true,
|
|
||||||
allowpaymentrequest: true,
|
|
||||||
async: true,
|
|
||||||
autofocus: true,
|
|
||||||
autoplay: true,
|
|
||||||
checked: true,
|
|
||||||
controls: true,
|
|
||||||
default: true,
|
|
||||||
defer: true,
|
|
||||||
disabled: true,
|
|
||||||
formnovalidate: true,
|
|
||||||
hidden: true,
|
|
||||||
ismap: true,
|
|
||||||
itemscope: true,
|
|
||||||
loop: true,
|
|
||||||
multiple: true,
|
|
||||||
muted: true,
|
|
||||||
nomodule: true,
|
|
||||||
novalidate: true,
|
|
||||||
open: true,
|
|
||||||
playsinline: true,
|
|
||||||
readonly: true,
|
|
||||||
required: true,
|
|
||||||
reversed: true,
|
|
||||||
selected: true,
|
|
||||||
truespeed: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
function is_element_node(node) {
|
function is_element_node(node) {
|
||||||
return node.nodeType == 1;
|
return node.nodeType == 1;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
|
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
|
||||||
|
pub static COMMON_JS: &str = include_str!("./common.js");
|
||||||
|
|
||||||
#[cfg(feature = "sledgehammer")]
|
#[cfg(feature = "sledgehammer")]
|
||||||
mod sledgehammer_bindings;
|
mod sledgehammer_bindings;
|
||||||
@@ -10,3 +11,13 @@ mod bindings;
|
|||||||
|
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
pub use bindings::Interpreter;
|
pub use bindings::Interpreter;
|
||||||
|
|
||||||
|
// Common bindings for minimal usage.
|
||||||
|
#[cfg(feature = "minimal_bindings")]
|
||||||
|
pub mod minimal_bindings {
|
||||||
|
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
|
||||||
|
#[wasm_bindgen(module = "/src/common.js")]
|
||||||
|
extern "C" {
|
||||||
|
pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,9 @@ mod js {
|
|||||||
node.value = value;
|
node.value = value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "initial_value":
|
||||||
|
node.defaultValue = value;
|
||||||
|
break;
|
||||||
case "checked":
|
case "checked":
|
||||||
node.checked = value === "true";
|
node.checked = value === "true";
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dioxus-liveview"
|
name = "dioxus-liveview"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||||
homepage = "https://dioxuslabs.com"
|
homepage = "https://dioxuslabs.com"
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ static INTERPRETER_JS: Lazy<String> = Lazy::new(|| {
|
|||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads)
|
let interpreter = interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads);
|
||||||
|
interpreter.replace("import { setAttributeInner } from \"./common.js\";", "")
|
||||||
|
});
|
||||||
|
|
||||||
|
static COMMON_JS: Lazy<String> = Lazy::new(|| {
|
||||||
|
let common = dioxus_interpreter_js::COMMON_JS;
|
||||||
|
common.replace("export", "")
|
||||||
});
|
});
|
||||||
|
|
||||||
static MAIN_JS: &str = include_str!("./main.js");
|
static MAIN_JS: &str = include_str!("./main.js");
|
||||||
@@ -89,11 +95,13 @@ static MAIN_JS: &str = include_str!("./main.js");
|
|||||||
/// processing user events and returning edits to the liveview instance
|
/// processing user events and returning edits to the liveview instance
|
||||||
pub fn interpreter_glue(url: &str) -> String {
|
pub fn interpreter_glue(url: &str) -> String {
|
||||||
let js = &*INTERPRETER_JS;
|
let js = &*INTERPRETER_JS;
|
||||||
|
let common = &*COMMON_JS;
|
||||||
format!(
|
format!(
|
||||||
r#"
|
r#"
|
||||||
<script>
|
<script>
|
||||||
var WS_ADDR = "{url}";
|
var WS_ADDR = "{url}";
|
||||||
{js}
|
{js}
|
||||||
|
{common}
|
||||||
{MAIN_JS}
|
{MAIN_JS}
|
||||||
main();
|
main();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||||
dioxus-html = { path = "../html", version = "^0.3.0", features = ["wasm-bind"] }
|
dioxus-html = { path = "../html", version = "^0.3.0", features = ["wasm-bind"] }
|
||||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0", features = [
|
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0", features = [
|
||||||
"sledgehammer"
|
"sledgehammer",
|
||||||
|
"minimal_bindings",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
js-sys = "0.3.56"
|
js-sys = "0.3.56"
|
||||||
|
|||||||
+9
-11
@@ -11,13 +11,13 @@ use dioxus_core::{
|
|||||||
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
|
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
|
||||||
};
|
};
|
||||||
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData, MountedData};
|
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData, MountedData};
|
||||||
use dioxus_interpreter_js::{get_node, save_template, Channel};
|
use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel};
|
||||||
use futures_channel::mpsc;
|
use futures_channel::mpsc;
|
||||||
use js_sys::Array;
|
use js_sys::Array;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::{any::Any, rc::Rc, sync::Arc};
|
use std::{any::Any, rc::Rc, sync::Arc};
|
||||||
use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast};
|
use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast, JsValue};
|
||||||
use web_sys::{console, Document, Element, Event, HtmlElement};
|
use web_sys::{console, Document, Element, Event};
|
||||||
|
|
||||||
use crate::{file_engine::WebFileEngine, Config};
|
use crate::{file_engine::WebFileEngine, Config};
|
||||||
|
|
||||||
@@ -135,14 +135,12 @@ impl WebsysDom {
|
|||||||
namespace,
|
namespace,
|
||||||
} = attr
|
} = attr
|
||||||
{
|
{
|
||||||
match namespace {
|
minimal_bindings::setAttributeInner(
|
||||||
Some(ns) if *ns == "style" => {
|
el.clone().into(),
|
||||||
el.dyn_ref::<HtmlElement>()
|
name,
|
||||||
.map(|f| f.style().set_property(name, value));
|
JsValue::from_str(value),
|
||||||
}
|
*namespace,
|
||||||
Some(ns) => el.set_attribute_ns(Some(ns), name, value).unwrap(),
|
);
|
||||||
None => el.set_attribute(name, value).unwrap(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for child in *children {
|
for child in *children {
|
||||||
|
|||||||
Reference in new issue
Block a user