[SalesForce] Lightning Components: handle event from one particular component

I'm working on a lightning application for managing work items & tasks. Currently, I'm having trouble with just updating just one instance of a workItem component I have. The way I have things now, all my workItems change in my application.

Other questions

  • How do I manage changing just one work item instance?
  • Is it a good choice to have my modal at the root of my application? Since my modal is at the root & not in the same DOM hierarchy as my workItem components, I can't use a "component" event to handle events up the chain & I feel like this is complicating things.
  • When should I be using application events? Any brief example scenarios would be appreciated.

Current Flow:

- User clicks "Add Task" button inside a workItem component, which 
  fires an application 'addTaskEvent' event.

- At my application root I have a 'taskModal' component. The 
  'taskModal' has a handler listening for the application
  "addTaskEvent" event & when it handles it, it just unhides the modal.
  When the modal is open, the user can insert a new task name, then 
  click either cancel or save. If save is hit in the modal, I fire 
  another application event, with the new task name packaged.

- My workItem component has a handler defined for the application event 
  the modal fired. My handler function for this event simply grabs the 
  new task name that the modal event gave me & I just push & update the 
  tasks for the work Item.

- **Problem**: when I push my new task, all of workItems get 
  updated with the new task, instead of just the workItem I initially 
  clicked. I realize this is because all of workItems are listening to 
  the same event the modal triggered.

taskboardApp.app

  <aura:application extends="force:slds">

    <c:AA_taskboard />  ---> workList.cmp --> workItem.cmp
    <c:AA_taskModal/>    
  </aura:application>

taskboard.cmp
the taksboard controller simply fetches the work items on init &
adds the work items to the workItems attribute.

<aura:component controller="AA_taskboardController">
    <aura:attribute name="workItems" type="agf__ADM_Work__c[]" description="all of the items for the current sprint"/>
    <aura:handler name="init" action="{!c.doInit}" value="{!this}"  description="fetches work item data"/>

    <div class="slds-container--x-large slds-container--center">
      <c:AA_workList workItems="{!v.workItems}" />
    </div>

    <c:AA_taskModal />
</aura:component>

Worklist.cmp

<aura:component>
    <aura:attribute name="workItems" type="agf__ADM_Work__c[]" />

      <ol class="work-list">
        <aura:iteration items="{!v.workItems}" var="workItem">
          <li class="work-item-container">
              <c:AA_workItem work="{!workItem}" tasks="{!workItem.agf__Tasks__r}"/>
          </li>
        </aura:iteration>
      </ol>

  </aura:component>

WorkItem.cmp

<aura:component>
    <aura:attribute name="work" type="agf__ADM_Work__c" />
    <aura:attribute name="tasks" type="agf__ADM_Task__c[]"/>
    <aura:attribute name="showTasks" type="Boolean" default="false" />
    <aura:registerEvent
              name="addTaskEvent"
              type="c:AA_addTaskEvent"
              description="
                an application event fired when user clicks add task button on this work item. 
                Task modal component at the root(taskboard.cmp) listens for this event."/>

  <aura:handler
        event="c:AA_updateTaskEvent"
        action="{!c.updateTask}"
        description="Handler for application event fired from the task 
        modal after the user had entered a new task name & hit save.
        This event sends up the taskname to a workItem component that's 
        listening for it"./>

        <div class="work-item">
          <header >{!v.work.agf__Subject__c}</header>
          <button  onclick="{!c.addTask}">Add Task</button>
          <!-- Tasks -->
            <ol class="work-tasks">
              <aura:iteration items="{!v.work.agf__Tasks__r}" var= "taskItem">
                <li class="task-item">
                  <c:AA_taskItem task="{!taskItem}" />
                </li>
              </aura:iteration>
            </ol>
        </div>
    <div>

  </aura:component>

