Feat: update fc_macro

This commit is contained in:
Jonathan Kelley 2021-01-21 02:25:44 -05:00
parent 6aeea9b790
commit 28ac37a8b2
12 changed files with 353 additions and 230 deletions

View file

@ -3,3 +3,4 @@ Clippy
tide_ssr
Liveview
Dioxus
VDoms

View file

@ -31,9 +31,10 @@
- [ ] (SSR) Implement stateful 3rd party string renderer
- [ ] (Macro) Make VText nodes automatically capture and format IE allow "Text is {blah}" in place of {format!("Text is {}",blah)}
# Project: Initial Release (TBD)
# Project: Initial VDOM support (TBD)
> Get the initial VDom + Event System + Patching + Diffing + Component framework up and running
- [x] (Core) Migrate virtual node into new VNode type
- [ ] (Macro) Allow components to specify their props as function args
- [ ] (Core) Arena allocate VNodes
- [ ] (Core) Allow VNodes to borrow arena contents
- [ ] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures)

View file

@ -8,6 +8,7 @@ members = [
"packages/redux",
"packages/core-macro",
"packages/router",
"packages/ssr",
# "packages/macro",
# TODO @Jon, share the validation code
# "packages/web",

View file

@ -7,12 +7,12 @@
# About
Dioxus is a new approach for creating performant cross platform user experiences in Rust. In Dioxus, the UI is represented as a tree of Virtual Nodes not bound to any specific renderer. Instead, external renderers can leverage Dioxus' virtual DOM and event system as a source of truth for rendering to a medium of their choice. Developers used to crafting react-based experiences should feel comfortable with Dioxus.
Dioxus is a new approach for creating performant cross platform user experiences in Rust. In Dioxus, the UI is represented as a tree of Virtual Nodes not bound to any specific renderer. Instead, external renderers can leverage Dioxus' virtual DOM and event system as a source of truth for rendering to a medium of their choice. Developers experienced with building react-based experiences should feel comfortable with Dioxus.
Dioxus is unique in the space of UI for Rust. Dioxus supports a renderer approach called "broadcasting" where two VDoms with separate renderers can sync their UI states remotely. Our goal as a framework is to work towards "Dioxus Liveview" where a server and client work in tandem, eliminating the need for frontend-specific APIs altogether.
## Features
Dioxus' goal is to be the most advanced UI system for Rust, targeting isomorphism and hybrid approaches. Our goal is to never be burdened with context switching between various programming languages, build tools, and environment idiosyncrasies.
Dioxus' goal is to be the most advanced UI system for Rust, targeting isomorphism and hybrid approaches. Our goal is to eliminate context-switching for cross-platform development - both in UI patterns and programming language. Hooks and components should work *everywhere* without compromise.
Dioxus Core supports:
- [ ] Hooks
@ -21,7 +21,7 @@ Dioxus Core supports:
- [ ] State management integrations
On top of these, we have several projects you can find in the `packages` folder.
- [x] `dioxuscli`: Testing, development, and packaging tools for Dioxus apps
- [x] `dioxus-cli`: Testing, development, and packaging tools for Dioxus apps
- [ ] `dioxus-router`: A hook-based router implementation for Dioxus web apps
- [ ] `Dioxus-vscode`: Syntax highlighting, code formatting, and hints for Dioxus html! blocks
- [ ] `Redux-rs`: Redux-style global state management
@ -47,19 +47,19 @@ fn Example(ctx: &Context<MyProps>) -> VNode {
}
```
Here, the `Context` object is used to access hook state, create subscriptions, and interact with the built-in context API. Props, children, and component APIs are accessible via the `Context` object. If using the functional component macro, it's possible to inline props into the function definition itself.
Here, the `Context` object is used to access hook state, create subscriptions, and interact with the built-in context API. Props, children, and component APIs are accessible via the `Context` object. The functional component macro makes life more productive by inlining props directly as function arguments, similar to how Rocket parses URIs.
```rust
// A very terse component!
#[functional_component]
fn Example(ctx: &Context<{ name: String }>) -> VNode {
#[fc]
fn Example(ctx: &Context, name: String) -> VNode {
html! { <div> "Hello {name}!" </div> }
}
// or
#[functional_component]
static Example: FC<{ name: String }> = |ctx| html! { <div> "Hello {:?name}!" </div> };
static Example: FC = |ctx, name: String| html! { <div> "Hello {:?name}!" </div> };
```
The final output of components must be a tree of VNodes. We provide an html macro for using JSX-style syntax to write these, though, you could use any macro, DSL, or templating engine. Work is being done on a terra template processor for existing templates.
@ -83,7 +83,7 @@ async fn user_data(ctx: &Context<()>) -> VNode {
Asynchronous components are powerful but can also be easy to misuse as they pause rendering for the component and its children. Refer to the concurrent guide for information on how to best use async components.
## Examples
We use `diopack` to build and test webapps. This can run examples, tests, build web workers, launch development servers, bundle, and more. It's general purpose, but currently very tailored to Dioxus for liveview and bundling. If you've not used it before, `cargo install --path pacakages/diopack` will get it installed.
We use the dedicated `dioxus-cli` to build and test dioxus web-apps. This can run examples, tests, build web workers, launch development servers, bundle, and more. It's general purpose, but currently very tailored to Dioxus for liveview and bundling. If you've not used it before, `cargo install --path pacakages/dioxus-cli` will get it installed. This CLI tool should feel like using `cargo` but with 1st party support for assets, bundling, and other important dioxus-specific features.
Alternatively, `trunk` works but can't run examples.

View file

@ -10,6 +10,7 @@ edition = "2018"
fern = { version = "0.6.0", features = ["colored"] }
log = "0.4.1"
dioxus = { path = "../packages/dioxus" }
dioxus-ssr = { path = "../packages/ssr" }
rand = "0.8.2"
@ -43,3 +44,7 @@ name = "doc_generator"
[[example]]
path = "router.rs"
name = "router"
[[example]]
path = "fc_macro.rs"
name = "fc_macro"

19
examples/fc_macro.rs Normal file
View file

@ -0,0 +1,19 @@
use dioxus::prelude::*;
use dioxus_ssr::TextRenderer;
// todo @Jon, support components in the html! macro
// let renderer = TextRenderer::new(|_| html! {<Example name="world"/>});
fn main() {
let renderer = TextRenderer::<()>::new(|_| html! {<div> "Hello world" </div>});
let output = renderer.render();
}
/// An example component that demonstrates how to use the functional_component macro
/// This macro makes writing functional components elegant, similar to how Rocket parses URIs.
///
/// You don't actually *need* this macro to be productive, but it makes life easier, and components cleaner.
/// This approach also integrates well with tools like Rust-Analyzer
#[fc]
fn example(ctx: &Context, name: String) -> VNode {
html! { <div> "Hello, {name}!" </div> }
}

View file

@ -1,6 +1,6 @@
[package]
name = "dioxus-core-macro"
version = "0.0.0"
version = "0.1.0"
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
edition = "2018"

View file

@ -1,14 +1,118 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{
parse::{Parse, ParseStream},
Signature,
};
use syn::{
parse_macro_input, Attribute, Block, FnArg, Ident, Item, ItemFn, ReturnType, Type, Visibility,
};
/// Label a function or static closure as a functional component.
/// This macro reduces the need to create a separate properties struct.
#[proc_macro_attribute]
pub fn fc(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let item = parse_macro_input!(item as FunctionComponent);
// let attr = parse_macro_input!(attr as FunctionComponentName);
function_component_impl(item)
// function_component_impl(attr, item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
fn function_component_impl(
// name: FunctionComponentName,
component: FunctionComponent,
) -> syn::Result<TokenStream> {
// let FunctionComponentName { component_name } = name;
let FunctionComponent {
block,
props_type,
arg,
vis,
attrs,
name: function_name,
return_type,
} = component;
// if function_name == component_name {
// return Err(syn::Error::new_spanned(
// component_name,
// "the component must not have the same name as the function",
// ));
// }
let ret_type = quote_spanned!(return_type.span()=> VNode);
// let ret_type = quote_spanned!(return_type.span()=> ::VNode);
// let ret_type = quote_spanned!(return_type.span()=> ::yew::html::Html);
let quoted = quote! {
#[doc(hidden)]
#[allow(non_camel_case_types)]
mod __component_blah {
use super::*;
#[derive(PartialEq)]
pub struct Props {
name: String
}
pub fn component(ctx: &mut Context<Props>) -> #ret_type {
let Props {
name
} = ctx.props;
#block
}
}
#[allow(non_snake_case)]
pub use __component_blah::component as #function_name;
// #vis struct #function_name;
// impl ::yew_functional::FunctionProvider for #function_name {
// type TProps = #props_type;
// fn run(#arg) -> #ret_type {
// #block
// }
// }
// #(#attrs)*
// #vis type #component_name = ::yew_functional::FunctionComponent<#function_name>;
};
// let quoted = quote! {
// #[doc(hidden)]
// #[allow(non_camel_case_types)]
// #vis struct #function_name;
// impl ::yew_functional::FunctionProvider for #function_name {
// type TProps = #props_type;
// fn run(#arg) -> #ret_type {
// #block
// }
// }
// #(#attrs)*
// #vis type #component_name = ::yew_functional::FunctionComponent<#function_name>;
// };
Ok(quoted)
}
/// A parsed version of the user's input
struct FunctionComponent {
// The actual contents of the function
block: Box<Block>,
// The user's props type
props_type: Box<Type>,
arg: FnArg,
vis: Visibility,
attrs: Vec<Attribute>,
@ -20,123 +124,150 @@ impl Parse for FunctionComponent {
fn parse(input: ParseStream) -> syn::Result<Self> {
let parsed: Item = input.parse()?;
match parsed {
Item::Fn(func) => {
let ItemFn {
attrs,
vis,
sig,
block,
} = func;
// Convert the parsed input into the Function block
let ItemFn {
attrs,
vis,
sig,
block,
} = ensure_fn_block(parsed)?;
if !sig.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
sig.generics,
"function components can't contain generics",
));
}
// Validate the user's signature
let sig = validate_signature(sig)?;
if sig.asyncness.is_some() {
return Err(syn::Error::new_spanned(
sig.asyncness,
"function components can't be async",
));
}
// Validate the return type is actually something
let return_type = ensure_return_type(sig.output)?;
if sig.constness.is_some() {
return Err(syn::Error::new_spanned(
sig.constness,
"const functions can't be function components",
));
}
// Get all the function args
let mut inputs = sig.inputs.into_iter();
if sig.abi.is_some() {
return Err(syn::Error::new_spanned(
sig.abi,
"extern functions can't be function components",
));
}
// Collect the first arg
let first_arg: FnArg = inputs
.next()
.unwrap_or_else(|| syn::parse_quote! { _: &() });
let return_type = match sig.output {
ReturnType::Default => {
return Err(syn::Error::new_spanned(
sig,
"function components must return `yew::Html`",
))
}
ReturnType::Type(_, ty) => ty,
};
// Extract the "context" object
let props_type = validate_context_arg(&first_arg)?;
let mut inputs = sig.inputs.into_iter();
let arg: FnArg = inputs
.next()
.unwrap_or_else(|| syn::parse_quote! { _: &() });
// Collect the rest of the args into a list of definitions to be used by the inline struct
let ty = match &arg {
FnArg::Typed(arg) => match &*arg.ty {
Type::Reference(ty) => {
if ty.lifetime.is_some() {
return Err(syn::Error::new_spanned(
&ty.lifetime,
"reference must not have a lifetime",
));
}
// Checking after param parsing may make it a little inefficient
// but that's a requirement for better error messages in case of receivers
// `>0` because first one is already consumed.
// if inputs.len() > 0 {
// let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect();
// return Err(syn::Error::new_spanned(
// params,
// "function components can accept at most one parameter for the props",
// ));
// }
let name = sig.ident;
if ty.mutability.is_some() {
return Err(syn::Error::new_spanned(
&ty.mutability,
"reference must not be mutable",
));
}
ty.elem.clone()
}
ty => {
let msg = format!(
"expected a reference to a `Properties` type (try: `&{}`)",
ty.to_token_stream()
);
return Err(syn::Error::new_spanned(ty, msg));
}
},
FnArg::Receiver(_) => {
return Err(syn::Error::new_spanned(
arg,
"function components can't accept a receiver",
));
}
};
// Checking after param parsing may make it a little inefficient
// but that's a requirement for better error messages in case of receivers
// `>0` because first one is already consumed.
if inputs.len() > 0 {
let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect();
return Err(syn::Error::new_spanned(
params,
"function components can accept at most one parameter for the props",
));
}
Ok(Self {
props_type: ty,
block,
arg,
vis,
attrs,
name: sig.ident,
return_type,
})
}
item => Err(syn::Error::new_spanned(
item,
"`function_component` attribute can only be applied to functions",
)),
}
Ok(Self {
props_type,
block,
arg: first_arg,
vis,
attrs,
name,
return_type,
})
}
}
/// Ensure the user's input is actually a functional component
fn ensure_fn_block(item: Item) -> syn::Result<ItemFn> {
match item {
Item::Fn(it) => Ok(it),
other => Err(syn::Error::new_spanned(
other,
"`function_component` attribute can only be applied to functions",
)),
}
}
/// Ensure the user's function actually returns a VNode
fn ensure_return_type(output: ReturnType) -> syn::Result<Box<Type>> {
match output {
ReturnType::Default => Err(syn::Error::new_spanned(
output,
"function components must return `dioxus::VNode`",
)),
ReturnType::Type(_, ty) => Ok(ty),
}
}
/// Validate the users's input signature for the function component.
/// Returns an error if any of the conditions prove to be wrong;
fn validate_signature(sig: Signature) -> syn::Result<Signature> {
if !sig.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
sig.generics,
"function components can't contain generics",
));
}
if sig.asyncness.is_some() {
return Err(syn::Error::new_spanned(
sig.asyncness,
"function components can't be async",
));
}
if sig.constness.is_some() {
return Err(syn::Error::new_spanned(
sig.constness,
"const functions can't be function components",
));
}
if sig.abi.is_some() {
return Err(syn::Error::new_spanned(
sig.abi,
"extern functions can't be function components",
));
}
Ok(sig)
}
fn validate_context_arg(first_arg: &FnArg) -> syn::Result<Box<Type>> {
if let FnArg::Typed(arg) = first_arg {
// Input arg is a reference to an &mut Context
if let Type::Reference(ty) = &*arg.ty {
if ty.lifetime.is_some() {
return Err(syn::Error::new_spanned(
&ty.lifetime,
"reference must not have a lifetime",
));
}
if ty.mutability.is_some() {
return Err(syn::Error::new_spanned(
&ty.mutability,
"reference must not be mutable",
));
}
Ok(ty.elem.clone())
} else {
let msg = format!(
"expected a reference to a `Context` object (try: `&mut {}`)",
arg.ty.to_token_stream()
);
return Err(syn::Error::new_spanned(arg.ty.clone(), msg));
}
} else {
return Err(syn::Error::new_spanned(
first_arg,
"function components can't accept a receiver",
));
}
}
fn collect_inline_args() {}
/// The named specified in the macro usage.
struct FunctionComponentName {
component_name: Ident,
}
@ -152,60 +283,3 @@ impl Parse for FunctionComponentName {
Ok(Self { component_name })
}
}
#[proc_macro_attribute]
pub fn function_component(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let item = parse_macro_input!(item as FunctionComponent);
let attr = parse_macro_input!(attr as FunctionComponentName);
function_component_impl(attr, item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
fn function_component_impl(
name: FunctionComponentName,
component: FunctionComponent,
) -> syn::Result<TokenStream> {
let FunctionComponentName { component_name } = name;
let FunctionComponent {
block,
props_type,
arg,
vis,
attrs,
name: function_name,
return_type,
} = component;
if function_name == component_name {
return Err(syn::Error::new_spanned(
component_name,
"the component must not have the same name as the function",
));
}
let ret_type = quote_spanned!(return_type.span()=> ::yew::html::Html);
let quoted = quote! {
#[doc(hidden)]
#[allow(non_camel_case_types)]
#vis struct #function_name;
impl ::yew_functional::FunctionProvider for #function_name {
type TProps = #props_type;
fn run(#arg) -> #ret_type {
#block
}
}
#(#attrs)*
#vis type #component_name = ::yew_functional::FunctionComponent<#function_name>;
};
Ok(quoted)
}

View file

@ -9,3 +9,4 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi
[dependencies]
dioxus-core = { path = "../core", version = "0.1.0" }
dioxus-core-macro = { path = "../core-macro", version = "0.1.0" }

View file

@ -1,57 +1,9 @@
pub mod prelude {
pub use dioxus_core::prelude::*;
pub use dioxus_core_macro::fc;
}
use dioxus_core::prelude::FC;
// Re-export core completely
pub use dioxus_core as core;
struct App {}
impl App {
fn at(&mut self, pat: &str) -> Route {
todo!()
}
// start the app in a new thread
// pass updates to it via state manager?
fn start(self) {}
}
fn new() -> App {
todo!()
}
struct Router {}
struct Route {}
struct RouteInfo {}
impl RouteInfo {
fn query<T>(&self) -> T {
todo!()
}
}
impl Route {
fn serve(&self, app: FC<RouteInfo>) {}
}
#[cfg(test)]
mod test {
use super::*;
use crate::prelude::*;
#[test]
fn app_example() {
let mut app = crate::new();
app.at("/").serve(|ctx| {
let parsed: u32 = ctx.props.query();
html! { <div>"hello " </div> }
});
app.start();
}
}

View file

@ -1,9 +1,10 @@
[package]
name = "dioxus-ssr"
version = "0.0.0"
version = "0.1.0"
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[dependencies]
dioxus-core = { path = "../core", version = "0.1.0" }

View file

@ -1,7 +1,75 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
//! Dioxus Server-Side-Rendering
//!
//! This crate demonstrates how to implement a custom renderer for Dioxus VNodes via the `TextRenderer` renderer.
//! The `TextRenderer` consumes a Dioxus Virtual DOM, progresses its event queue, and renders the VNodes to a String.
//!
//! While `VNode` supports "to_string" directly, it renders child components as the RSX! macro tokens. For custom components,
//! an external renderer is needed to progress the component lifecycles. The `TextRenderer` shows how to use the Virtual DOM
//! API to progress these lifecycle events to generate a fully-mounted Virtual DOM instance which can be renderer in the
//! `render` method.
//!
//! ```ignore
//! fn main() {
//! let renderer = TextRenderer::<()>::new(|_| html! {<div> "Hello world" </div>});
//! let output = renderer.render();
//! assert_eq!(output, "<div>Hello World</div>");
//! }
//! ```
//!
//! The `TextRenderer` is particularly useful when needing to cache a Virtual DOM in between requests
//!
use dioxus_core::prelude::FC;
/// The `TextRenderer` provides a way of rendering a Dioxus Virtual DOM to a String.
///
///
///
pub struct TextRenderer<T> {
_root_type: std::marker::PhantomData<T>,
}
impl<T> TextRenderer<T> {
/// Create a new text-renderer instance from a functional component root.
/// Automatically progresses the creation of the VNode tree to completion.
///
/// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom`
pub fn new(root: FC<T>) -> Self {
Self {
_root_type: std::marker::PhantomData {},
}
}
/// Create a new text renderer from an existing Virtual DOM.
/// This will progress the existing VDom's events to completion.
pub fn from_vdom() -> Self {
todo!()
}
/// Pass new args to the root function
pub fn update(&mut self, new_val: T) {
todo!()
}
/// Modify the root function in place, forcing a re-render regardless if the props changed
pub fn update_mut(&mut self, modifier: impl Fn(&mut T)) {
todo!()
}
/// Render the virtual DOM to a string
pub fn render(&self) -> String {
let mut buffer = String::new();
// iterate through the internal patch queue of virtual dom, and apply them to the buffer
/*
*/
todo!()
}
/// Render VDom to an existing buffer
/// TODO @Jon, support non-string buffers to actually make this useful
/// Currently, this only supports overwriting an existing buffer, instead of just
pub fn render_mut(&self, buf: &mut String) {
todo!()
}
}