2

I'm currently teaching (re-teaching) myself JavaScript and jQuery in order to rewrite a legacy application that runs on Lucee.

Part of the rewrite requires several additions to the basic functionality. One specific addition is adding a "Street Name" select field to an existing "County Name" -> "City Name" select fields.

I found this StackOverflow question: Coldfusion conditional select option does not work in IE9, Chrome and Firefox

Using the basics of that question; I wrote the following:

<!--- county.cfm --->
<cfif structKeyExists(url,'work_area_county_id')>
    <cfset loadState() />
</cfif>

<cfif structKeyExists(url,'work_area_city_id')>
    <cfset loadStreet() />
</cfif>

<cfset loadCounty() />

<cffunction name="loadCounty">
    <cfquery name="qCounty">
        SELECT work_area_county_id, work_area_county_name FROM work_area_county
    </cfquery>
</cffunction>   

<cffunction name="loadState">
    <cfset variables.work_area_county_id = url.work_area_county_id />
    <cfquery name="qCity">
        SELECT work_area_city_id, work_area_city_name, work_area_county_id FROM work_area_city WHERE work_area_county_id = <cfqueryparam value="#variables.work_area_county_id#" cfsqltype="cf_sql_int">
    </cfquery>
    <cfoutput>
        <select name="state" class="form-control">
            <option value="">Select City</option>
            <cfloop query="qCity">
                <option value="#work_area_city_id#">#work_area_city_name#</option>
            </cfloop>
        </select>
    </cfoutput>
</cffunction>

<cffunction name="loadStreet">
    <cfset variables.work_area_city_id = url.work_area_city_id />
    <cfquery name="qStreet">
        SELECT work_area_street_id, work_area_street_name, work_area_city_id FROM work_area_street WHERE work_area_city_id = <cfqueryparam value="#variables.work_area_city_id#" cfsqltype="cf_sql_int">
    </cfquery>
    <cfoutput>
        <select name="state" class="form-control">
            <option value="">Select Street</option>
            <cfloop query="qStreet">
                <option value="#work_area_street_id#">#work_area_street_name#</option>
            </cfloop>
        </select>
    </cfoutput>
</cffunction>
<!--- display.cfm --->
<cfinclude template="includes/county.cfm">

<cfoutput>
    <form name="state_populate">
        <select name="county" id="county">
            <option value="0">Select</option>
            <cfloop query="qCounty">
                <option value="#work_area_county_id#">
                    #work_area_county_name#
                </option>
            </cfloop>
        </select>
        <div id="city">
            <select name="city" class="form-control">
                <option value="">Select</option>
             </select>
        </div>
        <div id="street">
            <select name="street" class="form-control">
                <option value="">Select</option>
             </select>
        </div>
    </form>
</cfoutput>
// The JS

$('#county').change(function() {
    var value = $('#county').val();
    $.ajax({
        type: "get",
        url:'includes/county.cfm?work_area_county_id='+value,
        success: function(response) {
            $('#city').html(response);
        },
        error: function(jqXHR, status, error) {
            console.log(status + ": " + error);
        }
    });
});

The code above works well when selecting "County Name". It displays the "City Name" in the second select field properly. I then tried to add in the third select field "Street Name" and added what I "think" should work for the CFML and HTML pieces. When I got to the JS part of the code I hit a brick wall head on. I can't seem to find, perhaps for lack of the right search terms, how to add an additional AJAX call.

I found this question: Parallel asynchronous Ajax requests using jQuery

There were multiple answers but the closest one I "think" that's relevant to my question is this:

$.whenAll({
    val1: $.getJSON('/values/1'),
    val2: $.getJSON('/values/2')
})
    .done(function (results) {
        var sum = results.val1.value + results.val2.value;

        $('#mynode').html(sum);
    });

I believe they are called "promise"?

The code needs to keep the second select field "City Name" from changing and to properly use the AJAX "url" parameter with multiple values. I know I probably need something "similar" to this for the URL part in reference to the above code:

url:'includes/county.cfm?work_area_county_id='+ value + '&work_area_city_id=' + value

