0

I am having quite the trouble with scopes created by directives, and saving these dynamic elements' scope back to the parent.

Here is my directive:

app.directive('action', function() {
 return {
   restrict: "E",
   scope: {},
   templateUrl:'views/pages/projects/triggers/newaction.html',   
   controller: function($rootScope, $scope, $element) {
     $scope.groups = $scope.$parent.groups;  
     $scope.scenes = $scope.$parent.scenes;  
     $scope.actions = $scope.$parent.actions; 
     $scope.Delete = function(e) {
        //remove element and also destoy the scope that element
        $element.remove();
        $scope.$destroy();
      };
   }
 };
});

here is my controller:

.controller('NewTriggerCtrl', ['Auth', '$scope', 'toastr', '$state', '$stateParams', 'FBURL', '$filter', '$compile',
function(Auth, $scope, toastr, $state, $stateParams, FBURL, $filter, $compile) {

  var authData = Auth.$getAuth();
  var ref = new Firebase(FBURL + '/projects/' + authData.uid + '/' + $stateParams.projectid);
  // Submit operation

 var retriveActions = function() {
    // http://stackoverflow.com/questions/12649080/get-to-get-all-child-scopes-in-angularjs-given-the-parent-scope
    var theseactions = [];
    var ChildHeads = [$scope.$$childHead];
    var currentScope;
    while (ChildHeads.length) {
      currentScope = ChildHeads.shift();
      while (currentScope) {
         /* theseactions.push({
            type: currentScope.type,
            data: currentScope.data,
            data2: currentScope.data2
          }); */
          console.log("currentscope.type = " + currentScope.type);
        };
        currentScope = currentScope.$$nextSibling;
    }
    //return theseactions;
  };

  var retrieveactions2 = function() {
    var theseactions = [];
    var newevent = null;
    var newdata = null;
    var newdata2 = null;
    console.log("in retrieve actions");
    angular.forEach(angular.element(document.getElementsByClassName("newaction")), function(element){
        console.log("iterating");
        newevent = $(this).find('.newactionevent').value;
        newdata = $(this).find('.newactiondata').value;
        newdata2 = $(this).find('.newactiondata2').value;
        theseactions.push({
            event: newevent,
            data: newdata,
            data2: newdata2
        });
    });
    return theseactions;
  }


  $scope.ok = function(actions) {
    //console.log(retriveActions(actions));
    //retriveActions(actions);
    //console.log("$scope.trigger.actions = " + $scope.trigger.actions);
    //console.log("actions = " + actions);
    console.log(retrieveactions2());
    $scope.triggers.$add($scope.trigger).then(function (triggerRef) {
      ref.child('triggers').child(triggerRef.key())
        .update({created_at: Firebase.ServerValue.TIMESTAMP});
      toastr.success('Trigger Added!', 'Trigger has been created');
      $state.go('app.projects.edit', {projectid : $stateParams.projectid}, {reload: true});
    });
  };
  $scope.newaction = function() {
      var divElement = angular.element(document.querySelector('#actions'));
      var appendHtml = $compile('<action></action>')($scope);
      divElement.append(appendHtml);
  };
  $scope.cancel = function() {
      $state.go('app.projects.edit', {projectid : $stateParams.projectid}, {reload: true});
    };
  /////////////////////// *Submit operation

}])

here is my new trigger html:

