AngularJS 1.5 component won't invoke $onChanges upon change - javascript

I have an AngularJS 1.5 component inside an html page (not parent component)
and the component won't invoke $onChanges upon change.
HTML
<my-comp standards-data="standardsData"></my-comp>
Component
angular.module("bla").component('myComp', {
templateUrl: '/my-comp.tpl.html',
controller: myController,
bindings: {
standardsData: '<'
}
});
function myController() {
var self = this;
self.$onInit = function () {
self.standardsData = {};
}
self.$onChanges = function (changes) {
self.standardsData.something = changes.standardsData.currentValue.something;
};
}
When I fetch new data in the ctrl of the html that contains my component,
it won't influence the component. I get only into the $onInit of the component
and after $scope.standardsData changes (in the containing html/ctrl),
the $OnChanges of my component won't invoke.
Hope I described the problem correctly,
Thanks !

Your component binding to standardsData is a binding to an object reference, which does not change even when one of its properties is modified. As a result, $onChanges is not triggered. This is due to the fact that $onChanges uses a shallow $watch.
In other words, $onChanges is triggered only if we use primitives (i.e. non-objects) or change the reference of the javascript object bound into the component
So, you need to either: 1) Bind manually to the property/properties you want, rather than the object reference, or 2) Change the entire standardsData object when its data changes. You could also 3) Rewrite $onChanges functionality, I suppose (not recommended).
Option 1: Bind only to the property
Use if the parent controller/component merely changes a property or properties.
<my-comp standards-data-something="standardsData.something"></my-comp>
and
bindings: {
standardsDataSomething: '<'
}
Option 2: Change the reference
Use if the parent controller/component obtains completely new standardsData. You would keep your current HTML for this option and set:
standardsData = {newData}; //Reset the new data from http or wherever
to change the reference.
Some further reading you may find interesting:
http://blog.kwintenp.com/the-onchanges-lifecycle-hook/
http://www.codelord.net/2016/12/20/replacing-angulars-deep-watches-with-the-%24docheck-lifecycle-hook/

Related

Angular directives that call methods on child directives

