AngularJS : How to transclude and have both isolate scope and parent scope? - angularjs

I have a pattern wherein many item types are "editable". This means that I have lots of templates (one for each editable item type) that expect to have unique fields, but common functions (edit, save, cancel edit, delete, etc.). These common functions lead to lots of repetition on controllers: save, edit, cancel, etc., and very repetitive error-handling.
One way I looked at of dealing with this was to have each controller "decorate" itself (using a service), but it got messy as well.
I prefer a directive, say, 'editable':
<form name="editGroup" editable>
<div ng-show="editMode">
<!-- lots of fields, e.g. -->
<input type="text" ng-model="name"></input>
<span ng-show="editGroup.name.$error.required">The name is required</span>
<button type="submit" ng-click="save()">Save</button>
<button ng-click="cancel">Cancel</button>
</div>
<div ng-show="!editMode">
<!-- lots of text, e.g. -->
<span>{{name}}</span>
<button ng-click="edit()">Edit</button>
<button ng-click="delete()">Delete</button>
</div>
</form>
The problem is that all of the models come from the controller scope, since they are unique to this template, while the repetitive scope items, like the functions save() cancel() edit() delete() all come from the directive isolate scope.
I am, well, mixing scopes, and of course I have no way of knowing in advance what items need to be available. So if I transclude with:
isolate scope: I lose access to the controller models in the transcluded element, as well as the form for validations
controller scope (default): I lose access to the added on functions from the directive, which was the point of the directive in the first place!
I am doing something wrong here; what is the better (cleaner?) way to do this?

I managed to figure it out by shying away from ng-transclude and doing my own transclusion in the link function.
The following is the equivalent of the normal ng-transclude:
link: function (scope,element,attrs,ctrlr,transclude) {
var sc = scope.$parent.$new();
transclude(sc,function(clone,scope) {
element.append(clone); // or however else you want to manipulate the DOM
});
}
By adding the functions directly onto the transclude child scope, I was able to have everything work, without messing with the parent scope, which I really didn't want to do.
link: function (scope,element,attrs,ctrlr,transclude) {
var sc = scope.$parent.$new();
sc.editMode = false;
sc.save = function() {
};
sc.edit = function () {
sc.editMode = true;
};
// etc.
transclude(sc,function(clone,scope) {
element.append(clone); // or however else you want to manipulate the DOM
});
}
Best of both worlds!

Related

AngularJS component nesting

