[KULRICE-8862] Inner Views Created: 31/Jan/13  Updated: 16/Jan/15

Status: Open
Project: Kuali Rice Development
Component/s: Development, Roadmap, User Experience (UX)
Affects Version/s: 2.2
Fix Version/s: Backlog
Security Level: Public (Public: Anyone can view)

Type: Task Priority: Critical
Reporter: William Washington (Inactive) Assignee: Unassigned
Resolution: Unresolved Votes: 0
Labels: Old
Remaining Estimate: 3 weeks
Time Spent: Not Specified
Original Estimate: 3 weeks

Attachments: Text File kulrice-8862-inner_view_refresh_support.patch     Text File kulrice-8862.patch     Text File kulrice-8862.patch     JPEG File multiple inner views in iGPS.jpg    
Issue Links:
Relate
relates to KRRM-141 KRAD Phase 3 - Complete core features... Resolved
is related to KSAP-59 Add UTF-8 encoding to dynamic forms Closed
Rely
is relied upon by KSAP-265 Await Rice Upgrade: Replace Overrides... Open
Similar issues:
KULRICE-10396Extending a bean that has ids on inner beans causes issues, need to define id on inner beans with component id property
KULRICE-6831Inner scroll bar on firefox
KULRICE-7639Edit, Copy, or View Agenda in sampleapp yields stacktrace
KULRICE-2998Recreate the KIM views in the Rice database
KULRICE-3339Facilitate KEW method for using Principal Id in an Inner Application Role (see NetworkIdRoleAttribute class)
KULRICE-13147Inefficiencies in view lifecycle and rendering
KULRICE-5981Breadcrumbs with single page views
KULRICE-7873Optionally supress the view in breadcrumbs
KULRICE-8556Progressive disclosure of group/fields within a checkbox or radio group - verify functionality
KULRICE-6383Refactor view template
Epic Link: Components
Rice Module:
KRAD
KRAD Feature Area:
Document
Application Requirement:
KS
KAI Review Status: Not Required
KTI Review Status: Not Required

 Description   

"The InnerView component allows a page or a component from another view to be rendered with the view that contains the component. This is useful for the following reasons:

1. Content that is reused across many pages can be built in one view and reused. Prevents from having to include the data and controller methods for each view that needs it.
2. Creation of views can be broken into multiple efforts of form/controllers/view. Valuable for team work.Note there is a restriction that the inner view have the same theme as the parent view. That is the same set of CSS and JavaScript files. If the inner view contains additional CSS or JavaScript files they will be ignored, which might cause the view to malfunction."



 Comments   
Comment by Mark Fyffe (Inactive) [ 05/Sep/13 ]

Attaching a patch based on Rice 2.3.0-M3 for supporting inner views in KSAP 0.7. The way KSAP implements pop-up dialogs, a "popupForm" element is defined dynamically with an AJAX response then attached to a jquery-popover element. The content of the form is intended to be the full contents of the "kualiForm" <form> element on the dialog view to be rendered within the pop-up, but not the <form> element itself.

