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.

Nima
  • 937
  • 1
  • 13
  • 20

3 Answers3

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.
BenMorel
  • 34,448
  • 50
  • 182
  • 322
Mrchief
  • 75,126
  • 20
  • 142
  • 189
  • 1
    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. – Nima Aug 06 '11 at 13:00
  • Try using a data-* HTML5 style and select those based on your new attribute. Lik data-product="1" – turtlepick Aug 06 '11 at 13:09
  • 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. – Mrchief Aug 06 '11 at 13:10
  • 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. – Nima Aug 06 '11 at 14:37
  • 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. – Mrchief Aug 06 '11 at 16:49
  • 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... – Nima Aug 06 '11 at 17:02
  • Updated my answer, take a look and let me know if you have any questions. – Mrchief Aug 06 '11 at 17:35
  • 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); %>? – Nima Aug 06 '11 at 18:55
  • `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. – Mrchief Aug 06 '11 at 19:05
  • 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! – Nima Aug 06 '11 at 22:41
  • Good to know that! Glad it helped! – Mrchief Aug 06 '11 at 22:47
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="buttonpanel-@Model.Id" >
<a id="link-@Model.Id" >Somelinke</a>
</div>

Happy coding...

Tim
  • 591
  • 5
  • 10
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="txt_@Model.ID"> 
Amir Ismail
  • 3,865
  • 3
  • 20
  • 33
  • 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. – Mrchief Aug 06 '11 at 13:33
  • Well, it actually makes sense to include the id in the html. But I all the points made by @Mrchief also makes sense. But is this really the way to do it? – Nima Aug 06 '11 at 14:38
  • @Nima 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 samething – Amir Ismail Aug 07 '11 at 07:58
  • 1
    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. – Arkiliknam Sep 25 '12 at 08:57