+ Start a Discussion
Denise CrosbyDenise Crosby 

lightning component to create and save event

Hello Salesforce experts,
I am creating my first lightning component to get a list of contacts from an account, display input fields for an event subject and description and then create/save that event back to Salesforce. I am stuck on the creation of the event. I may be missing something very basic. Was wondering if anyone could point me in the right direction.

ContactListController.aspx
public with sharing class ContactListController {
    @AuraEnabled
    public static List<Contact> getContacts(Id recordId) {
        return [Select Id, FirstName, LastName, Email, Phone, Title From Contact Where AccountId = :recordId];
    }
}
NewMeeting.cmp
<aura:component controller="ContactListController" implements="force:lightningQuickAction,force:hasRecordId" access="global">
    
    <aura:attribute name="recordId" type="Id" />
    <aura:attribute name="Account" type="Account" />
    <aura:attribute name="Contacts" type="Contact" />
    <aura:attribute name="Columns" type="List" />
    
 <!--   <aura:attribute name="EventID" type="String" access="Public" default=""/>   
    <aura:attribute name="newEvent" type="Object" access="Private" />
    <aura:attribute name="newEventFields" type="Object" access="Private" />   -->
    
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    
    <force:recordData aura:id="accountRecord"
                      recordId="{!v.recordId}"
                      targetFields="{!v.Account}"
                      layoutType="FULL"
                      />

    <lightning:card iconName="standard:contact" title="{! 'New Meeting for ' + v.Account.Name}">
   <!--     <lightning:input Name="Subject" value="{!v.newEventFields.Subject}" label="Subject" placeholder="Subject" /> -->
        <lightning:input Name="Subject" value="Subject" label="Subject" placeholder="Subject" /> 

        <br/>
        <lightning:textarea Name="Desc" value="Notes from your meeting" label="Description" placeholder="Notes from your meeting" />
        <br/>
        <lightning:datatable data="{! v.Contacts }" columns="{! v.Columns }"  />
    </lightning:card>
    
    <div class="slds-text-align_center">
        <lightning:button variant="brand" label="Save" onclick="{!c.onSave}" />
    </div>
    
</aura:component>

NewMeetingController.js
({
    doInit : function(component, event, helper) {
        helper.onInit(component,event,helper);
    }
})

NewMeetingHelper.js
({
    onInit: function(component,event,helper) {
        component.set("v.Columns",[
            {label:"First Name", fieldName:"FirstName", type:"text"},
            {label:"Last Name", fieldName:"LastName", type:"text"},
            {label:"Title", fieldName:"Title", type:"text"}
        ]);
        
        var action = component.get("c.getContacts");
        action.setParams({
            recordId: component.get("v.recordId")
        });
        action.setCallback(this, function(data) {
            component.set("v.Contacts", data.getReturnValue()); 
        });
        $A.enqueueAction(action);
    }
})

I tried doing something like this in my helper Javascript, but couldn't get it working:
if (component.get('v.EventID') == '') { 
            component.find("newEvent").getNewRecord(
                "Event", // sObject type (objectApiName)
                null,      // recordTypeId
                false,     // skip cache?
                $A.getCallback(function() {
                    var rec = component.get("v.newEvent");
                	})
            )}






 
Best Answer chosen by Denise Crosby
Alain CabonAlain Cabon
Hi Denise,

The promises are not necessary here (for next time, with complicated callouts for instance).

action.setParams({
     evt: myevents[0],
     eventRelations: myeventrelations
});
public with sharing class NewMeetingLEX {
    @AuraEnabled
    public static List<Contact> getContacts(Id recordId) {
        return [Select Id, FirstName, LastName, Email, Phone, Title, OwnerId From Contact Where AccountId = :recordId];
    }
    
    @AuraEnabled
    public static String saveMainEvent(Event evt) {
        system.debug('myevent:' + evt);

        insert evt;
        return evt.id;
    }

    @AuraEnabled
    public static String saveEventRelations(List<EventRelation> eventRelations) {
        for (EventRelation er:eventRelations) {
            system.debug('myeventrelation:' + er);
        }
        insert eventRelations;
        return null;
        
    }
    
     @AuraEnabled
    public static String saveTotalEvent(Event evt,List<EventRelation> eventRelations) {
        system.debug('myevent:' + evt);
        insert evt;
        for (EventRelation er:eventRelations) {
            system.debug('myeventrelation:' + er);
            er.EventId = evt.id;
        }
        insert eventRelations;
        return null;     
    }
}

