using angular sub controllers for partial views? - angularjs

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.

Related

AngularJS: How a Directive without a Controller can work

just reading a write up from this link http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-6-using-controllers
it was hard me like novice in ng to understand their code sample. just tell me a example where people would write directive without controller ?
their code
(function() {
var app = angular.module('directivesModule');
app.directive('isolateScopeWithController', function () {
var controller = ['$scope', function ($scope) {
function init() {
$scope.items = angular.copy($scope.datasource);
}
init();
$scope.addItem = function () {
$scope.add();
//Add new customer to directive scope
$scope.items.push({
name: 'New Directive Controller Item'
});
};
}],
template = '<button ng-click="addItem()">Add Item</button><ul>' +
'<li ng-repeat="item in items">{{ ::item.name }}</li></ul>';
return {
restrict: 'EA', //Default in 1.3+
scope: {
datasource: '=',
add: '&',
},
controller: controller,
template: template
};
});
}());
Directive usage:
Attribute: <div isolate-scope-with-controller datasource="customers" add="addCustomer()"></div>
Element: <isolate-scope-with-controller datasource="customers" add="addCustomer()"></isolate-scope-with-controller>
How we can pass customer data directly to directive. basically we have model in controller and populate model and then pass that model data to directive via isolated scope or directive use controller scope. I am very confused the how above code can work, please help me to understand. thanks
The scenario that is being considered implies that the directive will be used in a part of the application, that already has a declared controller, the scope of which contains the properties datasource and add. In turn, new controllers will be instantiated for each of the instances of the directive and will have their own isolate scope.
In reality, it is much more common to create directives that do not have a controller, but rather use the link function. These directives can either rely on the parent controller, sometimes perform DOM manipulation, bind to JS events or simply serve as means to encapsulate part of your application.
You can find a good example of a directive that does not create its own controller here. It is taken from the Angular docs. You will find that it does not even belong to a parent scope in this case meaning that no controller is involved. In reality, an element like this would most probably report to a parent controller, which would then do something with the position.
You can read more about directives, the link function, and how directives work with controllers here.

Is one of these the more "Angular" way of communicating with a directive?

