+ Start a Discussion
Brian KesslerBrian Kessler 

Lightning Component Superbadge: What's Wrong with My New Boat Button?

Hi,

I'm currently working on the Lightning Component Framework Specialist Superbadge.

When I click "New", at least as I've interpretted the requirements, the "Friends with Boats" pages is behaving as expected, but Trailhead complains:
Challenge Not yet complete... here's what's wrong: 
The BoatSearchForm component's "New" button should launch the default boat record create page using logic in its controller.

Have I misunderstood a requirement?

Here is my markup:
 
<aura:component controller="BoatSearchFormAuraCtrl" >
	<aura:attribute access="private" name="boatList" type="BoatType__c[]" default="[]" />
	<aura:attribute access="private" name="showNewButton" type="Boolean" default="false" />
	<aura:attribute access="private" name="selectedBoat" type="BoatType__c" />
	
	<aura:handler name="init" value="{!this}" action="{!c.doInit}" />
	
	<h2 class="slds-page-header__title">Find a Boat</h2>
	<form>
		<lightning:layout horizontalAlign="center">
		    <lightning:select name="select" value="{!v.selectedBoat}">
		        <option value="">All Types</option>
		        <aura:iteration items="{!v.boatList}" var="boat">
		            <option value="{!boat.Name}" text="{!boat.Name}"></option>
		        </aura:iteration> 
		    </lightning:select>
		    <lightning:button name="Search" label="Search" variant="brand" />
		    <aura:if isTrue="{!v.showNewButton}">
		    	<lightning:button name="New" label="New" variant="neutral" onclick="{!c.createBoat}"/>
		    </aura:if>
		</lightning:layout>
	</form>	
</aura:component>
And here is my controller:
({
	doInit : function(component, event, helper) {
		component.set('v.showNewButton', $A.get('e.force:createRecord'));
		helper.setBoatTypeList(component);
	},
	
	createBoat : function(component) {
		var createRecordEvent = $A.get('e.force:createRecord');
		createRecordEvent.setParams({
			'entityApiName' : 'BoatType__c',
			'defaultFieldValues': {
				'Name': component.get('v.selectedBoat')
			},
		});
		createRecordEvent.fire();
	}
})





 
Best Answer chosen by Brian Kessler
PatMcClellan__cPatMcClellan__c
Hey Brian, I'm working on the same challenge, so this is more of a discussion than an answer...

The first issue I see is that you're launching the form for a new BoatType__c, when you should be launching a new Boat__c.

What happens if you don't select a boat type and you press the New button? I was getting an error, because it was trying to fill a default field parameter with a "" value; I added an if statement to my controller so it only fills the boatType field if there's a boat type selected.

It looks like we're handling the conditional appearance of the NEW button the same way, but I'm very cloudy on what the instructions are asking for: what standalone app are they talking about?
"The form’s controller checks whether the event.force:createRecord event is supported by a standalone app and either shows or hides the New button according to best practices." 


Here's my code (so far):
BoatSearchForm.cmp
<aura:component description="BoatSearchForm"
        controller="BoatSearchFormController"
        implements="flexipage:availableForAllPageTypes">

    <aura:registerEvent name="launchNewBoatForm" type="c:launchNewBoatForm"/>

    <!-- Handle component init in a client-side controller -->
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    <aura:handler name="launchNewBoatForm" event="c:launchNewBoatForm" action="{!c.handleNewBoatForm}"/>

    <!-- dynamically load the BoatTypes -->
    <aura:attribute name="BoatTypes" type="BoatType__c[]" />
    <aura:attribute name="selectedType" type="String" default="foo"/>
    <aura:attribute name="renderNew" type="Boolean" default="true"/>

    <article class="slds-card slds-m-bottom_large">
        <div class="slds-media__body">
            <div >

                <lightning:layout horizontalAlign="center" verticalAlign="center">
                    <lightning:layoutItem padding="horizontal-medium" >
                        <!-- Create a dropdown menu with options -->
                        <lightning:select aura:id="boatTypes" label="" name="selectType"
                                          onchange="{!c.handleChange}">
                            <option value="">All Types</option>
                            <aura:iteration items="{!v.BoatTypes}" var="boatType">
                                <option value="{!boatType.Id}" text="{!boatType.Name}"/>
                            </aura:iteration>
                        </lightning:select>

                    </lightning:layoutItem>


                    <lightning:layoutItem>
                        <div class="slds-button-group" role="group">
                            <lightning:button class="slds-button" variant="brand" label="Search" onclick="{!c.search}"/>

            <!--
            The form’s controller checks whether the event.force:createRecord event
            is supported by a standalone app and either shows or hides the New button
            according to best practices.
            -->

                            <aura:if isTrue="{!v.renderNew}">
                                <lightning:button class="slds-button" variant="neutral" label="New" onclick="{!c.newBoat}"/>
                            </aura:if>
                        </div>
                    </lightning:layoutItem>
                </lightning:layout>
            </div>
        </div>
    </article>