For consideration:

  1. I know my code is not great and I know it could be written better. I'm working hard to improve.
  2. I'm not a full time coder but I have been coding off and on for many years. I still consider myself a newbie with CFML/JavaScript/jQuery so a lot of the methodology goes over my head.
  3. The code will be written in cfscript and converted to a CFC in the future. For now I made it simple and used tags.
  4. I removed the irrelevant HTML code from the display.cfm.

Any input or guidance on the above would be greatly, greatly appreciated! :D

Grimdari
  • 353
  • 1
  • 16
  • I suggest following the approach in this post https://stackoverflow.com/questions/36893740/dynamic-dependent-triple-related-selects-in-coldfusion-third-not-holding-firs. Also, for simplicity, I suggest stripping away all formatting code until you get your functionality working. – Dan Bracuk Dec 03 '21 at 19:44
  • Thank you, Dan Bracuk. Looking now. – Grimdari Dec 03 '21 at 20:11
  • 1
    I looked at the link you provided, Dan. The end goal that I'm trying to reach is very similar and the approach they used in their CFC is basically what I have in place now. However, I'm developing on Lucee that does not support cfform or cfselect and I have learned to completely avoid those tags in favor of client side JavaScript/jQuery. I cannot see what YUI will do with the cfselect tags and how to translate that to jQuery. Thank you very much for the suggestion! :D – Grimdari Dec 03 '21 at 20:23
  • Your loadStreet function only uses one variable. That being the case, you should only have to pass it one value. – Dan Bracuk Dec 04 '21 at 14:43
  • 2
    `needs to keep the second select field "City Name" from changing ...` I think you may be worrying about something that isn't an issue :). The code would only be updating 1 select list at a time i.e. When selected "County" changes -> populate "City" list, when selected "City" changes -> populate "Street" list. So populating streets has no impact on the selected city name, or county for that matter. – SOS Dec 07 '21 at 07:50
  • Good point, SOS. Thank you! – Grimdari Dec 07 '21 at 16:52
  • The problem with the original code is that "county.cfm" tried to do too much - both retrieve data AND generating html. A cleaner approach would be to use a CFC for data retrieval, and the populate the lists using javascript, not CF. Rebuilding the select html on the server side just adds unnecessary complexity. All you need is two simple ajax calls, no nesting. – SOS Dec 09 '21 at 23:15

3 Answers3

2

I'm going to show you an example of how I'd deal with such a task. My answer is probably not the best or cleanest solution: I had terrible coding practices in the past (I still tend to write spaghetti and I constantly struggle against doing it). But, I'm in the process of changing that bad habbits and that is fun.

My solution is written with an OOP approach. I'd really recommend everyone trying to go that path, because that quickly begins to feel way more natural: It just feels right, especially when you need to fix or extend your code.

I'd also try using cfscript instead of tags then, because writing OOP with components and functions is simplier, especially if you have some experience with JavaScript (which is somehow similar). However, I'm providing the tag approach, because I think that is what you'd like.

This solution basically consists of 4 files:

  1. display.cfm: The entry template/page.
  2. components/CountiesDAO.cfc: A component that acts as a data access object for your counties with corresponding getters. Note that I'm mimicking the DB request with QoQ. You should be able to use your datasource instead with the real data there.
  3. countyForm.js: The JavaScript file with the jQuery ajax function to obtain the data deferred objects and populate the html containers with the response data (for more information please see my comments in the code).
  4. ajaxAPI.cfm: The template that echos/outputs all the data as JSON for your jQuery ajax requests.

The main problem is that you need to retrieve more JavaScript promises, for each ajax request one. In jQuery that happens with defered object.

Here are the files:

1. display.cfm:

<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>Page Title</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<body>

<cfset CountiesDAO= new components.CountiesDAO() />
<cfset queryWorkAreaCounties=CountiesDAO.getWorkAreaCounties()>