<div class="page page-newtrigger" ng-controller="NewTriggerCtrl">

  <!-- row -->
  <div class="row">


    <div class="col-md-12">

      <!-- tile -->
      <section class="tile tile-simple">


        <!-- tile body -->
        <div class="tile-body">

            <form name="form" class="form-horizontal form-validation" role="form" novalidate>
                  <div class="form-group mt-12" style="margin-top: 15px;">
                    <label for="name" class="col-sm-1 control-label">Name <span class="text-danger" style="font-size: 15px;">*</span></label>
                    <div class="col-sm-1">
                      <input type="text" name="name" class="form-control" id="name" placeholder="Trigger name..." ng-model="trigger.name" required>
                    </div>

                        <div class="btn-group col-sm-2">
                            <label class="btn btn-green" ng-model="trigger.type" uib-btn-radio="'astro'">Astro</label>
                            <label class="btn btn-green" ng-model="trigger.type" uib-btn-radio="'time'">Real-Time</label>
                            <label class="btn btn-green" ng-model="trigger.type" uib-btn-radio="'input'">Input</label>
                        </div>


                    <div class="animate-switch-container" ng-switch on="trigger.type">

                        <div class="animate-switch" ng-switch-when="astro">
                            <div class="btn-group col-sm-2">
                              <label class="btn btn-green" ng-model="trigger.event" uib-btn-radio="'sunrise'">Sunrise</label>
                              <label class="btn btn-green" ng-model="trigger.event" uib-btn-radio="'sunset'">Sunset</label>
                            </div>
                            <div class="col-sm-1">
                                <input type="text" name="offset" class="form-control" id="offset" placeholder="Offset (+/- minutes)" ng-model="trigger.option">
                            </div>
                        </div>

                        <div class="animate-switch" ng-switch-when="time">
                            <div class="btn-group col-sm-2">
                              <label class="btn btn-green" ng-model="trigger.event" uib-btn-radio="'repeat'">Repeat</label>
                              <label class="btn btn-green" ng-model="trigger.event" uib-btn-radio="'once'">Once</label>
                            </div>

                            <div class="animate-switch-container" ng-switch on="trigger.event">
                                <div class="animate-switch" ng-switch-when="repeat">
                                    <div class="col-sm-3 btn-group">
                                      <label class="btn btn-cyan" ng-model="trigger.option.mon" uib-btn-checkbox>Mon</label>
                                      <label class="btn btn-cyan" ng-model="trigger.option.tue" uib-btn-checkbox>Tue</label>
                                      <label class="btn btn-cyan" ng-model="trigger.option.wed" uib-btn-checkbox>Wed</label>
                                      <label class="btn btn-cyan" ng-model="trigger.option.thu" uib-btn-checkbox>Thur</label>
                                      <label class="btn btn-cyan" ng-model="trigger.option.fri" uib-btn-checkbox>Fri</label>
                                      <label class="btn btn-cyan" ng-model="trigger.option.sat" uib-btn-checkbox>Sat</label>
                                      <label class="btn btn-cyan" ng-model="trigger.option.sun" uib-btn-checkbox>Sun</label>  
                                    </div>
                                    <div class="col-sm-1">
                                        <input type="text" name="time" class="form-control" id="time" placeholder="Time (HH:mm:ss)" ng-model="trigger.data">
                                    </div>
                                </div>
                                <div class="animate-switch" ng-switch-when="once">
                                    <div class="col-sm-2" >
                                        <p class="input-group" ng-controller="DatepickerDemoCtrl">
                                          <input type="text" class="form-control" uib-datepicker-popup="{{format}}" ng-model="trigger.option" is-open="opened" min-date="minDate" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" ng-required="true" close-text="Close" />
                                          <span class="input-group-btn">
                                            <button type="button" class="btn btn-default" ng-click="open($event)"><i class="fa fa-calendar"></i></button>
                                          </span>
                                        </p>
                                    </div>
                                    <div class="col-sm-1">
                                        <input type="text" name="time" class="form-control" id="time" placeholder="Time (HH:mm:ss)" ng-model="trigger.data">
                                    </div>
                                </div>
                            </div>
                        </div>

                        <div class="animate-switch" ng-switch-when="input">
                            <div class="col-sm-1">
                                <select ng-init="1" ng-model="trigger.option" class="form-control mb-10">
                                    <option value="1">1</option>
                                    <option value="2">2</option>
                                    <option value="3">3</option>
                                    <option value="4">4</option>
                                    <option value="5">5</option>
                                    <option value="6">6</option>
                                    <option value="7">7</option>
                                    <option value="8">8</option>
                                </select>
                            </div>
                            <div class="col-sm-2">
                                <div class="btn-group">
                                  <label class="btn btn-green" ng-model="trigger.event" uib-btn-radio="'high'">Goes high</label>
                                  <label class="btn btn-green" ng-model="trigger.event" uib-btn-radio="'low'">Goes low</label>
                                </div>
                            </div>
                        </div>
                    </div>

                    <button class="btn btn-success b-0 pull-right" style="margin-right: 30px;" ng-click="newaction()"><i class="fa fa-plus mr-5"></i>New Action</button>

                  </div>
                  <div id="actions">
                  <action ng-repeat="action in trigger.actions"></action>
                  </div>

                  <div class="form-footer">
                    <button class="btn btn-success b-0 pull-right" ng-click="ok(trigger)" ng-disabled="form.$invalid">Submit</button>
                    <button class="btn btn-lightred btn-ef btn-ef-4 btn-ef-4c" ng-click="cancel()"><i class="fa fa-arrow-left"></i> Cancel</button>
                  </div>
            </form>

        </div>
        <!-- /tile body -->


      </section>
      <!-- /tile -->

    </div>

  </div>
  <!-- /row -->

</div>