As of Rice 2.3.0-M3 (I haven't yet checked later versions), there is no <div> wrapper providing clean and convenient access within the response page to all of the form's inner elements. Therefore, hidden inputs needed in order to point the inner view at a different controller and form from the page containing the popover are lost.

This patch adds an "innerViewWrapperId" property to the PageGroup component, then when this property is set on the currentPage, renders a div with the provided ID directly inside the <form> element facilitating the use of a Uif-FormView child with multiple pages to drive pop-up dialogs on an unassociated page.

For reference, the openPopup() and ksapAjaxSubmitForm() JS functions from KSAP 0.7 are below. These functions were taken directly from UW My Plan 1.5, and have only be modified from the UW version to deal with HTTP errors a little differently. The value passed in the getId parameter of the openPopup() function corresponds to the innerViewWrapperId property on the page component of the inner view.

/**
 * Open a popup which loads via ajax a separate view's component
 *
 * @param getId - Id of the component from the separate view to select to insert into popup.
 * @param retrieveData - Object of data used to passed to generate the separate view.
 * @param formAction - The action param of the popup inner form.
 * @param popupStyle - Object of css styling to apply to the initial inner div of the popup (will be replaced with remote component)
 * @param popupOptions - Object of settings to pass to the Bubble Popup jQuery Plugin.
 * @param e - An object containing data that will be passed to the event handler.
 */
function openPopup(getId, retrieveData, formAction, popupStyle, popupOptions, e) {
    stopEvent(e);
    fnCloseAllPopups();

    var popupOptionsDefault = {
        themePath:"../ks-myplan/jquery-popover/jquerypopover-theme/",
        manageMouseEvents:true,
        selectable:true,
        tail:{align:"middle", hidden:false},
        position:"left",
        align:"center",
        alwaysVisible:false,
        themeMargins:{total:"20px", difference:"5px"},
        themeName:"myplan",
        distance:"0px",
        openingSpeed:5,
        closingSpeed:5
    };

    var target = (e.currentTarget) ? e.currentTarget : e.srcElement;
    var popupItem = (typeof popupOptions.selector == "undefined") ? jQuery(target) : jQuery(target).parents(popupOptions.selector);

    if (!popupItem.HasPopOver()) popupItem.CreatePopOver({manageMouseEvents:false});
    var popupSettings = jQuery.extend(popupOptionsDefault, popupOptions);
    var popupHtml = jQuery('<div />').attr("id", "KSAP-Popover");
    if (popupStyle) {
        jQuery.each(popupStyle, function (property, value) {
            popupHtml.css(property, value);
        });
    }
    popupSettings.innerHtml = popupHtml.wrap("<div>").parent().clone().html();

    popupItem.ShowPopOver(popupSettings, false);
    popupItem.FreezePopOver();

    var popupId = popupItem.GetPopOverID();

    fnPositionPopUp(popupId);
    clickOutsidePopOver(popupId, popupItem);

    var retrieveForm = '<form id="retrieveForm" action="' + retrieveData.action + '" method="post" />'
    jQuery("body").append(retrieveForm);

    var elementToBlock = jQuery("#KSAP-Popover");

    var successCallback = function (htmlContent) {
        var component;
        if (jQuery("#requestStatus", htmlContent).length <= 0) {
            var popupForm = jQuery('<form />').attr("id", "popupForm").attr("action", formAction).attr("method", "post");
            component = jQuery("#" + getId, htmlContent).wrap(popupForm).parent();
        } else {
            var pageId = jQuery("#pageId").val();
            eval(jQuery("input[data-role='script'][data-for='" + pageId + "']", htmlContent).val().replace("#" + pageId, "body"));
            var errorMessage = '<img src="/student/ks-myplan/images/pixel.gif" alt="" class="icon"><div class="message">' + jQuery("#plan_item_action_response_page", htmlContent).data(kradVariables.VALIDATION_MESSAGES).serverErrors[0] + '</div>';
            component = jQuery("<div />").addClass("myplan-feedback error").html(errorMessage);
        }
        if (jQuery("#KSAP-Popover").length) {
            popupItem.SetPopOverInnerHtml(component);
            fnPositionPopUp(popupId);
            if (popupOptions.close || typeof popupOptions.close === 'undefined') jQuery("#" + popupId + " .jquerypopover-innerHtml").append('<img src="../ks-myplan/images/btnClose.png" class="myplan-popup-close"/>');
            jQuery("#" + popupId + " img.myplan-popup-close").on('click', function () {
                popupItem.HidePopOver();
                fnCloseAllPopups();
            });
        }
        runHiddenScripts(getId);
        elementToBlock.unblock();
    };

    ksapAjaxSubmitForm(retrieveData, successCallback, elementToBlock, "retrieveForm");
    jQuery("form#retrieveForm").remove();
}

/**
 *   Gathers information for submission to the controller via ajax
 *
 * @param data - Variables and data to be submitted to the controller
 * @param successCallback - Code block to run after a successful return from the controller
 * @param elementToBlock - The html object being effected by the controller call
 * @param formId - Id of the form the submit is being called on
 * @param blockingSettings - Settings for the html object
 */
function ksapAjaxSubmitForm(data, successCallback, elementToBlock, formId, blockingSettings) {
	data = ksapAdditionalFormData(data);

    var submitOptions = {
        data:data,
        success:function (response) {
            var tempDiv = document.createElement('div');
            tempDiv.innerHTML = response;
            var hasError = checkForIncidentReport(response);
            if (!hasError) successCallback(tempDiv);
            jQuery("#formComplete").empty();
        },
        error:function(jqXHR, textStatus,
                errorThrown) {
	         hideLoading();
	         showGrowl(textStatus + " "
	             + errorThrown,
	             "Error");
	     },
	     statusCode : {
	         400 : function() {
	             showGrowl(
	                 "400 Bad Request",
	                 "Fatal Error");
	         },
	         500 : function() {
	             showGrowl(
	                 "500 Internal Server Error",
	                 "Fatal Error");
	         }
	     }
    };

    if (elementToBlock != null && elementToBlock.length) {
        var elementBlockingOptions = {
            beforeSend:function () {
                if (elementToBlock.hasClass("unrendered")) {
                    elementToBlock.append('<img src="' + getConfigParam("kradImageLocation") + 'loader.gif" alt="Loading..." /> Loading...');
                    elementToBlock.show();
                }
                else {
                    var elementBlockingDefaults = {
                        baseZ:500,
                        message:'<img src="../ks-myplan/images/ajaxLoader16.gif" alt="loading..." />',
                        fadeIn:0,
                        fadeOut:0,
                        overlayCSS:{
                            backgroundColor:'#fff',
                            opacity:0
                        },
                        css:{
                            border:'none',
                            width:'16px',
                            top:'0px',
                            left:'0px'
                        }
                    };
                    elementToBlock.block(jQuery.extend(elementBlockingDefaults, blockingSettings));
                }
            },
            complete:function () {
                elementToBlock.unblock();
            },
            error:function(jqXHR, textStatus,
                    errorThrown) {
	   	         hideLoading();
		         showGrowl(textStatus + " "
		             + errorThrown,
		             "Error");
                if (elementToBlock.hasClass("unrendered")) {
                    elementToBlock.hide();
                }
                else {
                    elementToBlock.unblock();
                }
            }
        };
    }
    jQuery.extend(submitOptions, elementBlockingOptions);
    var form = jQuery("#" + ((formId) ? formId : "kualiForm"));
    form.ajaxSubmit(submitOptions);
}

I hope this helps as a starting point for implementing this feature!

Best wishes,
Mark

Comment by Mark Fyffe (Inactive) [ 17/Sep/13 ]

Note that the openPopup method quoted in a previous comment on this issue has been modified to enforce UTF-8 encoding on dynamically created the popup form.

If the inner view functionality in KRAD will include a standard means for popup forms based on the original KSAP code, it will be important to include the accept-charset attribute on the form.

Comment by Mark Fyffe (Inactive) [ 05/Nov/13 ]

Added another patch used by the IU Roadmap project to support dynamic page/component refreshes when posting from an inner view rather than the main view on the page.

This page adds a "formName" attribute to the KRAD Request object, and uses that attribute instead of the hard-coded "kualiForm" to identify the form to post on the page. We have also added formName as a attribute to the retrieveComponent() function in krad.actions.js

Hope this patch helps in the effort to arrive at a final solution for this issue!

Comment by Mark Fyffe (Inactive) [ 05/Nov/13 ]

Updated latest patch to support update-page as well as update-component.

Comment by Larry Symms [ 24/Apr/14 ]

Would like to pull this off the backlog for 2.5 or 2.6. Should be an easy fix given the patches from Mark.

Comment by Mark Fyffe (Inactive) [ 03/May/14 ]

We have been working to update the script side of inner view for the Degree Map project at IU, and have arrived at a solution that works in a more flexible fashion than the original My Plan code. This was developed on our Rice 2.3 version, and we'll be moving it to Rice 2.4 as a KRAD patch in the near future.

The inner view wrapper patch is no longer required, but the request/response patch is. We have not yet tested with update-component or update-page functions, but will cover that scenario as we work through the iGPS upgrade to Rice 2.4 over the next month.

The updated script for Degree Map is below. Note that this is condensed as a first pass - the full contribution will be broken down further, and include a component support on the Java side.

A few other key points regarding this solution:

  • Scripts from myplan.widgets.js have been deprecated, and consolidated into a single JS object: DmPopup.
    • To create an inner view popup, construct DmPopup, customize params as needed, then call open().
    • To close the popup, call close().
  • DmPopup is intended to be constructed only from the actionScript defined by a child of Uif-Action
  • Multiple inner views may exist at the same time, however...
    • In this implementation only one inner view per page ID may exist at a time.
    • While opening the second inner view with the same page ID, the first will be closed and removed.
    • The one-per-pageId restriction may be lifted through rewriting IDs on the inner view content prior to attaching to the popover (more on that below). This change is planned for the next iGPS revision.
  • Multiple inner views per "outer" view is the key element driving this revision: for Degree Map we have one inner view that pulls up a LightTable of courses then another invoked from the rows in the LightTable for pulling up a summary of the course. The summary view is also linked directly from the requirement, depending on whether or not a single course satisfies the requirement or if there is a choice. See attached screenshot.

The script defining DmPopup:

function DmPopup(innerViewId, action, data, e) {
	this.wrapperId = innerViewId + "_popup";
	this.formId = innerViewId + "_form";

	this.action = action;
	this.data = jQuery.extend(data, {
		pageId : innerViewId + "_page"
	});

	this.target = (e.currentTarget) ? e.currentTarget : e.srcElement;

	// TODO: move jquery-popover to KRAD, or switch to delivered equivalent
	this.appUrl = getConfigParam("kradUrl") + '/..';
	this.closeImageUrl = this.appUrl + '/ks-myplan/images/btnClose.png';
	this.loadingImageUrl = this.appUrl + '/ks-myplan/images/ajaxLoader16.gif';
	this.innerViewOptions.themePath = this.appUrl
			+ '/ks-myplan/jquery-popover/jquerypopover-theme/';
}

DmPopup.prototype = {
	innerViewOptions : {
		manageMouseEvents : true,
		selectable : true,
		tail : {
			align : 'middle',
			hidden : true
		},
		position : 'right',
		align : 'top',
		close : true,
		alwaysVisible : false,
		themeMargins : {
			total : '20px',
			difference : '5px'
		},
		sticky : true,
		themeName : 'transparent',
		distance : '0px',
		openingSpeed : 5,
		closingSpeed : 5,
	},

	open : function() {
		var _this = this;
		this.close();

		var target = jQuery(this.target);
		if (!target.HasPopOver())
			target.CreatePopOver({
				manageMouseEvents : false
			});

		var loadingImage = jQuery('<img />')
		loadingImage.attr('src', this.loadingImageUrl);
		loadingImage.attr('alt', 'Loading...');
		loadingImage.addClass('arm-dm-popup-loading');

		var wrapper = jQuery('<div />');
		wrapper.attr('id', this.wrapperId);
		wrapper.addClass('arm-dm-popup');
		wrapper.append(loadingImage);

		this.innerViewOptions.innerHtml = wrapper.wrap('<div>').parent()
				.clone().html();
		target.ShowPopOver(this.innerViewOptions, false);

		var submitData = ksapAdditionalFormData(this.data);
		var submitOptions = {
			data : submitData,

			success : function(response) {
				var closeImage = jQuery('<img />');
				closeImage.attr('src', _this.closeImageUrl);
				closeImage.attr('alt', 'Close');
				closeImage.addClass('arm-dm-popup-close');

				var innerViewForm = jQuery("#kualiForm", response);
				innerViewForm.attr("id", _this.formId);

				// TODO: refactor planner, then remove these workarounds
				innerViewForm.find(".myplan-view").removeClass("myplan-view");
				innerViewForm.find(".ks-Popover").removeClass("ks-Popover");

				var innerView = jQuery('<div />');
				innerView.addClass('arm-dm-popup-inner');
				innerView.append(innerViewForm);

				var innerViewWrapper = jQuery('#' + _this.wrapperId);
				innerViewWrapper.children('.arm-dm-popup-loading').remove();
				innerViewWrapper.append(innerView);
				innerViewWrapper.append(closeImage);

				jQuery('#' + _this.wrapperId + ' .arm-dm-popup-close').click(
						function() {
							_this.close();
						});

				// TODO: make KRAD not jump to the top when loading inner view
				var body = jQuery('body');
				var bodyWidth = body.width() - 50;
				var oScrollTop = body.scrollTop();
				var oScrollLeft = body.scrollLeft();

				runHiddenScripts(_this.formId);

				body.scrollTop(oScrollTop);
				body.scrollLeft(oScrollLeft);

				var innerViewOuter = jQuery('#' + target.GetPopOverID());

				var innerViewWidth = innerViewOuter.width();
				var innerViewRight = innerViewOuter.position().left
						+ innerViewWidth;

				if (innerViewRight > bodyWidth)
					innerViewOuter.css('left', (bodyWidth - innerViewWidth)
							+ 'px');

				_this.innerView = innerView;
			},

			error : function(jqXHR, textStatus, errorThrown) {
				_this.close();
			},

			statusCode : {
				400 : function() {
					showGrowl("400 Bad Request", "Fatal Error");
				},
				500 : function() {
					showGrowl("500 Internal Server Error", "Fatal Error");
				}
			}
		};

		var aform = jQuery('<form />');
		aform.attr('id', 'dm_ajax_form');
		aform.attr('action', this.action);
		aform.attr('method', 'POST');
		aform.ajaxSubmit(submitOptions);
	},

	close : function() {
		var wrapper = jQuery(this.target);
		wrapper.HidePopOver();
		wrapper.RemovePopOver();
		jQuery('#' + this.formId).parents("div.jquerypopover").remove();
		this.innerView = null;
	}

};

For reference, the action component that creates the inner view is defined below:

<bean parent="Uif-ActionLinkField" p:label="Courses"
	p:actionLabel="@\{#line.courseLink\}"
	p:actionScript="sdm_courseLink('@\{#line.refObjectId\}','@\{#line.displayTerm\}',@\{#line.singleCourse\},e)"
	p:render="@\{#line.courseLink ne null\}" />

And the function that receives the click event to create the inner view popover:

function sdm_courseLink(refid, termId, singleCourse, e) {
	var popup;
	if (singleCourse)
		popup = new DmPopup("planner_course_summary",
				getConfigParam("kradUrl")+"/../myplan/planner",
				{ methodToCall : "startDialog",
					"courseId" : refid,
					"termId" : termId
				}, e);
	else
		popup = new DmPopup("sdm_coursegrid", "coursegrid",
				{ methodToCall : "startDialog",
					"placeholderId" : refid,
					"termId" : termId
				}, e);
	stopEvent(e);
	popup.open();
}

For full lifecycle operation, there will be some notable steps that need to be taken beyond the client-only approach we've followed so far.

  • The name "KualiForm" is hard-coded throughout KRAD. This may be a non-issue, but will need to be confirmed. While one form per request is perfectly reasonable, care will need to be taken to ensure that the correct form is posted for update-component, update-page, and similar.
  • In the current implementation, it is on the application developer to provide unique IDs. In Rice 2.3 IDs are sequential, so collisions are unavoidable - though dynamic IDs are rarely referred to in iGPS so we've been able to avoid side effects so far. Rice 2.4 will address this through a more sophisticated ID generation approach, but there is still potential for collisions. Client-side rewriting will be insufficient, since that may break update-component callbacks. There will need to be a provision in the ID generation algorithm on the server-side to recognize an inner view and ensure that no duplicate IDs conflicting with the outer view are delivered to the page.
  • Scripts assuming one view per browser window will need to be cleaned up. We are aware of at least one: the script that jumps to the top of the page on load. More problematic scripts may exist and need to be checked for.

On a side note, it'll be interesting to see if/how next generation UIF components defined by UXI improve the delivery of inner views.

I hope these comments and sample code are useful!

Comment by Mark Fyffe (Inactive) [ 20/May/14 ]

Updated patch file for Rice 2.4.2

Generated at Thu Nov 21 14:59:21 CST 2019 using JIRA 6.1.5#6160-sha1:a61a0fc278117a0da0ec9b89167b8f29b6afdab2.