onSave : function(component, event, helper) {
        // if(helper.validateEventForm(component)) {
        //  component.set("v.simpleNewEvent.WhatId", component.get("v.recordId"));
        var contactselected =  component.find("contacts").getSelectedRows();
        var toastEvent = $A.get("e.force:showToast");
        
        var subjectCmp = component.find("subject");
        var subjectvalue = subjectCmp.get("v.value");
        
        var startDateCmp = component.find("startDate");
        var startDatevalue = startDateCmp.get("v.value");        
        
        var endDateCmp = component.find("endDate");
        var endDatevalue = endDateCmp.get("v.value"); 
        
        if (contactselected == null || contactselected.length == 0) {
            component.set("v.error", "Please choose at least one contact");
            
            toastEvent.setParams({
                "title": "Missing contacts!",
                "message": "Please choose at least one contact",
                "type":"error"
            });
            toastEvent.fire();
            
        } 
        else             
            if (subjectvalue == null || subjectvalue == "") {
                
                component.set("v.error", "Please enter a subject");
                
                toastEvent.setParams({
                    "title": "Missing subject!",
                    "message": "Please enter a subject",
                    "type":"error"
                });
                toastEvent.fire();            
            }
        
            else if (endDatevalue < startDatevalue) {
                component.set("v.error", "Start time is after end time");
                
                toastEvent.setParams({
                    "title": "Datetime error!",
                    "message": "Start time is after end time",
                    "type":"error"
                });
                toastEvent.fire();           
            }        
        
                else {
                    console.log('selected:' + contactselected[0].Id);
                    var myrecordId = component.get("v.recordId");
                    var myevents = [];
                    var myeventrelations = [];
                    var newEvent = component.get("v.newEvent");
                    
                    console.log(contactselected[0].Id);
                    myevents.push({ 'sobjectType':'Event',
                                   'WhoId': contactselected[0].Id, 
                                   'WhatId': myrecordId,
                                   'Subject': newEvent.Subject,
                                   'Description': newEvent.Description,
                                   'StartDateTime': newEvent.StartDateTime,
                                   'EndDateTime': newEvent.EndDateTime
                                  });
                    
                    console.log('myevents:' + myevents.length);
                    var i;
                    for (i=1; i<contactselected.length; i++) {
                        myeventrelations.push({ 'sobjectType':'EventRelation',
                                               'AccountId': myrecordId,
                                               'IsParent': true,
                                               'IsInvitee' : false,
                                               'IsWhat': false,
                                               'RelationId': contactselected[i].Id                                               
                                              });
                        
                    }
                    var action = component.get("c.saveTotalEvent");
                    action.setParams({
                        evt: myevents[0],
                        eventRelations: myeventrelations
                    });
                    action.setCallback(this, function(response) {
                        var state = response.getState();
                        if (component.isValid() && state == 'SUCCESS') {   
                            
                            toastEvent.setParams({
                                "title": "Success",
                                "message": "Meeting saved!",
                                "type":"success"
                            });
                            toastEvent.fire();
                            $A.get("e.force:closeQuickAction").fire();
                        } else {
                            component.set("v.error", response.getState()); 
                        } 
                    });  
                    $A.enqueueAction(action);
                } 
    },

We didn't get feedbacks from other people for improving the current code.

Your code works and seems stable (it is already a victory with Lex components).

Alain

All Answers

Alain CabonAlain Cabon
Hi Denise,

Behind the force:recordData and getNewRecord, you have the very new Beta Lightning Data Service

Lightning Data Service is powerful and simple to use. However, it’s not a complete replacement for writing your own data access code. Here are some considerations to keep in mind when using it.

Supported Objects: Lightning Data Service supports custom objects and the following.
https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/data_service_considerations.htm

... and Event is not supported by the Data Service (you tried something ... impossible by that way).

We can develop the creation of Event in LEX with the "old" alternative (an apex controller, the one you used for the reading) without the very new Data Service even if Salesforce is very proud of it and promotes that way all the time.
 
Alain CabonAlain Cabon
Hi Denise,

Here is a solution which "works". As I said, there are always many problems with the Lightning components.

A new annoying problem with the toasts and a popup in Lex (among many other, Beta)

Lightning Component force:showToast event displays toast message behind action window in Lightning Experience and Salesforce1​
https://success.salesforce.com/issues_view?id=a1p3A000000mCRqQAM&title=lightning-component-force-showtoast-displays-toast-message-behind-action-window-in-lightning-experience-and-salesforce1

