Search code examples
jqueryasp.net-mvc-3domencapsulationreusability

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


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.


Solution

  • 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.