MVC 3, reuse of partial views and jquery, without conflicting the DOM
Asked Answered
S

3

12

As i am still new to MVC 3 and jquery, i would like to know a best practice solution to how the following can be solved:

I have a view, where I use jquery ajax to fetch and display a partial view with some product details for product A. The loaded partial view consist of a bunch of html and jquery code, which is tied to the defined id's within the partial view.

Thus, i would like to reuse the same partial view to show details from other products on the same View (e.g. show product B details in a pop-up dialog). Whenever the pop-up is shown, the newly fetched partial view will conflict with the partial view for product A, as the same id's are used in the html.

Conceptual overview of the case

Is there a way to encapsulate the html and javascript in the partial view, and reuse it several pages without worry about any conflicts with ID's and stuff?

I hope my question makes sense. Thanks,

/Nima

UPDATED

Here is some pseudo code, outlining my issue:

VIEW

<script type="text/javascript">
$(document).ready(function () {

    $('.productItems').click(function () {
        var input = { productId: $(this).attr('data-productID') };
        var url = url = '<%: Url.Content("~/ProductDetails/ShowProductDetails") %>';


        // Show the modal box with product details
        $('#dialogBox').dialog({
            title: $(this).attr('data-productTitle')
        });


        // Fetch content in the background
        $.get(url, input, function (result, response) {
            $('#dialogBox').html(result);
            });
    });
});
</script>


<div id="detailsArea">
    <% Html.RenderPartial("ProductDetails", Model.Product); %>
</div>

<div id="productLinks">
  <span class="productItems" data-productID="123">Product B</a>
</div>

<div id="dialogBox" style="display: none;"></div>

Controller -> Action (ShowProductDetails)

public ActionResult ShowProductDetails(int productId)
{
  // Get product from db. and return the partial view

  return PartialView("ProductDetails", p);
}

Partial View (ProductDetails)

