feat: `custom_view` macro to allow implementing view types on arbitrary data
This commit is contained in:
parent
1f4c410f78
commit
2cc6ccaacb
|
@ -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<Self> {
|
||||
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<Rndr>;"
|
||||
)
|
||||
});
|
||||
|
||||
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<Rndr> for #self_ty
|
||||
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
|
||||
type State = <#view_ty as ::leptos::tachys::view::Render<Rndr>>::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<Rndr> for #self_ty
|
||||
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
|
||||
type Output<SomeNewAttr: ::leptos::tachys::html::attribute::Attribute<Rndr>> =
|
||||
<#view_ty as ::leptos::tachys::view::add_attr::AddAnyAttr<Rndr>>::Output<SomeNewAttr>;
|
||||
|
||||
fn add_any_attr<NewAttr: ::leptos::tachys::html::attribute::Attribute<Rndr>>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: ::leptos::tachys::view::RenderHtml<Rndr>,
|
||||
{
|
||||
let view = #view_fn;
|
||||
view.add_any_attr(attr)
|
||||
}
|
||||
}
|
||||
|
||||
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::RenderHtml<Rndr> for #self_ty
|
||||
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
|
||||
type AsyncOutput = <#view_ty as ::leptos::tachys::view::RenderHtml<Rndr>>::AsyncOutput;
|
||||
const MIN_LENGTH: usize = <#view_ty as ::leptos::tachys::view::RenderHtml<Rndr>>::MIN_LENGTH;
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
let view = #view_fn;
|
||||
::leptos::tachys::view::RenderHtml::<Rndr>::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::<Rndr>::to_html_with_buf(
|
||||
view,
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches
|
||||
);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
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::<Rndr>::to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||
view,
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches
|
||||
);
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &::leptos::tachys::hydration::Cursor<Rndr>,
|
||||
position: &::leptos::tachys::view::PositionState,
|
||||
) -> Self::State {
|
||||
let view = #view_fn;
|
||||
::leptos::tachys::view::RenderHtml::<Rndr>::hydrate::<FROM_SERVER>(
|
||||
view, cursor, position
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>(T);
|
||||
///
|
||||
/// #[custom_view]
|
||||
/// impl<T> CustomView for Foo<T>
|
||||
/// where
|
||||
/// T: ToString + Send + 'static,
|
||||
/// {
|
||||
/// // this will usually be `AnyView<Rndr>`, 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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue