CFGRID, CFGRIDUPDATE and extra values

3.5k Views Asked by At

I'm using CFGRID and CFGRIDUPDATE to insert values into a database. The problem is, each record needs to get an additional field that isn't in the grid. Is there any way to save that additional field to the record, or do I have to whip up an alternative to CFGRID?

Basically, I have a bunch of users I'm entering into a grid. The page is getting a category id. I want all of the users to be saved with that category id.

Another thing that would work is if I could get a list of all primary keys, including those for the just-created records, and update all of them with the category id. But it looks like CFGRIDUPDATE doesn't return any information about the rows that were created.

2

There are 2 best solutions below

1
On

There is another way. It involves handling directly the form variables which are posted by CFGRID. Here's some example code:

form.cfm:

<cfform method="post" action="post.cfm">
<cfoutput><input type="hidden" name="ParentID" value="#ParentID#"></cfoutput>
<cfgrid format="html" name="GridData" query="Records" insert="yes" delete="yes" selectmode="edit">
    <cfgridcolumn name="RecordID" display="no">
    <cfgridcolumn name="RecordName" width="150" header="Name" headeralign="left" dataalign="left" select="Yes" display="Yes">
    <cfgridcolumn name="RecordColor" width="150" header="Color" headeralign="left" dataalign="left" select="Yes" display="Yes">
</cfgrid>
<br />
<input type="submit" value="Save Records" />
</cfoutput>
</cfform>

then in post.cfm

<cfif isDefined("GridData.RowStatus.Action") and isArray(GridData.RowStatus.Action)>
    <cfloop from="1" to="#ArrayLen(GridData.RowStatus.Action)#" index="i">
        <cfswitch expression="#GridData.RowStatus.Action[i]#">
            <cfcase value="I">
                <cfquery name="Records_INSERT" datasource="#request.maindatasource#" blockfactor="100">
                    INSERT INTO Records (RecordName, RecordColor, RecordParent)
                    VALUES (
                        <cfqueryparam cfsqltype="cf_sql_varchar" value="#Trim(GridData.RecordName[i])#">,
                        <cfqueryparam cfsqltype="cf_sql_varchar" value="#Trim(GridData.RecordColor[i])#">,
                        <cfqueryparam cfsqltype="cf_sql_integer" value="#Val(ParentID)#">
                    )
                </cfquery>
            </cfcase>
            <cfcase value="U">
                <cfquery name="Records_UPDATE" datasource="#request.maindatasource#" blockfactor="100">
                    UPDATE Records
                    SET
                        RecordName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#Trim(GridData.RecordName[i])#">,
                        RecordColor = <cfqueryparam cfsqltype="cf_sql_varchar" value="#Trim(GridData.RecordColor[i])#">
                    WHERE
                        RecordID=<cfqueryparam cfsqltype="cf_sql_integer" value="#GridData.original.RecordID[i]#">
                </cfquery>
            </cfcase>
            <cfcase value="D">
                <cfquery name="Records_DELETE" datasource="#request.maindatasource#" blockfactor="100">
                    DELETE
                    FROM Records
                    WHERE
                        RecordID=<cfqueryparam cfsqltype="cf_sql_integer" value="#GridData.original.RecordID[i]#">
                </cfquery>
            </cfcase>
        </cfswitch>
    </cfloop>
</cfif>
3
On

-- original answer removed --

Based on your comment to my original answer, there is now an implied assumption that Category_ID is a foreign key, possibly on a join table, that cannot (for whatever reason) be included in the initial display query--or, that you simply wish to include a dynamic variable into the grid which will be included during inline inserts, without the user intervening (ie. physically preventing them from selecting the Category_ID themselves, but that it will still be dynamic in some capacity, say...by being fed from a URL var).

If correct, I believe the true question leans closer to:

Can a CFGRID update multiple tables/dynamic columns via CFGRIDUPDATE?

Short answer: No

Long answer: Yes, but not via CFGRIDUPDATE--rather, via CFQUERY and a little more work on the CFGRID, via CFC binds and the CFAJAXPROXY.

Solution:

1) You'll need two files for this solution. File #1 is your Component which wraps the query functionality; we'll call it cfgrid.cfc