here is my newaction html:

    <!-- row -->
  <div class="newaction row">
    <div class="col-md-10 col-sm-offset-2">
            <form name="form" class="form-horizontal form-validation" role="form" novalidate>
                  <div class="form-group mt-12" style="margin-top: 15px;">

                  <div class="col-sm-1"><button ng-click="Delete($event)" class="btn btn-danger"><i class="glyphicon glyphicon-trash"></i></button></div>

                        <div class="btn-group col-sm-2">
                            <select ng-init="1" ng-model="action.type" class="newactiontype form-control mb-10">
                                <option value="transition-scene">Transition Scene</option>
                                <option value="set-group-intensity">Set Group Intensity</option>
                                <option value="inject-trigger">Inject Trigger</option>
                                <option value="color-change">Color Change</option>
                            </select>
                        </div>


                    <div ng-if="action.type=='transition-scene'">
                        <div class="col-sm-3">
                          <select chosen="" class="newactionevent form-control mb-10" ng-options="scene.name for scene in scenes track by scene.name" ng-model="action.scene"></select>
                        </div>
                        <div class="col-sm-1">
                            <input type="text" name="fadetime" class="newactiondata form-control" id="fadetime" placeholder="Fade Time (seconds)" ng-model="action.fadetime">
                        </div>
                    </div>

                    <div ng-if="action.type=='set-group-intensity'">
                        <div class="col-sm-3">
                          <select multiple chosen="" class="newactionevent form-control mb-10" ng-options="group.name for group in groups track by group.name | filter: {type : 'white'}" ng-model="$parent.action.group"></select>
                        </div>
                        <div class="col-sm-1">
                            <input type="text" name="intensity" class="newactiondata form-control" id="intensity" placeholder="Intensity(%)" ng-model="action.intensity">
                        </div>
                        <div class="col-sm-1">
                            <input type="text" name="fadetime" class="newactiondata2 form-control" id="fadetime" placeholder="Fade Time (sec)" ng-model="action.fadetime">
                        </div>
                    </div>

                    <div ng-if="action.type=='inject-trigger'">
                        <div class="col-sm-2">
                          <input type="text" name="triggernumber" class="newactiondata form-control" id="triggernumber" placeholder="Trigger number..." ng-model="action.data">
                        </div>
                    </div>

                    <div ng-if="action.type=='color-change'">
                        <div class="col-sm-3">
                          <select multiple chosen="" class="newactionevent form-control mb-10" ng-options="group.name for group in groups track by group.name| filter:group.type!='white'" ng-model="action.group"></select>
                        </div>
                        <div class="col-sm-1">
                            <input colorpicker="rgb" ng-model="action.data" type="text" class="newactiondata form-control w-md mb-10">
                        </div>
                        <div class="col-sm-1">
                            <input type="text" name="fadetime" class="newactiondata2 form-control" id="fadetime" placeholder="Fade Time (sec)" ng-model="action.fadetime">
                        </div>
                    </div>
                  </div>
            </form>


    </div>

  </div>
  <!-- /row -->

I have multiple attempts at this, as you can see with the two different functions in NewTriggerCtrl. The first was to get all the child scopes and iterate through, however when I call this it locks the browser up with over 250,000 logs. So maybe I am passing the wrong scope?

I am relatively new to Angular, and have some experience in JQuery, and I attempted to name the inputs with classes and finding them with document calls, but that is not working either. I have an app running and I can create, detele, etc. groups, triggers (not the trigger actions), and scenes, So I understand the basics of controllers and scopes. But saving these child scopes to the main trigger (what I assume would be trigger.actions) has stumped me. Maybe there is a better way to this? I know my code may not be efficient, I am attempting to get a base then clean up later.

Thank you.

UPDATE:

Ok so new directive:

app.directive('action', function() {
 return {
   restrict: "E",
   scope: true,
   templateUrl:'views/pages/projects/triggers/newaction.html',   
   controller: function($rootScope, $scope, $element) {
     $scope.Delete = function(e) {
         console.log("$scope.action = " + $scope.action);
        //remove element and also destoy the scope that element
        $element.remove();
        $scope.$destroy();
      };
   }
 };
});

and entire trigger controller:

'use strict';