Component: new version WithoutHeader.
<aura:component controller="ContactListController" implements="force:lightningQuickActionWithoutHeader,force:hasRecordId" access="global">
    
    <aura:attribute name="recordId" type="Id" />
    <aura:attribute name="Account" type="Account" />
    <aura:attribute name="Contacts" type="Contact" />
    <aura:attribute name="Columns" type="List" />
    
    <aura:attribute access="private" name="error" type="String" default=""/>
   
    <aura:attribute name="newEvent" type="Event" default="{'sobjectType':'Event'}"/>
    
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    
    <force:recordData aura:id="accountRecord"
                      recordId="{!v.recordId}"
                      targetFields="{!v.Account}"
                      layoutType="FULL"
                      />
    
    <lightning:card iconName="standard:contact" title="{! 'New Meeting for ' + v.Account.Name}">
        <aura:set attribute="actions">  
            <div class="slds-text-align_center">
                <lightning:button variant="brand" label="Save" onclick="{!c.onSave}" />
                <lightning:button variant="brand" label="Cancel" onclick="{!c.onCancel}" />
            </div>
        </aura:set>
        <aura:set attribute="footer">  
            <div class="slds-text-align_center">
                <lightning:button variant="brand" label="Save" onclick="{!c.onSave}" />
                <lightning:button variant="brand" label="Cancel" onclick="{!c.onCancel}" />
            </div>
        </aura:set>
        <aura:if isTrue="{! !empty(v.error)}">
            <ui:message title="Error" severity="error" closable="true">
                {!v.error}
            </ui:message>
        </aura:if>
        <form class="slds-form--stacked">  
            <div class="slds-form-element">
                <lightning:input name="Subject" value="{!v.newEvent.Subject}" label="Subject" placeholder="Subject" />    
            </div>
            <div class="slds-form-element">
                <lightning:textarea name="Description" value="{!v.newEvent.Description}" label="Description" placeholder="Description" />    
            </div>
            <div class="slds-form-element">
                <ui:inputDateTime aura:id="startDate" label="Start" class="field" value="{!v.newEvent.StartDateTime}" displayDatePicker="true"/>
            </div>
            <div class="slds-form-element">
                <ui:inputDateTime aura:id="endDate" label="End" class="field" value="{!v.newEvent.EndDateTime}" displayDatePicker="true"/>
            </div>
            
            <div class="slds-form-element">
                <lightning:datatable aura:id="contacts" data="{! v.Contacts }" columns="{! v.Columns }"  />
            </div> 
            
        </form>      
    </lightning:card>  
</aura:component>
ContactListController​:
public with sharing class ContactListController {
    @AuraEnabled
    public static List<Contact> getContacts(Id recordId) {
        return [Select Id, FirstName, LastName, Email, Phone, Title, OwnerId From Contact Where AccountId = :recordId];
    }
    @AuraEnabled
    public static String saveEvents(List<Event> events) {
        for (Event evt:events) {
            system.debug('myevent:' + evt);
        }
        insert events;
        return null;
    }
}
JS controller: onSave is the part that you want.
({
    doInit : function(component, event, helper) {
        // helper.onInit(component,event,helper);
        var currenttime = new Date().toISOString();
        var currentime2 = new Date();
        currentime2.setMinutes(currentime2.getMinutes() + 30);
        var currentime_30min = currentime2.toISOString();
        component.set("v.newEvent.StartDateTime",currenttime);
        component.set("v.newEvent.EndDateTime", currentime_30min);
        
        component.set("v.Columns",[
            {label:"First Name", fieldName:"FirstName", type:"text"},
            {label:"Last Name", fieldName:"LastName", type:"text"},
            {label:"Title", fieldName:"Title", type:"text"}
        ]);
        
        var action = component.get("c.getContacts");
        action.setParams({
            recordId: component.get("v.recordId")
        });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (component.isValid() && state == 'SUCCESS') {   
                component.set("v.Contacts", response.getReturnValue()); 
            }           
        });
        $A.enqueueAction(action);
    },
    onSave : function(component, event, helper) {
        // if(helper.validateEventForm(component)) {
        component.set("v.simpleNewEvent.WhatId", component.get("v.recordId"));
        var contactselected =  component.find("contacts").getSelectedRows();
        var toastEvent = $A.get("e.force:showToast");
        if (contactselected == null || contactselected.length == 0) {
            component.set("v.error", "Please choose at least one contact");
                     
            toastEvent.setParams({
                "title": "Missing contacts!",
                "message": "Please choose at least one contact",
                "type":"error"
            });
            toastEvent.fire();
            
        } else {
            console.log('selected:' + contactselected[0].Id);
            var myrecordId = component.get("v.recordId");
            var myevents = [];
            var newEvent = component.get("v.newEvent");
            contactselected.forEach(function(contact)  { 
                console.log(contact);
                myevents.push({ 'sobjectType':'Event',
                               'WhoId': contact.Id, 
                               'WhatId': myrecordId,
                               'Subject': newEvent.Subject,
                               'Description': newEvent.Description,
                               'StartDateTime': newEvent.StartDateTime,
                               'EndDateTime': newEvent.EndDateTime
                              });
            });
            console.log('myevents:' + myevents.length);
            var action = component.get("c.saveEvents");
            action.setParams({
                events: myevents
            });
            action.setCallback(this, function(response) {
                var state = response.getState();
                if (component.isValid() && state == 'SUCCESS') {   
                    toastEvent.setParams({
                        "title": "Event saved!",
                        "message": "Be happy, Denise! Events created ...",
                        "type":"success"
                    });
                    toastEvent.fire();
                    $A.get("e.force:closeQuickAction").fire();
                } else {
                    component.set("v.error", response.getState()); 
                }         
            });
            $A.enqueueAction(action);      
        }
        //  }
    } ,
    onCancel : function(component, event, helper) {
          $A.get("e.force:closeQuickAction").fire();
    }
})

