xmlet / HtmlFlow

HtmlFlow Java DSL to write typesafe HTML
https://htmlflow.org/
MIT License
154 stars 28 forks source link

[Discussion] Do we need to add advice on usage of dynamic bloc #121

Open benjamin-dreux opened 5 days ago

benjamin-dreux commented 5 days ago

In the last few days I'm using more and more HtmlFlow.

At first i tended use dynamic bloc in the lowest place possible in the html tree.

Then I wonder what would happened if I switched to a using it the other way arround.

Now I'm back to the lowest place possible.

My reason is to be able to compose my page view more easly.

See some of my exemples

static void template(Div<?> container) {
        // spotless:off
        container
            .form()
                .attrMethod(EnumMethodType.POST)
                .attrId("contact")
                .<ContactEditModel>dynamic((form, model) -> form
                    .attrAction("/contacts/%s/update".formatted(model.id()))
                )           
                .of( ContactEditView::hiddenInputId )       
                .of( ContactEditView::isOrganizationField )       
                .of( ContactEditView::firstNameField)
                .of( ContactEditView::lastNameField)
                .of( ContactEditView::nameField)

                .of(ContactEditView::addressBlock)
                .of(ContactEditView::telephoneBlock)
                .of(ContactEditView::emailBlock)

                .of(ContactEditView::buttonsBlock)

            .__();
            // spotless:on
    }
   static void hiddenInputId(Form<?> parent) {
        // spotless:off
        parent.input()
            .attrType(EnumTypeInputType.HIDDEN)
            .attrName("id")
            .<ContactEditModel>dynamic((input, model) -> 
                    input 
                    .attrValue("%s".formatted(model.id()))
            )

        .__();
        // spotless:on
    }
....

This is one of the cool thing of this library, we can compose views. What I want to discuss is, should the documentation push the user in using the dynamic block on the lower position. What I assume to be a side benefit is the fact that the cache of the view is better used with the way of using htmlFlow.

fmcarvalho commented 4 days ago
  1. The dynamic method is available only in HtmlView.
  2. Only instances of HtmlView maintain an internal cache of static HTML blocks.
  3. Both HtmlView and HtmlDoc inherit from HtmlPage, but the latter does not perform pre-encoding or maintain an internal cache.

In terms of functionality, HtmlView and HtmlDoc offer the same features. You can build identical views, fragments, partials, or layouts with either approach. The main difference lies in the pre-encoding performed by HtmlView. For this reason, all examples at htmlflow.org/features include side-by-side comparisons of both approaches.

On the other hand, HtmlDoc has fewer requirements, as it can be used in a chainable manner with the of() scope method. Due to the internal caching mechanism, using partials or fragments with HtmlView comes with certain constraints. To address this, there is a dedicated section about the use of HtmlView and fragments: Layout and Partial Views (Fragments).

In contrast, fragments used with HtmlDoc do not have these issues.

benjamin-dreux commented 3 days ago

That's precisely the documentation with which I'm still unsure if I'm dooing the right thing.

In the exemple I provided, yes I'm using of builder to include a other block, but since the second block include a dynamic block, I assume that there is no performance penalty.

Given my basic testing it seems I get it correctly. If so, what I would like to see / propose is a more basic explanation of how to compose function call in order to build a view from the tinyest function up to a complete page.

I what to state loud and clear that If i'm using html flow the right way on this, I was after that kind of composition for a LOOOOONG time. In fact since I've toyed for the first time with react back in 2013, but in a java template environement.

fmcarvalho commented 2 days ago

1. Spring Petclinic Example

Have you already checked out https://github.com/xmlet/spring-petclinic?

I was expecting that the sample implementation of the Spring Petclinic using HtmlFlow would provide enough examples for most use cases when building a web app in Spring MVC with HtmlFlow, specifically regarding layouts, fragments, data binding, and similar features.


2. Use of dynamic

When you stated: "since the second block includes a dynamic block, I assume there is no performance penalty." — this statement is not entirely accurate. The concern here is not performance, but caching.

The purpose of dynamic is to prevent HtmlView from caching the dynamic parts of a web template. Since the output of these parts depends on the provided model, you cannot reuse the resulting HTML for subsequent renderings. Additionally, dynamic grants access to the model instance.

Your example is correct, but to better understand the role of dynamic, consider an alternative approach to achieve the same output in your example.

Imagine modifying your template function by moving the .of(ContactEditView::hiddenInputId) call into the previous dynamic block, like this:

.<ContactEditModel>dynamic((form, model) -> form
        .attrAction(format("/contacts/%s/update", model.id()))
        .of(ContactEditView::hiddenInputId)
)

This will compile without errors but result in a runtime exception:
IllegalStateException: You are already in a dynamic block! Do not use dynamic() chained inside another dynamic!

This occurs because the hiddenInputId also has a dynamic block and is being called from within other dynamic scope. Nested dynamic scopes are not allowed.

In this scenario, you don’t need to use dynamic or of at all. You can simply adjust the hiddenInputId function to accept the model as an argument:

static void hiddenInputId(Form<?> parent, ContactEditModel model) {
    parent
        .input()
            .attrType(EnumTypeInputType.HIDDEN)
            .attrName("id")
            .attrValue("%s".formatted(model.id()))
        .__();
}

Then, in the template function, use it like this:

.<ContactEditModel>dynamic((form, model) -> form
        .attrAction(format("/contacts/%s/update", model.id()))
        .of(__ -> hiddenInputId(form, model))
)

Both solutions are correct. Your original example and this alternative are equally valid. The primary difference lies in the number of static blocks managed by HtmlFlow. Your example has 3 static HTML blocks intertwined with 2 dynamic blocks, whereas my alternative proposal has 2 static HTML blocks with 1 dynamic block.

To help you better understand the rationale behind dynamic, I’ve created a gist with some images that illustrate this concept. Check it here: https://gist.github.com/fmcarvalho/c75091698c00c55439f305efd0d923a2

Note in the 3rd image of the Gist that each dynamic block of the template turns in a BiConsumer function that will be called later during the rendering process in HtmlFlow. After the pre-processing (i.e. pre-encoding) of a template by the HtmlFlow the dynamics disappear and are turned in continuations. These continuations are executed during rendering and cannot include additional dynamic calls. This is why nested dynamic blocks are not supported in this approach.

benjamin-dreux commented 1 day ago

We’re are getting close to what I’m looking for.

Your last exemple is what I started doing at first to limit the number of dynamic blocks, mostly because I’m lazy.

but the I thought that what I really want is cache as mouche possible of my template, and keep the least possible content inside my dynamic blocks.

I didn’t conduct any performance testing on this subject. Do you know which approche is more efficient and why?

if in such a case there is a clear benefit for one or the other that’s what I would like to see more proéminent on the project’s website.