</aura:component>
BoatSearchFormController.js
({
    doInit : function(component, event, helper){

        helper.loadBoatTypes(component);
    },

    handleChange : function(component, event, helper){
        console.log(component.find("boatTypes").get("v.value"));
        component.set("v.selectedType", component.find("boatTypes").get("v.value"));
    },

    search : function(component, event, helper){
        var selectedType = component.get("v.selectedType");
        console.log("Search button pressed " + selectedType)
    },

    newBoat : function(component, event, helper){
        var boatTypeId = component.get("v.selectedType");
        console.log("New button pressed " + boatTypeId);
        var requestNewBoat = component.getEvent("launchNewBoatForm");
        requestNewBoat.setParams({"boatTypeId": boatTypeId});
        requestNewBoat.fire();
    },

    handleNewBoatForm: function(component, event, helper){
        console.log("handleNewBoatForm handler called.")
        var boatTypeId = component.get("v.selectedType");

        console.log(boatTypeId);
        var createNewBoat = $A.get("e.force:createRecord");
        createNewBoat.setParams({
            "entityApiName": "Boat__c",
        })
        if(! boatTypeId==""){
            createNewBoat.setParams({
                "defaultFieldValues": {'BoatType__c': boatTypeId}
           })
        }
        createNewBoat.fire();
    },
    //more handlers here
})
BoatSearchFormHelper.js
({
    loadBoatTypes: function(component){
    //create the action
            console.log("Helper started");
            var action = component.get("c.getBoatTypes");

            //add the callback behavior for when the response is received
            action.setCallback(this,function(response){
            var state = response.getState();
            if (state === "SUCCESS"){
                component.set("v.BoatTypes", response.getReturnValue());
                console.log(response.getReturnValue());
                }
                else {
                console.log("Failed with state: " + state);
                }
            });

            //send action off to be executed
            $A.enqueueAction(action);
       },
})
BoatSearchFormController (Apex class)
public with sharing class BoatSearchFormController
{
    @AuraEnabled
    public static List<BoatType__c> getBoatTypes()
    {
        return [SELECT Id, Name from BoatType__c ORDER BY Name];
    }
}




 

All Answers

PatMcClellan__cPatMcClellan__c
Hey Brian, I'm working on the same challenge, so this is more of a discussion than an answer...

The first issue I see is that you're launching the form for a new BoatType__c, when you should be launching a new Boat__c.

What happens if you don't select a boat type and you press the New button? I was getting an error, because it was trying to fill a default field parameter with a "" value; I added an if statement to my controller so it only fills the boatType field if there's a boat type selected.

It looks like we're handling the conditional appearance of the NEW button the same way, but I'm very cloudy on what the instructions are asking for: what standalone app are they talking about?
"The form’s controller checks whether the event.force:createRecord event is supported by a standalone app and either shows or hides the New button according to best practices." 