app

  .controller('TriggersCtrl', ['Auth', '$scope', '$state', '$stateParams', '$firebaseArray', '$firebaseObject', 'FBURL',
    function(Auth, $scope, $state, $stateParams, $firebaseArray, $firebaseObject, FBURL) {

      // General database variable
      var authData = Auth.$getAuth();
      var ref = new Firebase(FBURL + '/projects/' + authData.uid + '/' + $stateParams.projectid);


      $scope.triggers = $firebaseArray(ref.child('triggers'));
      $scope.groups = $firebaseArray(ref.child('groups'));
      $scope.scenes = $firebaseArray(ref.child('scenes'));
      $scope.triggersObject = $firebaseObject(ref.child('triggers'));
      //////////////////////////// *General database variable

      // get the model
      if($stateParams.triggerid) {
        var id = $stateParams.triggerid;
        $scope.trigger = $firebaseObject(ref.child('triggers').child(id));
        $scope.actionsObject = $firebaseObject(ref.child('triggers').child(id).child('actions'));
      } else {
        $scope.trigger = {};
        $scope.actionsObject = {};
      }
    }])

  .controller('NewTriggerCtrl', ['Auth', '$scope', 'toastr', '$state', '$stateParams', 'FBURL', '$filter', '$compile', '$firebaseArray',
    function(Auth, $scope, toastr, $state, $stateParams, FBURL, $filter, $compile, $firebaseArray) {

      var authData = Auth.$getAuth();
      var ref = new Firebase(FBURL + '/projects/' + authData.uid + '/' + $stateParams.projectid);
      // Submit operation


    $scope.ok = function() {
        console.log("$scope.actions = " + $scope.actions);
        console.log("$scope.trigger.actions = " + $scope.trigger.actions);
        $scope.triggers.$add($scope.trigger).then(function (triggerRef) {
          ref.child('triggers').child(triggerRef.key())
            .update({created_at: Firebase.ServerValue.TIMESTAMP});
          toastr.success('Trigger Added!', 'Trigger has been created');
          $state.go('app.projects.edit', {projectid : $stateParams.projectid}, {reload: true});
        });
      };
      $scope.newaction = function() {
          var divElement = angular.element(document.querySelector('#actions'));
          var appendHtml = $compile('<action></action>')($scope);
          divElement.append(appendHtml);
      };
      $scope.cancel = function() {
          $state.go('app.projects.edit', {projectid : $stateParams.projectid}, {reload: true});
        };
      /////////////////////// *Submit operation

    }]);

The scopes for the actions are created, and when i delete they are logged as an object. But the actions are not saved into trigger. I tried creating an actionsObject and then ng-repeat="action in actionsObject" but that didnt work. I try $scope.trigger.actions = $scope.actionsObject (to no avail) [my thought is how I created the $scope.trigger and the $scope.actionsObject is that they should behave the same if I call "action in actionsObject" vs "action in trigger.actions"..?]. My assumption is that I append the newaction template, which it's scope is action, the ng-repeat="action in trigger.actions" part, does this create the bind for when I add the new element to #actions that it saved to the trigger.actions scope? Having "scope: true" in the directive gives me the scenes and groups perfectly (understand the inheritance a little better). I should note that I create a trigger (a single one) and add multiple actions (which groups and scenes are part of the ng-options which is why i needed those models). Does the multiple appends affect anything? This is my last major feature to work out. I appreciate the help!

  • Glad you got the scope inheritance figured out a bit better now. After your update though, you're starting to ask a second question which it looks like is more about firebase than angular scopes. – plong0 Aug 02 '16 at 15:01
  • I would suggest to install the Angular developer extension for Chrome and use the angular scope inspector that will appear in your developer tools. – plong0 Aug 02 '16 at 15:03
  • If I could get the newaction directive scope to add to the trigger.actions, I would be fine. I am starting to think the issue is that the ng-repeat="action in trigger.actions" is in the first newaction. when i add a newaction via the button, then does not include the ng-repeat, and thus is not binding to the trigger.actions. Ive tried all the different scopes (in fact if i do scope:false then the newaction [however many] are tied together and saves to trigger.action) but am unable to have each separate child scope added to trigger.actions. I have installed the ng-inspector, and I can see that – user2535660 Aug 02 '16 at 15:16
  • the scope for each action scope is there as expected, but this is not included in trigger.actions. – user2535660 Aug 02 '16 at 15:21
  • try using something like
    {{trigger.actions | json}}
    in your template to see what is in the trigger.actions on your scope and make sure the new action is there. Where are you adding to trigger.actions in newaction directive?
    – plong0 Aug 02 '16 at 15:34
  • yes, but where are you actually doing something like `$scope.trigger.actions.$add({foo: 'bar'})`? – plong0 Aug 02 '16 at 16:26
  • That was part of my original post. I attempted to find all the child scopes as in another stack post, but that didn't work. I assumed to $add in the $scope.ok in the NewTriggerCtrl. Maybe I can $add in the $scope.newaction? How would I pass the newly created action model by $(this) or appendHtml? The retrieveActions was the first attempt, but I was accessing the wrong scope and overloading the system (gathering all the scopes). – user2535660 Aug 02 '16 at 16:35
  • Yes I think in your `$scope.newaction` function would be the correct place. I see there that you are creating a DOM element - but let's be clear that generating a DOM element will not automatically update your angular data models. Instead you need to do the opposite - update your angular data models (ie. `$scope.trigger.actions.$add({foo: 'bar'})`) and let the `ng-repeat` on that model automatically update the DOM. – plong0 Aug 02 '16 at 16:36
  • 1
    That makes alot of sense. Working now! If you edit your answer I will accept. Thank you very much. Makes much more sense this way!! – user2535660 Aug 02 '16 at 16:49