<cfoutput>
    <select id="county">
        <option value="0">Select</option>
        <cfloop query="queryWorkAreaCounties">
            <option value="#queryWorkAreaCounties.work_area_county_id#">
                #queryWorkAreaCounties.work_area_county_name#
            </option>
        </cfloop>
    </select>
    <div id="cityContainer"></div>
    <div id="streetContainer"></div>
</cfoutput>

<!-- embedded jquery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="countyForm.js"></script>
</body>
</html>

2. components/CountiesDAO.cfc

<cfcomponent>

    <cffunction name="init">
        <cfset variables.queriesQoQ["work_area_counties"]=createWorkAreaCounties()>
        <cfset variables.queriesQoQ["Work_area_cities"]=createWorkAreaCities()>
        <cfset variables.queriesQoQ["Work_area_streets"]=createWorkAreaStreets()>
        <cfreturn this>
    </cffunction>

    <!--- functions to create Data to emulate a DB with QoQ --->
    <cffunction name="createWorkAreaCounties" 
                access="private" 
                returntype="query"
                hint="create query to simulate a DB data table for QoQ"> 

        <cfset work_area_countyTable= queryNew(
            "work_area_county_id, work_area_county_name",
            "integer,varchar",
            [ 
                {"work_area_county_id":1,"work_area_county_name":"Los Angeles County"}, 
                {"work_area_county_id":2,"work_area_county_name":"Cook County"},
                {"work_area_county_id":3,"work_area_county_name":"Harris County"},
                {"work_area_county_id":4,"work_area_county_name":"Maricopa County"}
            ])/>

        <cfreturn work_area_countyTable> 
    </cffunction> 


    <cffunction name="createWorkAreaCities" 
                access="private" 
                returntype="query"
                hint="create query to simulate a DB data table for QoQ"> 

        <cfset work_area_cityTable= queryNew(
            "work_area_city_id, work_area_city_name, work_area_county_id",
            "integer,varchar,integer",
            [ 
                {"work_area_city_id":1,"work_area_city_name":"Agoura Hills" , "work_area_county_id":1}, 
                {"work_area_city_id":2,"work_area_city_name":"Alhambra" , "work_area_county_id":1}, 
                {"work_area_city_id":3,"work_area_city_name":"Bradbury" , "work_area_county_id":1}, 
            
                {"work_area_city_id":4,"work_area_city_name":"Arlington Heights" , "work_area_county_id":2}, 
                {"work_area_city_id":5,"work_area_city_name":"Bellwood" , "work_area_county_id":2}, 
                {"work_area_city_id":6,"work_area_city_name":"Bridgeview" , "work_area_county_id":2}, 
            
                {"work_area_city_id":7,"work_area_city_name":"Baytown" , "work_area_county_id":3}, 
                {"work_area_city_id":8,"work_area_city_name":"Cove" , "work_area_county_id":3}, 
                {"work_area_city_id":9,"work_area_city_name":"The Woodlands" , "work_area_county_id":3}, 
            
                {"work_area_city_id":10,"work_area_city_name":"Avondale" , "work_area_county_id":4}, 
                {"work_area_city_id":11,"work_area_city_name":"Phoenix" , "work_area_county_id":4}, 
                {"work_area_city_id":12,"work_area_city_name":"Glendale" , "work_area_county_id":4}, 
            ])/>

        <cfreturn work_area_cityTable> 
    </cffunction> 

    <cffunction name="createWorkAreaStreets" 
                access="private" 
                returntype="query"
                hint="create query to simulate a DB data table for QoQ"> 

        <cfset work_area_streetTable= queryNew(
            "work_area_street_id, work_area_street_name, work_area_city_id",
            "integer,varchar,integer",
            [ 
                {"work_area_street_id":1,"work_area_street_name":"Street One Agoura Hills", "work_area_city_id": 1 },
                {"work_area_street_id":2,"work_area_street_name":"Street Two Agoura Hills", "work_area_city_id": 1 }, 
                {"work_area_street_id":3,"work_area_street_name":"Street Three Agoura Hills", "work_area_city_id": 1 }, 

                {"work_area_street_id":4,"work_area_street_name":"Street One Alhambra", "work_area_city_id": 2 },
                {"work_area_street_id":5,"work_area_street_name":"Street Two Alhambra", "work_area_city_id": 2 }, 
                {"work_area_street_id":6,"work_area_street_name":"Street Three Alhambra", "work_area_city_id": 2 },

                {"work_area_street_id":7,"work_area_street_name":"Street One Bradbury", "work_area_city_id": 3 },
                {"work_area_street_id":8,"work_area_street_name":"Street Two Bradbury", "work_area_city_id": 3 }, 
                
                {"work_area_street_id":9,"work_area_street_name":"Street One Arlington Heights", "work_area_city_id": 4 },
                {"work_area_street_id":10,"work_area_street_name":"Street Two Arlington Heights", "work_area_city_id": 4 }, 
                
                {"work_area_street_id":11,"work_area_street_name":"Street One Bellwood", "work_area_city_id": 5 },
                {"work_area_street_id":12,"work_area_street_name":"Street Two Bellwood", "work_area_city_id": 5 }, 
                
                {"work_area_street_id":13,"work_area_street_name":"Street One Bridgeview", "work_area_city_id": 6 },
                {"work_area_street_id":14,"work_area_street_name":"Street Two Bridgeview", "work_area_city_id": 6 }, 
                
                {"work_area_street_id":15,"work_area_street_name":"Street One Baytown", "work_area_city_id": 7 },
                {"work_area_street_id":16,"work_area_street_name":"Street Two Baytown", "work_area_city_id": 7 }, 
                
                {"work_area_street_id":17,"work_area_street_name":"Street One Cove", "work_area_city_id": 8 },
                {"work_area_street_id":18,"work_area_street_name":"Street Two Cove", "work_area_city_id": 8 }, 
                
                {"work_area_street_id":19,"work_area_street_name":"Street One The Woodlands", "work_area_city_id": 9 },
                {"work_area_street_id":20,"work_area_street_name":"Street Two The Woodlands", "work_area_city_id": 9 }, 
                
                {"work_area_street_id":21,"work_area_street_name":"Street One Avondale", "work_area_city_id": 10 },
                {"work_area_street_id":22,"work_area_street_name":"Street Two Avondale", "work_area_city_id": 10 }, 
                
                {"work_area_street_id":23,"work_area_street_name":"Street One Phoenix", "work_area_city_id": 11 },
                {"work_area_street_id":24,"work_area_street_name":"Street Two Phoenix", "work_area_city_id": 11 }, 
                
                {"work_area_street_id":25,"work_area_street_name":"Street One Glendale", "work_area_city_id": 12 },
                {"work_area_street_id":26,"work_area_street_name":"Street Two Glendale", "work_area_city_id": 12 },
            ])/>
        <cfreturn work_area_streetTable>
    </cffunction>


    <cffunction name="getWorkAreaCounties"  
                access="public" 
                returntype="query"
                hint="function to return all counties">  
        
        <cfset work_area_county=queriesQoQ["work_area_counties"]>
        
        <cfquery name="qCity" dbtype="query" >
            SELECT * FROM work_area_county
        </cfquery>
        
        <cfreturn qCity> 
    
    </cffunction>


    <cffunction name="getWorkAreaCitiesByCountyID"  
                access="public" 
                returntype="query"
                hint="function to return all cities of a county"> 
        
        <cfargument type="numeric" name="countyid" required="true">
        
        <cfset work_area_city=queriesQoQ["work_area_cities"]>
        
        <cfquery name="qCity" dbtype="query" >
            SELECT  work_area_city_id, work_area_city_name, work_area_county_id 
            FROM    work_area_city 
            WHERE   work_area_county_id = <cfqueryparam value="#arguments.countyid#" cfsqltype="cf_sql_int">
        </cfquery>
        
        <cfreturn qCity> 
    
    </cffunction>


    <cffunction name="getWorkAreaStreetsByCityID"  
                access="public" 
                returntype="query"
                hint="function to return all streets of a city">  
        
        <cfargument type="numeric" name="cityid" required="true">
        
        <cfset work_area_street=queriesQoQ["work_area_streets"]>
        
        <cfquery name="qStreet" dbtype="query" >
            SELECT  work_area_street_id, work_area_street_name, work_area_city_id 
            FROM    work_area_street 
            WHERE   work_area_city_id = <cfqueryparam value="#arguments.cityid#" cfsqltype="cf_sql_int">
        </cfquery>
        
        <cfreturn qStreet> 
    
    </cffunction> 