Still missing: 
  1. validation rules of the form if(helper.validateEventForm(component)) 
  2. better return value for: public static String saveEvents(List<Event> events)

I hope that will help you.

Alain
Alain CabonAlain Cabon
component.set("v.simpleNewEvent.WhatId", component.get("v.recordId")); 

Useless above.
Denise CrosbyDenise Crosby
Thank you so much Alain! Your toast made me laugh :)
OK, I got it to work. I'm going to make sure I understand everything that you did and may try to create just one event with multiple contacts if multiple contacts are selected... thank you!!!
And thanks for the explanation. LEX has a long way to go...but we are already on LEX and they want mobile development, so I have to learn it!
Thanks again...awesome!
Denise
Alain CabonAlain Cabon
You can select multiple contacts. Before going in production, you will have to verify or enforce the validation rules (end date time > start date time, the subject is not empty and so on as you know very well). Your question was about the saving of the events.

Let me know for the problems with the current code and I could try to solve these new problems (but not the toast message behind the action window for example, these problems are exhausting and time consuming; is it my code or is it a new issue  of Lex?).

It would be interesting if other people express their points of view on the Lex components but I feel lonely here.

You just want to know if the data service was usable here at the beginning. Other people could have a point of view, but in fact, a relatively few number of people are using these Lex components (it is difficult to overcome certain obstacles) and there is again a lot of "false lex" with underlying old VF Pages and just a new charter SLDS.

I tried answering another question here to initiate the basis of the discussion (without success):
https://developer.salesforce.com/forums/ForumsMain?id=9060G000000MOcrQAG
 
Alain
Denise CrosbyDenise Crosby
Hi Alain,
Thanks for the explanation and code. I see the limitations of LEX. Hopefully some improvements will come soon. I was able to modify your code to save a single event (with eventRelations) for multiple contacts. I still need to do the validations and it's looking good. 
Alain CabonAlain Cabon
Hi Denise,

Your problem was to create only one event with mutliple attendees. My code adds different event for each contact that was not your need indeed.
Another problem with Lex is that you don't have a real lookup component like in the "point and click" layout (mutli-objects combobox + pills).
So you have created only one "event" (outside the Data Service) and mutliple "eventrelation"(s) for your solution.

The creation of event relations is a better choice (real meeting) than multiple events with one different contact (spam) and was clearly missing in my solution (I missed this obvious part for your need).

It is interesting for the forum if you post your creation of the event relations (and choose your solution as best answer).

You have already tested many options of Lex and built a real solution for this event.

The big problem is the missing of a real lookup component like below (perhaps an open-source on github?) instead of a simple list even this alternative is heavily used and sufficient.

This custom lookup is too basic (not multi contacts possible)
https://developer.salesforce.com/blogs/developer-relations/2015/06/salesforce-lightning-inputlookup-missing-component.html

A real Aura lookup component  would be like below: (impossible to do with the current avalaible components). 
It is the mix of a mutli-object combobox and pills (that seems very difficult to write even for Salesforce and its army of developers):

User-added image

https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/aura_compref_lightning_pill.htm

https://www.lightningdesignsystem.com/components/pills