I created a component search-context, which works well. It's configurable and it does what it's supposed to do.
<search-context context-name="Groups"
compare-columns="['displayName']"
search-manager="$ctrl"
query-url="/group/search/{{contextId}}"
icon="fa fa-users"
on-resolve-item-url="resolveItemUrl(row)"></search-context>
Here it is in action, standalone.
There are various other search contexts, and I'd like to create a search-manager component such that I can write markup like this:
<search-manager>
<search-context context-name="Devices"
compare-columns="['displayName']"
search-manager="$ctrl"
query-url="/device/search/{{contextId}}"
icon="fa fa-laptop"></search-context>
<search-context context-name="Groups"
compare-columns="['displayName']"
search-manager="$ctrl"
query-url="/group/search/{{contextId}}"
icon="fa fa-users"
on-resolve-item-url="resolveGroupEditUrl(row)"></search-context>
</search-manager>
The general plan is for search-context to check whether it has a search-manager and if so suppress its own input/button controls, and the search-manager will supply input controls and supply the search term to the search contexts.
The examples in the AngularJS component documentation demonstrate dynamic child controls using ng-repeat in the control template, but it's not clear how to set things up to handle explicit markup such as I propose. If at all possible I'd prefer not to need to explicitly specify the search-manager="$ctrl" parent reference.
How does one go about this and what are the supporting topics one must research and understand? Just the key concept names would be a big help but an overview and a further-reading list would be awesome.
My first attempt at the template for search-manager looks like this
<div>
<div class="panel-heading">
<h3 class="panel-title">Search</h3>
<div class="input-group">
<input class="form-control" ng-model="$ctrl.term" />
<span class="input-group-btn" ng-click="$ctrl.search()">
<button class="btn btn-default">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
<div class="panel-body">
<ng-transclude></ng-transclude>
</div>
</div>
The code looks like this
function SearchManagerController($scope, $element, $attrs, $http) {
var ctrl = this;
ctrl.searchContext = [];
ctrl.registerSearchContext = function (searchContext) {
ctrl.searchContext.push(searchContext);
}
ctrl.search = function () {
ctrl.searchContext.forEach(function (searchContext) {
searchContext.search(ctrl.term);
});
};
}
angular.module("app").component("searchManager", {
templateUrl: "/app/components/search-manager.html",
controller: SearchManagerController,
transclude: true,
bindings: {
term: "#"
}
});
The child components are transcluded but they need a reference to the search-manager component, and $ctrl is not in scope.
How do we get a reference to the parent?
To obtain a reference to the parent all you need to do is require the parent in the search-context declaration. The double caret prefix means to search the parents. Single caret starts with the current object which will work but is slightly less efficient. The question mark means don't barf if you can't find it, just return undefined. This is necessary when the component may not always be parented by a search manager.
angular.module("app").component("searchContext", {
templateUrl: "/app/components/search-context.html",
controller: SearchContextController,
require: {
searchManagerCtrl: "?^^searchManager"
},
bindings: {
...
}
});
But what if you need the parent to have references to the children?
In the SearchContextController we implement the $onInit lifecycle event handler.
ctrl.$onInit = function () {
if (ctrl.searchManagerCtrl) {
ctrl.searchManagerCtrl.registerSearchContext(ctrl);
}
};
registerSearchContext is a method defined in the parent's controller for this purpose. The implementation essentially pushes each registered control into an array property we define on its scope, and then methods of the parent can enumerate the children.
For a directive this require trick is expressed slightly differently. You must declare the property searchManagerCtrl in the directive scope, and supply the expression directly as the value of require.
require: "?^^searchManager",
You must also supply a link function. One of the parameters of a link function is controller and a reference to searchManager will be passed in this parameter, at which point you can assign it to a property of the directive scope. The $onInit lifecycle event is still available for registering with the search manager, but refers to $scope rather than ctrl.

angular directive (2-way-data-binding) - parent is not updated via ng-click