</cfcomponent>

3. countyForm.js

// ajax function
function sendAjaxAndUpdateForm( 
                url, 
                selectorForValue,  
                selectorForHTMLResponse ,  
                callbackFunction ){

    let value = $( selectorForValue ).val();
    
    /* return the ajax request as deferred object with done()/fail(). For more information, please see:
    *  https://api.jquery.com/jquery.ajax/ 
    *  https://stackoverflow.com/questions/10931836/should-i-use-done-and-fail-for-new-jquery-ajax-code-instead-of-success-and
    */
    return $.ajax({
            method: "GET",
            url: url + value,
            }).done( function( result ) {
                
                //populate HTML div with returned html
                $( selectorForHTMLResponse ).html( result.contentHTML );

                // invoke callback if a callback has been submitted
                if ( callbackFunction && typeof( callbackFunction ) === "function") { 
                    callbackFunction();
                }
            
            }).fail( function( e ) { 
                    
                //log some info and alert about fail
                console.dir( e.responseText );
                alert('Ops! Something went wrong!');

            });
}


$( document ).ready(function() {

    // add listeners to HTML container and make use of callbacks
    $('#county').change(
        function() {
            sendAjaxAndUpdateForm(
                url='ajaxAPI.cfm?work_area_county_id=',
                selectorForValue='#county',
                selectorForHTMLResponse='#cityContainer',
                callbackFunction= function(){ $('#city').change(
                        function() {
                            sendAjaxAndUpdateForm(
                                url='ajaxAPI.cfm?work_area_city_id=',
                                selectorForValue='#city',
                                selectorForHTMLResponse='#streetContainer',
                                callbackFunction=function(){ $('#street').change( 
                                    function(){ alert( 'Street ID:' + $('#street').val() + 'for \'' + $( '#street option:selected' ).text() + '\' selected.' )}  )
                                }
                            );
                     })
                }
            );  
        });
        
});