Alain
Denise CrosbyDenise Crosby
Here is the code I ended up with. Certainly not perfect (I'm new at Javascript), but it works to create one event with multiple contacts using an Event and EventRelations for additional contacts.

NewMeetingLEX.aspx
public with sharing class NewMeetingLEX {
    @AuraEnabled
    public static List<Contact> getContacts(Id recordId) {
        return [Select Id, FirstName, LastName, Email, Phone, Title, OwnerId From Contact Where AccountId = :recordId];
    }
    
    @AuraEnabled
    public static String saveMainEvent(Event evt) {
        system.debug('myevent:' + evt);

        insert evt;
        return evt.id;
    }

    @AuraEnabled
    public static String saveEventRelations(List<EventRelation> eventRelations) {
        for (EventRelation er:eventRelations) {
            system.debug('myeventrelation:' + er);
        }
        insert eventRelations;
        return null;
        
    }

}

NewMeetingLEX.cmp
 
<aura:component controller="NewMeetingLEX" implements="force:lightningQuickActionWithoutHeader,force:hasRecordId" access="global">
    
    <aura:attribute name="recordId" type="Id" />
    <aura:attribute name="Account" type="Account" />
    <aura:attribute name="Contacts" type="Contact" />
    <aura:attribute name="Columns" type="List" />
    
    <aura:attribute access="private" name="error" type="String" default=""/>
   
    <aura:attribute name="newEvent" type="Event" default="{'sobjectType':'Event'}"/>
    
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    
    <force:recordData aura:id="accountRecord"
                      recordId="{!v.recordId}"
                      targetFields="{!v.Account}"
                      layoutType="FULL"
                      />
    
    <lightning:card iconName="standard:contact" title="{! 'Meeting for ' + v.Account.Name}">
        <aura:set attribute="actions">  
            <div class="slds-text-align_center">
                <lightning:button variant="brand" label="Save" onclick="{!c.onSave}" />
                <lightning:button variant="brand" label="Cancel" onclick="{!c.onCancel}" />
            </div> 
        </aura:set> 
        <aura:set attribute="footer">  
            <div class="slds-text-align_center">
                <lightning:button variant="brand" label="Save" onclick="{!c.onSave}" />
                <lightning:button variant="brand" label="Cancel" onclick="{!c.onCancel}" />
            </div>
        </aura:set>
        <aura:if isTrue="{! !empty(v.error)}">
            <ui:message title="Error" severity="error" closable="true">
                {!v.error}
            </ui:message>
        </aura:if>
        <form class="slds-form--stacked">  
            <div class="slds-form-element">
                <lightning:input aura:id="subject" name="Subject" value="{!v.newEvent.Subject}" label="Subject" placeholder="Subject" />    
            </div>
            <div class="slds-form-element">
                <lightning:textarea name="Description" value="{!v.newEvent.Description}" label="Description" placeholder="Description" />    
            </div>
            <div class="slds-form-element">
                <ui:inputDateTime aura:id="startDate" label="Start" class="field" value="{!v.newEvent.StartDateTime}" displayDatePicker="true"/>
            </div>
            <div class="slds-form-element">
                <ui:inputDateTime aura:id="endDate" label="End" class="field" value="{!v.newEvent.EndDateTime}" displayDatePicker="true"/>
            </div>
            
            <div class="slds-form-element">
                <lightning:datatable aura:id="contacts" data="{! v.Contacts }" columns="{! v.Columns }"  />
            </div> 
            
        </form>      
    </lightning:card>  
</aura:component>

NewMeetingLEXController.js
 
({
    doInit : function(component, event, helper) {
        // helper.onInit(component,event,helper);
        var currenttime = new Date().toISOString();
        var currentime2 = new Date();
        currentime2.setMinutes(currentime2.getMinutes() + 30);
        var currentime_30min = currentime2.toISOString();
        component.set("v.newEvent.StartDateTime",currenttime);
        component.set("v.newEvent.EndDateTime", currentime_30min);
        
        component.set("v.Columns",[
            {label:"First Name", fieldName:"FirstName", type:"text"},
            {label:"Last Name", fieldName:"LastName", type:"text"},
            {label:"Title", fieldName:"Title", type:"text"}
        ]);
        
        var action = component.get("c.getContacts");
        action.setParams({
            recordId: component.get("v.recordId")
        });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (component.isValid() && state == 'SUCCESS') {   
                component.set("v.Contacts", response.getReturnValue()); 
            }           
        });
        $A.enqueueAction(action);
    },
    onSave : function(component, event, helper) {
        // if(helper.validateEventForm(component)) {
      //  component.set("v.simpleNewEvent.WhatId", component.get("v.recordId"));
        var contactselected =  component.find("contacts").getSelectedRows();
        var toastEvent = $A.get("e.force:showToast");
        
        var subjectCmp = component.find("subject");
        var subjectvalue = subjectCmp.get("v.value");
        
        var startDateCmp = component.find("startDate");
        var startDatevalue = startDateCmp.get("v.value");        
            
        var endDateCmp = component.find("endDate");
        var endDatevalue = endDateCmp.get("v.value"); 
        
        if (contactselected == null || contactselected.length == 0) {
            component.set("v.error", "Please choose at least one contact");
                     
            toastEvent.setParams({
                "title": "Missing contacts!",
                "message": "Please choose at least one contact",
                "type":"error"
            });
            toastEvent.fire();
            
        } 
        else             
            if (subjectvalue == null || subjectvalue == "") {
         //   if (component.find("subject") == null || component.find("subject") == "") {
            component.set("v.error", "Please enter a subject");
                     
            toastEvent.setParams({
                "title": "Missing subject!",
                "message": "Please enter a subject",
                "type":"error"
            });
            toastEvent.fire();
            
        }

		else if (endDatevalue < startDatevalue) {
            component.set("v.error", "Start time is after end time");
                     
            toastEvent.setParams({
                "title": "Datetime error!",
                "message": "Start time is after end time",
                "type":"error"
            });
            toastEvent.fire();
            
        }        
        
        else {
            console.log('selected:' + contactselected[0].Id);
            var myrecordId = component.get("v.recordId");
            var myevents = [];
            var myeventrelations = [];
            var newEvent = component.get("v.newEvent");
           // contactselected.forEach(function(contact)  { 
                console.log(contactselected[0].Id);
                myevents.push({ 'sobjectType':'Event',
                               'WhoId': contactselected[0].Id, 
                               'WhatId': myrecordId,
                               'Subject': newEvent.Subject,
                               'Description': newEvent.Description,
                               'StartDateTime': newEvent.StartDateTime,
                               'EndDateTime': newEvent.EndDateTime
                              });
          //  });
            console.log('myevents:' + myevents.length);
            var action = component.get("c.saveMainEvent");
            action.setParams({
                evt: myevents[0]
            });
            action.setCallback(this, function(response) {
                var state = response.getState();
                if (component.isValid() && state == 'SUCCESS') {   
                    var i;
                    for (i=1; i<contactselected.length; i++) {
                        myeventrelations.push({ 'sobjectType':'EventRelation',
                                               'AccountId': myrecordId,
                                               'EventId': response.getReturnValue(),
                                               'IsParent': true,
                                               'IsInvitee' : false,
                                               'IsWhat': false,
                                               'RelationId': contactselected[i].Id

                        });
                        
                    }
                                        
                    var action1 = component.get("c.saveEventRelations");
                    action1.setParams({
                        eventRelations: myeventrelations
                        
                    });
                    $A.enqueueAction(action1);  
                    
                    toastEvent.setParams({
                        "title": "Success",
                        "message": "Meeting saved!",
                        "type":"success"
                    });
                    toastEvent.fire();
                    $A.get("e.force:closeQuickAction").fire();
                } else {
                    component.set("v.error", response.getState()); 
                }         
            });
            $A.enqueueAction(action);      
        }
        //  }
    } ,
    onCancel : function(component, event, helper) {
          $A.get("e.force:closeQuickAction").fire();
    }
})