Here's my code (so far):
BoatSearchForm.cmp
<aura:component description="BoatSearchForm"
        controller="BoatSearchFormController"
        implements="flexipage:availableForAllPageTypes">

    <aura:registerEvent name="launchNewBoatForm" type="c:launchNewBoatForm"/>

    <!-- Handle component init in a client-side controller -->
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    <aura:handler name="launchNewBoatForm" event="c:launchNewBoatForm" action="{!c.handleNewBoatForm}"/>

    <!-- dynamically load the BoatTypes -->
    <aura:attribute name="BoatTypes" type="BoatType__c[]" />
    <aura:attribute name="selectedType" type="String" default="foo"/>
    <aura:attribute name="renderNew" type="Boolean" default="true"/>

    <article class="slds-card slds-m-bottom_large">
        <div class="slds-media__body">
            <div >

                <lightning:layout horizontalAlign="center" verticalAlign="center">
                    <lightning:layoutItem padding="horizontal-medium" >
                        <!-- Create a dropdown menu with options -->
                        <lightning:select aura:id="boatTypes" label="" name="selectType"
                                          onchange="{!c.handleChange}">
                            <option value="">All Types</option>
                            <aura:iteration items="{!v.BoatTypes}" var="boatType">
                                <option value="{!boatType.Id}" text="{!boatType.Name}"/>
                            </aura:iteration>
                        </lightning:select>

                    </lightning:layoutItem>


                    <lightning:layoutItem>
                        <div class="slds-button-group" role="group">
                            <lightning:button class="slds-button" variant="brand" label="Search" onclick="{!c.search}"/>

            <!--
            The form’s controller checks whether the event.force:createRecord event
            is supported by a standalone app and either shows or hides the New button
            according to best practices.
            -->

                            <aura:if isTrue="{!v.renderNew}">
                                <lightning:button class="slds-button" variant="neutral" label="New" onclick="{!c.newBoat}"/>
                            </aura:if>
                        </div>
                    </lightning:layoutItem>
                </lightning:layout>
            </div>
        </div>
    </article>

</aura:component>
BoatSearchFormController.js
({
    doInit : function(component, event, helper){

        helper.loadBoatTypes(component);
    },

    handleChange : function(component, event, helper){
        console.log(component.find("boatTypes").get("v.value"));
        component.set("v.selectedType", component.find("boatTypes").get("v.value"));
    },

    search : function(component, event, helper){
        var selectedType = component.get("v.selectedType");
        console.log("Search button pressed " + selectedType)
    },

    newBoat : function(component, event, helper){
        var boatTypeId = component.get("v.selectedType");
        console.log("New button pressed " + boatTypeId);
        var requestNewBoat = component.getEvent("launchNewBoatForm");
        requestNewBoat.setParams({"boatTypeId": boatTypeId});
        requestNewBoat.fire();
    },

    handleNewBoatForm: function(component, event, helper){
        console.log("handleNewBoatForm handler called.")
        var boatTypeId = component.get("v.selectedType");

        console.log(boatTypeId);
        var createNewBoat = $A.get("e.force:createRecord");
        createNewBoat.setParams({
            "entityApiName": "Boat__c",
        })
        if(! boatTypeId==""){
            createNewBoat.setParams({
                "defaultFieldValues": {'BoatType__c': boatTypeId}
           })
        }
        createNewBoat.fire();
    },
    //more handlers here
})
BoatSearchFormHelper.js
({
    loadBoatTypes: function(component){
    //create the action
            console.log("Helper started");
            var action = component.get("c.getBoatTypes");

            //add the callback behavior for when the response is received
            action.setCallback(this,function(response){
            var state = response.getState();
            if (state === "SUCCESS"){
                component.set("v.BoatTypes", response.getReturnValue());
                console.log(response.getReturnValue());
                }
                else {
                console.log("Failed with state: " + state);
                }
            });

            //send action off to be executed
            $A.enqueueAction(action);
       },
})
BoatSearchFormController (Apex class)
public with sharing class BoatSearchFormController
{
    @AuraEnabled
    public static List<BoatType__c> getBoatTypes()
    {
        return [SELECT Id, Name from BoatType__c ORDER BY Name];
    }
}




 
This was selected as the best answer
PatMcClellan__cPatMcClellan__c
I found an answer about the standalone app reference, here: https://success.salesforce.com/answers?id=9063A000000lBcJQAU. See Chad V's answer.
 