4. ajaxAPI.cfm

<!--- Function to output content as JSON for a response of the ajax request --->
<cffunction name="outputJSON"  
            access="private" 
            returntype="void"
            hint="function to output data as application/json"> 
    <cfargument type="struct" name="contentStruct" required="true">
    <cfcontent reset = "true">
    <cfheader name="content-type" value="application/json">
    <cfoutput>#serializeJSON( contentStruct )#</cfoutput>
    <cfabort>
</cffunction> 
 
<!--- instantiate Data Access Object Component--->
<cfset CountiesDAO = new components.CountiesDAO() />


<cfif structKeyExists(url, "work_area_county_id") 
      and len( work_area_county_id ) gt 0>

    <cfset queryWorkAreaCities=CountiesDAO.getWorkAreaCitiesByCountyID( url.work_area_county_id )>
    
    <cfsavecontent variable="result.contentHTML">
        <cfoutput>
            <select name="city" id="city" class="form-control">
                <option value="">Select City</option>
                <cfloop query="queryWorkAreaCities">
                    <option value="#queryWorkAreaCities.work_area_city_id#">#queryWorkAreaCities.work_area_city_name#</option>
                </cfloop>
            </select>
        </cfoutput> 
    </cfsavecontent>
        
     <!--- echo json --->
     <cfset outputJSON( result )>

</cfif>



