Search code examples
c#htmlasp.net-corerazorrazor-pages

How to have OnPostAsync work on a PartialView rendered on my Layout?


I have a layout (_Layout.cshtml) that i want to render a view (Feedback.cshtml) onto as a seperate page alongside the RenderBody(). The view itself will come off from the right hand side of the layout, using a button to toggle it paning to and from the right. On that view there is a form for feedback. Below is example code for the files mentioned:

Layout.cshtml

<div class="feedback">
    @await Html.PartialRender("Feedback", new FeedbackModel())
</div>
<div class="content">
    @RenderBody()
</div>

Feedback.cshtml

@model FeedbackModel
<div id="offcanvas formContainer">
  <form id="feedbackForm" method="post">
    <!-- Form fields -->
    <button type="submit">Submit</button>
  </form>
</div>

Feedback.cshtml.cs

public class FeedbackModel : PageModel
{
    <!-- Properties and fields --!>

    public async Task<IActionResult> OnPostAsync()
    {
        <!-- Transform data, Add data to DB via repository methods --!>
        return Page();
    }
}

Everytime I click the "Submit" button on the Feedback page, it does nothing. It doesn't call the post method at all. If I append a asp-page-hander="@Model.OnPostAsync()" on the form or an onclick="@Model.OnPostAsync()" on the button - then before the layout even fully loads, it fires the OnPostAsync() method immediately, without me even opening the partial view on the page or clicking the button. Can anyone suggest a solution to this?

I thought this logic could instead be moved to a ViewComponent but I am not sure how I would be able to do OnPostAsync() when the only supplied method is InvokeAsync().


Solution

  • So here's one way. Create a Razor page called Feedback (as you have done) containing the form. Here's the PageModel:

    public class FeedbackModel : PageModel
    {
        [BindProperty]
        public string Comments { get; set; }
        [BindProperty]
        public string Email { get; set; }
        public void OnGet()
        {
        }
    
        public void OnPost() 
        { 
            // process the feedback        
        }
    }
    

    And here's the view:

    @page
    @model WebApplication3.Pages.FeedbackModel
    @{
        Layout = null;
    }
    <h3 class="fw-light text-muted">Provide your feedback!</h3>
    <form id="feedbackForm" method="post">
        <div class="form-group mb-3">
            <label asp-for="Email" class="control-label"></label>
            <input asp-for="Email" class="form-control" />
        </div>
        <div class="form-group mb-3">
            <label asp-for="Comments" class="control-label"></label>
            <textarea asp-for="Comments" class="form-control"></textarea>
        </div>
        <button type="button" class="btn btn-sm btn-success submit" data-bs-dismiss="offcanvas">Submit</button>
    </form>
    

    Note that the Layout is set to null. We only want the form HTML. Also, the submit button is type="button" so that the form is NOT submitted when it is clicked. In addition, it is wired up with a data-bs-dismiss attribute so that it closes the offcanvas when clicked.

    Add the offcanvas HTML to the Layout page:

    <div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight">
        <div class="offcanvas-body"></div>
    </div>
    

    Along with a button to open it:

    <button type="button" class="btn btn-primary feedback" data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight">Feedback</button>
    

    Finally, add the following script to the Layout page that fetches the HTML for the form when it is clicked and includes it in the offcanvas. It also wires up a click event handler to the non-submit button that posts the content of the form to the OnPost handler of the Feedback page:

    <script>
        const feedbackButton = document.querySelector('button.feedback');
        const offCanvas = document.querySelector('#offcanvasRight .offcanvas-body');
        feedbackButton.addEventListener('click', _=>{
            fetch(`/feedback`).then(response => response.text()).then(html => {
                offCanvas.innerHTML = html;
                const form = offCanvas.querySelector('#feedbackForm');
                const submit = form.querySelector('button.submit');
                submit.addEventListener('click', _=>{
                    const formData = new FormData(form);
                    fetch(`/feedback`, {
                        method:'post',
                        body:new URLSearchParams(formData)
                    });
                })
            })
        })
    </script>