NewMeetingLEXHelper.js
 
({
    onInit: function(component,event,helper) {
        component.set("v.Columns",[
            {label:"First Name", fieldName:"FirstName", type:"text"},
            {label:"Last Name", fieldName:"LastName", type:"text"},
            {label:"Title", fieldName:"Title", type:"text"}
        ]);
        
    /*    if (component.get('v.EventID') == '') { 
            component.find("newEvent").getNewRecord(
                "Event", // sObject type (objectApiName)
                null,      // recordTypeId
                false,     // skip cache?
                $A.getCallback(function() {
                    var rec = component.get("v.newEvent");
                	})
            )} */
                               
        
        var action = component.get("c.getContacts");
        action.setParams({
            recordId: component.get("v.recordId")
        });
        action.setCallback(this, function(data) {
            component.set("v.Contacts", data.getReturnValue()); 
        });
        $A.enqueueAction(action);
    }
})

 
Alain CabonAlain Cabon
We could perhaps improve the code for the errors for the validation of individual fields with inputCmp.set("v.errors", [{message:"Input not a number: " + value}]);  or throwing errors.

If you want to impress the javascript experts, just say that you have used the very new "Promises" one day.
I am not sure if these promises could be useful for the code here for our nested $A.enqueueAction(action).
I am going to try to find an example of Promises used for chaining $A.enqueueAction(action).

