Kentico / xperience-training-13

Kentico Xperience 13 training website
MIT License
26 stars 28 forks source link

Question about submitting forms on pages that use the generic "PageViewModel" #112

Open Clink50 opened 1 year ago

Clink50 commented 1 year ago

On our Home Page, we have the header, footer, metadata etc, which is all handled through the PageViewModel, but let's say that the page has a form on it that takes your name and age and a button to submit the form. If you leave the name blank, an validation error message should display that says that it's required. If the Home page has a model like @model PageViewModel<MyFormViewModel> when I submit the form, I not only have to bind the values for the name and age, but I also would have to bind all the values for PageViewModel, right? Because when the !ModelState.IsValid I have to return View("Index", model) where model has to be the entire PageViewModel.

Hopefully this makes sense, but basically I'm asking, if I have content that is managed by our marketing team and it needs to display on the Home page as well as have a form, how am I supposed to handle the form request and submission through MVC? I could do it all through JS, but I feel that defeats the purpose.

seangwright commented 1 year ago

The forms submit via javascript, so any rendered state on the page is persisted while the form submits. The form validation logic on the backend will then send a response with the re-rendered fields and validation messages, which is patched into the DOM.

Submission Request (XHR)

image

Submission Response (HTML)

image

Clink50 commented 1 year ago

Thanks for the quick response! So it's submitted through javascript then, I see. I was hoping to use the MVC way to submit the forms. I think I've almost gotten it, but would like your feedback to see if I should continue this way, or just use javascript.

So the model is @model PageViewModel<MyFormViewModel>, and I have a form tag with an action of /Home/Submit. In the Submit action, I take in the PageViewModel<MyFormViewModel> as a parameter, and check for !ModelState.IsValid. When that the model is invalid, I get the pageDataContext.Metadata from the TryRetrieve call and then call GetPageViewModel with the metadata and the MyFormViewModel. And finally return View("Index", viewModel);

[HttpPost]
public IActionResult Submit(PageViewModel<MyFormViewModel> model)
{
    if (!ModelState.IsValid) 
    {
        return DefaultViewWithErrorMessage(model.Data);
    }

    // ...success logic
}

private IActionResult DefaultViewWithErrorMessage(MyFormViewModel model)
{
    if (_pageDataContextRetriever.TryRetrieve<CMS.DocumentEngine.Types.Website.MyBasePage>(out var pageDataContext)
        && pageDataContext.Page != null)
    {
        var myModel = GetPageViewModel(pageDataContext.Metadata, model);

        return View(nameof(this.Index), myModel);
    }

    // ...do something if it can't retrieve the page data context
}
seangwright commented 1 year ago

Any forms created through the Form Builder feature will auto submit with JavaScript and this is by-design.

If you create your own custom MVC forms - like for login/register or something more complex than what the Form Builder can handle - you can handle submitting them however you want.

The two most common approaches:

  1. Use the PRG (POST-REDIRECT-GET) pattern using TempData
  2. Submit via JS using

PRG can require a lot of mechanical pieces to set up with MVC (it's a little easier with Razor Pages), but if you want a solution that doesn't require JavaScript, it's probably your best option - and it works with Xperience's Page Builder.

If you are ok requiring JavaScript, then the HTMX route is my favorite - it's very elegant, doesn't require any custom JS (just use the HTMX JS and .NET libraries), and is much lighter weight than jQuery's solution.

Clink50 commented 1 year ago

For this particular case, it is a custom form that's on our Home page.

We basically took the MedioClinic solution as a starter project for ours, and we are building our site on top of that. So I was trying to get a better understanding on how the Form submission works when you have to return not just a MyFormViewModel, but the entire PageViewModel<MyFormViewModel>.

I'll keep researching and working on the direction I'm going with for now just to see if I can get it to work, but I'll also try it out the JS way and pick the best option.

I would to try out HTMX, but we won't be able to use it for this project.

seangwright commented 1 year ago

Here's a code example of how the PRG architecture is implemented in an KX13 project.

There's a couple interacting pieces that you'll need to add to your project to get the full functionality, but you can see how it all comes together in the RegistrationController which uses this pattern.

KenticoDevTrev commented 1 year ago

Although you can copy it in, you can also just reference https://www.nuget.org/packages/XperienceCommunity.Baseline.Core.RCL/1.1.2

It's kentico agnostic and includes some core tools that you can leverage, including this.

Totally understand if you just want to grab the individual files though:

https://github.com/KenticoDevTrev/XperienceCommunity.Baseline/tree/master/src/Core/Core.RCL/Services https://github.com/KenticoDevTrev/XperienceCommunity.Baseline/tree/master/src/Core/Core.RCL/Attributes https://github.com/KenticoDevTrev/XperienceCommunity.Baseline/tree/master/src/Core/Core.RCL/Components/ImportModelState https://github.com/KenticoDevTrev/XperienceCommunity.Baseline/tree/master/src/Core/Core.RCL/Helpers