I have a nested directive with an isolated scope. An Array of objects is bound to it via 2 way data binding.
.directive('mapMarkerInput',['mapmarkerService', '$filter', '$timeout',function(mapMarkerService, $filter, $timeout) {
return {
restrict: 'EA',
templateUrl:'templates/mapmarkerInputView.html',
replace: true,
scope: {
mapmarkers: '='
},
link: function($scope, element, attrs) {
//some other code
$scope.addMapmarker = function($event) {
var mapmarker = {};
var offsetLeft = $($event.currentTarget).offset().left,
offsetTop = $($event.currentTarget).offset().top;
mapmarker.y_coord = $event.pageY - offsetTop;
mapmarker.x_coord = $event.pageX - offsetLeft;
mapmarker.map = $scope.currentMap;
$scope.mapmarkers = $scope.mapmarkers.concat(mapmarker);
};
$scope.deleteMapmarker = function(mapmarker) {
var index = $scope.mapmarkers.indexOf(mapmarker);
if(index !== -1) {
$scope.mapmarkers.splice(index,1);
}
};
//some other code
)
}]);
These 2 functions are triggered via ng-click:
<img ng-if="currentMap" ng-click="addMapmarker($event)" ng-src="/xenobladex/attachment/{{currentMap.attachment.id}}" />
<div class="mapmarker-wrapper" ng-repeat="mapmarker in shownMapmarkers" ng-click="setZIndex($event)" style="position: absolute; top: {{mapmarker.y_coord}}px; left: {{mapmarker.x_coord}}px;">
<!-- some other code -->
<div class="form-group">
<label>Name:</label>
<input ng-model="mapmarker.name" value="mapmarker.name" class="form-control" type="text">
</div>
<div class="form-group">
<label>Description:</label>
<input ng-model="mapmarker.description" value="mapmarker.description" class="form-control" type="text">
</div>
<button class="btn btn-danger" ng-click="deleteMapmarker(mapmarker)">Delete</button>
</div>
As you can see I am binding the name and description directly via ng-model and that works just fine. The properties are also available in the parent scope, but neither the delete nor the add works (its changed within the directives scope, but not the parent scope).
As far as I understand these changes should be applied, because I'm calling these functions via ng-click and I have other examples where this works. The only difference is, that I am binding to an array of objects and not a single object / property.
I tried using $timer and updateParent() ($scope.$apply() does not work -> throws an exception that the function is already within the digest cycle) but with no success, so it looks like these changes are not watched at all.
The directive code looks like this:
<map-marker-input ng-if="$parent.formFieldBind" mapmarkers="$parent.formFieldBind"></map-marker-input>
It is nested within a custom form field directive which gets the correct form field template dynamically and has therefore template: '<div ng-include="getTemplate()"></div>' as template, which creates a new child scope - that's why the $parent is needed here.
The binding definitely works in one way, the expected data is available within the directive and if I'm logging the data after changing it via delete or add, it's also correct, but only from the inside of the directive.
Because ng-model works I guess there might be a simple solution to the problem.
UPDATE
I created a plunkr with a simplified version:
http://plnkr.co/85oNM3ECFgCzyrSPahIr
Just click anywhere inside the blue area and new points are added from within the mapmarker directive. Right now I dont really prevent adding points if you delete or edit these - so you'll end up with a lot of points fast ;-)
There is a button to show the data from the parent scope and from the child scope.
If you edit the name or description of the one existing point that will also be changed in the parent scope (bound via ng-model). But all new points or deletions are ignored (bound within the functions called via ng-click).
If you want to update the parent scope, you need to access it via $parent once more,
i change
mapmarkers="$parent.formFieldBind"
to :
mapmarkers="$parent.$parent.formFieldBind"
ng-include create one more scope, so you need to access the parent once more.
http://plnkr.co/edit/27qF6ABUxIum8A3Hrvmt?p=preview

AngularJS - Directive to append input value to a list and bind to model

End goal: Associate multiple email addresses, each with a frequency setting (daily,weekly,monthly), to a notification.
I am attempting to define a directive which acts on a button element, such that, when the button is clicked, it takes the email address from the input element and the frequency from the drop-down next to the button and inserts them into a list below the input+button and binds the dynamic list to a property on the controller so it can be sent to the server when the user submits the form.
The form is all wired up - thats not the question.
I just want some help in building the directive.
Here's what i have so far:
HTML:
<section ng-controller="notificationmanagement as vm">
<input name="email" type="email"/>
<select>
<option>Daily</option>
<option>Monthly</option>
<option>Weekly</option>
</select>
<button data-cm-click type="button" class="btn btn-default"></button>
<div ng-model="vm.Subscribers"></div>
</section>
Directive:
commonModule.directive('cmClick', function () {
return function (scope, element) {
element.bind("click", function () {
// Is this the best way to get the value of the EmailAddress & Frequency?
var emailAddress = element[0].parentElement.parentElement.children[0].value;
var frequency = element[0].parentElement.parentElement.children[1].value;
var spanEmailTag = angular.element('<span>' + emailAddress + '</span>');
var spanFreqTag = angular.element('<span>' + frequency + '</span>');
angular.element(spanEmailTag).appendTo(element[0].parentElement.parentElement);
angular.element(spanFreqTag).appendTo(element[0].parentElement.parentElement);
// How to make sure each added email+frequency is available
// in the array bound to 'vm.Subscribers'
});
}
});
The structure of 'vm.Subscribers' should be something like this, in order for it be consumed by the server:
vm.Subscribers = [ {'EmailAddress':'joe#email.com', 'Frequency':'Daily'}, {'EmailAddress':'bob#email.com', 'Frequency':'Monthly'} ];
NOTE: I would ideally like to achieve this without relying on jQuery within the directive.
Any and all pointers/help/advice would be most appreciated!
If you want to encapsulate and reuse some functionality, then, by all means, use a directive.
But first, understand the MVVM (Model-View-ViewModel) paradigm and how Angular implements this.
For starters, assume that there is no View (and so, no DOM, directives, etc...) and that user inputs magically occur when something is exposed on the $scope (or, if you are using ControllerAs - as a property of the controller). Now, build you app's functionality starting from the controller. Define the data structure and the behavior with functions.
app.controller("notificationmanagement", function(){
// list of subscribers. You could also populate it from the backend.
this.subscribers = [];
// this is where the details of a new subscriber will be stored
// until subscriber is added
this.newSubscriber = {EmailAddress: null, Frequency: null};
// action to add a susbscriber
this.addSubscriber = function(){
this.subscribers.push(this.newSubscriber);
// clear the object (it's not necessary to declare all the properties)
this.newSubscriber = {};
}
});
That is, in a nutshell, all your app is doing. The controller doesn't (and shouldn't) care how this is displayed in the View. This is why DOM manipulation is frown upon, because it breaks separation of concerns.
Now, to the View. The View is mostly declarative:
<section ng-controller="notificationmanagement as vm">
<input ng-model="vm.newSubscriber.EmailAddress" type="email>
<select ng-model="vm.newSubscriber.Frequency">
<option value="Daily">Daily</option>
<option value="Monthly">Monthly</option>
<option value="Weekly">Weekly</option>
</select>
<button ng-click="vm.addSubscriber()"> Add </button>
<hr/>
<h3>All subscribers:</h3>
<div ng-repeat="s in vm.subscribers">
<span>{{s.EmailAddress}}</span>
<span>{{s.Frequency}}</span>
</div>
</section>
Notice how ng-model directives bind input data to controller's data. Notice ng-click that invokes an action on the controller. Notice ng-repeat that iterates and creates DOM elements based on that data. The View is purely driven by data, which is referred to by ViewModel.
Understand this first, then move onto directives.