<cfcomponent>

    <cfset this.dsn = "gridexample" />

    <cffunction name="getUsers" returntype="any" access="remote" output="false">
        <cfargument name="page" />
        <cfargument name="pageSize" />
        <cfargument name="gridsortcolumn" />
        <cfargument name="gridsortdirection" />

        <cfset var getUsers = 0 />

        <cfquery name="getUsers" datasource="#this.dsn#">
            SELECT Users.UserID, Users.FirstName, UserCategories.Category_ID
            FROM Users
        INNER JOIN UserCategories ON (Users.User_ID = UserCategories.UserID)

            <cfif arguments.gridsortcolumn neq "" or arguments.gridsortdirection neq ""> 
                order by #arguments.gridsortcolumn# #arguments.gridsortdirection#
                </cfif> 
        </cfquery>

        <cfreturn QueryConvertForGrid(getUsers, page, pageSize) />
    </cffunction>

    <cffunction name="addNewUser" returntype="string" access="remote" output="false">
        <cfargument name="fullname" type="string" required="true" />
        <cfargument name="category_id" type="numeric" required="true" />

        <cfquery datasource="#this.dsn#">
        INSERT INTO Users
        (
            fullname
        )
        VALUES
        (
            <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.fullname#">
        )
        </cfquery>

        <cfquery name="getPkey" datasource="#this.dsn#">
        SELECT Max(User_ID) as PKey
        FROM Users
        </cfquery>

        <cfquery datasource="#this.dsn#">
        INSERT INTO UserCategories
        (
            User_ID,
            Category_ID
        )
        VALUES
        (
            <cfqueryparam cfsqltype="cf_sql_integer" value="#getPKey.PKey#" />
            <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.category_id#" />
        )
        </cfquery>

        <cfreturn "User Added" />
    </cffunction>

    <cffunction name="editUser" access="remote">
        <cfargument name="gridaction">
        <cfargument name="gridrow">
        <cfargument name="gridchanged"> 

    </cffunction>

</cfcomponent>

Pay attention to these key pieces of the CFC:

a) getUsers() returns the current user data along with their CategoryID. You'll have to re-write this query to match your schema, but the key takeaway is that this is a data population query, so all the data that's necessary for creating a user should also be present for updating the user as well. This also assumes you only have 1 CategoryID per User (which many developers de-normalize by leaving the CategoryID on the Users table--I'll leave that to your discretion).

b) addNewUser() takes expects the form/grid submission to pass along the new name--as well as the category_id--but we know ahead of time we're not going to be asking that the Category_ID be filled out by the person entering the form/grid data--we'll do that programmatically. The end result is still the same, however--the query is going to need to know both values. For brevity, I've left off <CFTRANSACTION> calls, but bear it in mind--you're about to execute three queries in succession, one of which (the 3rd) depends on dynamic data from the other (the 1st and 2nd)--so you'll need to keep concurrency in mind when you move forward with this type of design.

c) Disregard editUser() for now--you will need to populate it at some point--it simply needs to exist for this demonstration to work.

2) The second file you'll need is the front end--the grid itself, we'll call it cfgrid.cfm.

We'll go through this one, top to bottom, as its quite large, and each chunk of code will need explanation:

<cfparam name="URL.Category_ID" default=4 />

The first line of the template parameterizes a URL variable, which we want to use to programmatically provide behind-the-scenes assignment to new users. Use your own mechanism to supply a dynamic Category_ID as needed.

<cfajaxproxy cfc="cfgrid" jsclassname="dataproxy">

This line causes ColdFusion to create a javascript object named 'dataproxy' and wrap it in a container necessary to provide access the core functions that exist in the CFC you point it to...and in this case, you are pointing it to 'cfgrid', which is our first file mentioned above (cfgrid.cfc). Therefore, you can now comfortably expect that you have a javascript object with getUsers() and addNewUser() methods.

<html>
<head>
<script type="text/javascript" src="/CFIDE/scripts/ajax/ext/package/toolbar/toolbar.js"></script>

Here, you begin your HTML document tags, and include a reference to one of the Ajax libraries included in ColdFusion, the toolbar.js file.

<script type="text/javascript">
var dataproxy = new dataproxy();

dataproxy.setCallbackHandler(handleResult);

function handleResult(response)
{
    alert(response);
}

Here, you create a local instance of the dataproxy object (remember above, from the <CFAJAXPROXY> call) and assign a callback handler, which points to another javascript function 'handleResult'. This is the process you employ when dealing with asynchronous communication--a fundamental part of working in Ajax.

function init()
{
    grid = ColdFusion.Grid.getGridObject("UserGrid");

    var gridHead = grid.getView().getHeaderPanel(true);

    var tbar = new Ext.Toolbar(gridHead);

    tbar.addButton({text:"Add User", handler:onAdd });
}