1) Validate that an Attribute Value is Defined

var isDefined = !$A.util.isUndefined(cmp.get("v.label"));

2) Validate that an Attribute Value is Empty

var isEmpty = $A.util.isEmpty(cmp.get("v.label"));

https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_attr_values.htm

3) Default Error Handling

The framework can handle and display errors using the default error component, ui:inputDefaultError. This component is dynamically created when you set the errors using the inputCmp.set("v.errors",[{message:"my error message"}]) syntax. The following example shows how you can handle a validation error and display an error message. Here is the markup.

<ui:inputNumber aura:id="inputCmp"/>

inputCmp.set("v.errors", [{message:"Input not a number: " + value}]);

https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_validate_fields.htm

4) Throwing and Handling Errors

   throw new Error("You don't have permission to edit this record.");

https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_throw_error.htm?search_text=throwing

5) Using JavaScript Promises (advanced technique).

Create a Promise  This firstPromise function returns a Promise.

Chaining Promises
When you need to coordinate or chain together multiple callbacks, promises can be useful. The generic pattern is:

https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_promises.htm
 
Alain CabonAlain Cabon
As we have noticed, the nested callbacks work well but the code is awful ( Callback Hell  http://callbackhell.com/  )
We need to improve that part.

Advanced technique: Using Javascript Promises to chain asynchronous actions in Salesforce Lightning

$A.enqueueAction(appendAction) tells Lightning to make the server-side call to append() at some later time. Once the framework has a result from the server, it calls the callback function so that you can do something with that result. In this case, updating the message attribute on our component.

This is fine until you need to start chaining calls to the server e.g. we want to append more than one “a” to the string.
You could just extend what we have already, and make another call in the callback:

We’re entering both callback hell and the pyramid of doom. Every further action that we need to chain is going to add another layer to this pyramid, and I haven’t even included error handlers here.

Promises are a way to avoid all of this.

http://nebulaconsulting.co.uk/using-javascript-promises-to-chain-asynchronous-actions-in-salesforce-lightning/

I will improve your posted code for the best result as possible (avoiding the callback hell (pyramid) and better error handling.
Denise CrosbyDenise Crosby
Thank you Alain! Most of the ideas you are posting here are over my head, but I will try to catch up. :)
Alain CabonAlain Cabon
Hi Denise,

The promises are not necessary here (for next time, with complicated callouts for instance).

action.setParams({
     evt: myevents[0],
     eventRelations: myeventrelations
});
public with sharing class NewMeetingLEX {
    @AuraEnabled
    public static List<Contact> getContacts(Id recordId) {
        return [Select Id, FirstName, LastName, Email, Phone, Title, OwnerId From Contact Where AccountId = :recordId];
    }
    
    @AuraEnabled
    public static String saveMainEvent(Event evt) {
        system.debug('myevent:' + evt);

        insert evt;
        return evt.id;
    }

    @AuraEnabled
    public static String saveEventRelations(List<EventRelation> eventRelations) {
        for (EventRelation er:eventRelations) {
            system.debug('myeventrelation:' + er);
        }
        insert eventRelations;
        return null;
        
    }
    
     @AuraEnabled
    public static String saveTotalEvent(Event evt,List<EventRelation> eventRelations) {
        system.debug('myevent:' + evt);
        insert evt;
        for (EventRelation er:eventRelations) {
            system.debug('myeventrelation:' + er);
            er.EventId = evt.id;
        }
        insert eventRelations;
        return null;     
    }
}

onSave : function(component, event, helper) {
        // if(helper.validateEventForm(component)) {
        //  component.set("v.simpleNewEvent.WhatId", component.get("v.recordId"));
        var contactselected =  component.find("contacts").getSelectedRows();
        var toastEvent = $A.get("e.force:showToast");
        
        var subjectCmp = component.find("subject");
        var subjectvalue = subjectCmp.get("v.value");
        
        var startDateCmp = component.find("startDate");
        var startDatevalue = startDateCmp.get("v.value");        
        
        var endDateCmp = component.find("endDate");
        var endDatevalue = endDateCmp.get("v.value"); 
        
        if (contactselected == null || contactselected.length == 0) {
            component.set("v.error", "Please choose at least one contact");
            
            toastEvent.setParams({
                "title": "Missing contacts!",
                "message": "Please choose at least one contact",
                "type":"error"
            });
            toastEvent.fire();
            
        } 
        else             
            if (subjectvalue == null || subjectvalue == "") {
                
                component.set("v.error", "Please enter a subject");
                
                toastEvent.setParams({
                    "title": "Missing subject!",
                    "message": "Please enter a subject",
                    "type":"error"
                });
                toastEvent.fire();            
            }
        
            else if (endDatevalue < startDatevalue) {
                component.set("v.error", "Start time is after end time");
                
                toastEvent.setParams({
                    "title": "Datetime error!",
                    "message": "Start time is after end time",
                    "type":"error"
                });
                toastEvent.fire();           
            }        
        
                else {
                    console.log('selected:' + contactselected[0].Id);
                    var myrecordId = component.get("v.recordId");
                    var myevents = [];
                    var myeventrelations = [];
                    var newEvent = component.get("v.newEvent");
                    
                    console.log(contactselected[0].Id);
                    myevents.push({ 'sobjectType':'Event',
                                   'WhoId': contactselected[0].Id, 
                                   'WhatId': myrecordId,
                                   'Subject': newEvent.Subject,
                                   'Description': newEvent.Description,
                                   'StartDateTime': newEvent.StartDateTime,
                                   'EndDateTime': newEvent.EndDateTime
                                  });
                    
                    console.log('myevents:' + myevents.length);
                    var i;
                    for (i=1; i<contactselected.length; i++) {
                        myeventrelations.push({ 'sobjectType':'EventRelation',
                                               'AccountId': myrecordId,
                                               'IsParent': true,
                                               'IsInvitee' : false,
                                               'IsWhat': false,
                                               'RelationId': contactselected[i].Id                                               
                                              });
                        
                    }
                    var action = component.get("c.saveTotalEvent");
                    action.setParams({
                        evt: myevents[0],
                        eventRelations: myeventrelations
                    });
                    action.setCallback(this, function(response) {
                        var state = response.getState();
                        if (component.isValid() && state == 'SUCCESS') {   
                            
                            toastEvent.setParams({
                                "title": "Success",
                                "message": "Meeting saved!",
                                "type":"success"
                            });
                            toastEvent.fire();
                            $A.get("e.force:closeQuickAction").fire();
                        } else {
                            component.set("v.error", response.getState()); 
                        } 
                    });  
                    $A.enqueueAction(action);
                } 
    },

We didn't get feedbacks from other people for improving the current code.

Your code works and seems stable (it is already a victory with Lex components).

Alain
This was selected as the best answer
Denise CrosbyDenise Crosby
Thank you so much Alain. Your code looks a lot cleaner than mine. :) I am going to use your code if that is ok... and I will mark your as Best Answer. Thanks again for your wonderful help.
Alain CabonAlain Cabon
Hi Denise, 