2 Answers2

0

Your action directive use an isolate scope, and that is for internal use by the directive only.

The data you want to save in your Actions-array (to be submitted with the form) should go directly into the action-object and not on the scope. You can access the action-object in two ways:

Use a child scope (by specifying scope: true instead of scope: {}) This allows you to access the parent's scope variables by inheritance:

  • $scope.action refers to the current action object (from ng-repeat)
  • $scope.groups refers to the groups array from the parent scope

This means you can assign data to $scope.action.something and it will affect the action object immediately. This is the beauty of angular: Manipulate the data directly, and skip the tedious "loop through everything to get the values".

You can also use an isolate scope (using scope: {} syntax). When using an isolate scope you need to specify explicitly the variables you want to use.

{
    ...
    scope: {
        theActionObject: '=',
        groupList: '=',
        sceneList: '='
    }
    ...
}

This can then be referenced in you directive's link or controller function as $scope.theActionObject.

$scope.theActionObject.something = 'test';

// Access the groups array
alert($scope.groupList.length);

And to tie it together you need to specify the references in the HTML:

              <div id="actions">
                  <action ng-repeat="action in trigger.actions" 
                          the-action-object="action" 
                          group-list="groups" 
                          scene-list="scenes"></action>
              </div>

Hope this will help you!

ulvesked
  • 429
  • 3
  • 11
  • how does theActionObject get bound to $scope.trigger.actions in this case? – user2535660 Aug 02 '16 at 00:09
  • It does not. In case of an isolate scope (the last example) variables are bound in the html-markup. As you can see in the html i use `the-action-object="action"`. This will bind the ng-repeat scope variable `action` to the isolate scope variable theActionObject. Note that I bind the action item of each iteration in ng-repeat. It is not the whole array, but the current item. So, for the first action, $scope.theActionObject will be bound to `trigger.actions[0]` and the next action will bind $scope.theActionObject to `trigger.actions[1]` and so on. – ulvesked Aug 02 '16 at 11:50
  • I tried this isolate scope, and I cannot get the groupList and sceneList to bind to the parent controllers' scenes and groups. I am starting to think the issue is that the ng-repeat="action in trigger.actions" is inside the first newaction. when I add a newaction via the button, it does not have a ng-repeat, and is appended after the first newaction with the ng-repeat. – user2535660 Aug 02 '16 at 15:20
  • After studying your code more closely, I notice that you create a new element "manually". Since you use ng-repeat in your code, you can simply add a new object to the actions array and it will be rendered automatically, just like the first one. This way, it gets the correct scope and all other angular features which may not be available when you create the element yourself. Try it! :) – ulvesked Aug 03 '16 at 06:22
  • Also, to delete an action, simply remove it from the `actions`-array and the element will disappear from the page. – ulvesked Aug 03 '16 at 06:23
0

where are your groups, scenes and actions objects defined on the $parent scope? You would need to define them in your NewTriggerCtrl

$scope.groups = [];
$scope.scenes = [];
$scope.actions = [];

Since you are isolating scope, you would normally bind them through attributes like:

scope: {groups: '=', scenes: '=', actions: '=' }

and

<action ng-repeat="action in trigger.actions" groups="groups" scenes="scenes" actions="actions"></action>

An alternative to isolate scope would be to use a prototypical inherited scope with scope: true

Either way, binding $scope.foo = $scope.$parent.foo; seems unnecessary.

Those two methods are for if you actually need new scopes for each action directive. If you don't, then you can just leave the scope property out of your action directive definition and the action directives will use whatever scope they are rendered in.

plong0
  • 2,140
  • 1
  • 19
  • 18
  • does the fact that I am creating multiple newaction elements require that I use isolated scopes? – user2535660 Aug 02 '16 at 00:10
  • It only matters in the sense that if they share a scope, changes to $scope in one instance occur in the other instances. If for example the newaction directive is assigning to $scope things that should be specific to only that instance - then you would need an isolate or prototypical inherited scope. If the directive is not assigning anything specific to its instance onto $scope, then sharing scope is fine. – plong0 Aug 02 '16 at 15:00