Sharing data between directives using attributes instead of services - angularjs

I wanted to make a directive that would essentially act like a specialized input field. After some logic & user input, the 'value' attribute would be populated with a string of comma separated timeslots (hh:mm).
<time-slot value=""></time-slot>
becomes
<time-slot value="01:00,02:00,03:00"></time-slot>
I'd like to provide the flexibility for anyone to place a scope reference in the 'value' attribute tag -- whenever the attribute value is updated, so is the scope reference. (In the code below, myModel.times would be in the MyController scope).
<div ng-controller="MyController">
<time-slot value="{{ myModel.times }}"></time-slot>
</div>
I have had no problems accessing the 'value' attribute in the directive. However, I have not achieved two-way binding -- myModel.times never captures the changed value, even though the contents of the attribute have been changed when I inspect the element during runtime. I am using $attrs.$set to alter the value attribute.
I'm not sure if I'm missing something conceptually, or just missing some extra syntax. To keep this directive modular and shareable, I don't want to use a service to share data between the controller and directive, nor do I want to use a cascading scope. I think it would be optimal if the value attribute can simply be referenced by a scope variable and used as desired, much like a simple input tag:
<input ng-model="model.someText"></input>

An example with two-way data binding: See plunkr.
angular.module('myApp', [])
.directive('timeSlots', function() {
return {
scope: { value: '=' },
link: function($scope, $elem, $attrs) {
// you can access $scope.value here (after it has been interpolated)
}
};
})
.controller('MainCtrl', ['$scope', function($scope) {
$scope.value = 42;
}]);
In HTML:
<div ng-controller="MainCtrl">
<time-slots value="value"></time-slots>
</div>

Related

using angular sub controllers for partial views?