<script type="text/javascript">

   function SetProductTabContent(selectedTab) {
        $("#productDescriptionContent > div").css('display', 'none');

        switch (selectedTab) {

            case '#tab-1':
                $('#productDescriptionText').css('display', 'block');
                break;

            case '#tab-2':
                $('#productSpecificationText').css('display', 'block');
                break;   
        }


$(document).ready(function () {
    // Get all the menu items
    var menuItems = $("#productMenu a");

    // Select the first tab as default
    menuItems.first().addClass("menuItemActive");

    // Handle the look of the tabs, when user selects one. 
    menuItems.click(function () {

        var item = $(this);

        // Get content for the selected tab
        SetProductTabContent(item.attr('href'));

        menuItems.removeClass("menuItemActive");
        item.addClass("menuItemActive");
        return false;
    });
});
</script>


<div id="productMenu" style="">
    <a href="#tab-1">
        <div class="menuItemHeader">Menu1</div>
    </a>
    <a href="#tab-2">
        <div class="menuItemHeader">Menu2 </div>
    </a>
</div>


<div id="productDescriptionContent">

        <div id="productDescriptionText" style="display: none;">
            <%: Model.Product.Description %>
        </div>
        <div id="productSpecificationText" style="display: none;">
            <%: Model.Product.Description2%>
        </div>
</div>

ISSUE When the partial view gets loaded twice in the DOM, the divs conflicts.

Steel answered 6/8, 2011 at 12:43 Comment(1)
What about showing the content of the dialogbox in an iframe?Steel
U
8

Yes. As you pointed out, do not use ids and id selectors in your JavaScript. Instead use class selectors:

E.g., in your view's markup:

<div class="container">Partial View content</div>

JS:

var $div = $('div.container');
// do something

To eliminate possibility of selecting other tags with same class name, assign a programmatic name the elements in partial view which is used only as a selector handle and not as a CSS class.

While ID based lookup is the best performance wise, in this case, it makes more sense to go by the [tag+class] based lookup to avoid id conflicts. [tag+class] based lookup comes pretty close to id selectors in terms of performance.

Also, you can gain further improvement by limiting the lookup scope:

<div class="container">Partial View content <span class="child">Child content </span></div>

var $span = $(span.child')  // scope of lookup here is entire document

However, if you know that child is inside container div, you can limit the scope by saying:

var $div = $('div.container').children('span.child'); // or just '.child'

Another tip is to do the lookup once and reuse it:

// less performant
function doSomething() {

    // do something here
    $('div.container').css('color', 'red');

    // do other things
    $('div.container').find('.child');

   // do more things
    $('div.container').click(function() {...});
}


// better
function doSomething() {
    var $div = $('div.container');

    // do something here
    $div.css('color', 'red');

    // do other things
    $div.find('.child');

   // do more things
    $div.click(function() {...});

   // or chaining them when appropriate
   $('div.container').css('color', 'red').click(function() { ... });


}

Update: Refactoring OP's post to demo the concept:

<script type="text/javascript">

       function SetProductTabContent(selectedTab, ctx) {
            var $container = $("div.pv_productDescriptionContent", ctx);

            // this will find only the immediate child (as you had shown with '>' selector)
            $container.children('div').css('display', 'none');  

            switch (selectedTab) {

                case '#tab-1':
                    $('div.pv_productDescriptionText', $container).css('display', 'block');
                    // or $container.children('div.pv_productDescriptionText').css('display', 'block');
                    break;

                case '#tab-2':
                    $('div.pv_productSpecificationText', $container).css('display', 'block');
                    // or $container.children('div.pv_productSpecificationText').css('display', 'block');
                    break;   
            }


    function SetUpMenuItems(ctx) {
        // Get all the menu items within the passed in context (parent element)
        var menuItems = $("div.pv_productMenu a", ctx);

        // Select the first tab as default
        menuItems.first().addClass("menuItemActive");

        // Handle the look of the tabs, when user selects one. 
        menuItems.click(function () {

            var item = $(this);

            // Get content for the selected tab
            SetProductTabContent(item.attr('href'), ctx);

            menuItems.removeClass("menuItemActive");
            item.addClass("menuItemActive");
            return false;
        });
    }
    </script>


<div style="" class="pv_productMenu">
    <a href="#tab-1">
        <div class="menuItemHeader">
            Menu1</div>
    </a><a href="#tab-2">
        <div class="menuItemHeader">
            Menu2
        </div>
    </a>
</div>
<div class="pv_productDescriptionContent">
    <div class="pv_productDescriptionText" style="display: none;">
        <%: Model.Product.Description %>
    </div>
    <div class="pv_productSpecificationText" style="display: none;">
        <%: Model.Product.Description2%>
    </div>
</div>

Note: I removed document.ready wrapper since that will not fire when you load the partial view. Instead, I refactored your View's JS to call the setup function and also pass in the scope (which will avoid selecting other divs with same class):

// Fetch content in the background
$.get(url, input, function (result, response) {
       $('#dialogBox').html(result);
       SetUpMenuItems($('#dialogBox'));   
});

Obviously, you can modify this further as you deem fit in your app, what I've shown is an idea and not the final solution.

  • If you load #dialog again, they will overwrite existing markup, hence there won't be duplicate.
  • If you load the partial view again in some other container, you can pass that as the context and that will prevent you accessing the children of #dialog
  • I came up with this arbitrary prefix pv_ for programmatic class handles. That way, you can tell looking at the class name if it is for CSS or for use in your script.
Underbodice answered 6/8, 2011 at 12:58 Comment(11)
the problem with class selectors is, that if I e.g. do some action on product B (in the pop-up), the same will occur on Product A. Those two should not depend on each other.Steel
Try using a data-* HTML5 style and select those based on your new attribute. Lik data-product="1"Arguelles
That is where scope lookup helps. You can lookup within a certain scope. Further, you can name your selectors such that they will be unique: class="partial_view_main_container". This doesn't have to be a css class that you would use for styling, but just for looking up your partial view's content.Underbodice
I still dont see how your solution will work. I have added some code snippet, hope it makes my question more clear. The answer from @Miroprocessor makes sense, as using the ids part of the html. But i would like to know if there is a better solution, as you write the issue with using ids as part of the html.Steel
I've successfully used it in a large scale project. If you can post some markup for your partial view and sample JS showing how you would access the elements, I can show it to you.Underbodice
I have updated the code sample for my partialView. Let me know, if you need more information. I am looking forward to see your solution. I have started to implement the combined html id, and it starts to look a bit messy...Steel
Updated my answer, take a look and let me know if you have any questions.Underbodice
It works perfectly!! Though i have some questions: Can you elaborate on your note about the document.ready wrapper is not fired in partial views? Does this mean it only fires first time the partial view is rendered (if you see my view code)? In addition, how would you call "SetupMenuItems" the first time the partial view is rendered? You know, when you use: <% Html.RenderPartial("ProductDetails", Model.Product); %>?Steel
document.ready is called when the DOM load is complete, which is when your view gets rendered. A ajax load which appends html to an existing div doesn't trigger the same. You can wrap SetupMenuItems in a anonymous function and invoke it immediately: (function(){ /* SetupMenuItems code goes here */})();. However, passing ctx might be tricky. You can try (function(ctx){ /* SetupMenuItems code goes here */})($(this).closest('div')); as the ctx but not sure if this will work.Underbodice
Actually, the document.ready seems to run (again) after the ajax load appends the html into the DOM. Thus, i be calling SetupMenuItems from document.ready, even though it gets triggered each time the ajax call fetches the partial view. But i love the solution of passing along ctx, it is a very clean solution compared to the combined product and html div id. Thanks alot, i really appreciate it!Steel
Good to know that! Glad it helped!Underbodice
M
0

Simplest way to do that is, make ids of products as part of html tags ids

something like that

<input type="text" id="txt_<%=Model.ID%>"> 

if you use Razor

<input type="text" id="[email protected]"> 
Mammy answered 6/8, 2011 at 12:59 Comment(4)
This is a messy approach in the long term. Few examples: 1) Onus is on you to generate unique ids everytime (so between requests, you need to keep a tab of it somehow). 2) You need to inject this somehow in yuor javascript (unless you're writing inline scripts) also. 3) Your javscript cannot be unit tested as it depends on dynamically geberated ids.Underbodice
Well, it actually makes sense to include the id in the html. But I all the points made by @Underbodice also makes sense. But is this really the way to do it?Steel
@Steel it is just the simplest way to prevent the conflicts between divs when the partial view loaded twice, but there are many other ways to do the samethingMammy
Although its not mentioned in the question, if you are using MVC's built in databinding (eg Html.TextboxFor()), manually setting the id will break the returning databinding.Addington
I
0

I'm surprised this hasn't come up more often. I guess most developers aren't creating their own controls with partials. Here is something that I came with that is works pretty well and not that hard to implement in MVC4.

First I tried passing a dynamic model to the partial and that doesn't work. (sad face) Then I went the typed partial view route. I created a collection of objects called steps (because build a wizard control).

public class WizardStep
{
    public string Id { get; set; }
    public string Class { get; set; }
}

public class WizardSteps
{

    public WizardSteps()
    {
        Steps = new List<WizardStep>();
        Steps.Add(new WizardStep() {Id = "Step1"});
        Steps.Add(new WizardStep() { Id = "Step2" });
        Steps.Add(new WizardStep() { Id = "Step3" });
        Steps.Add(new WizardStep() { Id = "Step4" });
        Steps.Add(new WizardStep() { Id = "Step5" });

    }
    public List<WizardStep> Steps { get; set; }

}

Razor code looks like this:

@Html.Partial("_WizardButtonPanel", @Model.WizardSteps.Steps.First())

or

@Html.Partial("_WizardButtonPanel", @Model.WizardSteps.Steps.Skip(1).First() )

or

@Html.Partial("_WizardButtonPanel", @Model.WizardSteps.Steps.Skip(2).First())

and more

@Html.Partial("_WizardButtonPanel",@Model.WizardSteps.Steps.Skip(3).First())

The partial view looks something like this:

@model SomeProject.Models.WizardStep
<div id="[email protected]" >
<a id="[email protected]" >Somelinke</a>
</div>

Happy coding...

Investment answered 9/8, 2013 at 21:20 Comment(1)
BTW looking using class selector will cause issues in a wizard type control.Investment

© 2022 - 2024 — McMap. All rights reserved.