PatMcClellan__cPatMcClellan__c
@Brian, one other thing I'm doing different than your code is the value of the BoatType selector options. I'm plugging in the BoatType.Id rather than the name, so I can provide the Id value as BoatType__c in the new Boat__c form. 

I'd suggest you be very specific about the difference between a Boat and a BoatType. You're using the var name "boat" in your selector, when in fact, they are boatTypes. It won't break your code in the selector, but changing it to boatType will help your own clarity.
PatMcClellan__cPatMcClellan__c
For some reason, my selector is now out of vertical alignment with my buttons. Can't figure it out. Here's an image, with markup below. Any ideas?

User-added image
 
<aura:component description="BoatSearchForm"
    controller="BoatSearchFormController"
    implements="flexipage:availableForAllPageTypes">

<aura:registerEvent name="launchNewBoatForm" type="c:launchNewBoatForm"/>

<!-- Handle component init in a client-side controller -->
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
<aura:handler name="launchNewBoatForm" event="c:launchNewBoatForm" action="{!c.handleNewBoatForm}"/>

<!-- dynamically load the BoatTypes -->
<aura:attribute name="BoatTypes" type="BoatType__c[]" />
<aura:attribute name="selectedType" type="String" default="foo"/>
<aura:attribute name="standAlone" type="Boolean" default="true"/>

<article class="slds-card slds-m-bottom_medium">
    <div class="slds-media__body">
        <div>

            <lightning:layout horizontalAlign="center" verticalAlign="center" >
                <lightning:layoutItem padding="horizontal-medium">
                    <!-- Create a dropdown menu with options -->
                    <lightning:select aura:id="boatTypes" label="" name="selectType"
                                      onchange="{!c.handleChange}">
                        <option value="">All Types</option>
                        <aura:iteration items="{!v.BoatTypes}" var="boatType">
                            <option value="{!boatType.Id}">{!boatType.Name}</option>
                        </aura:iteration>
                    </lightning:select>

                </lightning:layoutItem>


                <lightning:layoutItem>

                    <div class="slds-button-group" role="group">
                        <lightning:button class="slds-button" variant="brand" label="Search" onclick="{!c.search}"/>

                        <aura:if isTrue="{!v.standAlone}">
                            <lightning:button class="slds-button" variant="neutral" label="New" onclick="{!c.newBoat}"/>
                        </aura:if>
                    </div>
                </lightning:layoutItem>
            </lightning:layout>
        </div>
    </div>
</article>

</aura:component>

 
PatMcClellan__cPatMcClellan__c
FYI, I changed the verticalAlign="end" and that fixed it. Odd.
Brian KesslerBrian Kessler
Hi @Pat,

Thanks for all the great input.
Great call on the Boat/Boat Type confusion.

Allow me to repay the favour by suggesting the use of JavaScript ES6 maps instead of a second handler:

Here is my present code:

Markup:
<aura:component controller="BoatSearchFormAuraCtrl" >
	<aura:attribute access="private" name="boatTypeList" type="BoatType__c[]" default="[]" />
	<aura:attribute access="private" name="boatTypeIdByNameMap" type="Map" />
	<aura:attribute access="private" name="selectedBoatType" type="BoatType__c" />
	<aura:attribute access="private" name="showNewButton" type="Boolean" default="false" />
	
	<aura:handler name="init" value="{!this}" action="{!c.doInit}" />
	
	<h2 class="slds-page-header__title">Find a Boat</h2>
	<form>
		<lightning:layout horizontalAlign="center" verticalAlign="center">
		    <lightning:select name="select" value="{!v.selectedBoatType}">
		        <option value="">All Types</option>
		        <aura:iteration items="{!v.boatTypeList}" var="boatType">
		            <option value="{!boatType.Name}">{!boatType.Name}</option>
		        </aura:iteration> 
		    </lightning:select>
		    <lightning:button name="Search" label="Search" variant="brand" />
		    <aura:if isTrue="{!v.showNewButton}">
		    	<lightning:button name="New" label="New" variant="neutral" onclick="{!c.createBoat}"/>
		    </aura:if>
		</lightning:layout>
	</form>	