<cfif structKeyExists( url, "work_area_city_id" ) and len( work_area_city_id ) gt 0>
        
    <cfset queryWorkAreaStreets=CountiesDAO.getWorkAreaStreetsByCityID( url.work_area_city_id )>
    
    <cfsavecontent variable="result.contentHTML">
        <cfoutput>
        <select name="street" id="street" class="form-control">
            <option value="">Select Street</option>
            <cfloop query="queryWorkAreaStreets">
                <option value="#queryWorkAreaStreets.work_area_street_id#">#queryWorkAreaStreets.work_area_street_name#</option>
            </cfloop>
        </select>
        </cfoutput>
    </cfsavecontent>

    <!--- echo json --->
    <cfset outputJSON( result )>

</cfif>

The above solution is far from finished, but it should just give you an option to start playing around and have some fun.

AndreasRu
  • 1,053
  • 8
  • 14
  • Thank you! :D Interesting way of doing the AJAX in your countyform.js file. I didn't think it was possible to chain functions like you did with the #county change piece. Thank you AndreasRu! :D – Grimdari Dec 09 '21 at 21:35
  • As a side question, how is the speed? I know that right now the database only has one county and its streets, however, the database will grow and add more and more streets as time goes on. – Grimdari Dec 09 '21 at 21:40
  • Thanks for the solution marking! :D Regarding the chains: In case you'd need more/deeper chains, you could pull each chain code out to its own function. Regarding the speed: Crucial is to have the DB table indexed correctly and to SQL-SELECT using primary keys (,countyID, cityID). Probablly you' may need to add a searchbox input text field with autosuggestion, e.g. for street selection. But such SQL shouldn't also be a big issue in speed when filtering the streets using the like % operator in combination with primary keys (e.g. cityID). – AndreasRu Dec 09 '21 at 23:14
  • @AndreasRu - Just curious, why use CF code to populate the lists instead of jQuery? Seems like more moving pieces.. – SOS Dec 13 '21 at 17:46
  • 1
    @SOS - Good question. It's not a golden rule. It's just because in such a scenario I (personally) prefer creating one complete HTML block instead of script-creating it in the browser. If you need to extend or change the HTML, you simply do it right there in the final HTML code: It's a kind of "WYSIWYG" editing preference. – AndreasRu Dec 16 '21 at 09:46
1

(I started to write this up before, but got pulled away ....)

Tbh the county.cfm script strikes me as trying too hard to be "everything to everyone" :). It's acting as a cfinclude and endpoint for ajax, both retrieving data and generating html. Resulting in less readable and more complex code than is necessary IMO.

A cleaner approach would be to separate the data retrieval and html/dom manipulation. Let the CF server handle data retrieval and let the client side manipulate the DOM. That way only two (2) scripts are required.

Create a cfc that only returns data. Then inside the form, call the cfc functions via ajax and use the response to populate the select lists with javascript.

YourComponent.CFC

Start by creating a component with three remote functions: getCounties(), getCities(selected_county_id), getStreets(selected_city_id). Each function simply runs a query and returns the results as an array of structures, formatted as a json string.

<cfcomponent>

    <cffunction name="getCounties" access="remote" returntype="string" returnFormat="plain">
        <!--- query is a function local object, use "local" scope --->
        <cfquery name="local.result">
            SELECT  work_area_county_id, work_area_county_name
            FROM    work_area_county
            ORDER BY work_area_county_name
        </cfquery>
        
        <!--- convert query into workable format: array of structures --->
        <cfreturn serializeJSON(local.result, "struct")>
    </cffunction>
    
    <cffunction name="getCities" access="remote" returntype="string" returnFormat="plain">
        <cfargument name="work_area_county_id" type="numeric" required="true">
        
        <cfquery name="local.result">
            SELECT  work_area_city_id, work_area_city_name
            FROM    work_area_city 
            <!--- correct sql type is "integer" or "cf_sql_intEGER" --->
            WHERE   work_area_county_id = <cfqueryparam value="#arguments.work_area_county_id#" cfsqltype="integer">
            ORDER BY work_area_city_name
        </cfquery>
        
        <cfreturn serializeJSON(local.result, "struct")>
    </cffunction>

    
    <cffunction name="getStreets" access="remote" returntype="string" returnFormat="plain">
        <cfargument name="work_area_city_id" type="numeric" required="true">
        
        <cfquery name="local.result">
           SELECT   work_area_street_id, work_area_street_name
           FROM     work_area_street 
           WHERE    work_area_city_id = <cfqueryparam value="#arguments.work_area_city_id#" cfsqltype="integer">
           ORDER BY work_area_street_name
        </cfquery>
        
        <cfreturn serializeJSON(local.result, "struct")>
    </cffunction>
    