This init() function creates the javascript objects necessary to drive communication to the cfgrid. You obtain a reference to the cfgrid via ColdFusion.Grid.getGridObject, and from there, access the grid's header, which allows you to target to toolbar, and add a button "Add User", which you then programmatically decide to call a new function when it is clicked...that function is named "onAdd"...

function onAdd(button,event)
{
    ColdFusion.Window.show('addUserWin');
}

Unsurprisingly, here is that onAdd() function, which displays a new window to add a user (its the window that contains the input field for the user's fullname).

Finally, the new AddUserWin Window above is going to need its own function to add a user, since we will need to provide the Category_ID dynamically--as opposed to letting the user supply it. So, addUser() will do just that:

function addUser()
{
    var f = document.frmUser;

    dataproxy.addNewUser(
        f.txtFullname.value,
        f.txtCategory_ID.value
    );

    ColdFusion.Window.hide('addUserWin');

    grid.refresh();
}
</script>
</head>

In addUser(), we refer to the <FORM> below by its name (frmUser), and call our javascript proxy object's addNewUser() method--which maps to the CFC. As expected, it'll need to know the values of the new user, so we'll pass it the value of txtFullname and txtCategory_ID. We finish by hiding the window and refreshing the grid.

Remember, we're asynchronous now, so we don't need to read a result and display it--that result will fire via the callback handler assigned above in the handleResult() method.

Now, build the arguments that will feed the population of the CFGRID:

<cfset args = StructNew() />

<cfset args.name = "UserGrid" />
<cfset args.format = "html" />
<cfset args.bindOnLoad = "true" />
<cfset args.bind = "cfc:cfgrid.getUsers({cfgridpage},{cfgridpagesize},{cfgridsortcolumn},{cfgridsortdirection})" />
<cfset args.selectmode = "edit" />
<cfset args.onchange = "cfc:cfgrid.editUser({cfgridaction},{cfgridrow},{cfgridchanged})" />

Here we: 1. Name the grid "UserGrid" (as we have referred to it by that name in javascript above), 2. Make it render using html, 3. Tell it to bind its data when the page loads, 4. Bind that data via the cfgrid.cfc, calling the getUsers() method (and passing in the current parameters of the cfgrid via its page, pagesize, sortcolumn, and sortdirection values), 5. Make it editable, and 6. Assign an onChange handler, in case we also want to allow the users to be edited. This last part (unfortunately) is necessary, so I was unable to strip it out for this example.

Now, build the <CFFORM> and <CFGRID>:

<cfform>
    <cfgrid attributeCollection="#args#">
        <cfgridcolumn name="User_ID" display="false">
        <cfgridcolumn name="Category_ID" display="false">
        <cfgridcolumn name="FullName" header="Full Name">
    </cfgrid>
</cfform>

Here, we populate the grid with our arguments specified above, and you'll note we display only the "FullName" field on the grid, yet User_ID and Category_ID are still present, and a part of the dataset; they simply aren't displayed to the front-end.

Last but not least, the window that will pop-up when a user clicks the "Add New User" button, which provides the interface we need to allow user entry, while at the same time, control (behind the scenes) what Category_ID is dynamically supplied:

<cfwindow name="addUserWin" modal="true" resizable="false" title="Add New User">
    <form name="frmUser">
<input type="hidden" name="txtCategory_ID" value="<cfoutput>#URL.Category_ID#</cfoutput>" />
    <table width="100%">
        <tr>
            <td>Fullname</td>
            <td><input type="text" name="txtFullname" value=""></td>
        </tr>
        <tr>
            <td colspan="2"><input type="button" value="Add User" onclick="javascript:addUser();"></td>
        </tr>
    </form>
</cfwindow>

This call to CFWINDOW provides the necessary "pop-up" to encapsulate the form. Note that the form name is frmUser, as we have referred to it above in code. Also, note that the name of the fields match (including their case) what are referred to in javascript. This form displays the Fullname field to the user to fill out, while the Category_ID stays hidden, but is still programmatically driven by you--via your URL parameter at the top of this code example. Finally, the button, when clicked, fires the addUser() method, which you'll recall is the one that talks to the javascript object--which in turn--talks to the CFC--and commits your data to the database.

Finally, just before you complete this template, don't forget to fire your init() javascript function!

<cfset ajaxOnLoad("init")>

</html>

Hope this helps.

Adapted from CRUD with cfgrid html format (Source: Anuj Gakhar)