workItemController.js

  ({
            toggleShowTasks: function(component, event, helper) {
            component.set("v.showTasks" , !component.get('v.showTasks'));
            },


          addTask: function(component, event, helper){
            var appEvent = $A.get("e.c:AA_addTaskEvent");
            appEvent.setParams({ work : component.get("v.work") });
            appEvent.fire();
            console.log('fired addtask event from WORK')
          },

          updateTask: function(component, event, helper){

            console.log('fired update task in work item-------------------');
            var taskName = event.getParams("taskName").taskName;
            var newTask = {agf__Subject__c: taskName};

            var workTasks = component.get("v.tasks");
            console.log("the work item-----", workTasks)

            workTasks.push(newTask);
            component.set("v.tasks", workTasks);
            //all work items get pushed my new task since workItem.cmp
            //has a listener
          }

  })

taskModal.cmp

<aura:component>
    <div class="task-modal hide" aura:id="task-modal">
        <!-- other code removed for brefvity -->
        <ui:inputText aura:id="task-input" class="slds-input"/>

         <button onclick="{!c.cancelTask}" >Cancel</button>
         <button onclick="{!c.saveTask}" >Save</button>
    </div>

  </aura:component>

taskModalController.js

  ({
    showTaskModal: function(component, event, helper) {
          var taskModal = component.find('task-modal');
          $A.util.removeClass(taskModal, "hide")
          var workItem = event.getParams('work').work;
          console.log('event.getParams(work??)', workItem);

          component.set('v.work', workItem);

    },
      cancelModal: function(component, event, helper){

      },
      //when our modal opens we attach a work item to the modal, so it should be available here by now.
      //note: I tried to pass the work component via the addTask event but you can't. There's no type value with component for the attribute you creaate in an event.
      saveTask: function(component, event, helper){
        var taskInput = component.find('task-input');
        var taskName = taskInput.get("v.value");
        var updateEvent = $A.get("e.c:AA_updateTaskEvent");
        updateEvent.setParams({'taskName': taskName});
        updateEvent.fire();

        var taskModal = component.find('task-modal');
        $A.util.addClass(taskModal, "hide")
        //clear modal text
        taskInput.set('v.value', "")
        console.log("FIRED updateTask from saveTask function");
      },

      cancelNewTask: function(component, event, helper){
        var taskModal = component.find('task-modal');
        $A.util.addClass(taskModal, "hide")
      }
  })

Best Answer

You can fire an application event from the Modal component(which you are doing now), which carries necessary data and a unique indentifier(might me workitem id) to identify a specific child component.

Also, passing the workitem object from the workItem Component would also work too but you have access it incorrectly in the modal component.

So do the following changes:

taskModalController.js

1.Access the workitem using event.getParam():

showTaskModal : function(component, event, helper) {

    var taskModal = component.find('task-modal');
    $A.util.removeClass(taskModal, "hide")
    var workItem = event.getParam('work'); // Now you'll get the workitem object
    console.log('event.getParam(work??)', workItem);
    component.set('v.work', workItem);
}

2.Add the workItem object/id to the AA_updateTaskEvent app event along with taskName.

saveTask: function(component, event, helper){
    var taskInput = component.find('task-input');
    var taskName = taskInput.get("v.value");
    var updateEvent = $A.get("e.c:AA_updateTaskEvent");

    updateEvent.setParams(
        {
            'taskName': taskName,
            'workItemId':component.get("v.work").Id
        }

    );

    updateEvent.fire();
    // other logics
},

3.Check if you need to add the task in workItem child.

workItemController.js

updateTask: function(component, event, helper){

    console.log('fired update task in work item-------------------');
    var taskName = event.getParam("taskName"); // Get the task name
    var workItemId = event.getParam("workItemId"); // Get the workitemid 
    var workItem = component.get("v.work");

    //if it turns out true, then we have got to the workitem component whose's task should be updated.
    if(workItem.Id == workItemId){
        var newTask = {agf__Subject__c: taskName};

        var workTasks = component.get("v.tasks");
        console.log("the work item-----", workTasks)

        workTasks.push(newTask);
        component.set("v.tasks", workTasks);
    }

    //all work items get pushed my new task since workItem.cmp
    //has a listener
}

To answer your other questions.

1) I have already answer it above.

2) In my opinion its better to keep the modal in the root rather than in the workItem which causes unnecessary DOM to be present. Honestly, I don't see anything complicate here.

3) Application events are best suited in places where components that aren't part of containment hierarchy need to communicate. Your discussion to keep the modal standalone(common to your app/component) and communicating with app event is a perfect example.