</cfcomponent>

Display.cfm

Inside the form, add in ajax calls to populate the lists. Populate the "county" list when the document loads, and the "city/street" lists on the appropriate change event. There's room for improvement, but here's a small example

<script type="text/javascript">
$(document).ready(function()
{
    // On load, populate county list
    $.getJSON( 
        "YourComponent.cfc?method=getCounties",{},
        function(response){
            fillList("#county", response, "work_area_county_id", "work_area_county_name");
            // refresh city and state
            $("#city").trigger("change");

        });     
    
    // When county changes, re-populate cities
    $("#county").on("change", function(evt) {
        
        var county_id = $(this).val();
        $.getJSON( 
            "YourComponent.cfc?method=getCities&work_area_county_id="+ county_id, {},
            function(response){
                fillList("#city", response, "work_area_city_id", "work_area_city_name")
                // refresh city and state
                $("#city").trigger("change");
        });         
    });
    
    // On city change, re-populate streets
    $("#city").on("change", function(evt) {
        $('#street').attr('disabled', true);
        
        var city_id = $(this).val();
        $.getJSON( 
            "YourComponent.cfc?method=getStreets&work_area_city_id="+ city_id, {},
            function(response){
                fillList("#street", response, "work_area_street_id", "work_area_street_name")
        });         
    });
    
    // populates select list with provided data 
    function fillList( id, rows, value, text) {
        // reinitialize
        $( id ).empty().append( $("<option>")
                .text( "Select" )
                .val( 0 )
        ); 
        // populate 
        $.each(rows, function(index, data) {
             $(id).append( $("<option>")
                    .val( data[value] )
                    .text( data[text] )
            ); 
        });
        // enable
        $(id).attr('disabled', false);
    }
});
</script>   

<form name="state_populate">
    <!--- select list must have an "id" --->
    <select id="county" name="county">
        <option value="0">Select</option>
    </select>
    <select id="city" name="city">
        <option value="0">Select</option>
    </select>
    <select id="street" name="street">
        <option value="0">Select</option>
    </select>
</form>
SOS
  • 6,430
  • 2
  • 11
  • 29
  • 1
    That's a nice and clean solution!!! I like the pure JSON approach using the YourComponent.CFC as a remote CFC to retrieve the pure data (without any HTML envolved) in server side cfml scripting. – AndreasRu Dec 16 '21 at 09:54
1

For future viewers of this; I wanted to post my own answer to my question. While waiting for an answer to be posted, I kept trying to get the code to work. Many days went by and what I pasted below is the result.

Since both answers from AndreasRu and SOS are better, I ended up not using what I wrote. Perhaps someone can use my code as an example of what not to do. :D