</aura:component>

Controller:
({
	doInit : function(component, event, helper) {
		component.set('v.showNewButton', $A.get('e.force:createRecord'));
		helper.setBoatTypeCollections(component);
	},
	
	createBoat: function (component) {
		var createRecordEvent = $A.get('e.force:createRecord');
		var selectedBoatType = component.get('v.selectedBoatType');
		var selectedBoatTypeId = (selectedBoatType)
			? component.get('v.boatTypeIdByNameMap').get(selectedBoatType)
			: null;
		
		createRecordEvent.setParams({
			'entityApiName' : 'Boat__c',
			'defaultFieldValues': {
				'BoatType__c': selectedBoatTypeId
			},
		});
		createRecordEvent.fire();
	}
})

Helper:
({
    setBoatTypeCollections : function(component) {
        var action = component.get('c.getBoatTypes');
        action.setCallback(this, function(response){
            if (response.getState() === 'SUCCESS') {
                component.set('v.boatTypeList', response.getReturnValue());
                this.setBoatTypeIdByNameMap(component);
            }
        });
        $A.enqueueAction(action);
    },

    setBoatTypeIdByNameMap : function(component) {
        var boatTypeList = component.get('v.boatTypeList');
        var boatTypeIdByNameMap = new Map();
        for (var i = 0; i < boatTypeList.length; i++) {
            var boatType = boatTypeList[i];
            boatTypeIdByNameMap.set(boatType.Name, boatType.Id);
        }
        component.set('v.boatTypeIdByNameMap', boatTypeIdByNameMap);
    }
})



I'm still not passing yet as "Friends with Boats" is apparently both the right and wrong name for this Lightning application.  :-/
... But I'll start a clean, new thread for that....

 
PatMcClellan__cPatMcClellan__c
Hey Brian, thanks for the map suggestion, but I didn't find it necessary. I simply plugged the boatType.id into the value inside the aura:iteration, while displaying boatType.name. That way, the id is directly accessible in component.find("boatTypes").get("v.value").

However, you got me thinking about maps, and I believe that's going to be really handy in future steps. I'm thinking that the first time
BoatSearchResults.cmp loads and its controller calls to load all boats (because All Types is the default), I can load that SOQL result into a map:
{boatTypeId: {list<boats>}}. Then, subsequent searches can simply pull results from that map instead of making another (redundant) SOQL call. I'm not sure if that aligns to the instructions... haven't gotten that far yet.
Brian KesslerBrian Kessler
I tried plugging boatType.Id directly into my aura:iteration, but something seemed to be going wrong and my selections never seemed to take.  I thought it was because the var was a Object/Boat__c while the value was an Id/String...   Perhaps I should have used boatType.id instead of boatType.Id (or vice-versa).... It is really frustrating that the API names can be unpredictable.

Maps are really useful for so many things... unfortunately, Lightning support for them is limited.  For example, you can't iterate over them.  :-(
But, yes, you can definitely use maps to reduce your need for SOQL statements.

Whether it conflicts with the instructions, I do wish they'd learn to test for results rather than implementation details.  It pisses me off that they enforce bad practices.
 
@jeronimoburgers ☁@jeronimoburgers ☁
Banging my head over the 2nd step of the supber badge. Functionality-wise the BoatSeachForm.cmp component does exactly what it's supposed to to. Pretty much along the lines of Patt's code. 

Still... "Challenge Not yet complete... here's what's wrong: The BoatSearchForm component's "New" button doesn't launch the new boat page with the correct default field values using logic in its controller."
 
, handleNew : function (component, event, helper) {
		var selectedBoatTypeId = component.get("v.selectedBoatTypeId");
        var createRecordEvent = $A.get("e.force:createRecord");
    	createRecordEvent.setParams({
        	"entityApiName": "Boat__c",
    	});
        
   
        if (!selectedBoatTypeId=="") {
            createRecordEvent.setParams({
            	"defaultFieldValues": {"BoatType__c": selectedBoatTypeId}
    		});
		}
        
    	createRecordEvent.fire();
	}

