diff --git a/leptos_macro/src/custom_view.rs b/leptos_macro/src/custom_view.rs new file mode 100644 index 000000000..805b04db2 --- /dev/null +++ b/leptos_macro/src/custom_view.rs @@ -0,0 +1,166 @@ +use proc_macro::TokenStream; +use quote::{quote, ToTokens}; +use syn::{parse::Parse, parse_macro_input, ImplItem, ItemImpl}; + +pub fn custom_view_impl(tokens: TokenStream) -> TokenStream { + let input = parse_macro_input!(tokens as CustomViewMacroInput); + input.into_token_stream().into() +} + +#[derive(Debug)] +struct CustomViewMacroInput { + impl_block: ItemImpl, +} + +impl Parse for CustomViewMacroInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let impl_block = input.parse()?; + Ok(CustomViewMacroInput { impl_block }) + } +} + +impl ToTokens for CustomViewMacroInput { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let ItemImpl { + impl_token, + generics, + self_ty, + items, + .. + } = &self.impl_block; + let impl_span = &impl_token; + let view_ty = items + .iter() + .find_map(|item| match item { + ImplItem::Type(ty) => (ty.ident == "View").then_some(&ty.ty), + _ => None, + }) + .unwrap_or_else(|| { + proc_macro_error::abort!( + impl_span, + "You must include `type View = ...;` to specify the type. \ + In most cases, this will be `type View = AnyView;" + ) + }); + + let view_fn = items + .iter() + .find_map(|item| match item { + ImplItem::Fn(f) => { + (f.sig.ident == "into_view").then_some(&f.block) + } + _ => None, + }) + .unwrap_or_else(|| { + proc_macro_error::abort!( + impl_span, + "You must include `fn into_view(self) -> Self::View` to \ + specify the view function." + ) + }); + let generic_params = &generics.params; + let where_preds = + &generics.where_clause.as_ref().map(|wc| &wc.predicates); + + tokens.extend(quote! { + #impl_token<#generic_params, Rndr> ::leptos::tachys::view::Render for #self_ty + where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds { + type State = <#view_ty as ::leptos::tachys::view::Render>::State; + + fn build(self) -> Self::State { + let view = #view_fn; + view.build() + } + + fn rebuild(self, state: &mut Self::State) { + let view = #view_fn; + view.rebuild(state); + } + } + + #impl_token<#generic_params, Rndr> ::leptos::tachys::view::add_attr::AddAnyAttr for #self_ty + where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds { + type Output> = + <#view_ty as ::leptos::tachys::view::add_attr::AddAnyAttr>::Output; + + fn add_any_attr>( + self, + attr: NewAttr, + ) -> Self::Output + where + Self::Output: ::leptos::tachys::view::RenderHtml, + { + let view = #view_fn; + view.add_any_attr(attr) + } + } + + #impl_token<#generic_params, Rndr> ::leptos::tachys::view::RenderHtml for #self_ty + where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds { + type AsyncOutput = <#view_ty as ::leptos::tachys::view::RenderHtml>::AsyncOutput; + const MIN_LENGTH: usize = <#view_ty as ::leptos::tachys::view::RenderHtml>::MIN_LENGTH; + + async fn resolve(self) -> Self::AsyncOutput { + let view = #view_fn; + ::leptos::tachys::view::RenderHtml::::resolve(view).await + } + + fn dry_resolve(&mut self) { + // TODO... The problem is that view_fn expects to take self + // dry_resolve is the only one that takes &mut self + // this can only have an effect if walking over the view would read from + // resources that are not otherwise read synchronously, which is an interesting + // edge case to handle but probably (?) irrelevant for most actual use cases of + // this macro + } + + fn to_html_with_buf( + self, + buf: &mut String, + position: &mut ::leptos::tachys::view::Position, + escape: bool, + mark_branches: bool, + ) { + let view = #view_fn; + ::leptos::tachys::view::RenderHtml::::to_html_with_buf( + view, + buf, + position, + escape, + mark_branches + ); + } + + fn to_html_async_with_buf( + self, + buf: &mut ::leptos::tachys::ssr::StreamBuilder, + position: &mut ::leptos::tachys::view::Position, + escape: bool, + mark_branches: bool, + ) where + Self: Sized, + { + let view = #view_fn; + ::leptos::tachys::view::RenderHtml::::to_html_async_with_buf::( + view, + buf, + position, + escape, + mark_branches + ); + } + + fn hydrate( + self, + cursor: &::leptos::tachys::hydration::Cursor, + position: &::leptos::tachys::view::PositionState, + ) -> Self::State { + let view = #view_fn; + ::leptos::tachys::view::RenderHtml::::hydrate::( + view, cursor, position + ) + } + } + }); + } +} diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index cdb18eec0..e85303af1 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -19,6 +19,7 @@ mod params; mod view; use crate::component::unmodified_fn_name_from_fn_name; mod component; +mod custom_view; mod slice; mod slot; @@ -857,3 +858,36 @@ pub fn params_derive( pub fn slice(input: TokenStream) -> TokenStream { slice::slice_impl(input) } + +/// Implements the traits needed to make something [`IntoView`] on some type. +/// +/// The renderer relies on the implementation of several traits, implementing which for a custom +/// struct involves significant boilerplate. This macro is intended to make it easier to implement +/// a view type for custom data, by allowing you to provide some custom view logic for the type. +/// +/// ```rust +/// use leptos::custom_view; +/// +/// struct Foo(T); +/// +/// #[custom_view] +/// impl CustomView for Foo +/// where +/// T: ToString + Send + 'static, +/// { +/// // this will usually be `AnyView`, but for simple types +/// // you may be able to specify the output type easily +/// type View = String; +/// +/// fn into_view(self) -> Self::View { +/// self.0.to_string().to_ascii_uppercase() +/// } +/// } +#[proc_macro_error::proc_macro_error] +#[proc_macro_attribute] +pub fn custom_view( + _args: proc_macro::TokenStream, + s: TokenStream, +) -> TokenStream { + custom_view::custom_view_impl(s) +}