How to avoid "sausage" type binding when having nested model

I have nested model and I am trying to avoid vm.someObject.someChild.ChildOfChild.name type of situations. Is there a way to set the model for outer <div> so that I can instead do ChildOfChild.name or even name. In Silverlight this was called DataContext. I put "vm" on the $scope, but in html I would like to avoid having to type the full path to attribute.
For example:
<div>
{{someObject.Id}}
<div>
{{someObject.name.first}}
{{someObject.name.last}}
</div>
<div>
{{someObject.someChild.name.first}}
</div>
</div>
I would like to do something like this
<div datacontext = someObject>
{{Id}}
<div datacontext = name>
{{first}}
{{last}}
</div>
<div datacontext = someChild.name>
{{first}}
</div>
</div>
You can do this with a custom directive.
HTML:
<div ng-app="myApp" ng-controller="myCtrl as ctrl">
<div>
Access from deepObj: {{ctrl.deepObj.one.two.three.four}}
</div>
<div scope-context="ctrl.deepObj.one.two.three">
Access from third level: {{four}}
</div>
</div>
JS:
var myApp = angular.module('myApp', []);
var myCtrl = function() {
this.deepObj = {one: {two: {three: {four: "value"}}}};
};
myApp.directive('scopeContext', function () {
return {
restrict:'A',
scope: true,
link:function (scope, element, attrs) {
var scopeContext = scope.$eval(attrs.scopeContext);
angular.extend(scope, scopeContext);
}
};
});
See the documentation on $compile for information on what scope: true does.
Make sure you don't call the directive something like data-context as an attribute starting with data- has a special meaning in HTML5.
Here is the plunker: http://plnkr.co/edit/rMUQlaNsH8RTWiRrmohx?p=preview
Note that this can break two-way bindings for primitive values on the scope context. See this plunker for an example: http://plnkr.co/edit/lCuNMxVaLY4l4k5tzHAn?p=preview
You could try/abuse ng-init
Try ng-init, you'll have one more ., but it's better than the other answer I've seen proposed:
<div ng-init="x = foo.bar.baz">
{{x.id}}
{{x.name}}
</div>
BUT Be warned, doing this actually creates a value on your scope, so doing this with something like ng-model if you're reusing the same name, or in a repeater, will produce unexpected results.
Why a custom directive for this probably isn't a good idea
What #rob suggests above is clever, and I've seen it suggested before. But there are issues, which he touches on, in part at least:
Scope complexity: Adding n-scopes that need to be created (with prototypical inheritence) whenever views are compiled.
View processing complexity: Adding an additional directive (again for no real functional benefit) that needs to be checked on each node when the view is compiled.*
Readability? The next Angular developer will likely less readable because it's different.
Forms Validation: If you're doing anything with forms in Angular, this might break things like validation.
ng-model woahs: Setting things with ng-model this way will not be at all intuitive. You'll have to use $parent.whatever or $parent.$parent.whatever depending on how may contexts deep you are.
* For reference, views are $compiled more than you think: For every item in a repeater, whenever it's changed for example.
A common idea that just doesn't jive with Angular
I feel like this question comes up frequently in StackOverflow, but I'm unable to find other similar questions ATM. ... regardless, if you look at the approaches above, and the warnings given about what the side effects will be, you should be able to discern it's probably not a good idea to do what you're trying to do just for the sake of readability.