Thanks by the way for tackling the question arround how to hide the New button based on the context. Learnt something today. Attribute type could be a function reference ;-)
component.set("v.showNewButton", $A.get("e.force:createRecord"));
Anyways - anybody in the clear on this Superbadge... help appreciated!

Jeroen
 
@jeronimoburgers ☁@jeronimoburgers ☁
The root-cause with regards to me not being able to complete the Step 2? 

--> I put my code in helper functions, per best practice. That made the test to fail. Moving the code to the controller, it worked immediately.
@jeronimoburgers ☁@jeronimoburgers ☁
Paras, It might be as simple that you’re declaration of your controller function should follow foo(component, event, helper) instead of foo(component). I had my code in a helper and it failed though functionally and technically correct. Just try. Jeroen
Paras Shah 15Paras Shah 15
I have completed the challenge by updating the code by myself.

Anyways Thanks Jeroen for your response
shashi kumar 58shashi kumar 58
Lightning Component Framework Specialist superbadge step 2 
Solution please follow the below links where i have given all the steps:--
https://developer.salesforce.com/forums/ForumsMain?id=9060G0000005NG8QAM
sai sekhar 1sai sekhar 1
@paras Shah 15 can you please provide your version i got stuck.
Manish Anand 22Manish Anand 22
Hi All,

I am stuck in challenge 2. Functionality is working as expected. But I get error as - Challenge Not yet complete... here's what's wrong: 
The BoatSearch component should instantiate the BoatSearchForm and BoatSearchResults components.

Below is my BoatSearch component.
<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes" access="global" >
    <lightning:card title="Find a Boat"  class="slds-m-top_10px">
        <c.BoatSearchForm/>
    </lightning:card>
    <lightning:card title="Matching Boats">
        <c.BoatSearchResults/>
    </lightning:card>
</aura:component>
Any lead is apprciated.

Thanks,
Manish.
PatMcClellan__cPatMcClellan__c
@Manish, I know the problem, and it's a source of confusion for many. When you call a component, you have to use a colon after the c, instead of a period.  
Vipul Sharma 15Vipul Sharma 15
Hi All, 
I am able to pass Step 2 , it uses less code, here we are focusing on main component BoatSearchForm, so below is its component and controller
<aura:component implements="flexipage:availableForAllPageTypes" controller="BoatType" >
    <aura:attribute name ="BoatType" type="BoatType__c[]" />
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    <aura:attribute name ="selectedBoat" type="BoatType__c" />
    <aura:attribute name ="showNew" type="Boolean" />
    <aura:attribute name ="BTselectedValue" type="String" />
    
    <lightning:layout horizontalAlign= "center">
        <lightning:select name="select" value="{!v.BTselectedValue}" >
            <option value="">All Types</option>
            <aura:iteration items="{!v.BoatType}" var="t">
                <option value="{!t.Id}">{!t.Name}</option>
            </aura:iteration>
        </lightning:select>
        <lightning:button variant="brand" label="Search" />
        <aura:if isTrue="{!v.showNew}">
            <lightning:button variant="Neutral" label="New" onclick="{!c.handleClickNew}" />
        </aura:if>
    </lightning:layout>
</aura:component>

controller:
({
    doInit : function(component, event, helper) {
        var isEnabled = $A.get("e.force:createRecord");
        if(isEnabled){
            component.set("v.showNew",true);
        } 
        var action = component.get("c.getBoatNames");
        action.setCallback(this, function(response){
            if(response.getState() === "SUCCESS"){
                component.set("v.BoatType",response.getReturnValue());
            }
        });
        $A.enqueueAction(action);
    },
    handleClickNew : function(component, event, helper) {
        var navEvt = $A.get("e.force:createRecord");
        navEvt.setParams({"entityApiName":"Boat__c"});
        if(component.get("v.BTselectedValue") != ""){
            navEvt.setParams({"defaultFieldValues": {"BoatType__c": component.get("v.BTselectedValue")}});
        }
        navEvt.fire();
    }
})
 
Any advices ..  ... :)