I am looking for advice on how to implement a hierarchical structure in Angular, where a directive (<partition>) can call a method on a child directive's controller (<property-value>).
I have put together a detailed example here:
https://jsfiddle.net/95kjjxkh/1/
As you can see, my code contains an outer directive, <partition>, which displays one or more <property-value> directives within.
The <property-value> directive offers an editing method, editItem(), which allows the user to change the value of a single entry. (To keep my example short, I simply assign a random number here, but in my production app, a modal will appear, to query the user for a new value.)
This works fine. However, in the outer directive, <partition>, I would like to add the ability to create a new, blank <property-value> directive and then immediately call its editing method so that the user can enter an initial value. If no initial value is entered, the new item would be discarded.
I have seen examples of inner directives calling methods on enclosing directives, but not the other way around.
Is there a way to do this? Alternatively, is there a better way for me to build this kind of view?
You can always use $broadcast to talk both ways. To your parent as well as to your childrens.
In your Child controller you can do the following
app.directive('propertyValue', function() {
return {
require : '^partition'
restrict: 'E',
scope: {
item: '='
},
with this you will get the parent controller in child directive's link function like this
link:function(scope,element,attrs,partitionCtrl){
partitionCtrl.getChildCtrl(element)
}
in partition controller create getChildCtrl function and with that call "propertyvalue" controller function
controller: function ($scope, ItemFactory) {
// your code
var propValueCtrl =undefined;
this.getChildCtrl =function(elem)
{
propValueCtrl = elem.controller();
}
this.callChildFunction = function()
{
propValueCtrl.Edit();// whatever is the name of function
}
call this function when needed in property link function.
Hope this helps.

Using $onChanges vs $onInit in angular component

Is there a difference between using Controller1 vs Controller2?
angular.module('app', [])
.component('foo', {
templateUrl: 'foo.html',
bindings: {
user: '<',
},
controller: Controller1, //Or Controller2
});
function Controller1(){
this.$onInit = function(){
this.user = angular.copy(this.user);
};
this.$onChanges = function(changes){
if(changes.user && !changes.user.isFirstChange()){
this.user = angular.copy(changes.user.currentValue);
}
};
}
function Controller2(){
this.$onChanges = function(changes){
if(changes.user){
this.user = angular.copy(changes.user.currentValue);
}
};
}
Why should I bother with $onInit when I can just do the same this in $onChanges and save some rows?
Is this type of initialization better in $onChanges and $onInit better for some other kind of initialization?
$onInit
Called on each controller after all the controllers on an element have been constructed and had their bindings initialized (and before the pre & post linking functions for the directives on this element). This is a good place to put initialization code for your controller.
$onChanges
The $onChanges lifecycle hook gets called for a few reasons. The first, is on component initialisation, it passes down that initial changes Object at runtime, so we can grab our data straight away. The second reason it gets called is only when changes occur to '<' (one-way databinding) and '#' (for evaluated DOM attribute values) that are being bound to from the parent component. Once $onChanges gets called, you’ll get a special changes Object back that you can hook into, which we’ll explore in the upcoming sections.
The primary practical difference is $onInit will be called only on the directive initialisation but $onChanges will be called during initialisation and when < and # variables changes.

AngularJs Directive: How to dynamically set attributes on ng-repeat element

I am relatively new to AngularJS. While venturing into directive creation, I can across this problem: How to dynamically add / remove attributes on the children of the directive's element when these children are dynamically added with 'ng-repeat'?
First, I thought of this solution:
template
...
a.list-group-item(ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)', ng-href='playlist/{{ playlist._id }})
...
*directive
link: function(scope, elm, attrs) {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
...
Result
It turns out, my code never enters this angular.forEach loop because listItems is empty. I suppose it's because the ng-repeat is waiting for the scope.playlists to populate with the data from a async call to a server via $resource.
temporary fix
in the directive definition, I added a boolean variable that checks for the presence of 'add' in the element's attributes: var adding = 'add' in attrs ? true : false;
And then in the template,
a.list-group-item(ng-if='adding', ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)')
a.list-group-item(ng-if='!adding', ng-repeat='playlist in playlists', ng-href='playlist/{{playlist._id }}')
While it works fine, it is obviously not DRY at all. HELP!
Instead of removing attributes, change your click handler.
Add $event to the list of arguments and conditionally use preventDefault().
<a ng-click='addToPlaylist($event,playlist)' ng-href='playlist'>CLICK ME</a>
In your controller:
$scope.addToPlaylist = function(event,playlist) {
if (!$scope.adding) return;
//otherwise
event.preventDefault();
//do add operation
};
When not adding, the function returns and the href is fetched. Otherwise the default is prevented and the click handler does the add operation.
From the Docs:
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event
The way that you are trying to do things may not be the most Angularish (Angularist? Angularyist?) way. When using angular.element() to select child elements as you are trying to do here, you can make sure the child elements are ready as follows:
link: function(scope, elm, attrs) {
elm.ready(function() {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
});
}
However, this is unlikely to work in your situation, as #charlietfl points out below. If you want to avoid the solution you already have (which I think is better than your first attempt), you will have to reimplement your code altogether.
I would suggest defining an additional directive that communicates with its parent directive using the require property of the directive definition object. The new directive would have access to an add property of the parent (this.add in the parent directive's controller) and could be programmed to behave accordingly. The implementation of that solution is beyond the scope of this answer.
Update:
I decided to give the implementation something of a shot. The example is highly simplified, but it does what you are trying to do: alter the template of a directive based on the attributed passed to it. See the example here.
The example uses a new feature in Angular 1: components. You can read more about injectable templates and components here. Essentially, components allow you to define templates using a function with access to your element and its attributes, like so:
app.component('playlistComponent', {
// We can define out template as a function that returns a string:
template: function($element, $attrs) {
var action = 'add' in $attrs
? 'ng-click="$ctrl.addToPlaylist(playlist, track)"'
: 'ng-href="playlist/{{playlist._id}}"';
return '<a class="list-group-item" ng-repeat="playlist in playlists" ' +
action + '></a>';
},
// Components always use controllers rather than scopes
controller: ['playlistService', function(playlists) {
this.playlists = playlists;
this.addToPlaylist = function(playlist, track) {
// Some logic
};
}]
});

Exposing angular directive function to another module

Suppose I have a module with a directive as follows (this is a rough not tested)
I need to implement 3 basic things
Configuration for the element that will appear
Event listeners that the base controller can use
Public methods that the base controller can call
angular.module("componentModule",[]) .directive("myComp",function(){
return{
replace:true,
template:'<h2>This is my component</h2>',
scope:{config= "#"},
link:function(scope,element,attr){
this.deleteElement = function(id){
//writing the code to delete this component
//This is a API function that the user can call to delete
}
if (!scope.config.visible){
//this is a configuration object for the element
this.visible(false)}
}
} })
then i have my base HTML like containing the directive call like below
<div myComm="first" config="eleConfig"></myComp>
<div myComm="second" config="newEleConfig"></myComp>
I have a separate controller for my base HTML as follows,
angular.module("baseApp",['componentModule'])
.controller('baseCtrl',function(){
$scope.eleConfig = {
visible:true,
delete:function(e){
//This is called if we call the delete method
}
}
//this is how the delete method is to be called
$scope.first.deleteElement();
})
Question
How to call the deleteElement() method in the baseCtrl as shown above (want to do it the same way KENDO UI does)
The pattern that angular uses is to expose the directive API to the scope. This is how ng-model and ng-form both expose ngModelController and ngFormController APIs.
Here is how I would do it:
angular.module("componentModule",[])
.directive("myComp",function($parse){
return{
replace:true,
scope: {
config: '&'
},
template:'<h2>This is my component</h2>',
controller: function($scope) {
//Directive API functions should be added to the directive controller here or in the link function (if they need to do DOM manipulation)
},
link:function(scope,element, attr, ctrl){
//add to directive controller
if(scope.config().visible) {
//element should be visible, etc.
}
ctrl.deleteElement = function(){
//if this function is called we want to call the config.delete method:
if(scope.config && scope.config.delete) {
//calling the scope.config() method returns the config object from the parent
scope.config().delete(element);
}
}
if(attr.myComp) {
//change to scope.$parent
scope.$parent[attr.myComp] = ctrl;
}
}
}
})
Assuming markup of:
<div my-comp="first" config="configObject"></div>
<div my-comp="second" config="configObject"></div>
In your base controller
$scope.first.deleteElement();
or
$scope.second.deleteElement();
would delete the appropriate element.
UPDATE:
I've updated the directive based on your updated question. You want to pass a config object into the directive. The best way to do that is with an & binding. If you use the & binding, you need to remember that the directive will create a new scope, and you have to attach the controller to $scope.$parent.
In your first requirement, you said you want to write the delete function in the directive, but in the case of KendoUI the actual delete(change) function implementation is done in the base controller and the delete(change) event triggered when the component value changes, which in turn calls the delete function defined in the base controller by the directive.
If you want to implement something like KendoUI does then look at this
link toplunker
Switch on the browser console to see the log. KendoUI component's change event happens automatically when the input element changes but in this case i manually triggered the delete event after 3 seconds.

How to update directives based on service changes?

I have a directive(parent-directive) containing a slider(mySlider), that on stop event, call an angular $resource service with 2 params and the service return an object.
Directives structure:
<parent-directive>
<div ui-slider="slider.options" ng-model="mySlider" id="my-slider">
<span child-directive-one></span>
<span child-directive-two></span>
<span child-directive-three></span>
<div>
<span child-directive-four></child-directive-four>
</div>
</parent-directive
Whenever the user drag the slider, the service is called with different params and retieve new result, based on it I need to update the child directives.
I have in mind three ways:
using ng-model for all child elements instead directives, binding them on the scope of a controller in parent-directive;
the second one, that I don't know how to do it, is to create a controller in the parent-directive, that send and receive data from the service and share it to child-directives in order to update them.
the last one is to to create a state variable in the service and update it using a controller like to point 1.(see it above) and use a $watch to supervise the variable state and when it's changed then update the child-directives.
How should I proceed?
Please have a look here to see a brief code:
http://jsfiddle.net/v5xL0dg9/2/
Thanks!
ngModel is intended for two way binding, i.e. controls that allow the user to interfere with the value. From the description, it seems they are display-only components. So I would advise against using the ngModel.
Normally child directives require their parent. This allows them to call methods on the parent controller. What you need is the opposite: the parent controller needs to call methods on the children. It can be done: the children call a registerChild() method, and the parent iterates all registered children when it needs to call them. I find this implementation cumbersome.
Services are globals/singletons. I would vote against tying the service implementation to the UI needs.
My advice looks like your implementation of option 3, but with the parent controller holding the data:
1) Place the data you want to share with the child directives in a member variable of the parent controller:
myApp.directive('parentDirective', ['myService', function(myService){
...
controller: function($scope) {
...
this.sharedThing = ...;
}
}]);
The sharedThing can be updated when the service returns new data, or any other time it is necessary.
2) Have the children require the parent (just like your option 2), and watch this property:
myApp.directive('childDirectiveOne', function() {
return {
...
require: 'parentDirective',
link: function(scope, elem, attrs, parentDirective) {
scope.$watch(
function() {
return parentDirective.sharedThing;
},
function(newval) {
// do something with the new value; most probably
// you want to place it in the scope
}
});
}
};
});
Depending on the nature of the data, a deep watch may be required.

Categories

Resources