AngularJs can't access form object in controller ($scope)

I am using bootstrap-ui more specifically modal windows. And I have a form in a modal, what I want is to instantiate form validation object. So basically I am doing this:
<form name="form">
<div class="form-group">
<label for="answer_rows">Answer rows:</label>
<textarea name="answer_rows" ng-model="question.answer_rows"></textarea>
</div>
</form>
<pre>
{{form | json}}
</pre
I can see form object in the html file without no problem, however if I want to access the form validation object from controller. It just outputs me empty object. Here is controller example:
.controller('EditQuestionCtrl', function ($scope, $modalInstance) {
$scope.question = {};
$scope.form = {};
$scope.update = function () {
console.log($scope.form); //empty object
console.log($scope.question); // can see form input
};
});
What might be the reasons that I can't access $scope.form from controller ?
Just for those who are not using $scope, but rather this, in their controller, you'll have to add the controller alias preceding the name of the form. For example:
<div ng-controller="ClientsController as clients">
<form name="clients.something">
</form>
</div>
and then on the controller:
app.controller('ClientsController', function() {
// setting $setPristine()
this.something.$setPristine();
};
Hope it also contributes to the overall set of answers.
The normal way if ng-controller is a parent of the form element:
please remove this line:
$scope.form = {};
If angular sets the form to your controllers $scope you overwrite it with an empty object.
As the OP stated that is not the case here. He is using $modal.open, so the controller is not the parent of the form. I don't know a nice solution. But this problem can be hacked:
<form name="form" ng-init="setFormScope(this)">
...
and in your controller:
$scope.setFormScope= function(scope){
this.formScope = scope;
}
and later in your update function:
$scope.update = function () {
console.log(this.formScope.form);
};
Look at the source code of the 'modal' of angular ui bootstrap, you will see the directive has
transclude: true
This means the modal window will create a new child scope whose parent here is the controller $scope, as the sibling of the directive scope. Then the 'form' can only be access by the newly created child scope.
One solution is define a var in the controller scope like
$scope.forms = {};
Then for the form name, we use something like forms.formName1. This way we could still access it from our controller by just call $scope.forms.formName1.
This works because the inheritance mechanism in JS is prototype chain. When child scope tries to create the forms.formName1, it first tries to find the forms object in its own scope which definitely does not have it since it is created on the fly. Then it will try to find it from the parent(up to the prototype chain) and here since we have it defined in the controller scope, it uses this 'forms' object we created to define the variable formName1. As a result we could still use it in our controller to do our stuff like:
if($scope.forms.formName1.$valid){
//if form is valid
}
More about transclusion, look at the below Misco's video from 45 min. (this is probably the most accurate explanation of what transcluded scopes are that I've ever found !!!)
www.youtube.com/watch?v=WqmeI5fZcho
No need for the ng-init trickery, because the issue is that $scope.form is not set when the controller code is run. Remove the form = {} initialization and get access to the form using a watch:
$scope.$watch('form', function(form) {
...
});
I use the documented approach.
https://docs.angularjs.org/guide/forms
so, user the form name, on "save" click for example just pass the formName as a parameter and hey presto form available in save method (where formScopeObject is greated based upon the ng-models specifications you set in your form OR if you are editing this would be the object storing the item being edited i.e. a user account)
<form name="formExample" novalidate>
<!-- some form stuff here -->
Name
<input type="text" name="aField" ng-model="aField" required="" />
<br /><br />
<input type="button" ng-click="Save(formExample,formScopeObject)" />
</form>
To expand on the answer by user1338062: A solution I have used multiple times to initialize something in my controller but had to wait until it was actually available to use:
var myVarWatch = $scope.$watch("myVar", function(){
if(myVar){
//do whatever init you need to
myVarWatch(); //make sure you call this to remove the watch
}
});
For those using Angular 1.5, my solution was $watching the form on the $postlink stage:
$postLink() {
this.$scope.$watch(() => this.$scope.form.$valid, () => {
});
}

Categories

Resources