I asked this question on the Programmer's stack exchange, but didn't get any replies, so I thought I'd try my luck here...
I am working on a project where I would like to encapsulate a directive library and distribute it to other developers to use. I would like to keep the changes to the model within this encapsulated code, so I don't really want the dev's changing the scope variables outside of the lib.
In my code, I have 2 different approaches to communicating with my lib from the parent controller.
The first theory is to create a lib that contains a directive and a service. The parent controller would call the service, which would handle all of the changes to the lib model and the directive would react depending on these changes.
The second theory is to put all the functions to change the model in the directive itself and call the directive scope on the parent to make the changes.
Here is a plunker that shows what I'm asking in better detail. It's a simple example, but illustrates the 2 different methods.
http://plnkr.co/edit/CR350Vx7NiHs5tkjNWZL?p=preview
I'm leaning towards the second method as it just seems cleaner to implement from a development scenario.
Any advice from the Angular experts out there?
Plunker Html:
<body ng-app="myApp">
This is Example 1 - Using a service to modify directive
<div ng-controller="Example1Ctrl">
<example1-directive ng-model='example1Model'></example1-directive>
<br>
<br>
<input type="button" value="Change Example 1" ng-click='changeExample1()' />
</div>
<br>
<br>
This is Example 2 - Modifying directive in the scope of the directive
<div ng-controller="Example2Ctrl">
<example2-directive ng-model='example2Model'></example2-directive>
<br>
<br>
<input type="button" value="Change Example 2" ng-click='changeExample2()' />
</div>
</body>
Plunker js
var app = angular.module("myApp", []);
//--------------------------------------------------
//-------- This is example 1
//--------------------------------------------------
app.controller("Example1Ctrl", function($scope, example1Svc) {
$scope.example1Model = {
value: "Example 1 - Original Value"
}
$scope.changeExample1 = function() {
example1Svc.change($scope.example1Model, "Example 1 - Changed Value");
}
});
/// This part would be encapsulated in a lib
app.directive("example1Directive", function() {
return {
restrict: "E",
scope: {
model: "=ngModel"
},
template: "{{model.value}}"
}
});
app.service("example1Svc", function() {
this.change = function(example1Model, newValue) {
example1Model.value = newValue;
}
})
// End lib
//--------------------------------------------------
//-------- This is example 2
//--------------------------------------------------
app.controller("Example2Ctrl", function($scope, example1Svc) {
$scope.example2Model = {
value: "Example 2 - Original Value"
}
$scope.changeExample2 = function() {
$scope.example2Model.change("Example 2 - Changed Value");
}
});
/// This part would be encapsulated in a lib
app.directive("example2Directive", function() {
return {
restrict: "E",
scope: {
model: "=ngModel"
},
template: "{{model.value}}",
controller: function ($scope) {
$scope.model.change = function(newValue) {
$scope.model.value = newValue;
}
}
}
});
// end lib
I'm somewhat confused by your example #1. What does exampleSvc.change do?
Example #2 definitely goes against MVVM best practice, as it couples the controller with the view. Controllers (of views) should be view-agnostic. They should only change the ViewModel to reflect the current state of the app. The View would then react (however the View chooses to) to changes in the ViewModel.
In particular, these lines "offend" the best practice in my mind:
$scope.model.change = function(newValue) {
$scope.model.value = newValue;
}
Now your controller relies on the view to define what the function does (or whether it is defined to begin with). Also, what if another directive decides to change the .change function?
EDIT:
Take a look at this SO question, and in particular an answer by Mark.
EDIT #2:
There is an interesting case for when some event needs to reach whichever directives or child controllers that could be interested. Use $scope.$broadcast (in controller) and $scope.$on (in directive) to handle. Here's a plunker
I'm going to agree with #New Dev and add a couple more thoughts. If you're building a directive library you don't want to also bundle controllers that the consumer of the library has to use. Your directives should be more or less self contained and provide enough of an api to be extensible and potentially used in other directives.
What does this mean? Your directives may want to define a controller so that they can be injected into other directives. E.g.
//your library
directiveModule.directive("example1Directive", function() {
return {
controller: function($scope, $element, $attrs) {
...
},
...
}
});
-
//application
app.directive("appDirective", function() {
return {
require: '?example1Directive',
link: function(scope, element, attrs, example1Directive) {
...
}
});
You may also want to specify various options that can be set on your directive with parameters e.g.
<div example1-directive="{opt1: 'val', opt2: scopeProp}"></div>
Your directive would then need to parse the the attribute and execute on the scope to generate the options. There's lots more you can do, I'd suggest taking a look at ngmodules.org and see what other people are doing.

Place element outside the controller and have them still work AngularJS

How can I control variables and elements using filters that are outside the controller?
I have set up an example to better help me explain my question http://embed.plnkr.co/7E5Ls3oH0q4HuEZsewJL/
You will see I have a div that is being toggled using ng-show and then inside a search input that filters a list of names. The problem arrises when I need to take the toggled div and the search filter and put it outside the 'MainCtrl'. Is there a way that I can have these sitting outside the controller but still interacting with the content of the 'MainCtrl'?
There are several ways to communicate between components which aren't prototypically linked in your app. You could use Angular events to broadcast your search and then handle it from your controller. Or, you could even use $rootScope in both components as a global space both can use to store variables and methods. These are bad ideas - they will make your life harder down the road.
Instead, whenever you need to share information across controllers and/or directives which aren't directly linked, your first thought should to create a service.
Such a service might look like this:
app.factory('Search', function() {
var search = {
results: [],
query: '',
showDetails: false
};
return search;
});
You would inject it into both the controller and the controller or directive (I chose to create a directive) for the search box and initialize the service to a scope variable in each:
app.controller('MainCtrl', function ($scope, friendsFactory, Search) {
$scope.friends = friendsFactory.query();
$scope.search = Search;
});
app.directive('search', function(Search){
return {
restrict: 'E',
scope: {},
templateUrl: 'directive-search.html',
link: {
pre: function(scope, elem, attrs) {
scope.search = Search;
}
}
}
});
You could use that variable in your views, as the model for the search box and the filter for your ng-repeat:
Directive Template
<div ng-class="'details'" ng-show="search.showDetails">
<label for="">Search Names</label>
<input type="text" ng-model="search.query" />
</div>
Filter
<div class="content" ng-controller="MainCtrl">
<a ng-click="search.showDetails = !search.showDetails"> Click Me</a>
<div ng-repeat="friend in friends | filter:search.query | limitTo: 5">
{{friend.name}}
</div>
</div>
Plunker Demo
Note: this simple implementation allows for one individual search/results pair on the page. If you need to have more than one, you will need to further develop the service to allow for more than one instance.

Sharing data between directives using attributes instead of services

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>

Changing property of controller from directive

EDIT: see jsfiddle
I have a list of items, that I show as text via a directive (all is simplified here, it's more comlicated than just text, but it's the same in principle). Like so:
<body ng:controller="BaseController">
...
<div ng:controller="Controller">
<itemdir ng:repeat="item in items" item="item"></item>
</div>
...
<input ng:model="currentItem" />
</body>
When I click it, it should show the content of the clicked item in an input.
items array as well as currentItem belong to BaseController scope.
The directive produces a template (see below) with ng:click which should change BaseController scope's property (called currentItem). However it does not do anything to it (input value is not changed to the new current item). In Batarang for Chrome I can see that the currentItem property is visible and changed in the scope of the directive but not of the BaseController.
module.directive 'itemdir', () ->
restrict: 'E'
replace: true
template: '<div ng:click="show(item)"></div>'
controller: 'EditorController'
scope:
item: '=item'
link: ($scope, $element, $attrs) ->
update = ->
$element.html($scope.item)
$scope.$watch('item', update)
For changing the property I tried a method show(item) which is defined in the BaseController's scope which only assigns the item parameter to $scope.currentItem.
It doesn't work even when I change the ng:click value from show(item) to currentItem = item
I know this is some scope issue, but it seems I still don't grasp all the details of it.
So, looking at the provided jsFiddle we can see that the BaseController is being used both in a directive and in top div. This introduced a subtle issue since it was possible to invoke the show(item) method from top-buttons and HTML produced by directives, but those methods were invoked on different controllers and writing to different scopes.
Now, it is hard to deduce from your question if the use of BaseController in a directive was intentional or not (in the question the directive has the EditorController) but assuming that this was by accident and you want to keep BaseController for a div and still invoke methods on it from a directive you need to take special care when creating isolated scopes (as the name implies those are really isolated so not inheriting from a parent scope). Basically you need to make sure that the show method is available in an isolated scope and points to the right method in the parent scope.
Taking your example you would define your directive like this (please note show : '&ngClick'):
module.directive('itemdir', function () {
return {
restrict:'E',
replace:true,
template:'<div ng:click="show(item)" class="clickable"></div>',
scope : {item : '=', show : '&ngClick'},
link:function ($scope, $element, $attrs) {
$element.html($scope.item)
}
}
});
Here is the working jsFiddle: http://jsfiddle.net/pkozlowski_opensource/M9B93/
In the future you might find AngularJS Batarang extension for Chrome (http://blog.angularjs.org/2012/07/introducing-angularjs-batarang.html) useful as it allows to visualize scopes and their content.

Resources