I am creating a series of dynamic form components using AngularJS 1.5.
I'm using ng-if to show and hide a form compoenent. When the component is destroyed, I remove it from and array in it's parent, but I also want to set the value of the two way bound object that I have passed in to null.
The below is a simplified version of the code I am using:
app.component('textInput', {
bindings: {
model: '='
},
require: {
parent:'^formPanel'
},
templateUrl: 'app/shared/form/formFields/textInput/textInputView.html',
controller: function() {
var self = this;
this.$onInit = function() {
this.parent.addField(this);
},
this.$onDestroy = function() {
this.model = null;
console.log(this);
this.parent.removeField(this);
}
}
});
This logs the value as null, but outside the scope of the component, angular hasn't registered the change and I am unable to run a digest cycle because there is already one in progress for the $onDestroy event.
You should be able to use a callback to achieve this.
So a function similar to this in the parent scope (this is the callback):
$scope.setMyModel = function(val){
$scope.myModel = val;
};
And this in the component HTML file to pass the callback to the component:
<test-input
my-model="myModel"
my-callback="setMyModel "
/>
And this in the component's bindings to collect the callback:
bindings: {
myModel: '=',
myCallback: '='
},
Followed by a call to the callback in the $onDestroy function:
this.$onDestroy = function() {
this.myCallback(null);
this.parent.removeField(this);
}
There may be a better way, but this will ensure that the parent scope is always in control of setting the value that myModel holds.
Related
in the new project I'm working on I've started using the components instead of directives.
however, I've encountered an issue where I cannot find a concrete standard way to do it.
It's easy to notify an event from child to parent, you can find it on my plunkr below, but what's the correct way to notify a event from parent to child?
Angular2 seems to solve this issue by using something like this: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-child-local-var
But I don't tink there's a possibilty to define a "pointer" to the child component like the example did with #timer
In order to mantain a possible easy conversion to Angular2 I want to avoid:
event emitting (emit and broadcast from the scopes)
using the require from the child (and then add a callback to the parent..UGLY)
using a one-way binding, injecting the scope in the child and then "watch" this property.. MORE UGLY
Example code:
var app = angular.module('plunker', []);
app.controller('RootController', function() {
});
app.component('parentComponent', {
template: `
<h3>Parent component</h3>
<a class="btn btn-default btn-sm" ng-click="$ctrl.click()">Notify Child</a>
<span data-ng-bind="$ctrl.childMessage"></span>
<child-component on-change="$ctrl.notifiedFromChild(count)"></child-component>
`,
controller: function() {
var ctrl = this;
ctrl.notifiedFromChild = function(count){
ctrl.childMessage = "From child " + count;
}
ctrl.click = function(){
}
},
bindings: {
}
});
app.component('childComponent', {
template: `
<h4>Child component</h4>
<a class="btn btn-default btn-sm" ng-click="$ctrl.click()">Notify Parent</a>
`,
controller: function() {
var ctrl = this;
ctrl.counter = 0;
ctrl.click = function(){
ctrl.onChange({ count: ++ctrl.counter });
}
},
bindings: {
onChange: '&'
}
});
You can find an example here:
http://plnkr.co/edit/SCK8XlYoYCRceCP7q2Rn?p=preview
This is a possible solution I created
http://plnkr.co/edit/OfANmt4zLyPG2SZyVNLr?p=preview
where the child requires the parent, and then child sets a parent reference to the child... now parent can use the child... ugly but it's like angular2 example above
Communicating Events from Parent to Child in AngularJS Components
Publish Directive $API Using Expression Binding
To allow parent components to communicate events to a child component, have the child publish an API:
<grid-component grid-on-init="$ctrl.gridApi=$API; $ctrl.someFn($API)">
</grid-component>
JS
app.component('gridComponent', {
//Create API binding
bindings: {gridOnInit: "&"},
template: `
<h4>Grid component</h4>
<p> Save count = {{$ctrl.count}}</p>
`,
controller: function() {
var ctrl = this;
this.$onInit = function() {
ctrl.count = 0;
ctrl.api = {};
//Publish save function
ctrl.api.save = save;
//Invoke Expression with $API as local
ctrl.gridOnInit({$API: ctrl.api});
};
function save(){
console.log("saved!");
ctrl.count++;
}
}
});
The above example invokes the Angular Expression defined by the grid-on-init attribute with its API exposed as $API. The advantage to this approach is that the parent can react to child initialization by passing a function to the child component with the Angular Expression.
From the Docs:
The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the directive's element. These local properties are useful for aliasing values for templates. The keys in the object hash map to the name of the property on the isolate scope; the values define how the property is bound to the parent scope, via matching attributes on the directive's element:
& or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given <my-component my-attr="count = count + value"> and the isolate scope definition scope: { localFn:'&myAttr' }, the isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression to the parent scope. This can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment($amount) then we can specify the amount value by calling the localFn as localFn({$amount: 22}).
-- AngularJS Comprehensive Directive API -- scope
As a convention, I recommend prefixing local variables with $ to distinguish them from parent variables.
Alternately use Bi-Directional Binding
NOTE: To ease the transition to Angular 2+, avoid the use of bi-directional = binding. Instead use one-way < binding and expression & binding. For more information, see AngularJS Developer Guide - Understanding Components.
To allow parent components to communicate events to a child component, have the child publish an API:
<grid-component api="$ctrl.gridApi"></grid-component>
In the above example, the grid-component uses bindings to publish its API onto the parent scope using the api attribute.
app.component('gridComponent', {
//Create API binding
bindings: {api: "="},
template: `
<h4>Grid component</h4>
<p> Save count = {{$ctrl.count}}</p>
`,
controller: function() {
var ctrl = this;
this.$onInit = function() {
ctrl.count = 0;
ctrl.api = {};
//Publish save function
ctrl.api.save = save;
};
function save(){
console.log("saved!");
ctrl.count++;
}
}
});
Then the parent component can invoke the child save function using the published API:
ctrl.click = function(){
console.log("Search clicked");
ctrl.gridApi.save();
}
The DEMO on PLNKR.
Here is an easy way: http://morrisdev.com/2017/03/triggering-events-in-a-child-component-in-angular/
basically, you add a bound variable called "command" (or whatever you want) and use the $onChanges to pay attention to changes of that variable and trigger whatever event it says to trigger manually.
I personally like to put all my variables into an object called "settings" and send that to all my components. However, a change to a value within an object does NOT trigger the $onChanges event, so you NEED to tell it to trigger the event with a flat variable.
I'd say it is not the "proper" way to do it, but it sure is a lot easier to program, a lot easier to understand, and a lot easier to convert to A2 later on down the road.
I faced with same question. What do you think about this approach: to use inheritance via require instead of Bi-Directional Binding?
http://plnkr.co/edit/fD1qho3eoLoEnlvMzzbw?p=preview
var app = angular.module('plunker', []);
app.controller('RootController', function() {
});
app.component('filterComponent', {
template: `
<h3>Filter component</h3>
<a class="btn btn-default btn-sm" ng-click="$ctrl.click()">Search</a>
<span data-ng-bind="$ctrl.childMessage"></span>
<grid-component api="$ctrl.gridApi"></grid-component>
`,
controller: function() {
var ctrl = this;
ctrl.click = function(){
console.log("Search clicked");
ctrl.gridApi.save();
};
}
});
app.component('gridComponent', {
require: {parent:'^^filterComponent'},
bindings: {api: "<"},
template: `
<h4>Grid component</h4>
<p> Save count = {{$ctrl.count}}
`,
controller: function() {
var ctrl = this;
this.$onInit = function() {
ctrl.count = 0;
ctrl.api = {};
ctrl.api.save = save;
ctrl.parent.gridApi = ctrl.api;
};
function save(){
console.log("saved!");
ctrl.count++;
}
}
});
Or we can define setter method for parent to make it more explicit.
http://plnkr.co/edit/jmETwGt32BIn3Tl0yDzY?p=preview
var app = angular.module('plunker', []);
app.controller('RootController', function() {
});
app.component('filterComponent', {
template: `
<h3>Filter component</h3>
<a class="btn btn-default btn-sm" ng-click="$ctrl.click()">Search</a>
<span data-ng-bind="$ctrl.childMessage"></span>
<grid-component pass-api="$ctrl.setGridApi(api)"></grid-component>
`,
controller: function() {
var ctrl = this;
var gridApi = {};
ctrl.setGridApi = function(api){
gridApi = api;
};
ctrl.click = function(){
console.log("Search clicked");
gridApi.save();
};
}
});
app.component('gridComponent', {
bindings: {
passApi:'&'
},
template: `
<h4>Grid component</h4>
<p> Save count = {{$ctrl.count}}
`,
controller: function() {
var ctrl = this;
this.$onInit = function() {
ctrl.count = 0;
ctrl.api = {};
ctrl.api.save = save;
ctrl.passApi({api: ctrl.api});
};
function save(){
console.log("saved!");
ctrl.count++;
}
}
});
SIMPLE: You just need one property 1 way bound because 2 way binding only calls onChanges at creation.
Set a new boolean property on the parent controller.
vm.changeNow = false;
//update this to vm.changeNow = !vm.changeNow when you want to tell the component to
//call a method.
Open Child Component, in the bindings section,
bindings: {
a2waybind: '=',
changenow: '<'
}
You now need an $onChanges event on the child.
$onChanges() {
// do that sweet stuff you wanted to listen from the parent for.
}
Now when calling the template:
childComponent a2waybind="$ctrl.mycoolvalue" changenow="$ctrl.changeNow" /childComponent"
A second way to do this is in your child component:
var vm = this;
var myprop;
Object.defineProperty(vm, 'mytwowayprop', {
get() {
return myprop;
},
set(value) {
myprop = value;
vm.onchangeseventbecausemypropchanged();
}
});
vm.onchangeseventbecausemypropchanged = function () {//woot}
This will allow you to have a defined onchanges event when your two way binding property changes both internally and externally.
How can I listen to angular component binding change and perform actions?
angular.module('myapp')
.component('myComponent', {
templateUrl: 'some.html',
controller: MyController,
controllerAs: 'myCtrl',
bindings: {
items: '<'
}
});
now when items changes I want to perform another action using this value,
How can I do it?
You can add the $onChanges method to the controller
$onChanges(changesObj) is called whenever one-way bindings are updated. The changesObj is a hash whose keys are the names of the bound properties that have changed, and the values are an object of the form.
Following example handles canChange change event.
angular.module('app.components', [])
.component('changeHandler', {
controller: function ChangeHandlerController() {
this.$onChanges = function (changes) {
if (changes.canChange)
this.performActionWithValueOf(changes.canChange);
};
},
bindings: {
canChange: '<'
},
templateUrl: 'change-handler.html'
});
Requires AngularJS >= 1.5.3 and works only with one-way data-binding (as in the example above).
Docs: https://docs.angularjs.org/guide/component
Reference: http://blog.thoughtram.io/angularjs/2016/03/29/exploring-angular-1.5-lifecycle-hooks.html
now when items changes I want to perform another action using this value,
How can I do it?
But I want to avoid using the dying $scope
If you don't want to use $scope you can use a property setter to detect any changes e.g. :
class MyController {
private _items: string[] = []
set items(value:string[]){
this._items = value;
console.log('Items changed:',value);
}
get items():string[]{
return this._items;
}
}
const ctrl = new MyController();
ctrl.items = ['hello','world']; // will also log to the console
Please note that you shouldn't use it for complex logic (reasons : https://basarat.gitbooks.io/typescript/content/docs/tips/propertySetters.html) 🌹
Here's an ES5.1 version of basarat's answer:
function MyController() {
var items = [];
Object.defineProperty(this, 'items', {
get: function() {
return items;
},
set: function(newVal) {
items = newVal;
console.log('Items changed:', newVal);
}
});
}
Using Object.defineProperty(). Supported by all major browsers and IE9+.
I've discovered a way but not sure it's the most efficient. First bring in $scope as a dependency and set it to this._scope or the like in your constructor. I have the following then in my $onInit function:
this._scope.$watch(() => {
return this.items;
},
(newVal, oldVal) => {
// Do what you have to here
});
It's highly inspired by the answer here: Angularjs: 'controller as syntax' and $watch
Hope it helps, it's what I'm going to use until I'm told otherwise.
Currently you can't use angular watchers without $scope as change detection is based around $scope. Even if you use expressions in HTML it would delegate watch functionality to $scope.
Even if you create some other mechanism to watch you will need to remember to unwatch manually - and with $scope it's done automatically.
This approach might help:
import { Input } from '#angular/core';
class MyComponent {
#Input set items(value) {
if (this._items !== value) {
console.log(`The value has been changed from "${this._items}" to "${value}"`);
this._items = value;
}
}
private _items;
get items() {
return this._items;
}
}
I have 3 nested, isolate scope, directives (see CodePen here) and I am able to pass a function (that takes an argument) from the outermost directive to the innermost directive (passing the function from outer directive to intermediate directive to inner directive).
What I'm failing to understand is what needs to be done to pass the argument from the inner directive back through intermediate directive back to the outer directive.
Again, see the CodePen example.
Note: Given only 2 isolate scope directives I can get this to work with something similar to the following...
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName(name)"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name); // alerts 'Henry'
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '&'
},
template: "<span ng-click='addName({name: testName})'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = 'Henry';
}
}
});
The above code gives me an Alert box with 'Henry' (just like I'd expect).
It's when I add an third, intermediate, isolate scope directive that I run into problems...
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName(name)"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name); // alerts 'Henry'
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '&'
},
template: '<div><my-dir-3 add-name="addName({name: testName})"></my-dir-3></div>',
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '&'
},
template: "<span ng-click='addName({name: testName})'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = 'Henry';
}
}
});
This code gives me an alert box with undefined...
A common misconception is that "& is for passing functions". This isn't technically correct.
What & does is create a function on the directive scope that, when called, returns the result of the expression evaluated against the parent scope.
This function takes an object as an argument that will override local variables in the expression with those from the directive scope (the {name: testName}) object in this case.
If you were to look under the hood, the $scope.addName method in myDir2 would look like this (simplified):
$scope.addName = function(locals) {
return $parse(attr.addName)($scope.$parent, locals);
}
Your second directive works because the expression it is binding to is
addName(name)
This expression has a local variable name, that is overridden with the value of testName from the directive when executed with
addName({name: testName}) //in the directive.
Remember - the addName function in myDir2 IS NOT the same as the addName function in myDir1. It is a new function that evaluates the expression
addName(name)
against the parent scope and returns the result.
When you apply this logic to myDir3, the expression that is evaluated is:
addName({name: testName})
Note that the only local variable in this expression is "testName". So when you call in myDir3 with
addName({name: testName})
there is no local variable name to override, and testName is left undefined.
Phew! No wonder this confuses JUST ABOUT EVERYBODY!
How to fix in your example:
You want the expressions to evaluate to the actual function in myDir1.
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name); // alerts 'Henry'
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '&'
},
// addName() returns the actual function addName from myDir1
template: '<div><my-dir-3 add-name="addName()"></my-dir-3></div>',
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '&'
},
//calls addName() on myDir2, which returns addName from myDir1, then invokes it passing testName as an argument
template: "<span ng-click='addName()(testName)'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = 'Henry';
}
}
});
Here is the working Pen
Final note - the reason why '&' is more appropriate than '=' here is that '=' is going to actually set up a $watch and two-way bind the variables between the directives. This means that myDir2 could actually change the function appName in myDir1, which is not required and undesirable. It also requires setting up two $watchs, which I try to avoid for performance reasons in Angular.
There are two ways to pass a function in an isolated scope. While '&' will make sure that what you are passing is in-fact a function, you can also pass a function as a bound variable with '=', and invoke it only when you need. This method has drawbacks, but it will leave the control of the invocation to the component that is in-charge of that invocation.
Your codepen working
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name);
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '='
},
template: '<div><my-dir-3 add-name="addName"></my-dir-3></div>'
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '='
},
template: "<span ng-click='addName(testName)'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = "Henry"
}
}
});
The problem with your code with three nested directives is that testName does not exist in isolated scope of myDir2. You can set that from myDir3, but for that you will have to create an object in scope of myDir2 and set its property to testName and then use that property in myDir2
The working example is here
http://codepen.io/anon/pen/Wveqwx
'myDir2' when you have 3 directives is referencing 'testName', but it doesn't exist in that scope. It was mapped to 'name' in the 'myDir3'. Because of this, changing the template in myDir2 as follows, should fix it:
template: '<div><my-dir-3 add-name="addName({name: name})"></my-dir-3></div>'
You can access an elements scope using isolateScope()
In the myDir1 directive replace the controller to this instead
link: function($scope, elem) {
$scope.addName = function(name) {
var e = elem.find('my-dir-3');
var s = e.isolateScope();
alert(s.testName);
};
Another way is to potentially use $$nextSibling to get the next scope after current parent (haven't really used this so may want to read up on it)
This is probably not the 'angular way' to do things. I think removing the isolated scope from the child directives and having them reference the parent directive model would be more ideal.
I am developing a directive which shows and hides it's contents based on a click event (ng-click) defined in it's template. On some views where the directive is used I'd like to be able to know if the directive is currently showing or hiding it's contents so I can respond to the DOM changes. The directive has isolated scope and I am trying to notify the parent scope when the directive has been "toggled". I'm attempting to accomplish this by passing a callback function to the directive where it is used that can be called when the directive's state changes i.e hides or shows
I'm not sure how to correctly implement this being that the state of the directive (hidden or shown) is stored in the directive's isolated scope and is determined after the ng-click. Therefore I need to call the parent scope's function from within the directive and not from withing the view.
This will make WAAY more sense with an example. Here is a plunked demonstrating what I'd like to do:
http://plnkr.co/edit/hHwwxjssOKiphTSO1VIS?p=info
var app = angular.module('main-module',[])
app.controller('MainController', function($scope){
$scope.myValue = 'test value';
$scope.parentToggle = function(value){
$scope.myValue = value;
};
});
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="toggle();">Click Me</a>',
replace: true,
scope: {
OnToggle: '&'
},
link: function($scope, elem, attrs, controller) {
$scope.toggleValue = false;
$scope.toggle = function () {
$scope.toggleValue = !$scope.toggleValue;
$scope.OnToggle($scope.toggleValue)
};
}
};
});
I'm relatively new to Angular. Is this a bad idea to begin with? Should I be using a service or something rather than passing around function refs?
Thanks!
Update
You can also use & to bind the function of the root scope (that is actually the purpose of &).
To do so the directive needs to be slightly changed:
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="f()">Click Me</a>',
replace: true,
scope: {
toggle: '&'
},
controller: function($scope) {
$scope.toggleValue = false;
$scope.f = function() {
$scope.toggleValue = !$scope.toggleValue;
$scope.toggle({message: $scope.toggleValue});
};
}
};
});
You can use like this:
<div toggle="parentToggle(message)"></div>
Plunk
You could bind the function using =. In addition ensure the property name in your scope and tag are matching (AngularJS translates CamelCase to dash notation).
Before:
scope: {
OnToggle: '&'
}
After:
scope: {
onToggle: '='
}
Furthermore don't use on-toggle="parentToggle({value: toggleValue})" in your main template. You do not want to call the function but just passing a pointer of the function to the directive.
Plunk
I have an object outside the AngularJS scope that looks something like:
obj = { isBig : true, name: "John" ... }
it has many parameters and it was created outside the angular scope and it updates outside of angularJS (for example - via ajax call that I make with jQuery).
inside my controller I write:
$scope.obj = obj;
How do I create an event that listens to any of the object's changes and updates the view ? So the event is the objects's changes
Specifically I have a parameter called obj.isAjaxCallFailed = true/false that I use in the view as <div ng-show="obj.isAjaxCallFailed"></div>
It's not working. I tried using $on and I can't figure out any way to do it.
Use $watch() on obj to listen for changes (in your controller):
$scope.$watch(function() {
return obj;
}, function(value) {
if(value) {
$scope.obj = obj;
}
}, true);
At the place in your code where you create or update obj, you use $rootScope.$apply() to kickoff the watches:
function someCallbackOutsideAngular(data) {
var $rootScope = angular.injector(['ng']).get('$rootScope');
$rootScope.$apply(function() {
obj.someKey = data.someValue;
});
}
Note: angular.injector(['ng']) should probably set somewhere outside the callback (no need to execute it all the time).