I am creating a survey that has a dozen question 'response types' (multiple choice, sliders, check all that apply, etc.)
The definitions for each response type are stored essentially as key-value pairs in the db.
For example:
Multiple choice Response Type
Settings (one-off widget-specific properties):
[0] key: 'Number of choices', value: 3
[1] key: 'Choice type', value: 'radio'
Options (n-sized array of options):
[0] key: 'red', value: 1
[1] key: 'yellow': value: 2
[2] key: 'blue': value: 3
Slider Response Type
Settings (one-off widget-specific properties):
[0] key: 'Minimum', value: 1
[1] key: 'Maximum', value 10
Options (n-sized array of options):
(no options)
Notice that each response type needs unique settings to tell it how this widget is built. (slider wants a 'number of ticks' value, multiple choice wants an n-sized array of text labels)
So I need an HTML snippet for each response type and - I guess - each one needs its own controller.
In my question page view: I load the template like this:
<div ng-include src="vm.getResponseTemplate(vm.question.responseType)"></div>
which gets me a URL:
"questions/responses/slider-template.html"
"questions/responses/multiple-choice-template.html"
My question is, since I need a controller for each of these, how do I load it? It needs access to the parent controller for the page, since that's where all the settings are held.
I've looked nested controllers, but I'm not entirely sure how I'd implement that in my case.
Can I just add a similar line to the question page to load the script?
<script src="questions/responses/slider-controller.js"></script>
If so, what does slider-controller contain?"
The question page's controller looks like this:
(function () {
appModule.controller('tenant.views.questions.index', [
'$scope', 'abp.services.app.survey',
function ($scope, surveyService) {
var vm = this; }
]);
})();
Do I do ...
(function () {
appModule.controller('tenant.views.questions.responses.slider'...
?
I mean, I can set it up with way, but now it's scoped to the specific controller and does not have access to the parent controller.
Since you re using ng-include to include the templates, the concept will be like
<div ng-controller="ParentCtrl">
// Template 1
<div ng-controller="SliderCtrl">
</div>
// Template 2
<div ng-controller="MultipleChoiceCtrl">
</div>
</div>
Same with
<div ng-controller="ParentCtrl">
// Template 1
<ng-include="'slider-template.html'"/>
// Template 2
<ng-include="'multiple-choice.html'"/>
</div>
You just need to add ng-controller in every templates that you have included like examples below
slider-template.html
<div ng-controller="sliderCtrl">
// setup your code
</div>
multiple-choice-template.html
<div ng-controller="MultipleCtrl">
// setup your code
</div>
and in js controller just defined the controller
Js Controller
(function () {
appModule.controller('ParentCtrl', [
'$scope', 'abp.services.app.survey',
function ($scope, surveyService) {
var vm = this; }
]).controller('SliderCtrl', [
'$scope', 'abp.services.app.slider',
function ($scope, surveyService) {
var vm = this; }
]).controller('MultipleCtrl', [
'$scope', 'abp.services.app.slider',
function ($scope, surveyService) {
var vm = this; }
]);
})();
This is frustrating. I can see the properties in my object when I spit it out to console:
var vmChild = this;
vmChild.parent = $scope.$parent;
console.log(vmChild)
Object: parent > m > $parent > vm > responseValue > Array[1] > 3
That 3 is the actual value from the control in my parent. That's the value I want to read.
I can even do this
console.log($scope.$parent.$parent);
and drill down to my value:
vm > responseValue > Array[1] > 3
Yet when I do this:
console.log(vmChild.parent.$parent.vm);
I get this:
vm > responseValue > Array[0]
I think you're missing the purpose of angularjs main concept. You should be using directives.
Let say, your form is using the controller FormCtrl and template form.html, then in form.html you can do something like this:
(For simplicity sake, let's say you have a method in FormCtrl that tells every widget what they are. E.g. whatAmI(setting), etc. And another method to get the settings -- well maybe that's unnecessary)
<form>
<div ng-repeat="setting in ctrl.settings">
<my-checkbox ng-if="ctrl.whatAmI(setting) == 'checkbox'"
config="ctrl.getMyConfig(setting)"><my-checkbox>
<!-- ... and so on -->
</div>
</form>
So, and now, if you need to update properties from a directive to the outer scope, a good approach is to create an isolated scope in the directive and just use a 2-way binding for the properties that need to be updated upwards. Then give the directive this properties as attributes (Disclaimer, if your planning to upgrade some time in the near future to angular2, then you should avoid 2-way bindings, and use the &-binding, but.... it's not so straight forward as 2-way-bindings... it's a hard choice)
Something like this:
<my-checkbox property1="ctrl.property1"....></my-checkbox>
And in the directive:
myMod.directive('myCheckbox', {
restrict: 'EA',
scope: {}, // Use an isolated scope
controller: 'MyDirectiveController',
bindToController: {
config: '#', // this is a 1-way binding (see note below)
property1: '=' // define 2-way-binding with the '='
},
controllerAs: 'my-ctrl', // controller's name on our template
templateUrl: 'my-checkbox.html'
});
Note: If you only want to pass stuff from the top to the directive, then you don't need 2-way bindings, 1-way-binding is the right thing to do (#)... the problem here would be, that when using 1-way bindings, then the variables are passed as strings, so you would have to parse your stringyfied config back to a real variable with e.g. JSON. At the end it saves some performance on every digest cycle, but you can also use a 2-way binding there to make things easier.... I guess the decision also depends on the amounts.
On the other hand, if you don't know in the ng-repeat loop which property of FormCtrl you want to pass to the my-checkbox directive, then I would make things simple, and give it the property in the config, which the getMyConfig() method in FormCtrl would have to add. That also means you have to use 2-way-binding for the config attribute on the bindToScope.

How to pass string and bool variables automatically to directive parent scope

I bind variable in a directive like this :
<path-filter-modal is-opened="filterModalIsOpened">
And in the directive I use '=' binding like this:
scope: {
isOpened: '='
}
When I change variable in directive, a parent scope contains own value.
How can I make that the parent scope contain the same value?
For objects it works well but not with strings and booleans.
Notice that I use controller that is defined in my directive in my directive to change values.
Because JavaScript is designed to be so.
Defining an isolate scope in the directive creates a new $scope object, which is a separate $scope object. Its only relationship with the parent scope is that: $isolateScope.$parent === $parentScope. It doesn't inherits from $parentScope prototypical.
When you assign some primitive type (string/boolean) to $scope.isOpened, actually JavaScript engine will create a new variable isOpened on $scope. It is totally not related to $parentScope.isOpened.
But now, Angular syncs the two variables for you implicitly. So binding primitive variables still makes two-way binding work well. Please check JSFiddle.
If you binds to some object type, the child scope and parent scope are referencing to the exactly the same copy of an object in the memory. Changing on the parent scope will change the child scope automatically. So two-way binding is always recommended to bind objects, not primitive types.
Check this JSFiddle. I bind a primitive and an object to the directive myDirective. Then modify them inside the link function:
scope.primitiveParam = 'primitive from directive';
// $parent.primitive and primitiveParam refer to different memory;
// Angular is responsible to sync them.
console.log(scope.$parent.primitive);
console.log(scope.primitiveParam);
scope.objectParam.name = 'object from directive';
// $parent.obj and objectParam refer to an identical object
console.log(scope.$parent.obj.name);
console.log(scope.objectParam.name);
console.log(scope.objectParam === scope.$parent.obj);
And the result is like:
primitive from parent
primitive from directive
object from directive
object from directive
For more details: Understanding Scopes (here are many intuitive images illustrating the concepts clearly)
RE: For objects it works well but not with strings and booleans
I think it's the usual case of prototypal inheritance problem. When the model come from object it works well, but if it come from non-objects there's a possibility that the ng-model is created on child scope.
To solve that problem, use modern approach, use Controller as approach. Or put the filterModelIsOpened in an object. The first approach is better.
<div ng-controller="SomeController as s">
<path-filter-modal is-opened="s.filterModalIsOpened">
</div>
function SomeController() { // no need to use $scope
this.filterModalIsOpened = false;
}
Or if you are using older version of Angular, you cannot use Controller as approach. Just create your own alias in the controller:
<div ng-controller="SomeController">
<path-filter-modal is-opened="s.filterModalIsOpened">
</div>
function SomeController($scope) {
$scope["s"] = this;
this.filterModalIsOpened = false;
}
Here's a good article explaining the prototypal inheritance: http://codetunnel.io/angularjs-controller-as-or-scope/
Here are the demo why you should always prefix your model, be they are object or primitive.
Not recommended. Live code demo: http://jsfiddle.net/hdks813z/1/
<div ng-app="App" ng-controller="Ctrl">
<div ng-if="true">
<input type="checkbox" ng-model="filterModalCanBeOpened"/>
<the-directive primitive-param="filterModalCanBeOpened"></the-directive>
</div>
<hr/>
<p>
The value below doesn't react to changes in primitive(non-object) property
that is created a copy on a directive(e.g., ng-repeat, ng-if) that creates
child scope
</p>
$scope.primitive: {{filterModalCanBeOpened}}
</div>
angular.module('App', [])
.directive('theDirective', function () {
return {
restrict: 'AE',
scope: {
primitiveParam: '='
},
template: '<div>primitiveParam from directive: {{ primitiveParam }}; </div>',
link: function (scope) {
}
};
})
.controller('Ctrl', ['$scope', function ($scope) {
$scope.filterModalCanBeOpened = true;
}]);
Recommended: Live code demo: http://jsfiddle.net/2rpv27kt/
<div ng-app="App" ng-controller="Ctrl as c">
<div ng-if="true">
<input type="checkbox" ng-model="c.filterModalCanBeOpened"/>
<the-directive primitive-param="c.filterModalCanBeOpened"></the-directive>
</div>
<hr/>
<p>
The value below react to changes in primitive(non-object) property that is
addressed directly by its alias c, creating child scope on it would be
impossible. So the primitive below react to changes on
the c's filterModalCanBeOpened.
</p>
c.primitive: {{c.filterModalCanBeOpened}}
</div>
angular.module('App', [])
.directive('theDirective', function () {
return {
restrict: 'AE',
scope: {
primitiveParam: '='
},
template: '<div>primitiveParam from directive: {{ primitiveParam }}; </div>',
link: function (scope) {
}
};
})
.controller('Ctrl', [function () {
this.filterModalCanBeOpened = true;
}]);

How do I assign an attribute to ng-controller in a directive's template in AngularJS?

I have a custom attribute directive (i.e., restrict: "A") and I want to pass two expressions (using {{...}}) into the directive as attributes. I want to pass these attributes into the directive's template, which I use to render two nested div tags -- the outer one containing ng-controller and the inner containing ng-include. The ng-controller will define the controller exclusively used for the template, and the ng-include will render the template's HTML.
An example showing the relevant snippets is below.
HTML:
<div ng-controller="appController">
<custom-directive ctrl="templateController" tmpl="template.html"></custom-directive>
</div>
JS:
function appController($scope) {
// Main application controller
}
function templateController($scope) {
// Controller (separate from main controller) for exclusive use with template
}
app.directive('customDirective', function() {
return {
restrict: 'A',
scope: {
ctrl: '#',
tmpl: '#'
},
// This will work, but not what I want
// Assigning controller explicitly
template: '<div ng-controller="templateController">\
<div ng-include="tmpl"></div>\
</div>'
// This is what I want, but won't work
// Assigning controller via isolate scope variable from attribute
/*template: '<div ng-controller="ctrl">\
<div ng-include="tmpl"></div>\
</div>'*/
};
});
It appears that explicitly assigning the controller works. However, I want to assign the controller via an isolate scope variable that I obtain from an attribute located inside my custom directive in the HTML.
I've fleshed out the above example a little more in the Plunker below, which names the relevant directive contentDisplay (instead of customDirective from above). Let me know in the comments if this example needs more commented clarification:
Plunker
Using an explicit controller assignment (uncommented template code), I achieve the desired functionality. However, when trying to assign the controller via an isolate scope variable (commented template code), it no longer works, throwing an error saying 'ctrl' is not a function, got string.
The reason why I want to vary the controller (instead of just throwing all the controllers into one "master controller" as I've done in the Plunker) is because I want to make my code more organized to maintain readability.
The following ideas may be relevant:
Placing the ng-controller tags inside the template instead of wrapping it around ng-include.
Using one-way binding ('&') to execute functions instead of text binding ('#').
Using a link function instead of / in addition to an isolate scope.
Using an element/class directive instead of attribute directive.
The priority level of ng-controller is lower than that of ng-include.
The order in which the directives are compiled / instantiated may not be correct.
While I'm looking for direct solutions to this issue, I'm also willing to accept workarounds that accomplish the same functionality and are relatively simple.
I don't think you can dynamically write a template key using scope, but you certainly do so within the link function. You can imitate that quite succinctly with a series of built-in Angular functions: $http, $controller, $compile, $templateCache.
Plunker
Relevant code:
link: function( scope, element, attrs )
{
$http.get( scope.tmpl, { cache: $templateCache } )
.then( function( response ) {
templateScope = scope.$new();
templateCtrl = $controller( scope.ctrl, { $scope: templateScope } );
element.html( response.data );
element.children().data('$ngControllerController', templateCtrl);
$compile( element.contents() )( templateScope );
});
}
Inspired strongly by this similar answer.

Angular: What is a good way to hide undefined attributes that exist in isolated scopes?

I wanted to rewrite this fiddle as it no longer worked in angular 1.2.1. From this exercise, I learned that a template is apparently always needed now in the isolated scopes.
somewhere in the directive:
template: '<p>myAttr1 = {{myAttr1}} // Passed by my-attr1<br>
myAttr2 = {{myAttr2}} // Passed by my-alias-attr2 <br>
myAttr3 = {{myAttr3}} // From controller
</p>',
I was not able,however, to successfully add this to the template:
<p ng-show="myAttr4">myAttr4= {{myAttr4}} // Hidden and missing from attrs</p>
What is a good way to hide undefined attributes that are defined on the isolated scope but not given a value from the dom?
my humble fiddle
EDIT: I use a directive called my-d1 to encapsulate the bootstrap tags. I use my-d2 to demo how to use the # in isolated scopes.
Working version merged with Sly's suggestions
I ran into the same template issue in Angular 1.2.0, see the first entry in the 1.2.0 breaking changes:
Child elements that are defined either in the application template or in some other directives template do not get the isolate scope. In theory, nobody should rely on this behavior, as it is very rare - in most cases the isolate directive has a template.
I'm not exactly sure what the issue is that you are encountering - it might be some incorrect markup or you are misnaming the scope variables listed in your isolate scope.
Using ng-show will correctly hide the element if the attribute has not been passed in.
i.e. your example here is correct: <p ng-show="myAttr4">myAttr4= {{myAttr4}}</p>
Updated version of your Fiddle: http://jsfiddle.net/Sly_cardinal/6paHM/1/
HTML:
<div ng-app='app'>
<div class="dir" my-directive my-attr1="value one" my-attr3='value three'>
</div>
<div class="dir" my-directive my-attr1="value one" my-attr3='value three' my-attr4='value four'>
</div>
</div>
JavaScript:
var app = angular.module('app', []);
app.directive('myDirective', function () {
return {
// can copy from $attrs into scope
scope: {
one: '#myAttr1',
two: '#myAttr2',
three: '#myAttr3'
},
controller: function ($scope, $element, $attrs) {
// can copy from $attrs to controller
$scope.four = $attrs.myAttr4 || 'Fourth value is missing';
},
template: '<p>myAttr1 = {{one}} // Passed by my-attr1</p> '+
'<p ng-show="two">myAttr2 = {{two}} // Passed by my-alias-attr2 </p>'+
'<p>myAttr3 = {{three}} // From controller</p>'+
'<p ng-show="four">myAttr4= {{four}} // Has a value and is shown</p>'
}
});

Checkbox not binding to scope in angularjs

I am trying to bind a checkbox to scope using ng-model. The checkbox's initial state corresponds to the scope model just fine, but when I check/uncheck the checkbox, the model does not change. Some things to note is that the template is dynamically loaded at runtime using ng-include
app.controller "OrdersController", ($scope, $http, $location, $state, $stateParams, Order) ->
$scope.billing_is_shipping = false
$scope.bind_billing_to_shipping = ->
console.log $scope.billing_is_shipping
<input type="checkbox" ng-model="billing_is_shipping"/>
When I check the box the console logs false, when I uncheck the box, the console again logs false. I also have an order model on the scope, and if I change the checkbox's model to be order.billing_is_shipping, it works fine
I struggled with this problem for a while. What worked was to bind the input to an object instead of a primitive.
<!-- Partial -->
<input type="checkbox" ng-model="someObject.someProperty"> Check Me!
// Controller
$scope.someObject.someProperty = false
If the template is loaded using ng-include, you need to use $parent to access the model defined in the parent scope since ng-include if you want to update by clicking on the checkbox.
<div ng-app ng-controller="Ctrl">
<div ng-include src="'template.html'"></div>
</div>
<script type="text/ng-template" id="template.html">
<input type="checkbox" ng-model="$parent.billing_is_shipping" ng-change="checked()"/>
</script>
function Ctrl($scope) {
$scope.billing_is_shipping = true;
$scope.checked = function(){
console.log($scope.billing_is_shipping);
}
}
DEMO
In my directive (in the link function) I had created scope variable success like this:
link: function(scope, element, attrs) {
"use strict";
scope.success = false;
And in the scope template included input tag like:
<input type="checkbox" ng-model="success">
This did not work.
In the end I changed my scope variable to look like this:
link: function(scope, element, attrs) {
"use strict";
scope.outcome = {
success : false
};
And my input tag to look like this:
<input type="checkbox" ng-model="outcome.success">
It now works as expected. I knew an explanation for this, but forgot, maybe someone will fill it in for me. :)
Expanding on Matt's answer, please see this Egghead.io video that addresses this very issue and provides an explanation for: Why binding properties directly to $scope can cause issues
see: https://groups.google.com/forum/#!topic/angular/7Nd_me5YrHU
Usually this is due to another directive in-between your ng-controller
and your input that is creating a new scope. When the select writes
out it value, it will write it up to the most recent scope, so it
would write it to this scope rather than the parent that is further
away.
The best practice is to never bind directly to a variable on the scope
in an ng-model, this is also known as always including a "dot" in
your ngmodel.

Resources