I am glad that I helped you. I am less involved on the forum these days because I am passing the very new Superbadge Lightning Component Framework Specialist (very difficult, I am on the 8th challenge, but even if that works, the robot refused the solution, I am like a locksmith trying all the keys, exhausting). I prefer not to think about the ultimate superbadge (coming soon).

( a good idea of the difficulty is when there are very few people having already it among the "champions":   https://trailhead-leaderboard-developer-edition.na35.force.com/    )

Superbadge Lightning Component Framework Specialist is very important because of this ..
http://certification.salesforce.com/platformdeveloperII

I have used some parts of the code posted here so helping you helps me too and I think you could try this Superbadge (very difficult, and takes a long time to gothrough because we don't know all the needed "tricks" ... and the whims of the robot) . 

While solving the puzzles of Salesforce, I am experimenting many new techniques at least (needed by the solutions) that could help us in the future.

Have a nice evening.

Alain
Alain CabonAlain Cabon
You brought me good luck, Denise for the Superbadge Lightning Component Framework Specialist.

The last challenges (3/10) were simpler than the first ones. I won 7,000 points (10 challenges x 500 + 2000 points bonus) with this superbadge that's why they are also very interesting for the goal of the 200,000 points (about 200 badges) for me. The "champions" are over 300 badges.

The most difficult challenge with Salesforce is still here for the 5,000 points (it is really very difficult for getting the best answers) but by chance, I met some people like you who also build complete solutions with the Lex components and who don't forget to select a best answer
Thanks.. 
Denise CrosbyDenise Crosby
Alain,
Congratulations, I'm glad I could help you. :) I also want to finish those superbadges. I am still on Apex Specialist...
Alain CabonAlain Cabon
That was my first superbadge (I am a java developer basically). Strangely you may find that you are more comfortable passing the Superbadge Lightning Component Framework Specialist because you are currently writing code for Lex but there is the first formidable challenge of the Lightning Components Basics (only the name is basics, 9 steps). With your skills and the "basics" (communication with the events), you should go very far I am sure (and the result is more fun).