My CFC:

    component displayname="pull_locations" output="false" {
        public function getCounties(returntype="query") {
            getCounties = queryExecute(
                sql = "SELECT work_area_county_id, work_area_county_name FROM work_area_county"
            );
        return getCounties;
        }
        remote function getCitiesByCounty(required numeric county_id) returnFormat="JSON" {
            getCity = queryExecute(
                sql = "SELECT work_area_county_id, work_area_city_id, work_area_city_name FROM work_area_city WHERE work_area_county_id = :need",
                params = {
                    need: {
                        value: arguments.county_id,
                        type: "cf_sql_integer"
                    }
                }               
            );
            rData = {
                "status" = "success",
                "data" = queryToArray(getCity)
            };
        return serializeJSON(rData);
        }
        remote function getStreetsByCity(required numeric city_id) returnFormat="JSON" {
            getStreet = queryExecute(
                sql = "SELECT work_area_city_id, work_area_street_id, work_area_street_name FROM work_area_street WHERE work_area_city_id = :need",
                params = {
                    need: {
                        value: arguments.city_id,
                        type: "cf_sql_integer"
                    }
                }               
            );
            rData = {
                "status" = "success",
                "data" = QueryToArray(getStreet)
            };
        return serializeJSON(rData);
        }
// Thank you Ben Nadel! :D
        public function queryToArray(required query Data) output=false {
            var LOCAL = StructNew();
            LOCAL.Columns = ListToArray( ARGUMENTS.Data.ColumnList );
            LOCAL.QueryArray = ArrayNew( 1 );
            for (LOCAL.RowIndex = 1 ; LOCAL.RowIndex LTE ARGUMENTS.Data.RecordCount ; LOCAL.RowIndex = (LOCAL.RowIndex + 1)){
                LOCAL.Row = StructNew();
                for (LOCAL.ColumnIndex = 1 ; LOCAL.ColumnIndex LTE ArrayLen( LOCAL.Columns ) ; LOCAL.ColumnIndex = (LOCAL.ColumnIndex + 1)){
                    LOCAL.ColumnName = LOCAL.Columns[ LOCAL.ColumnIndex ];
                    LOCAL.Row[ LOCAL.ColumnName ] = ARGUMENTS.Data[ LOCAL.ColumnName ][ LOCAL.RowIndex ];
                }
                ArrayAppend( LOCAL.QueryArray, LOCAL.Row );
            }
        return LOCAL.QueryArray;
        }
    }

My JavaScript/jQuery:

            function loadCity(county) {
                if(county != '') {
                    $.ajax({
                        url: "cfc/data.cfc?method=getCitiesByCounty",
                        dataType: "json",
                        type: "get",
                        async: false,
                        cache: false,
                        data: {
                            county_id: county,
                        },
                        success: function (response) {
                            response = JSON.parse(response);
                            if(response.status == 'success') {
                                rData = response.data;
                                let html = '<option value="">Select City</option>';
                                rData.forEach(element => {
                                    html += '<option value="'+element.WORK_AREA_CITY_ID+'">'+element.WORK_AREA_CITY_NAME+'</option>';
                                });
                                $('#city').html(html);
                                let html2 = '<option value="">Select Street</option>';
                                $('#street').html(html2);
                            }
                            else {
                                alert('Error occured while pulling City name.');
                            }
                        },
                        error: function(jqXHR, status, error) {
                            console.log(status + ": " + error);
                        }
                    })
                }
            }
            function loadStreet(city) {
                if(city != '') {
                    $.ajax({
                        url: "cfc/data.cfc?method=getStreetsByCity",
                        dataType: "json",
                        type: "get",
                        async: false,
                        cache: false,
                        data: {
                            city_id: city,
                        },
                        success : function(response) {
                            response = JSON.parse(response);
                            if(response.status == 'success') {
                                rData = response.data;
                                let html = '<option value="">Select Street</option>';
                                rData.forEach(element => {
                                    html += '<option value="'+element.WORK_AREA_STREET_ID+'">'+element.WORK_AREA_STREET_NAME+'</option>';
                                });
                                $('#street').html(html);
                            }
                            else {
                                alert('Error occured while pulling Street name.');
                            }
                        },
                        error: function(jqXHR, status, error) {
                            console.log(status + ": " + error);
                        }
                    })
                }
            }
Grimdari
  • 353
  • 1
  • 16
  • Thanks for posting! I prefer this approach, letting javascript to do the work. Assuming you're calling the js methods `on('change')` of the lists, our examples aren't that different, `$.getJSON()` just wraps a lot of boilerplate `$.ajax()` stuff. There are actually several elements in your example that are better than ours, like being written in cfscript instead of cfml and js error handling. (cont'd) – SOS Dec 15 '21 at 04:28
  • A few thoughts about the cfc. Don't forget to localize the function variables. Also, since there's no error handling in the cfc, control will shift to the jquery `error` method if there's problem with the ajax call. So returning a structure with a status, instead of just a query, doesn't buy you much. All in all, good to see several different approaches. – SOS Dec 15 '21 at 04:28